mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
refactor(console): make custom languages become available language options (#2038)
This commit is contained in:
parent
e3de677bd3
commit
4995ab9461
11 changed files with 257 additions and 246 deletions
37
packages/console/src/hooks/use-ui-languages.ts
Normal file
37
packages/console/src/hooks/use-ui-languages.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { builtInLanguages as builtInUiLanguages } from '@logto/phrases-ui';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { CustomPhraseResponse } from '@/types/custom-phrase';
|
||||
|
||||
import { RequestError } from './use-api';
|
||||
|
||||
const useUiLanguages = () => {
|
||||
const {
|
||||
data: customPhraseList,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<CustomPhraseResponse[], RequestError>('/api/custom-phrases');
|
||||
|
||||
const languages = useMemo(
|
||||
() =>
|
||||
[
|
||||
...new Set([
|
||||
...builtInUiLanguages,
|
||||
...(customPhraseList?.map(({ languageTag }) => languageTag) ?? []),
|
||||
]),
|
||||
]
|
||||
.slice()
|
||||
.sort(),
|
||||
[customPhraseList]
|
||||
);
|
||||
|
||||
return {
|
||||
languages,
|
||||
error,
|
||||
isLoading: !customPhraseList && !error,
|
||||
mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUiLanguages;
|
|
@ -1,6 +1,6 @@
|
|||
import { builtInLanguageOptions } from '@logto/phrases-ui';
|
||||
import { languages as uiLanguageNameMapping } from '@logto/language-kit';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -8,8 +8,9 @@ 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 useUiLanguages from '@/hooks/use-ui-languages';
|
||||
|
||||
import useCustomPhrasesContext from '../hooks/use-custom-phrases-context';
|
||||
import useLanguageEditorContext from '../hooks/use-language-editor-context';
|
||||
import { SignInExperienceForm } from '../types';
|
||||
import ManageLanguageModal from './ManageLanguageModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -23,9 +24,17 @@ const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
|||
const { watch, control, register } = useFormContext<SignInExperienceForm>();
|
||||
const isAutoDetect = watch('languageInfo.autoDetect');
|
||||
const [isManageLanguageFormOpen, setIsManageLanguageFormOpen] = useState(false);
|
||||
const { languages } = useUiLanguages();
|
||||
|
||||
const { context: customPhrasesContext, Provider: CustomPhrasesContextProvider } =
|
||||
useCustomPhrasesContext();
|
||||
const languageOptions = useMemo(() => {
|
||||
return languages.map((languageTag) => ({
|
||||
value: languageTag,
|
||||
title: uiLanguageNameMapping[languageTag],
|
||||
}));
|
||||
}, [languages]);
|
||||
|
||||
const { context: languageEditorContext, Provider: LanguageEditorContextProvider } =
|
||||
useLanguageEditorContext(languages);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -51,7 +60,7 @@ const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
|||
name="languageInfo.fallbackLanguage"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select value={value} options={builtInLanguageOptions} onChange={onChange} />
|
||||
<Select value={value} options={languageOptions} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<div className={styles.defaultLanguageDescription}>
|
||||
|
@ -60,14 +69,15 @@ const LanguagesForm = ({ isManageLanguageVisible = false }: Props) => {
|
|||
: t('sign_in_exp.others.languages.default_language_description_fixed')}
|
||||
</div>
|
||||
</FormField>
|
||||
<CustomPhrasesContextProvider value={customPhrasesContext}>
|
||||
<LanguageEditorContextProvider value={languageEditorContext}>
|
||||
<ManageLanguageModal
|
||||
isOpen={isManageLanguageFormOpen}
|
||||
languageTags={languages}
|
||||
onClose={() => {
|
||||
setIsManageLanguageFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</CustomPhrasesContextProvider>
|
||||
</LanguageEditorContextProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,14 +7,14 @@ import { useCallback, useContext, useEffect, useMemo } from 'react';
|
|||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Delete from '@/icons/Delete';
|
||||
import { CustomPhraseResponse } from '@/types/custom-phrase';
|
||||
|
||||
import { CustomPhrasesContext } from '../../hooks/use-custom-phrases-context';
|
||||
import { CustomPhraseResponse } from '../../types';
|
||||
import { LanguageEditorContext } from '../../hooks/use-language-editor-context';
|
||||
import { createEmptyUiTranslation, flattenTranslation } from '../../utilities';
|
||||
import EditSection from './EditSection';
|
||||
import * as style from './LanguageEditor.module.scss';
|
||||
|
@ -24,22 +24,17 @@ const emptyUiTranslation = createEmptyUiTranslation();
|
|||
const LanguageEditor = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const {
|
||||
selectedLanguageTag,
|
||||
appendToCustomPhraseList,
|
||||
setIsCurrentCustomPhraseDirty,
|
||||
stopAddingLanguage,
|
||||
} = useContext(CustomPhrasesContext);
|
||||
const { selectedLanguage, setIsDirty, stopAddingLanguage } = useContext(LanguageEditorContext);
|
||||
|
||||
const isBuiltIn = isBuiltInLanguageTag(selectedLanguageTag);
|
||||
const isBuiltIn = isBuiltInLanguageTag(selectedLanguage);
|
||||
|
||||
const translationEntries = useMemo(
|
||||
() => Object.entries((isBuiltIn ? resource[selectedLanguageTag] : en).translation),
|
||||
[isBuiltIn, selectedLanguageTag]
|
||||
() => Object.entries((isBuiltIn ? resource[selectedLanguage] : en).translation),
|
||||
[isBuiltIn, selectedLanguage]
|
||||
);
|
||||
|
||||
const { data: customPhrase, mutate } = useSWR<CustomPhraseResponse, RequestError>(
|
||||
`/api/custom-phrases/${selectedLanguageTag}`,
|
||||
`/api/custom-phrases/${selectedLanguage}`,
|
||||
{
|
||||
shouldRetryOnError: (error: unknown) => {
|
||||
if (error instanceof RequestError) {
|
||||
|
@ -76,7 +71,7 @@ const LanguageEditor = () => {
|
|||
* for the `isDirty` state does not work correctly when comparing form data with empty / undefined values.
|
||||
* Reference: https://github.com/react-hook-form/react-hook-form/issues/4740
|
||||
*/
|
||||
setIsCurrentCustomPhraseDirty(isDirty && Object.keys(dirtyFields).length > 0);
|
||||
setIsDirty(isDirty && Object.keys(dirtyFields).length > 0);
|
||||
}, [
|
||||
/**
|
||||
* Note: `isDirty` is used to trigger this `useEffect`; for `dirtyFields` object only marks filed dirty at field level.
|
||||
|
@ -84,9 +79,11 @@ const LanguageEditor = () => {
|
|||
*/
|
||||
isDirty,
|
||||
dirtyFields,
|
||||
setIsCurrentCustomPhraseDirty,
|
||||
setIsDirty,
|
||||
]);
|
||||
|
||||
const { mutate: globalMutate } = useSWRConfig();
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const upsertCustomPhrase = useCallback(
|
||||
|
@ -99,17 +96,17 @@ const LanguageEditor = () => {
|
|||
})
|
||||
.json<CustomPhraseResponse>();
|
||||
|
||||
appendToCustomPhraseList(updatedCustomPhrase);
|
||||
void globalMutate('/api/custom-phrases');
|
||||
|
||||
stopAddingLanguage();
|
||||
|
||||
return updatedCustomPhrase;
|
||||
},
|
||||
[api, appendToCustomPhraseList, stopAddingLanguage]
|
||||
[api, globalMutate, stopAddingLanguage]
|
||||
);
|
||||
|
||||
const onSubmit = handleSubmit(async (formData: Translation) => {
|
||||
const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguageTag, formData);
|
||||
const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData);
|
||||
void mutate(updatedCustomPhrase);
|
||||
toast.success(t('general.saved'));
|
||||
});
|
||||
|
@ -118,10 +115,10 @@ const LanguageEditor = () => {
|
|||
reset(defaultFormValues);
|
||||
}, [
|
||||
/**
|
||||
* Note: trigger form reset when selectedLanguageTag changed,
|
||||
* Note: trigger form reset when selectedLanguage changed,
|
||||
* for the `defaultValues` will not change when switching between languages with unavailable custom phrases.
|
||||
*/
|
||||
selectedLanguageTag,
|
||||
selectedLanguage,
|
||||
defaultFormValues,
|
||||
reset,
|
||||
]);
|
||||
|
@ -129,8 +126,8 @@ const LanguageEditor = () => {
|
|||
return (
|
||||
<div className={style.languageEditor}>
|
||||
<div className={style.title}>
|
||||
{languages[selectedLanguageTag]}
|
||||
<span>{selectedLanguageTag}</span>
|
||||
{languages[selectedLanguage]}
|
||||
<span>{selectedLanguage}</span>
|
||||
{isBuiltIn && (
|
||||
<span className={style.builtInFlag}>
|
||||
{t('sign_in_exp.others.manage_language.logto_provided')}
|
||||
|
|
|
@ -1,32 +1,36 @@
|
|||
import { isLanguageTag, languages, LanguageTag } from '@logto/language-kit';
|
||||
import {
|
||||
isLanguageTag,
|
||||
LanguageTag,
|
||||
languages as uiLanguageNameMapping,
|
||||
} from '@logto/language-kit';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { CustomPhrasesContext } from '../../hooks/use-custom-phrases-context';
|
||||
import { LanguageEditorContext } from '../../hooks/use-language-editor-context';
|
||||
import AddLanguageSelector from './AddLanguageSelector';
|
||||
import LanguageItem from './LanguageItem';
|
||||
import * as style from './LanguageNav.module.scss';
|
||||
|
||||
const LanguageNav = () => {
|
||||
const {
|
||||
displayingLanguages,
|
||||
selectedLanguageTag,
|
||||
languages,
|
||||
selectedLanguage,
|
||||
isAddingLanguage,
|
||||
isCurrentCustomPhraseDirty,
|
||||
isDirty,
|
||||
setConfirmationState,
|
||||
setSelectedLanguageTag,
|
||||
setPreSelectedLanguageTag,
|
||||
setPreAddedLanguageTag,
|
||||
setSelectedLanguage,
|
||||
setPreSelectedLanguage,
|
||||
setPreAddedLanguage,
|
||||
startAddingLanguage,
|
||||
} = useContext(CustomPhrasesContext);
|
||||
} = useContext(LanguageEditorContext);
|
||||
|
||||
const languageOptions = Object.keys(languages).filter(
|
||||
const languageOptions = Object.keys(uiLanguageNameMapping).filter(
|
||||
(languageTag): languageTag is LanguageTag =>
|
||||
isLanguageTag(languageTag) && !displayingLanguages.includes(languageTag)
|
||||
isLanguageTag(languageTag) && !languages.includes(languageTag)
|
||||
);
|
||||
|
||||
const onAddLanguage = (languageTag: LanguageTag) => {
|
||||
if (isCurrentCustomPhraseDirty || isAddingLanguage) {
|
||||
setPreAddedLanguageTag(languageTag);
|
||||
if (isDirty || isAddingLanguage) {
|
||||
setPreAddedLanguage(languageTag);
|
||||
setConfirmationState('try-add-language');
|
||||
|
||||
return;
|
||||
|
@ -36,25 +40,25 @@ const LanguageNav = () => {
|
|||
};
|
||||
|
||||
const onSwitchLanguage = (languageTag: LanguageTag) => {
|
||||
if (isCurrentCustomPhraseDirty || isAddingLanguage) {
|
||||
setPreSelectedLanguageTag(languageTag);
|
||||
if (isDirty || isAddingLanguage) {
|
||||
setPreSelectedLanguage(languageTag);
|
||||
setConfirmationState('try-switch-language');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedLanguageTag(languageTag);
|
||||
setSelectedLanguage(languageTag);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.languageNav}>
|
||||
<AddLanguageSelector options={languageOptions} onSelect={onAddLanguage} />
|
||||
<div className={style.languageItemList}>
|
||||
{displayingLanguages.map((languageTag) => (
|
||||
{languages.map((languageTag) => (
|
||||
<LanguageItem
|
||||
key={languageTag}
|
||||
languageTag={languageTag}
|
||||
isSelected={selectedLanguageTag === languageTag}
|
||||
isSelected={selectedLanguage === languageTag}
|
||||
onClick={() => {
|
||||
onSwitchLanguage(languageTag);
|
||||
}}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { LanguageTag } from '@logto/language-kit';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
@ -6,42 +7,42 @@ import ConfirmModal from '@/components/ConfirmModal';
|
|||
import ModalLayout from '@/components/ModalLayout';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import { CustomPhrasesContext } from '../../hooks/use-custom-phrases-context';
|
||||
import { LanguageEditorContext } from '../../hooks/use-language-editor-context';
|
||||
import LanguageEditor from './LanguageEditor';
|
||||
import LanguageNav from './LanguageNav';
|
||||
import * as style from './index.module.scss';
|
||||
|
||||
type ManageLanguageModalProps = {
|
||||
isOpen: boolean;
|
||||
languageTags: LanguageTag[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ManageLanguageModal = ({ isOpen, onClose }: ManageLanguageModalProps) => {
|
||||
const ManageLanguageModal = ({ isOpen, languageTags, onClose }: ManageLanguageModalProps) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const {
|
||||
preSelectedLanguageTag,
|
||||
preAddedLanguageTag,
|
||||
preSelectedLanguage,
|
||||
preAddedLanguage,
|
||||
isAddingLanguage,
|
||||
isCurrentCustomPhraseDirty,
|
||||
isDirty,
|
||||
confirmationState,
|
||||
setSelectedLanguageTag,
|
||||
setPreSelectedLanguageTag,
|
||||
setSelectedLanguage,
|
||||
setPreSelectedLanguage,
|
||||
setConfirmationState,
|
||||
startAddingLanguage,
|
||||
stopAddingLanguage,
|
||||
resetSelectedLanguageTag,
|
||||
} = useContext(CustomPhrasesContext);
|
||||
} = useContext(LanguageEditorContext);
|
||||
|
||||
const onCloseModal = () => {
|
||||
if (isAddingLanguage || isCurrentCustomPhraseDirty) {
|
||||
if (isAddingLanguage || isDirty) {
|
||||
setConfirmationState('try-close');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
resetSelectedLanguageTag();
|
||||
setSelectedLanguage(languageTags[0] ?? 'en');
|
||||
};
|
||||
|
||||
const onConfirmUnsavedChanges = () => {
|
||||
|
@ -51,13 +52,13 @@ const ManageLanguageModal = ({ isOpen, onClose }: ManageLanguageModalProps) => {
|
|||
onClose();
|
||||
}
|
||||
|
||||
if (confirmationState === 'try-switch-language' && preSelectedLanguageTag) {
|
||||
setSelectedLanguageTag(preSelectedLanguageTag);
|
||||
setPreSelectedLanguageTag(undefined);
|
||||
if (confirmationState === 'try-switch-language' && preSelectedLanguage) {
|
||||
setSelectedLanguage(preSelectedLanguage);
|
||||
setPreSelectedLanguage(undefined);
|
||||
}
|
||||
|
||||
if (confirmationState === 'try-add-language' && preAddedLanguageTag) {
|
||||
startAddingLanguage(preAddedLanguageTag);
|
||||
if (confirmationState === 'try-add-language' && preAddedLanguage) {
|
||||
startAddingLanguage(preAddedLanguage);
|
||||
}
|
||||
|
||||
setConfirmationState('none');
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { LanguageTag } from '@logto/language-kit';
|
||||
import { builtInLanguageOptions } from '@logto/phrases-ui';
|
||||
import { LanguageTag, languages as uiLanguageNameMapping } from '@logto/language-kit';
|
||||
import {
|
||||
AppearanceMode,
|
||||
ConnectorResponse,
|
||||
|
@ -17,6 +16,7 @@ import Card from '@/components/Card';
|
|||
import Select from '@/components/Select';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
import PhoneInfo from '@/icons/PhoneInfo';
|
||||
|
||||
import * as styles from './Preview.module.scss';
|
||||
|
@ -34,6 +34,8 @@ const Preview = ({ signInExperience, className }: Props) => {
|
|||
const { data: allConnectors } = useSWR<ConnectorResponse[], RequestError>('/api/connectors');
|
||||
const previewRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const { languages } = useUiLanguages();
|
||||
|
||||
const modeOptions = useMemo(() => {
|
||||
const light = { value: AppearanceMode.LightMode, title: t('sign_in_exp.preview.light') };
|
||||
const dark = { value: AppearanceMode.DarkMode, title: t('sign_in_exp.preview.dark') };
|
||||
|
@ -56,14 +58,18 @@ const Preview = ({ signInExperience, className }: Props) => {
|
|||
}, [modeOptions, mode]);
|
||||
|
||||
const availableLanguageOptions = useMemo(() => {
|
||||
if (signInExperience && !signInExperience.languageInfo.autoDetect) {
|
||||
return builtInLanguageOptions.filter(
|
||||
({ value }) => value === signInExperience.languageInfo.fallbackLanguage
|
||||
);
|
||||
}
|
||||
const availableLanguageTags =
|
||||
signInExperience && !signInExperience.languageInfo.autoDetect
|
||||
? languages.filter(
|
||||
(languageTag) => languageTag === signInExperience.languageInfo.fallbackLanguage
|
||||
)
|
||||
: languages;
|
||||
|
||||
return builtInLanguageOptions;
|
||||
}, [signInExperience]);
|
||||
return availableLanguageTags.map((languageTag) => ({
|
||||
value: languageTag,
|
||||
title: uiLanguageNameMapping[languageTag],
|
||||
}));
|
||||
}, [languages, signInExperience]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableLanguageOptions[0]) {
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
import { LanguageTag } from '@logto/language-kit';
|
||||
import { builtInLanguages as builtInUiLanguages } from '@logto/phrases-ui';
|
||||
import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
|
||||
import { CustomPhraseResponse } from '../types';
|
||||
|
||||
const noop = () => {
|
||||
throw new Error('Context provider not found');
|
||||
};
|
||||
|
||||
export type ConfirmationState = 'none' | 'try-close' | 'try-switch-language' | 'try-add-language';
|
||||
|
||||
export type Context = {
|
||||
displayingLanguages: LanguageTag[];
|
||||
selectedLanguageTag: LanguageTag;
|
||||
preSelectedLanguageTag: LanguageTag | undefined;
|
||||
preAddedLanguageTag: LanguageTag | undefined;
|
||||
isAddingLanguage: boolean;
|
||||
isCurrentCustomPhraseDirty: boolean;
|
||||
confirmationState: ConfirmationState;
|
||||
setSelectedLanguageTag: React.Dispatch<React.SetStateAction<LanguageTag>>;
|
||||
resetSelectedLanguageTag: () => void;
|
||||
setPreSelectedLanguageTag: React.Dispatch<React.SetStateAction<LanguageTag | undefined>>;
|
||||
setPreAddedLanguageTag: React.Dispatch<React.SetStateAction<LanguageTag | undefined>>;
|
||||
setIsCurrentCustomPhraseDirty: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
appendToCustomPhraseList: (customPhrase: CustomPhraseResponse) => void;
|
||||
setConfirmationState: React.Dispatch<React.SetStateAction<ConfirmationState>>;
|
||||
startAddingLanguage: (languageTag: LanguageTag) => void;
|
||||
stopAddingLanguage: (isCanceled?: boolean) => void;
|
||||
};
|
||||
|
||||
export const CustomPhrasesContext = createContext<Context>({
|
||||
displayingLanguages: [],
|
||||
selectedLanguageTag: 'en',
|
||||
preSelectedLanguageTag: undefined,
|
||||
preAddedLanguageTag: undefined,
|
||||
isAddingLanguage: false,
|
||||
isCurrentCustomPhraseDirty: false,
|
||||
confirmationState: 'none',
|
||||
setSelectedLanguageTag: noop,
|
||||
resetSelectedLanguageTag: noop,
|
||||
setPreSelectedLanguageTag: noop,
|
||||
setPreAddedLanguageTag: noop,
|
||||
setIsCurrentCustomPhraseDirty: noop,
|
||||
appendToCustomPhraseList: noop,
|
||||
setConfirmationState: noop,
|
||||
startAddingLanguage: noop,
|
||||
stopAddingLanguage: noop,
|
||||
});
|
||||
|
||||
const useCustomPhrasesContext = () => {
|
||||
const { data: customPhraseList, mutate: mutateCustomPhraseList } = useSWR<
|
||||
CustomPhraseResponse[],
|
||||
RequestError
|
||||
>('/api/custom-phrases');
|
||||
|
||||
const existedLanguageTags = useMemo(
|
||||
() =>
|
||||
[
|
||||
...new Set([
|
||||
...builtInUiLanguages,
|
||||
...(customPhraseList?.map(({ languageTag }) => languageTag) ?? []),
|
||||
]),
|
||||
]
|
||||
.slice()
|
||||
.sort(),
|
||||
[customPhraseList]
|
||||
);
|
||||
|
||||
const [displayingLanguages, setDisplayingLanguages] =
|
||||
useState<LanguageTag[]>(existedLanguageTags);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayingLanguages(existedLanguageTags);
|
||||
}, [existedLanguageTags]);
|
||||
|
||||
const defaultLanguageTag = useMemo(() => existedLanguageTags[0] ?? 'en', [existedLanguageTags]);
|
||||
const [selectedLanguageTag, setSelectedLanguageTag] = useState<LanguageTag>(defaultLanguageTag);
|
||||
const [preSelectedLanguageTag, setPreSelectedLanguageTag] = useState<LanguageTag>();
|
||||
const [preAddedLanguageTag, setPreAddedLanguageTag] = useState<LanguageTag>();
|
||||
const [isAddingLanguage, setIsAddingLanguage] = useState(false);
|
||||
const [isCurrentCustomPhraseDirty, setIsCurrentCustomPhraseDirty] = useState(false);
|
||||
const [confirmationState, setConfirmationState] = useState<ConfirmationState>('none');
|
||||
|
||||
const appendToCustomPhraseList = useCallback(
|
||||
(customPhrase: CustomPhraseResponse) => {
|
||||
void mutateCustomPhraseList([
|
||||
customPhrase,
|
||||
...(customPhraseList?.filter(
|
||||
({ languageTag }) => languageTag !== customPhrase.languageTag
|
||||
) ?? []),
|
||||
]);
|
||||
},
|
||||
[customPhraseList, mutateCustomPhraseList]
|
||||
);
|
||||
|
||||
const startAddingLanguage = useCallback(
|
||||
(languageTag: LanguageTag) => {
|
||||
setDisplayingLanguages([...new Set([languageTag, ...existedLanguageTags])].slice().sort());
|
||||
setSelectedLanguageTag(languageTag);
|
||||
setIsAddingLanguage(true);
|
||||
},
|
||||
[existedLanguageTags]
|
||||
);
|
||||
|
||||
const stopAddingLanguage = useCallback(
|
||||
(isCanceled = false) => {
|
||||
if (isAddingLanguage) {
|
||||
if (isCanceled) {
|
||||
setDisplayingLanguages(existedLanguageTags);
|
||||
}
|
||||
|
||||
setIsAddingLanguage(false);
|
||||
}
|
||||
},
|
||||
[existedLanguageTags, isAddingLanguage]
|
||||
);
|
||||
|
||||
const resetSelectedLanguageTag = useCallback(() => {
|
||||
setSelectedLanguageTag(defaultLanguageTag);
|
||||
}, [defaultLanguageTag]);
|
||||
|
||||
const context = useMemo<Context>(() => {
|
||||
return {
|
||||
displayingLanguages,
|
||||
selectedLanguageTag,
|
||||
preSelectedLanguageTag,
|
||||
preAddedLanguageTag,
|
||||
isAddingLanguage,
|
||||
isCurrentCustomPhraseDirty,
|
||||
confirmationState,
|
||||
setSelectedLanguageTag,
|
||||
resetSelectedLanguageTag,
|
||||
setPreSelectedLanguageTag,
|
||||
setPreAddedLanguageTag,
|
||||
setIsCurrentCustomPhraseDirty,
|
||||
appendToCustomPhraseList,
|
||||
setConfirmationState,
|
||||
startAddingLanguage,
|
||||
stopAddingLanguage,
|
||||
};
|
||||
}, [
|
||||
displayingLanguages,
|
||||
selectedLanguageTag,
|
||||
preSelectedLanguageTag,
|
||||
preAddedLanguageTag,
|
||||
isAddingLanguage,
|
||||
isCurrentCustomPhraseDirty,
|
||||
confirmationState,
|
||||
resetSelectedLanguageTag,
|
||||
appendToCustomPhraseList,
|
||||
startAddingLanguage,
|
||||
stopAddingLanguage,
|
||||
]);
|
||||
|
||||
return {
|
||||
context,
|
||||
Provider: CustomPhrasesContext.Provider,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCustomPhrasesContext;
|
|
@ -0,0 +1,115 @@
|
|||
import { LanguageTag } from '@logto/language-kit';
|
||||
import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const noop = () => {
|
||||
throw new Error('Context provider not found');
|
||||
};
|
||||
|
||||
export type ConfirmationState = 'none' | 'try-close' | 'try-switch-language' | 'try-add-language';
|
||||
|
||||
export type Context = {
|
||||
languages: LanguageTag[];
|
||||
selectedLanguage: LanguageTag;
|
||||
preSelectedLanguage?: LanguageTag;
|
||||
preAddedLanguage?: LanguageTag;
|
||||
isAddingLanguage: boolean;
|
||||
isDirty: boolean;
|
||||
confirmationState: ConfirmationState;
|
||||
setSelectedLanguage: React.Dispatch<React.SetStateAction<LanguageTag>>;
|
||||
setPreSelectedLanguage: React.Dispatch<React.SetStateAction<LanguageTag | undefined>>;
|
||||
setPreAddedLanguage: React.Dispatch<React.SetStateAction<LanguageTag | undefined>>;
|
||||
setIsDirty: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setConfirmationState: React.Dispatch<React.SetStateAction<ConfirmationState>>;
|
||||
startAddingLanguage: (languageTag: LanguageTag) => void;
|
||||
stopAddingLanguage: (isCanceled?: boolean) => void;
|
||||
};
|
||||
|
||||
export const LanguageEditorContext = createContext<Context>({
|
||||
languages: [],
|
||||
selectedLanguage: 'en',
|
||||
preSelectedLanguage: undefined,
|
||||
preAddedLanguage: undefined,
|
||||
isAddingLanguage: false,
|
||||
isDirty: false,
|
||||
confirmationState: 'none',
|
||||
setSelectedLanguage: noop,
|
||||
setPreSelectedLanguage: noop,
|
||||
setPreAddedLanguage: noop,
|
||||
setIsDirty: noop,
|
||||
setConfirmationState: noop,
|
||||
startAddingLanguage: noop,
|
||||
stopAddingLanguage: noop,
|
||||
});
|
||||
|
||||
const useLanguageEditorContext = (defaultLanguages: LanguageTag[]) => {
|
||||
const [languages, setLanguages] = useState(defaultLanguages);
|
||||
|
||||
useEffect(() => {
|
||||
setLanguages(defaultLanguages);
|
||||
}, [defaultLanguages]);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<LanguageTag>(languages[0] ?? 'en');
|
||||
const [preSelectedLanguage, setPreSelectedLanguage] = useState<LanguageTag>();
|
||||
const [preAddedLanguage, setPreAddedLanguage] = useState<LanguageTag>();
|
||||
const [isAddingLanguage, setIsAddingLanguage] = useState(false);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [confirmationState, setConfirmationState] = useState<ConfirmationState>('none');
|
||||
|
||||
const startAddingLanguage = useCallback(
|
||||
(language: LanguageTag) => {
|
||||
setLanguages([...new Set([language, ...defaultLanguages])].slice().sort());
|
||||
setSelectedLanguage(language);
|
||||
setIsAddingLanguage(true);
|
||||
},
|
||||
[defaultLanguages]
|
||||
);
|
||||
|
||||
const stopAddingLanguage = useCallback(
|
||||
(isCanceled = false) => {
|
||||
if (isAddingLanguage) {
|
||||
if (isCanceled) {
|
||||
setLanguages(defaultLanguages);
|
||||
}
|
||||
setIsAddingLanguage(false);
|
||||
}
|
||||
},
|
||||
[defaultLanguages, isAddingLanguage]
|
||||
);
|
||||
|
||||
const context = useMemo<Context>(
|
||||
() => ({
|
||||
languages,
|
||||
selectedLanguage,
|
||||
preSelectedLanguage,
|
||||
preAddedLanguage,
|
||||
isAddingLanguage,
|
||||
isDirty,
|
||||
confirmationState,
|
||||
setSelectedLanguage,
|
||||
setPreSelectedLanguage,
|
||||
setPreAddedLanguage,
|
||||
setIsDirty,
|
||||
setConfirmationState,
|
||||
startAddingLanguage,
|
||||
stopAddingLanguage,
|
||||
}),
|
||||
[
|
||||
confirmationState,
|
||||
isAddingLanguage,
|
||||
isDirty,
|
||||
languages,
|
||||
preAddedLanguage,
|
||||
preSelectedLanguage,
|
||||
selectedLanguage,
|
||||
startAddingLanguage,
|
||||
stopAddingLanguage,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
context,
|
||||
Provider: LanguageEditorContext.Provider,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLanguageEditorContext;
|
|
@ -14,6 +14,7 @@ import ConfirmModal from '@/components/ConfirmModal';
|
|||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
|
||||
import Preview from './components/Preview';
|
||||
|
@ -33,6 +34,7 @@ const SignInExperience = () => {
|
|||
const { tab } = useParams();
|
||||
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
|
||||
const { settings, error: settingsError, updateSettings, mutate: mutateSettings } = useSettings();
|
||||
const { error: languageError, isLoading: isLoadingLanguages } = useUiLanguages();
|
||||
const [dataToCompare, setDataToCompare] = useState<SignInExperienceType>();
|
||||
|
||||
const methods = useForm<SignInExperienceForm>();
|
||||
|
@ -90,7 +92,7 @@ const SignInExperience = () => {
|
|||
await saveData();
|
||||
});
|
||||
|
||||
if ((!settings && !settingsError) || (!data && !error)) {
|
||||
if ((!settings && !settingsError) || (!data && !error) || isLoadingLanguages) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
|
@ -98,6 +100,10 @@ const SignInExperience = () => {
|
|||
return <div>{settingsError.body?.message ?? settingsError.message}</div>;
|
||||
}
|
||||
|
||||
if (languageError) {
|
||||
return <div>{languageError.body?.message ?? languageError.message}</div>;
|
||||
}
|
||||
|
||||
if (!settings?.signInExperienceCustomized) {
|
||||
return (
|
||||
<Welcome
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import type { LanguageTag } from '@logto/language-kit';
|
||||
import { SignInExperience, SignInMethodKey } from '@logto/schemas';
|
||||
import type { Translation } from '@logto/schemas';
|
||||
|
||||
export type SignInExperienceForm = Omit<SignInExperience, 'signInMethods'> & {
|
||||
signInMethods: {
|
||||
|
@ -13,8 +11,3 @@ export type SignInExperienceForm = Omit<SignInExperience, 'signInMethods'> & {
|
|||
};
|
||||
createAccountEnabled: boolean;
|
||||
};
|
||||
|
||||
export type CustomPhraseResponse = {
|
||||
languageTag: LanguageTag;
|
||||
translation: Translation;
|
||||
};
|
||||
|
|
7
packages/console/src/types/custom-phrase.ts
Normal file
7
packages/console/src/types/custom-phrase.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { LanguageTag } from '@logto/language-kit';
|
||||
import { Translation } from '@logto/schemas';
|
||||
|
||||
export type CustomPhraseResponse = {
|
||||
languageTag: LanguageTag;
|
||||
translation: Translation;
|
||||
};
|
Loading…
Add table
Reference in a new issue