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 { SignInExperience } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import FormField from '@/components/FormField';
|
||||
import Select from '@/components/Select';
|
||||
import Switch from '@/components/Switch';
|
||||
import * as textButtonStyles from '@/components/TextButton/index.module.scss';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
|
||||
import useLanguageEditorContext from '../hooks/use-language-editor-context';
|
||||
|
@ -21,8 +24,10 @@ type Props = {
|
|||
|
||||
const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
||||
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 selectedDefaultLanguage = watch('languageInfo.fallbackLanguage');
|
||||
const [isManageLanguageFormOpen, setIsManageLanguageFormOpen] = useState(false);
|
||||
const { languages } = useUiLanguages();
|
||||
|
||||
|
@ -36,6 +41,15 @@ const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
|||
const { context: languageEditorContext, Provider: LanguageEditorContextProvider } =
|
||||
useLanguageEditorContext(languages);
|
||||
|
||||
useEffect(() => {
|
||||
if (!languages.includes(selectedDefaultLanguage)) {
|
||||
setValue(
|
||||
'languageInfo.fallbackLanguage',
|
||||
signInExperience?.languageInfo.fallbackLanguage ?? 'en'
|
||||
);
|
||||
}
|
||||
}, [languages, selectedDefaultLanguage, setValue, signInExperience]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.title}>{t('sign_in_exp.others.languages.title')}</div>
|
||||
|
|
|
@ -7,20 +7,27 @@
|
|||
padding: _.unit(6) _.unit(5);
|
||||
font: var(--font-title-large);
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> span {
|
||||
margin-left: _.unit(2);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
.languageInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.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;
|
||||
> span {
|
||||
margin-left: _.unit(2);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
|
||||
.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 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 { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Delete from '@/icons/Delete';
|
||||
import { CustomPhraseResponse } from '@/types/custom-phrase';
|
||||
|
@ -24,10 +26,17 @@ const emptyUiTranslation = createEmptyUiTranslation();
|
|||
const LanguageEditor = () => {
|
||||
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 isDefaultLanguage = signInExperience?.languageInfo.fallbackLanguage === selectedLanguage;
|
||||
|
||||
const translationEntries = useMemo(
|
||||
() => Object.entries((isBuiltIn ? resource[selectedLanguage] : en).translation),
|
||||
[isBuiltIn, selectedLanguage]
|
||||
|
@ -105,6 +114,39 @@ const LanguageEditor = () => {
|
|||
[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 updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData);
|
||||
void mutate(updatedCustomPhrase);
|
||||
|
@ -126,12 +168,19 @@ const LanguageEditor = () => {
|
|||
return (
|
||||
<div className={style.languageEditor}>
|
||||
<div className={style.title}>
|
||||
{languages[selectedLanguage]}
|
||||
<span>{selectedLanguage}</span>
|
||||
{isBuiltIn && (
|
||||
<span className={style.builtInFlag}>
|
||||
{t('sign_in_exp.others.manage_language.logto_provided')}
|
||||
</span>
|
||||
<div className={style.languageInfo}>
|
||||
{uiLanguageNameMapping[selectedLanguage]}
|
||||
<span>{selectedLanguage}</span>
|
||||
{isBuiltIn && (
|
||||
<span className={style.builtInFlag}>
|
||||
{t('sign_in_exp.others.manage_language.logto_provided')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!isBuiltIn && (
|
||||
<IconButton onClick={onDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
|
@ -187,6 +236,28 @@ const LanguageEditor = () => {
|
|||
/>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,7 +23,12 @@ const LanguageItem = ({ languageTag, isSelected, onClick }: Props) => {
|
|||
<div
|
||||
ref={itemRef}
|
||||
className={classNames(style.languageItem, isSelected && style.selected)}
|
||||
onClick={onClick}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<div className={style.languageName}>{languages[languageTag]}</div>
|
||||
<div className={style.languageTag}>{languageTag}</div>
|
||||
|
|
|
@ -69,11 +69,12 @@ const useLanguageEditorContext = (defaultLanguages: LanguageTag[]) => {
|
|||
if (isAddingLanguage) {
|
||||
if (isCanceled) {
|
||||
setLanguages(defaultLanguages);
|
||||
setSelectedLanguage(languages[0] ?? 'en');
|
||||
}
|
||||
setIsAddingLanguage(false);
|
||||
}
|
||||
},
|
||||
[defaultLanguages, isAddingLanguage]
|
||||
[defaultLanguages, isAddingLanguage, languages]
|
||||
);
|
||||
|
||||
const context = useMemo<Context>(
|
||||
|
|
|
@ -92,6 +92,13 @@ const sign_in_exp = {
|
|||
custom_values: 'Custom values',
|
||||
clear_all: 'Clear all',
|
||||
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: {
|
||||
title: 'AUTHENTICATION',
|
||||
|
|
|
@ -94,6 +94,13 @@ const sign_in_exp = {
|
|||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // 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: {
|
||||
title: 'AUTHENTICATION',
|
||||
|
|
|
@ -89,6 +89,13 @@ const sign_in_exp = {
|
|||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // 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: {
|
||||
title: 'AUTHENTICATION',
|
||||
|
|
|
@ -92,6 +92,13 @@ const sign_in_exp = {
|
|||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // 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: {
|
||||
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_description:
|
||||
'{{language}} is set as your default language and can’t be deleted. ', // UNTRANSLATED
|
||||
got_it: 'Got It', // UNTRANSLATED
|
||||
},
|
||||
authentication: {
|
||||
title: 'AUTHENTICATION',
|
||||
|
|
|
@ -89,6 +89,13 @@ const sign_in_exp = {
|
|||
custom_values: 'Custom values', // UNTRANSLATED
|
||||
clear_all: 'Clear all', // 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: {
|
||||
title: '身份验证',
|
||||
|
|
Loading…
Add table
Reference in a new issue