mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(console): delete custom phrases (#2065)
This commit is contained in:
parent
4995ab9461
commit
68e88840bf
11 changed files with 160 additions and 26 deletions
|
@ -1,13 +1,16 @@
|
||||||
import { languages as uiLanguageNameMapping } from '@logto/language-kit';
|
import { languages as uiLanguageNameMapping } from '@logto/language-kit';
|
||||||
|
import { SignInExperience } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import FormField from '@/components/FormField';
|
import FormField from '@/components/FormField';
|
||||||
import Select from '@/components/Select';
|
import Select from '@/components/Select';
|
||||||
import Switch from '@/components/Switch';
|
import Switch from '@/components/Switch';
|
||||||
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
||||||
|
import { RequestError } from '@/hooks/use-api';
|
||||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||||
|
|
||||||
import useLanguageEditorContext from '../hooks/use-language-editor-context';
|
import useLanguageEditorContext from '../hooks/use-language-editor-context';
|
||||||
|
@ -21,8 +24,10 @@ type Props = {
|
||||||
|
|
||||||
const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { watch, control, register } = useFormContext<SignInExperienceForm>();
|
const { data: signInExperience } = useSWR<SignInExperience, RequestError>('/api/sign-in-exp');
|
||||||
|
const { watch, control, register, setValue } = useFormContext<SignInExperienceForm>();
|
||||||
const isAutoDetect = watch('languageInfo.autoDetect');
|
const isAutoDetect = watch('languageInfo.autoDetect');
|
||||||
|
const selectedDefaultLanguage = watch('languageInfo.fallbackLanguage');
|
||||||
const [isManageLanguageFormOpen, setIsManageLanguageFormOpen] = useState(false);
|
const [isManageLanguageFormOpen, setIsManageLanguageFormOpen] = useState(false);
|
||||||
const { languages } = useUiLanguages();
|
const { languages } = useUiLanguages();
|
||||||
|
|
||||||
|
@ -36,6 +41,15 @@ const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
||||||
const { context: languageEditorContext, Provider: LanguageEditorContextProvider } =
|
const { context: languageEditorContext, Provider: LanguageEditorContextProvider } =
|
||||||
useLanguageEditorContext(languages);
|
useLanguageEditorContext(languages);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!languages.includes(selectedDefaultLanguage)) {
|
||||||
|
setValue(
|
||||||
|
'languageInfo.fallbackLanguage',
|
||||||
|
signInExperience?.languageInfo.fallbackLanguage ?? 'en'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [languages, selectedDefaultLanguage, setValue, signInExperience]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.title}>{t('sign_in_exp.others.languages.title')}</div>
|
<div className={styles.title}>{t('sign_in_exp.others.languages.title')}</div>
|
||||||
|
|
|
@ -7,20 +7,27 @@
|
||||||
padding: _.unit(6) _.unit(5);
|
padding: _.unit(6) _.unit(5);
|
||||||
font: var(--font-title-large);
|
font: var(--font-title-large);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
> span {
|
.languageInfo {
|
||||||
margin-left: _.unit(2);
|
display: flex;
|
||||||
font: var(--font-body-medium);
|
align-items: center;
|
||||||
color: var(--color-caption);
|
|
||||||
}
|
|
||||||
|
|
||||||
.builtInFlag {
|
> span {
|
||||||
display: inline-block;
|
margin-left: _.unit(2);
|
||||||
font: var(--font-label-medium);
|
font: var(--font-body-medium);
|
||||||
color: var(--color-text);
|
color: var(--color-caption);
|
||||||
background-color: var(--color-surface-variant);
|
}
|
||||||
padding: _.unit(0.5) _.unit(2);
|
|
||||||
border-radius: 10px;
|
.builtInFlag {
|
||||||
|
display: inline-block;
|
||||||
|
font: var(--font-label-medium);
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-surface-variant);
|
||||||
|
padding: _.unit(0.5) _.unit(2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import { languages, LanguageTag } from '@logto/language-kit';
|
import { languages as uiLanguageNameMapping, LanguageTag } from '@logto/language-kit';
|
||||||
import resource, { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
import resource, { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
||||||
import en from '@logto/phrases-ui/lib/locales/en';
|
import en from '@logto/phrases-ui/lib/locales/en';
|
||||||
import { Translation } from '@logto/schemas';
|
import { SignInExperience, Translation } from '@logto/schemas';
|
||||||
import cleanDeep from 'clean-deep';
|
import cleanDeep from 'clean-deep';
|
||||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useSWR, { useSWRConfig } from 'swr';
|
import useSWR, { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import ConfirmModal from '@/components/ConfirmModal';
|
||||||
|
import IconButton from '@/components/IconButton';
|
||||||
import useApi, { RequestError } from '@/hooks/use-api';
|
import useApi, { RequestError } from '@/hooks/use-api';
|
||||||
import Delete from '@/icons/Delete';
|
import Delete from '@/icons/Delete';
|
||||||
import { CustomPhraseResponse } from '@/types/custom-phrase';
|
import { CustomPhraseResponse } from '@/types/custom-phrase';
|
||||||
|
@ -24,10 +26,17 @@ const emptyUiTranslation = createEmptyUiTranslation();
|
||||||
const LanguageEditor = () => {
|
const LanguageEditor = () => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
const { selectedLanguage, setIsDirty, stopAddingLanguage } = useContext(LanguageEditorContext);
|
const { data: signInExperience } = useSWR<SignInExperience, RequestError>('/api/sign-in-exp');
|
||||||
|
|
||||||
|
const { languages, selectedLanguage, setIsDirty, setSelectedLanguage, stopAddingLanguage } =
|
||||||
|
useContext(LanguageEditorContext);
|
||||||
|
|
||||||
|
const [isDeletionAlertOpen, setIsDeletionAlertOpen] = useState(false);
|
||||||
|
|
||||||
const isBuiltIn = isBuiltInLanguageTag(selectedLanguage);
|
const isBuiltIn = isBuiltInLanguageTag(selectedLanguage);
|
||||||
|
|
||||||
|
const isDefaultLanguage = signInExperience?.languageInfo.fallbackLanguage === selectedLanguage;
|
||||||
|
|
||||||
const translationEntries = useMemo(
|
const translationEntries = useMemo(
|
||||||
() => Object.entries((isBuiltIn ? resource[selectedLanguage] : en).translation),
|
() => Object.entries((isBuiltIn ? resource[selectedLanguage] : en).translation),
|
||||||
[isBuiltIn, selectedLanguage]
|
[isBuiltIn, selectedLanguage]
|
||||||
|
@ -105,6 +114,39 @@ const LanguageEditor = () => {
|
||||||
[api, globalMutate, stopAddingLanguage]
|
[api, globalMutate, stopAddingLanguage]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onDelete = useCallback(() => {
|
||||||
|
if (!customPhrase && !isDefaultLanguage) {
|
||||||
|
stopAddingLanguage(true);
|
||||||
|
setSelectedLanguage(
|
||||||
|
languages.find((languageTag) => languageTag !== selectedLanguage) ?? 'en'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsDeletionAlertOpen(true);
|
||||||
|
}, [
|
||||||
|
customPhrase,
|
||||||
|
isDefaultLanguage,
|
||||||
|
languages,
|
||||||
|
selectedLanguage,
|
||||||
|
setSelectedLanguage,
|
||||||
|
stopAddingLanguage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onConfirmDeletion = useCallback(async () => {
|
||||||
|
setIsDeletionAlertOpen(false);
|
||||||
|
|
||||||
|
if (isDefaultLanguage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.delete(`/api/custom-phrases/${selectedLanguage}`);
|
||||||
|
|
||||||
|
await globalMutate('/api/custom-phrases');
|
||||||
|
|
||||||
|
setSelectedLanguage(languages.find((languageTag) => languageTag !== selectedLanguage) ?? 'en');
|
||||||
|
}, [api, globalMutate, isDefaultLanguage, languages, selectedLanguage, setSelectedLanguage]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (formData: Translation) => {
|
const onSubmit = handleSubmit(async (formData: Translation) => {
|
||||||
const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData);
|
const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData);
|
||||||
void mutate(updatedCustomPhrase);
|
void mutate(updatedCustomPhrase);
|
||||||
|
@ -126,12 +168,19 @@ const LanguageEditor = () => {
|
||||||
return (
|
return (
|
||||||
<div className={style.languageEditor}>
|
<div className={style.languageEditor}>
|
||||||
<div className={style.title}>
|
<div className={style.title}>
|
||||||
{languages[selectedLanguage]}
|
<div className={style.languageInfo}>
|
||||||
<span>{selectedLanguage}</span>
|
{uiLanguageNameMapping[selectedLanguage]}
|
||||||
{isBuiltIn && (
|
<span>{selectedLanguage}</span>
|
||||||
<span className={style.builtInFlag}>
|
{isBuiltIn && (
|
||||||
{t('sign_in_exp.others.manage_language.logto_provided')}
|
<span className={style.builtInFlag}>
|
||||||
</span>
|
{t('sign_in_exp.others.manage_language.logto_provided')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isBuiltIn && (
|
||||||
|
<IconButton onClick={onDelete}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
|
@ -187,6 +236,28 @@ const LanguageEditor = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeletionAlertOpen}
|
||||||
|
title={
|
||||||
|
isDefaultLanguage
|
||||||
|
? 'sign_in_exp.others.manage_language.default_language_deletion_title'
|
||||||
|
: 'sign_in_exp.others.manage_language.deletion_title'
|
||||||
|
}
|
||||||
|
confirmButtonText={
|
||||||
|
isDefaultLanguage ? 'sign_in_exp.others.manage_language.got_it' : 'general.delete'
|
||||||
|
}
|
||||||
|
confirmButtonType={isDefaultLanguage ? 'primary' : 'danger'}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsDeletionAlertOpen(false);
|
||||||
|
}}
|
||||||
|
onConfirm={onConfirmDeletion}
|
||||||
|
>
|
||||||
|
{isDefaultLanguage
|
||||||
|
? t('sign_in_exp.others.manage_language.default_language_deletion_description', {
|
||||||
|
language: uiLanguageNameMapping[selectedLanguage],
|
||||||
|
})
|
||||||
|
: t('sign_in_exp.others.manage_language.deletion_description')}
|
||||||
|
</ConfirmModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,12 @@ const LanguageItem = ({ languageTag, isSelected, onClick }: Props) => {
|
||||||
<div
|
<div
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
className={classNames(style.languageItem, isSelected && style.selected)}
|
className={classNames(style.languageItem, isSelected && style.selected)}
|
||||||
onClick={onClick}
|
onClick={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={style.languageName}>{languages[languageTag]}</div>
|
<div className={style.languageName}>{languages[languageTag]}</div>
|
||||||
<div className={style.languageTag}>{languageTag}</div>
|
<div className={style.languageTag}>{languageTag}</div>
|
||||||
|
|
|
@ -69,11 +69,12 @@ const useLanguageEditorContext = (defaultLanguages: LanguageTag[]) => {
|
||||||
if (isAddingLanguage) {
|
if (isAddingLanguage) {
|
||||||
if (isCanceled) {
|
if (isCanceled) {
|
||||||
setLanguages(defaultLanguages);
|
setLanguages(defaultLanguages);
|
||||||
|
setSelectedLanguage(languages[0] ?? 'en');
|
||||||
}
|
}
|
||||||
setIsAddingLanguage(false);
|
setIsAddingLanguage(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[defaultLanguages, isAddingLanguage]
|
[defaultLanguages, isAddingLanguage, languages]
|
||||||
);
|
);
|
||||||
|
|
||||||
const context = useMemo<Context>(
|
const context = useMemo<Context>(
|
||||||
|
|
|
@ -92,6 +92,13 @@ const sign_in_exp = {
|
||||||
custom_values: 'Custom values',
|
custom_values: 'Custom values',
|
||||||
clear_all: 'Clear all',
|
clear_all: 'Clear all',
|
||||||
unsaved_description: 'Changes won’t be saved if you leave this page without saving.',
|
unsaved_description: 'Changes won’t be saved if you leave this page without saving.',
|
||||||
|
deletion_title: 'Do you want to delete the added language?',
|
||||||
|
deletion_description:
|
||||||
|
'After deletion, your users won’t be able to browse in that language again.',
|
||||||
|
default_language_deletion_title: 'Default language can’t be deleted.',
|
||||||
|
default_language_deletion_description:
|
||||||
|
'{{language}} is set as your default language and can’t be deleted. ',
|
||||||
|
got_it: 'Got It',
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
title: 'AUTHENTICATION',
|
title: 'AUTHENTICATION',
|
||||||
|
|
|
@ -94,6 +94,13 @@ const sign_in_exp = {
|
||||||
custom_values: 'Custom values', // UNTRANSLATED
|
custom_values: 'Custom values', // UNTRANSLATED
|
||||||
clear_all: 'Clear all', // UNTRANSLATED
|
clear_all: 'Clear all', // UNTRANSLATED
|
||||||
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
||||||
|
deletion_title: 'Do you want to delete the added language?', // UNTRANSLATED
|
||||||
|
deletion_description:
|
||||||
|
'After deletion, your users won’t be able to browse in that language again.', // UNTRANSLATED
|
||||||
|
default_language_deletion_title: 'Default language can’t be deleted.', // UNTRANSLATED
|
||||||
|
default_language_deletion_description:
|
||||||
|
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
||||||
|
got_it: 'Got It', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
title: 'AUTHENTICATION',
|
title: 'AUTHENTICATION',
|
||||||
|
|
|
@ -89,6 +89,13 @@ const sign_in_exp = {
|
||||||
custom_values: 'Custom values', // UNTRANSLATED
|
custom_values: 'Custom values', // UNTRANSLATED
|
||||||
clear_all: 'Clear all', // UNTRANSLATED
|
clear_all: 'Clear all', // UNTRANSLATED
|
||||||
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
||||||
|
deletion_title: 'Do you want to delete the added language?', // UNTRANSLATED
|
||||||
|
deletion_description:
|
||||||
|
'After deletion, your users won’t be able to browse in that language again.', // UNTRANSLATED
|
||||||
|
default_language_deletion_title: 'Default language can’t be deleted.', // UNTRANSLATED
|
||||||
|
default_language_deletion_description:
|
||||||
|
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
||||||
|
got_it: 'Got It', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
title: 'AUTHENTICATION',
|
title: 'AUTHENTICATION',
|
||||||
|
|
|
@ -92,6 +92,13 @@ const sign_in_exp = {
|
||||||
custom_values: 'Custom values', // UNTRANSLATED
|
custom_values: 'Custom values', // UNTRANSLATED
|
||||||
clear_all: 'Clear all', // UNTRANSLATED
|
clear_all: 'Clear all', // UNTRANSLATED
|
||||||
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
||||||
|
deletion_title: 'Do you want to delete the added language?', // UNTRANSLATED
|
||||||
|
deletion_description:
|
||||||
|
'After deletion, your users won’t be able to browse in that language again.', // UNTRANSLATED
|
||||||
|
default_language_deletion_title: 'Default language can’t be deleted.', // UNTRANSLATED
|
||||||
|
default_language_deletion_description:
|
||||||
|
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
||||||
|
got_it: 'Got It', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
title: 'AUTENTICAÇÃO',
|
title: 'AUTENTICAÇÃO',
|
||||||
|
|
|
@ -99,6 +99,7 @@ const sign_in_exp = {
|
||||||
default_language_deletion_title: 'Default language can’t be deleted.', // UNTRANSLATED
|
default_language_deletion_title: 'Default language can’t be deleted.', // UNTRANSLATED
|
||||||
default_language_deletion_description:
|
default_language_deletion_description:
|
||||||
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
||||||
|
got_it: 'Got It', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
title: 'AUTHENTICATION',
|
title: 'AUTHENTICATION',
|
||||||
|
|
|
@ -89,6 +89,13 @@ const sign_in_exp = {
|
||||||
custom_values: 'Custom values', // UNTRANSLATED
|
custom_values: 'Custom values', // UNTRANSLATED
|
||||||
clear_all: 'Clear all', // UNTRANSLATED
|
clear_all: 'Clear all', // UNTRANSLATED
|
||||||
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
unsaved_description: 'Changes won’t be saved if you leave this page without saving.', // UNTRANSLATED
|
||||||
|
deletion_title: 'Do you want to delete the added language?', // UNTRANSLATED
|
||||||
|
deletion_description:
|
||||||
|
'After deletion, your users won’t be able to browse in that language again.', // UNTRANSLATED
|
||||||
|
default_language_deletion_title: 'Default language can’t be deleted.', // UNTRANSLATED
|
||||||
|
default_language_deletion_description:
|
||||||
|
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
||||||
|
got_it: 'Got It', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
title: '身份验证',
|
title: '身份验证',
|
||||||
|
|
Loading…
Add table
Reference in a new issue