mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
fix(console,phrases): improve error handling when user associated app is removed (#5389)
* fix(console,phrases): improve error handling when user associated app is removed * chore: add changeset * refactor(console): update per review comments
This commit is contained in:
parent
17fe38443c
commit
04ec78a917
21 changed files with 94 additions and 29 deletions
.changeset
packages
console/src
components/ApplicationName
hooks
pages
ApplicationDetails/ApplicationDetailsContent/Branding
SignInExperience/PageContent/Content/LanguagesForm/ManageLanguage/LanguageEditor
utils
phrases/src/locales
de/translation/admin-console
en/translation/admin-console
es/translation/admin-console
fr/translation/admin-console
it/translation/admin-console
ja/translation/admin-console
ko/translation/admin-console
pl-pl/translation/admin-console
pt-br/translation/admin-console
pt-pt/translation/admin-console
ru/translation/admin-console
tr-tr/translation/admin-console
zh-cn/translation/admin-console
zh-hk/translation/admin-console
zh-tw/translation/admin-console
6
.changeset/silent-windows-behave.md
Normal file
6
.changeset/silent-windows-behave.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@logto/console": patch
|
||||
"@logto/phrases": patch
|
||||
---
|
||||
|
||||
improve error handling when user associated application is removed
|
|
@ -1,10 +1,14 @@
|
|||
import type { Application } from '@logto/schemas';
|
||||
import { adminConsoleApplicationId } from '@logto/schemas';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import { shouldRetryOnError } from '@/utils/request';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -16,13 +20,32 @@ type Props = {
|
|||
function ApplicationName({ applicationId, isLink = false }: Props) {
|
||||
const isAdminConsole = applicationId === adminConsoleApplicationId;
|
||||
|
||||
const { data } = useSWR<Application>(!isAdminConsole && `api/applications/${applicationId}`);
|
||||
const fetchApi = useApi({ hideErrorToast: true });
|
||||
const fetcher = useSwrFetcher<Application>(fetchApi);
|
||||
const { data, error } = useSWR<Application, RequestError>(
|
||||
!isAdminConsole && `api/applications/${applicationId}`,
|
||||
{
|
||||
fetcher,
|
||||
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
|
||||
}
|
||||
);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getTo } = useTenantPathname();
|
||||
|
||||
const name = (isAdminConsole ? <>Admin Console ({t('system_app')})</> : data?.name) ?? '-';
|
||||
const name = useMemo(() => {
|
||||
if (isAdminConsole) {
|
||||
return `Admin Console (${t('system_app')})`;
|
||||
}
|
||||
if (data?.name) {
|
||||
return data.name;
|
||||
}
|
||||
if (error?.status === 404) {
|
||||
return `${applicationId} (${t('general.deleted')})`;
|
||||
}
|
||||
return '-';
|
||||
}, [applicationId, data?.name, error?.status, isAdminConsole, t]);
|
||||
|
||||
if (isLink && !isAdminConsole) {
|
||||
if (isLink && !isAdminConsole && data?.name) {
|
||||
return (
|
||||
<Link className={styles.link} to={getTo(`/applications/${applicationId}`)}>
|
||||
{name}
|
||||
|
|
|
@ -2,7 +2,9 @@ import type React from 'react';
|
|||
import { useMemo } from 'react';
|
||||
import type { SWRConfig } from 'swr';
|
||||
|
||||
import useApi, { RequestError } from './use-api';
|
||||
import { shouldRetryOnError } from '@/utils/request';
|
||||
|
||||
import useApi from './use-api';
|
||||
import useSwrFetcher from './use-swr-fetcher';
|
||||
|
||||
const useSwrOptions = (): Partial<React.ComponentProps<typeof SWRConfig>['value']> => {
|
||||
|
@ -12,15 +14,7 @@ const useSwrOptions = (): Partial<React.ComponentProps<typeof SWRConfig>['value'
|
|||
const config = useMemo(
|
||||
() => ({
|
||||
fetcher,
|
||||
shouldRetryOnError: (error: unknown) => {
|
||||
if (error instanceof RequestError) {
|
||||
const { status } = error;
|
||||
|
||||
return status !== 401 && status !== 403;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
shouldRetryOnError: shouldRetryOnError({ ignore: [401, 403] }),
|
||||
}),
|
||||
[fetcher]
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { type ApplicationSignInExperience } from '@logto/schemas';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
||||
import { shouldRetryOnError } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* SWR fetcher for application sign-in experience
|
||||
|
@ -18,13 +19,7 @@ const useApplicationSignInExperienceSWR = (applicationId: string) => {
|
|||
`api/applications/${applicationId}/sign-in-experience`,
|
||||
{
|
||||
fetcher,
|
||||
shouldRetryOnError: (error: unknown) => {
|
||||
if (error instanceof RequestError) {
|
||||
return error.status !== 404;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,11 +24,12 @@ import Table from '@/ds-components/Table';
|
|||
import Tag from '@/ds-components/Tag';
|
||||
import Textarea from '@/ds-components/Textarea';
|
||||
import { Tooltip } from '@/ds-components/Tip';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
import type { CustomPhraseResponse } from '@/types/custom-phrase';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
import { shouldRetryOnError } from '@/utils/request';
|
||||
|
||||
import * as styles from './LanguageDetails.module.scss';
|
||||
import { hiddenLocalePhraseGroups, hiddenLocalePhrases } from './constants';
|
||||
|
@ -79,13 +80,7 @@ function LanguageDetails() {
|
|||
`api/custom-phrases/${selectedLanguage}`,
|
||||
{
|
||||
fetcher,
|
||||
shouldRetryOnError: (error: unknown) => {
|
||||
if (error instanceof RequestError) {
|
||||
return error.status !== 404;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
23
packages/console/src/utils/request.ts
Normal file
23
packages/console/src/utils/request.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { RequestError } from '@/hooks/use-api';
|
||||
|
||||
type Options = {
|
||||
ignore: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a SWR request should be retried based on the error status code
|
||||
* @param options.ignore - an array of status codes to exclude from retrying
|
||||
* @returns An anonymous function that takes an error and returns a boolean. Returns `true` to retry the request, `false` otherwise.
|
||||
*/
|
||||
export const shouldRetryOnError = (options?: Options) => {
|
||||
return (error: unknown): boolean => {
|
||||
if (error instanceof RequestError) {
|
||||
const { status } = error;
|
||||
const { ignore } = options ?? {};
|
||||
|
||||
return !ignore?.includes(status);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Aktivieren',
|
||||
reminder: 'Erinnerung',
|
||||
delete: 'Löschen',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'MEHR OPTIONEN',
|
||||
close: 'Schließen',
|
||||
copy: 'Kopieren',
|
||||
|
|
|
@ -26,6 +26,7 @@ const general = {
|
|||
enable: 'Enable',
|
||||
reminder: 'Reminder',
|
||||
delete: 'Delete',
|
||||
deleted: 'Deleted',
|
||||
more_options: 'MORE OPTIONS',
|
||||
close: 'Close',
|
||||
copy: 'Copy',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Habilitar',
|
||||
reminder: 'Recordatorio',
|
||||
delete: 'Eliminar',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'MÁS OPCIONES',
|
||||
close: 'Cerrar',
|
||||
copy: 'Copiar',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Activer',
|
||||
reminder: 'Rappel',
|
||||
delete: 'Supprimer',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: "PLUS D'OPTIONS",
|
||||
close: 'Fermer',
|
||||
copy: 'Copier',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Abilita',
|
||||
reminder: 'Promemoria',
|
||||
delete: 'Elimina',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'PIÙ OPZIONI',
|
||||
close: 'Chiudi',
|
||||
copy: 'Copia',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: '有効にする',
|
||||
reminder: 'リマインダー',
|
||||
delete: '削除',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'その他のオプション',
|
||||
close: '閉じる',
|
||||
copy: 'コピーする',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: '활성화',
|
||||
reminder: '리마인더',
|
||||
delete: '삭제',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: '더 많은 설정',
|
||||
close: '닫기',
|
||||
copy: '복사',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Włącz',
|
||||
reminder: 'Przypomnienie',
|
||||
delete: 'Usuń',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'WIĘCEJ OPCJI',
|
||||
close: 'Zamknij',
|
||||
copy: 'Kopiuj',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Habilitar',
|
||||
reminder: 'Lembrete',
|
||||
delete: 'Excluir',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'MAIS OPÇÕES',
|
||||
close: 'Fechar',
|
||||
copy: 'Copiar',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Ativar',
|
||||
reminder: 'Lembrete',
|
||||
delete: 'Eliminar',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'MAIS OPÇÕES',
|
||||
close: 'Fechar',
|
||||
copy: 'Copiar',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Включить',
|
||||
reminder: 'Напоминание',
|
||||
delete: 'Удалить',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'Дополнительные опции',
|
||||
close: 'Закрыть',
|
||||
copy: 'Копировать',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: 'Etkinleştir',
|
||||
reminder: 'Hatırlatıcı',
|
||||
delete: 'Sil',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: 'DAHA FAZLA SEÇENEK',
|
||||
close: 'Kapat',
|
||||
copy: 'Kopyala',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: '启用',
|
||||
reminder: '提示',
|
||||
delete: '删除',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: '更多选项',
|
||||
close: '关闭',
|
||||
copy: '复制',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: '啟用',
|
||||
reminder: '提示',
|
||||
delete: '刪除',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: '更多選項',
|
||||
close: '關閉',
|
||||
copy: '複製',
|
||||
|
|
|
@ -26,6 +26,8 @@ const general = {
|
|||
enable: '啟用',
|
||||
reminder: '提示',
|
||||
delete: '刪除',
|
||||
/** UNTRANSLATED */
|
||||
deleted: 'Deleted',
|
||||
more_options: '更多選項',
|
||||
close: '關閉',
|
||||
copy: '複製',
|
||||
|
|
Loading…
Add table
Reference in a new issue