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',
Roles = 'roles',
Logs = 'logs',
Branding = 'branding',
}
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 CaretUp from '@/assets/icons/caret-up.svg';
import FormCard from '@/components/FormCard';
import { isDevFeaturesEnabled } from '@/consts/env';
import { openIdProviderConfigPath } from '@/consts/oidc';
import { AppDataContext } from '@/contexts/AppDataProvider';
import Button from '@/ds-components/Button';
@ -27,7 +28,7 @@ type Props = {
oidcConfig: SnakeCaseOidcConfig;
};
function EndpointsAndCredentials({ app: { type, secret, id }, oidcConfig }: Props) {
function EndpointsAndCredentials({ app: { type, secret, id, isThirdParty }, oidcConfig }: Props) {
const { tenantEndpoint } = useContext(AppDataContext);
const [showMoreEndpoints, setShowMoreEndpoints] = useState(false);
@ -50,7 +51,8 @@ function EndpointsAndCredentials({ app: { type, secret, id }, oidcConfig }: Prop
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">
<CopyToClipboard
isFullWidth

View file

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

View file

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

View file

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

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Gib einen Anwendungsnamen ein',
application_deleted: 'Anwendung {{name}} wurde erfolgreich gelöscht',
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: {
name_column: 'Rolle',
description_column: 'Beschreibung',

View file

@ -61,6 +61,18 @@ const application_details = {
enter_your_application_name: 'Enter your application name',
application_deleted: 'Application {{name}} has been successfully deleted',
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: {
name_column: 'Role',
description_column: 'Description',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Ingresa el nombre de tu aplicación',
application_deleted: 'Se ha eliminado exitosamente la aplicación {{name}}',
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: {
name_column: 'Rol',
description_column: 'Descripción',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Entrez le nom de votre application',
application_deleted: "L'application {{name}} a été supprimée avec succès.",
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: {
name_column: 'Rôle',
description_column: 'Description',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Inserisci il nome della tua applicazione',
application_deleted: "L'applicazione {{name}} è stata eliminata con successo",
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: {
name_column: 'Ruolo',
description_column: 'Descrizione',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'アプリケーション名を入力してください',
application_deleted: 'アプリケーション{{name}}が正常に削除されました',
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: {
name_column: '役割',
description_column: '説明',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: '어플리케이션 이름을 입력해 주세요.',
application_deleted: '{{name}} 어플리케이션이 성공적으로 삭제되었어요.',
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: {
name_column: '역할',
description_column: '설명',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Wpisz nazwę swojej aplikacji',
application_deleted: 'Aplikacja {{name}} została pomyślnie usunięta',
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: {
name_column: 'Role',
description_column: 'Opis',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Digite o nome do seu aplicativo',
application_deleted: 'O aplicativo {{name}} foi excluído com sucesso',
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: {
name_column: 'Função',
description_column: 'Descrição',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Insira o nome da aplicação',
application_deleted: 'Aplicação {{name}} eliminada com sucesso',
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: {
name_column: 'Nome da função',
description_column: 'Descrição',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Введите название своего приложения',
application_deleted: 'Приложение {{name}} успешно удалено',
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: {
name_column: 'Роль',
description_column: 'Описание',

View file

@ -67,6 +67,27 @@ const application_details = {
enter_your_application_name: 'Uygulama adı giriniz',
application_deleted: '{{name}} Uygulaması başarıyla silindi',
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: {
name_column: 'Rol',
description_column: 'Açıklama',

View file

@ -64,6 +64,27 @@ const application_details = {
enter_your_application_name: '输入你的应用名称',
application_deleted: '应用 {{name}} 成功删除。',
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: {
name_column: '角色',
description_column: '描述',

View file

@ -64,6 +64,27 @@ const application_details = {
enter_your_application_name: '輸入你的應用程式名稱',
application_deleted: '應用 {{name}} 成功刪除。',
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: {
name_column: '角色',
description_column: '描述',

View file

@ -65,6 +65,27 @@ const application_details = {
enter_your_application_name: '輸入你的應用程式姓名',
application_deleted: '應用 {{name}} 成功刪除。',
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: {
name_column: '角色',
description_column: '描述',