0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(console): : add application branding tab (#5176)

* feat(console,phrases): add new third-party applicaiton to the guide library

add new third-party applicaiton to the guide library

* fix(test): fix rebase issue

fix rebase issue

* feat(console): hide some fields for third-party apps

hide some form fields for third-party apps

* feat(console): add application branding tab

add application branding tab

* fix(console): fix fields not updated after logo deletion bug

fix fields not updated after logo deletion bug

* fix(console): fix the third-party hidden fields

fix the third-party hidden fields logic

* fix(console): fix style class not found error

fix style class not found error

* fix(console): force bool type for the thirdPartyApplicaitonEnabled variable

force bool type for the thirdPartyApplicaitonEnabled variable
This commit is contained in:
simeng-li 2024-01-05 14:55:15 +08:00 committed by GitHub
parent 5538946a9b
commit e9e0f18dcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 711 additions and 10 deletions

View file

@ -2,6 +2,7 @@ export enum ApplicationDetailsTabs {
Settings = 'settings', Settings = 'settings',
Roles = 'roles', Roles = 'roles',
Logs = 'logs', Logs = 'logs',
Branding = 'branding',
} }
export enum ApiResourceDetailsTabs { export enum ApiResourceDetailsTabs {

View file

@ -0,0 +1,79 @@
import { type ApplicationSignInExperience } from '@logto/schemas';
import classNames from 'classnames';
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import ImageUploader from '@/ds-components/Uploader/ImageUploader';
import useImageMimeTypes from '@/hooks/use-image-mime-types';
import * as styles from './index.module.scss';
type Props = {
isDarkModeEnabled?: boolean;
};
function LogoUploader({ isDarkModeEnabled }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [uploadLogoError, setUploadLogoError] = useState<string>();
const [uploadDarkLogoError, setUploadDarkLogoError] = useState<string>();
const { description } = useImageMimeTypes();
const { control } = useFormContext<ApplicationSignInExperience>();
return (
<div>
<div className={styles.container}>
<Controller
name="branding.logoUrl"
control={control}
render={({ field: { onChange, value, name } }) => (
<ImageUploader
className={isDarkModeEnabled ? styles.multiColumn : undefined}
name={name}
value={value ?? ''}
actionDescription={t('sign_in_exp.branding.logo_image_url')}
onCompleted={onChange}
onUploadErrorChange={setUploadLogoError}
onDelete={() => {
onChange('');
}}
/>
)}
/>
{/* Show the dark mode logto uploader only if dark mode is enabled in the global sign-in-experience */}
{isDarkModeEnabled && (
<Controller
name="branding.darkLogoUrl"
control={control}
render={({ field: { onChange, value, name } }) => (
<ImageUploader
name={name}
value={value ?? ''}
className={value ? styles.darkMode : undefined}
actionDescription={t('sign_in_exp.branding.dark_logo_image_url')}
onCompleted={onChange}
onUploadErrorChange={setUploadDarkLogoError}
onDelete={() => {
onChange('');
}}
/>
)}
/>
)}
</div>
{uploadLogoError && (
<div className={classNames(styles.description, styles.error)}>
{t('sign_in_exp.branding.logo_image_error', { error: uploadLogoError })}
</div>
)}
{uploadDarkLogoError && (
<div className={classNames(styles.description, styles.error)}>
{t('sign_in_exp.branding.logo_image_error', { error: uploadDarkLogoError })}
</div>
)}
<div className={styles.description}>{description}</div>
</div>
);
}
export default LogoUploader;

View file

@ -0,0 +1,33 @@
@use '@/scss/underscore' as _;
.container {
display: flex;
> * {
flex: 1;
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&.darkMode {
background-color: #000;
}
}
.multiColumn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.description {
font: var(--font-body-2);
color: var(--color-text-secondary);
margin-top: _.unit(1);
}
.error {
color: var(--color-error);
}

View file

@ -0,0 +1,171 @@
import { type Application, type ApplicationSignInExperience } from '@logto/schemas';
import { useCallback, useEffect } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import DetailsForm from '@/components/DetailsForm';
import FormCard, { FormCardSkeleton } from '@/components/FormCard';
import RequestDataError from '@/components/RequestDataError';
import FormField from '@/ds-components/FormField';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import useUserAssetsService from '@/hooks/use-user-assets-service';
import { trySubmitSafe } from '@/utils/form';
import { uriValidator } from '@/utils/validator';
import LogoUploader from './LogoUploader';
import useApplicationSignInExperienceSWR from './use-application-sign-in-experience-swr';
import useSignInExperienceSWR from './use-sign-in-experience-swr';
import { formatFormToSubmitData, formatResponseDataToForm } from './utils';
type Props = {
application: Application;
};
function Branding({ application }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const formMethods = useForm<ApplicationSignInExperience>({
defaultValues: {
tenantId: application.tenantId,
applicationId: application.id,
branding: {},
},
});
const {
handleSubmit,
register,
reset,
formState: { isDirty, isSubmitting, errors },
} = formMethods;
const api = useApi();
const { data, error, mutate } = useApplicationSignInExperienceSWR(application.id);
const { data: sieData, error: sieError, mutate: sieMutate } = useSignInExperienceSWR();
const { isReady: isUserAssetsServiceReady, isLoading: isUserAssetsServiceLoading } =
useUserAssetsService();
const isApplicationSieLoading = !data && !error;
const isSieLoading = !sieData && !sieError;
const isLoading = isApplicationSieLoading || isSieLoading || isUserAssetsServiceLoading;
const onSubmit = handleSubmit(
trySubmitSafe(async (data) => {
if (isSubmitting) {
return;
}
const response = await api
.put(`api/applications/${application.id}/sign-in-experience`, {
json: formatFormToSubmitData(data),
})
.json<ApplicationSignInExperience>();
void mutate(response);
toast.success(t('general.saved'));
})
);
const onRetryFetch = useCallback(() => {
void mutate();
void sieMutate();
}, [mutate, sieMutate]);
useEffect(() => {
if (!data) {
return;
}
reset(formatResponseDataToForm(data));
}, [data, reset]);
if (isLoading) {
return <FormCardSkeleton />;
}
// Show error details if the error is not 404
if (error && error.status !== 404) {
return <RequestDataError error={error} onRetry={onRetryFetch} />;
}
const isDarkModeEnabled = sieData?.color.isDarkModeEnabled;
return (
<FormProvider {...formMethods}>
<DetailsForm
isDirty={isDirty}
isSubmitting={isSubmitting}
onDiscard={reset}
onSubmit={onSubmit}
>
<FormCard
title="application_details.branding.branding"
description="application_details.branding.branding_description"
>
<FormField title="application_details.branding.display_name">
<TextInput {...register('displayName')} />
</FormField>
{isUserAssetsServiceReady && (
<FormField title="application_details.branding.display_logo">
<LogoUploader isDarkModeEnabled={isDarkModeEnabled} />
</FormField>
)}
{/* Display the TextInput field if image upload service is not available */}
{!isUserAssetsServiceReady && (
<FormField title="application_details.branding.display_logo">
<TextInput
{...register('branding.logoUrl', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
error={errors.branding?.logoUrl?.message}
/>
</FormField>
)}
{/* Display the Dark logo field only if the dark mode is enabled in the global sign-in-experience */}
{!isUserAssetsServiceReady && isDarkModeEnabled && (
<FormField title="application_details.branding.display_logo_dark">
<TextInput
{...register('branding.darkLogoUrl', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
error={errors.branding?.darkLogoUrl?.message}
/>
</FormField>
)}
</FormCard>
<FormCard
title="application_details.branding.more_info"
description="application_details.branding.more_info_description"
>
<FormField title="application_details.branding.terms_of_use_url">
<TextInput
{...register('termsOfUseUrl', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
error={errors.termsOfUseUrl?.message}
placeholder="https://"
/>
</FormField>
<FormField title="application_details.branding.privacy_policy_url">
<TextInput
{...register('privacyPolicyUrl', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
error={errors.privacyPolicyUrl?.message}
placeholder="https://"
/>
</FormField>
</FormCard>
</DetailsForm>
</FormProvider>
);
}
export default Branding;

View file

@ -0,0 +1,32 @@
import { type ApplicationSignInExperience } from '@logto/schemas';
import useSWR from 'swr';
import useApi, { RequestError } from '@/hooks/use-api';
import useSwrFetcher from '@/hooks/use-swr-fetcher';
/**
* SWR fetcher for application sign-in experience
*
* hide error toast, because we will handle the error in the component
* allow 404 error, if the sign-in experience is not set
*/
const useApplicationSignInExperienceSWR = (applicationId: string) => {
const fetchApi = useApi({ hideErrorToast: true });
const fetcher = useSwrFetcher<ApplicationSignInExperience>(fetchApi);
return useSWR<ApplicationSignInExperience, RequestError>(
`api/applications/${applicationId}/sign-in-experience`,
{
fetcher,
shouldRetryOnError: (error: unknown) => {
if (error instanceof RequestError) {
return error.status !== 404;
}
return true;
},
}
);
};
export default useApplicationSignInExperienceSWR;

View file

@ -0,0 +1,13 @@
import { type SignInExperience } from '@logto/schemas';
import useSWR from 'swr';
import { type RequestError } from '@/hooks/use-api';
/**
* We need SIE isDarkModeEnabled to determine if we should show the dark mode logo forms
*/
const useSignInExperienceSWR = () => {
return useSWR<SignInExperience, RequestError>('api/sign-in-exp');
};
export default useSignInExperienceSWR;

View file

@ -0,0 +1,41 @@
import { type ApplicationSignInExperience } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
/**
* Format the form data to match the API request body
* - Omit `applicationId` and `tenantId` from the request body
* - Remove the empty `logoUrl` and `darkLogoUrl` fields in the `branding` object
**/
export const formatFormToSubmitData = (
data: ApplicationSignInExperience
): Omit<ApplicationSignInExperience, 'applicationId' | 'tenantId'> => {
const { branding, applicationId, tenantId, ...rest } = data;
return {
...rest,
branding: {
...conditional(branding.logoUrl && { logoUrl: branding.logoUrl }),
...conditional(branding.darkLogoUrl && { darkLogoUrl: branding.darkLogoUrl }),
},
};
};
/**
* Format the response data to match the form data
*
* Fulfill the branding object with empty string if the `logoUrl` or `darkLogoUrl` is not set.
* Otherwise, the RHF won't update the branding fields properly with the undefined value.
*/
export const formatResponseDataToForm = (
data: ApplicationSignInExperience
): ApplicationSignInExperience => {
const { branding, ...rest } = data;
return {
...rest,
branding: {
logoUrl: branding.logoUrl ?? '',
darkLogoUrl: branding.darkLogoUrl ?? '',
},
};
};

View file

@ -11,6 +11,7 @@ import { Trans, useTranslation } from 'react-i18next';
import CaretDown from '@/assets/icons/caret-down.svg'; import CaretDown from '@/assets/icons/caret-down.svg';
import CaretUp from '@/assets/icons/caret-up.svg'; import CaretUp from '@/assets/icons/caret-up.svg';
import FormCard from '@/components/FormCard'; import FormCard from '@/components/FormCard';
import { isDevFeaturesEnabled } from '@/consts/env';
import { openIdProviderConfigPath } from '@/consts/oidc'; import { openIdProviderConfigPath } from '@/consts/oidc';
import { AppDataContext } from '@/contexts/AppDataProvider'; import { AppDataContext } from '@/contexts/AppDataProvider';
import Button from '@/ds-components/Button'; import Button from '@/ds-components/Button';
@ -27,7 +28,7 @@ type Props = {
oidcConfig: SnakeCaseOidcConfig; oidcConfig: SnakeCaseOidcConfig;
}; };
function EndpointsAndCredentials({ app: { type, secret, id }, oidcConfig }: Props) { function EndpointsAndCredentials({ app: { type, secret, id, isThirdParty }, oidcConfig }: Props) {
const { tenantEndpoint } = useContext(AppDataContext); const { tenantEndpoint } = useContext(AppDataContext);
const [showMoreEndpoints, setShowMoreEndpoints] = useState(false); const [showMoreEndpoints, setShowMoreEndpoints] = useState(false);
@ -50,7 +51,8 @@ function EndpointsAndCredentials({ app: { type, secret, id }, oidcConfig }: Prop
targetBlank: true, targetBlank: true,
}} }}
> >
{tenantEndpoint && ( {/* Hide logto endpoint field in third-party application's form. @simeng-li FIXME: remove isDevFeatureEnabled flag */}
{tenantEndpoint && (!isDevFeaturesEnabled || !isThirdParty) && (
<FormField title="application_details.logto_endpoint"> <FormField title="application_details.logto_endpoint">
<CopyToClipboard <CopyToClipboard
isFullWidth isFullWidth

View file

@ -6,6 +6,7 @@ import { Trans, useTranslation } from 'react-i18next';
import FormCard from '@/components/FormCard'; import FormCard from '@/components/FormCard';
import MultiTextInputField from '@/components/MultiTextInputField'; import MultiTextInputField from '@/components/MultiTextInputField';
import { isDevFeaturesEnabled } from '@/consts/env';
import FormField from '@/ds-components/FormField'; import FormField from '@/ds-components/FormField';
import type { MultiTextInputRule } from '@/ds-components/MultiTextInput/types'; import type { MultiTextInputRule } from '@/ds-components/MultiTextInput/types';
import { import {
@ -29,7 +30,7 @@ function Settings({ data }: Props) {
formState: { errors }, formState: { errors },
} = useFormContext<Application>(); } = useFormContext<Application>();
const { type: applicationType } = data; const { type: applicationType, isThirdParty } = data;
const isNativeApp = applicationType === ApplicationType.Native; const isNativeApp = applicationType === ApplicationType.Native;
const uriPatternRules: MultiTextInputRule = { const uriPatternRules: MultiTextInputRule = {
@ -55,12 +56,16 @@ function Settings({ data }: Props) {
placeholder={t('application_details.application_name_placeholder')} placeholder={t('application_details.application_name_placeholder')}
/> />
</FormField> </FormField>
<FormField title="application_details.description"> {/* Hide description field in third-party application's form. @simeng-li FIXME: remove isDevFeatureEnabled flag */}
<TextInput {(!isDevFeaturesEnabled || !isThirdParty) && (
{...register('description')} <FormField title="application_details.description">
placeholder={t('application_details.description_placeholder')} <TextInput
/> {...register('description')}
</FormField> placeholder={t('application_details.description_placeholder')}
/>
</FormField>
)}
{applicationType !== ApplicationType.MachineToMachine && ( {applicationType !== ApplicationType.MachineToMachine && (
<Controller <Controller
name="oidcClientMetadata.redirectUris" name="oidcClientMetadata.redirectUris"

View file

@ -23,6 +23,7 @@ import Drawer from '@/components/Drawer';
import PageMeta from '@/components/PageMeta'; import PageMeta from '@/components/PageMeta';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { ApplicationDetailsTabs } from '@/consts'; import { ApplicationDetailsTabs } from '@/consts';
import { isDevFeaturesEnabled } from '@/consts/env';
import { openIdProviderConfigPath } from '@/consts/oidc'; import { openIdProviderConfigPath } from '@/consts/oidc';
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal'; import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
import TabNav, { TabNavItem } from '@/ds-components/TabNav'; import TabNav, { TabNavItem } from '@/ds-components/TabNav';
@ -33,6 +34,7 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
import { applicationTypeI18nKey } from '@/types/applications'; import { applicationTypeI18nKey } from '@/types/applications';
import { trySubmitSafe } from '@/utils/form'; import { trySubmitSafe } from '@/utils/form';
import Branding from './components/Branding';
import EndpointsAndCredentials from './components/EndpointsAndCredentials'; import EndpointsAndCredentials from './components/EndpointsAndCredentials';
import GuideDrawer from './components/GuideDrawer'; import GuideDrawer from './components/GuideDrawer';
import GuideModal from './components/GuideModal'; import GuideModal from './components/GuideModal';
@ -57,11 +59,13 @@ function ApplicationDetails() {
const { data, error, mutate } = useSWR<ApplicationResponse, RequestError>( const { data, error, mutate } = useSWR<ApplicationResponse, RequestError>(
id && `api/applications/${id}` id && `api/applications/${id}`
); );
const { const {
data: oidcConfig, data: oidcConfig,
error: fetchOidcConfigError, error: fetchOidcConfigError,
mutate: mutateOidcConfig, mutate: mutateOidcConfig,
} = useSWR<SnakeCaseOidcConfig, RequestError>(openIdProviderConfigPath); } = useSWR<SnakeCaseOidcConfig, RequestError>(openIdProviderConfigPath);
const isLoading = (!data && !error) || (!oidcConfig && !fetchOidcConfigError); const isLoading = (!data && !error) || (!oidcConfig && !fetchOidcConfigError);
const requestError = error ?? fetchOidcConfigError; const requestError = error ?? fetchOidcConfigError;
const [isReadmeOpen, setIsReadmeOpen] = useState(false); const [isReadmeOpen, setIsReadmeOpen] = useState(false);
@ -227,6 +231,11 @@ function ApplicationDetails() {
</TabNavItem> </TabNavItem>
</> </>
)} )}
{isDevFeaturesEnabled && data.isThirdParty && (
<TabNavItem href={`/applications/${data.id}/${ApplicationDetailsTabs.Branding}`}>
{t('application_details.branding.branding')}
</TabNavItem>
)}
</TabNav> </TabNav>
<TabWrapper <TabWrapper
isActive={tab === ApplicationDetailsTabs.Settings} isActive={tab === ApplicationDetailsTabs.Settings}
@ -264,6 +273,15 @@ function ApplicationDetails() {
</TabWrapper> </TabWrapper>
</> </>
)} )}
{isDevFeaturesEnabled && data.isThirdParty && (
<TabWrapper
isActive={tab === ApplicationDetailsTabs.Branding}
className={styles.tabContainer}
>
<Branding application={data} />
</TabWrapper>
)}
</> </>
)} )}
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} onConfirm={reset} /> <UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} onConfirm={reset} />

View file

@ -88,7 +88,7 @@ const useApplicationsData = (isThirdParty = false) => {
const { data } = thirdPartyApplicationsData; const { data } = thirdPartyApplicationsData;
const [_, totalCount] = data ?? []; const [_, totalCount] = data ?? [];
const hasThirdPartyApplications = totalCount && totalCount > 0; const hasThirdPartyApplications = Boolean(totalCount && totalCount > 0);
return { return {
...(isThirdParty ? thirdPartyApplicationsData : firstPartyApplicationsData), ...(isThirdParty ? thirdPartyApplicationsData : firstPartyApplicationsData),

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Gib einen Anwendungsnamen ein', enter_your_application_name: 'Gib einen Anwendungsnamen ein',
application_deleted: 'Anwendung {{name}} wurde erfolgreich gelöscht', application_deleted: 'Anwendung {{name}} wurde erfolgreich gelöscht',
redirect_uri_required: 'Gib mindestens eine Umleitungs-URI an', redirect_uri_required: 'Gib mindestens eine Umleitungs-URI an',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Rolle', name_column: 'Rolle',
description_column: 'Beschreibung', description_column: 'Beschreibung',

View file

@ -61,6 +61,18 @@ const application_details = {
enter_your_application_name: 'Enter your application name', enter_your_application_name: 'Enter your application name',
application_deleted: 'Application {{name}} has been successfully deleted', application_deleted: 'Application {{name}} has been successfully deleted',
redirect_uri_required: 'You must enter at least one redirect URI', redirect_uri_required: 'You must enter at least one redirect URI',
branding: {
branding: 'Branding',
branding_description:
"Customize your application's display name and logo on the consent screen.",
more_info: 'More info',
more_info_description: 'Offer users more details about your application on the consent screen.',
display_name: 'Display name',
display_logo: 'Display logo',
display_logo_dark: 'Display logo (dark)',
terms_of_use_url: 'Application terms of use URL',
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Role', name_column: 'Role',
description_column: 'Description', description_column: 'Description',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Ingresa el nombre de tu aplicación', enter_your_application_name: 'Ingresa el nombre de tu aplicación',
application_deleted: 'Se ha eliminado exitosamente la aplicación {{name}}', application_deleted: 'Se ha eliminado exitosamente la aplicación {{name}}',
redirect_uri_required: 'Debes ingresar al menos un URI de Redireccionamiento', redirect_uri_required: 'Debes ingresar al menos un URI de Redireccionamiento',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Rol', name_column: 'Rol',
description_column: 'Descripción', description_column: 'Descripción',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Entrez le nom de votre application', enter_your_application_name: 'Entrez le nom de votre application',
application_deleted: "L'application {{name}} a été supprimée avec succès.", application_deleted: "L'application {{name}} a été supprimée avec succès.",
redirect_uri_required: 'Vous devez entrer au moins un URI de redirection.', redirect_uri_required: 'Vous devez entrer au moins un URI de redirection.',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Rôle', name_column: 'Rôle',
description_column: 'Description', description_column: 'Description',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Inserisci il nome della tua applicazione', enter_your_application_name: 'Inserisci il nome della tua applicazione',
application_deleted: "L'applicazione {{name}} è stata eliminata con successo", application_deleted: "L'applicazione {{name}} è stata eliminata con successo",
redirect_uri_required: 'Devi inserire almeno un URI di reindirizzamento', redirect_uri_required: 'Devi inserire almeno un URI di reindirizzamento',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Ruolo', name_column: 'Ruolo',
description_column: 'Descrizione', description_column: 'Descrizione',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'アプリケーション名を入力してください', enter_your_application_name: 'アプリケーション名を入力してください',
application_deleted: 'アプリケーション{{name}}が正常に削除されました', application_deleted: 'アプリケーション{{name}}が正常に削除されました',
redirect_uri_required: 'リダイレクトURIを少なくとも1つ入力する必要があります', redirect_uri_required: 'リダイレクトURIを少なくとも1つ入力する必要があります',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: '役割', name_column: '役割',
description_column: '説明', description_column: '説明',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: '어플리케이션 이름을 입력해 주세요.', enter_your_application_name: '어플리케이션 이름을 입력해 주세요.',
application_deleted: '{{name}} 어플리케이션이 성공적으로 삭제되었어요.', application_deleted: '{{name}} 어플리케이션이 성공적으로 삭제되었어요.',
redirect_uri_required: '반드시 최소 하나의 Redirect URI 를 입력해야 해요.', redirect_uri_required: '반드시 최소 하나의 Redirect URI 를 입력해야 해요.',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: '역할', name_column: '역할',
description_column: '설명', description_column: '설명',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Wpisz nazwę swojej aplikacji', enter_your_application_name: 'Wpisz nazwę swojej aplikacji',
application_deleted: 'Aplikacja {{name}} została pomyślnie usunięta', application_deleted: 'Aplikacja {{name}} została pomyślnie usunięta',
redirect_uri_required: 'Musisz wpisać co najmniej jeden adres URL przekierowania', redirect_uri_required: 'Musisz wpisać co najmniej jeden adres URL przekierowania',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Role', name_column: 'Role',
description_column: 'Opis', description_column: 'Opis',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Digite o nome do seu aplicativo', enter_your_application_name: 'Digite o nome do seu aplicativo',
application_deleted: 'O aplicativo {{name}} foi excluído com sucesso', application_deleted: 'O aplicativo {{name}} foi excluído com sucesso',
redirect_uri_required: 'Você deve inserir pelo menos um URI de redirecionamento', redirect_uri_required: 'Você deve inserir pelo menos um URI de redirecionamento',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Função', name_column: 'Função',
description_column: 'Descrição', description_column: 'Descrição',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Insira o nome da aplicação', enter_your_application_name: 'Insira o nome da aplicação',
application_deleted: 'Aplicação {{name}} eliminada com sucesso', application_deleted: 'Aplicação {{name}} eliminada com sucesso',
redirect_uri_required: 'Deve inserir pelo menos um URI de redirecionamento', redirect_uri_required: 'Deve inserir pelo menos um URI de redirecionamento',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Nome da função', name_column: 'Nome da função',
description_column: 'Descrição', description_column: 'Descrição',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Введите название своего приложения', enter_your_application_name: 'Введите название своего приложения',
application_deleted: 'Приложение {{name}} успешно удалено', application_deleted: 'Приложение {{name}} успешно удалено',
redirect_uri_required: 'Вы должны ввести по крайней мере один URI перенаправления', redirect_uri_required: 'Вы должны ввести по крайней мере один URI перенаправления',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Роль', name_column: 'Роль',
description_column: 'Описание', description_column: 'Описание',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Uygulama adı giriniz', enter_your_application_name: 'Uygulama adı giriniz',
application_deleted: '{{name}} Uygulaması başarıyla silindi', application_deleted: '{{name}} Uygulaması başarıyla silindi',
redirect_uri_required: 'En az 1 yönlendirme URIı girmelisiniz', redirect_uri_required: 'En az 1 yönlendirme URIı girmelisiniz',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: 'Rol', name_column: 'Rol',
description_column: 'Açıklama', description_column: 'Açıklama',

View file

@ -64,6 +64,27 @@ const application_details = {
enter_your_application_name: '输入你的应用名称', enter_your_application_name: '输入你的应用名称',
application_deleted: '应用 {{name}} 成功删除。', application_deleted: '应用 {{name}} 成功删除。',
redirect_uri_required: '至少需要输入一个重定向 URI。', redirect_uri_required: '至少需要输入一个重定向 URI。',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: '角色', name_column: '角色',
description_column: '描述', description_column: '描述',

View file

@ -64,6 +64,27 @@ const application_details = {
enter_your_application_name: '輸入你的應用程式名稱', enter_your_application_name: '輸入你的應用程式名稱',
application_deleted: '應用 {{name}} 成功刪除。', application_deleted: '應用 {{name}} 成功刪除。',
redirect_uri_required: '至少需要輸入一個重定向 URL。', redirect_uri_required: '至少需要輸入一個重定向 URL。',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: '角色', name_column: '角色',
description_column: '描述', description_column: '描述',

View file

@ -65,6 +65,27 @@ const application_details = {
enter_your_application_name: '輸入你的應用程式姓名', enter_your_application_name: '輸入你的應用程式姓名',
application_deleted: '應用 {{name}} 成功刪除。', application_deleted: '應用 {{name}} 成功刪除。',
redirect_uri_required: '至少需要輸入一個重定向 URL。', redirect_uri_required: '至少需要輸入一個重定向 URL。',
branding: {
/** UNTRANSLATED */
branding: 'Branding',
/** UNTRANSLATED */
branding_description:
"Customize your application's display name and logo on the consent screen.",
/** UNTRANSLATED */
more_info: 'More info',
/** UNTRANSLATED */
more_info_description: 'Offer users more details about your application on the consent screen.',
/** UNTRANSLATED */
display_name: 'Display name',
/** UNTRANSLATED */
display_logo: 'Display logo',
/** UNTRANSLATED */
display_logo_dark: 'Display logo (dark)',
/** UNTRANSLATED */
terms_of_use_url: 'Application terms of use URL',
/** UNTRANSLATED */
privacy_policy_url: 'Application privacy policy URL',
},
roles: { roles: {
name_column: '角色', name_column: '角色',
description_column: '描述', description_column: '描述',