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:
parent
5538946a9b
commit
e9e0f18dcf
26 changed files with 711 additions and 10 deletions
|
@ -2,6 +2,7 @@ export enum ApplicationDetailsTabs {
|
|||
Settings = 'settings',
|
||||
Roles = 'roles',
|
||||
Logs = 'logs',
|
||||
Branding = 'branding',
|
||||
}
|
||||
|
||||
export enum ApiResourceDetailsTabs {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 ?? '',
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '説明',
|
||||
|
|
|
@ -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: '설명',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: 'Описание',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '描述',
|
||||
|
|
|
@ -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: '描述',
|
||||
|
|
|
@ -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: '描述',
|
||||
|
|
Loading…
Add table
Reference in a new issue