From edfdb3d58317c81cb273e15ad2fc18e5bae135dc Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Mon, 18 Jul 2022 19:45:45 +0800 Subject: [PATCH] refactor: remove "as" --- packages/connector-sendgrid-mail/src/mock.ts | 9 ++-- packages/console/package.json | 5 +-- .../src/components/MultiTextInput/types.ts | 12 ++++-- .../src/components/MultiTextInput/utils.ts | 12 +++++- .../src/components/Transfer/DraggableItem.tsx | 4 +- .../UnsavedChangesAlertModal/index.tsx | 20 ++++++--- .../components/ConnectorContent.tsx | 43 ++++++++++--------- .../components/SenderTester/index.tsx | 28 +++++------- .../Connectors/components/Guide/index.tsx | 43 +++++++++---------- .../UserDetails/components/UserSettings.tsx | 11 +++-- packages/console/src/utilities/json.ts | 9 ---- packages/console/src/utilities/markdown.ts | 39 ----------------- .../en/translation/admin-console/errors.ts | 1 + .../zh-cn/translation/admin-console/errors.ts | 1 + pnpm-lock.yaml | 2 +- 15 files changed, 103 insertions(+), 136 deletions(-) delete mode 100644 packages/console/src/utilities/json.ts delete mode 100644 packages/console/src/utilities/markdown.ts diff --git a/packages/connector-sendgrid-mail/src/mock.ts b/packages/connector-sendgrid-mail/src/mock.ts index 3cf409ad6..dfe68e787 100644 --- a/packages/connector-sendgrid-mail/src/mock.ts +++ b/packages/connector-sendgrid-mail/src/mock.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-syntax */ import { Content, ContextType, @@ -10,11 +9,14 @@ import { const receivers: EmailData[] = [{ email: 'foo@logto.io' }]; const sender: EmailData = { email: 'noreply@logto.test.io', name: 'Logto Test' }; +const personalizations: Personalization[] = [{ to: receivers }]; +const content: Content[] = [{ type: ContextType.Text, value: 'This is a test template.' }]; + export const mockedParameters: PublicParameters = { - personalizations: [{ to: receivers }] as Personalization[], + personalizations, from: sender, subject: 'Test SendGrid Mail', - content: [{ type: 'text/plain', value: 'This is a test template.' }] as Content[], + content, }; export const mockedApiKey = 'apikey'; @@ -31,4 +33,3 @@ export const mockedConfig: SendGridMailConfig = { }, ], }; -/* eslint-enable no-restricted-syntax */ diff --git a/packages/console/package.json b/packages/console/package.json index 28e081251..b2893fbca 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -71,7 +71,7 @@ "react-markdown": "^8.0.0", "react-modal": "^3.14.4", "react-paginate": "^8.1.2", - "react-router-dom": "^6.2.2", + "react-router-dom": "6.2.2", "react-syntax-highlighter": "^15.5.0", "recharts": "^2.1.10", "remark-gfm": "^3.0.1", @@ -88,8 +88,7 @@ "extends": "@silverhand/react", "rules": { "complexity": "off", - "@typescript-eslint/prefer-nullish-coalescing": "off", - "no-restricted-syntax": "off" + "@typescript-eslint/prefer-nullish-coalescing": "off" } }, "stylelint": { diff --git a/packages/console/src/components/MultiTextInput/types.ts b/packages/console/src/components/MultiTextInput/types.ts index 9e40c3c5a..2b6c97861 100644 --- a/packages/console/src/components/MultiTextInput/types.ts +++ b/packages/console/src/components/MultiTextInput/types.ts @@ -1,7 +1,11 @@ -export type MultiTextInputError = { - required?: string; - inputs?: Record; -}; +import { z } from 'zod'; + +export const multiTextInputErrorGuard = z.object({ + required: z.string().optional(), + inputs: z.record(z.number(), z.string().optional()).optional(), +}); + +export type MultiTextInputError = z.infer; export type MultiTextInputRule = { required?: string; diff --git a/packages/console/src/components/MultiTextInput/utils.ts b/packages/console/src/components/MultiTextInput/utils.ts index ba3187e3f..398938da5 100644 --- a/packages/console/src/components/MultiTextInput/utils.ts +++ b/packages/console/src/components/MultiTextInput/utils.ts @@ -1,4 +1,6 @@ -import { MultiTextInputError, MultiTextInputRule } from './types'; +import { t } from 'i18next'; + +import { MultiTextInputError, multiTextInputErrorGuard, MultiTextInputRule } from './types'; export const validate = ( value?: string[], @@ -55,5 +57,11 @@ export const convertRhfErrorMessage = (errorMessage?: string): MultiTextInputErr return; } - return JSON.parse(errorMessage) as MultiTextInputError; + const result = multiTextInputErrorGuard.safeParse(errorMessage); + + if (!result.success) { + throw new Error(t('admin_console.errors.invalid_error_message_format')); + } + + return result.data; }; diff --git a/packages/console/src/components/Transfer/DraggableItem.tsx b/packages/console/src/components/Transfer/DraggableItem.tsx index 7523d87af..ccbe34c9d 100644 --- a/packages/console/src/components/Transfer/DraggableItem.tsx +++ b/packages/console/src/components/Transfer/DraggableItem.tsx @@ -1,5 +1,5 @@ import { Nullable } from '@silverhand/essentials'; -import type { Identifier, XYCoord } from 'dnd-core'; +import type { Identifier } from 'dnd-core'; import { ReactNode, useContext, useEffect, useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; @@ -52,7 +52,7 @@ const DraggableItem = ({ id, children, sortIndex, moveItem }: Props) => { const clientOffset = monitor.getClientOffset(); // Get pixels to the top - const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; + const hoverClientY = clientOffset?.y ?? 0 - hoverBoundingRect.top; // Only perform the move when the mouse has crossed half of the items height // When dragging downwards, only move when the cursor is below 50% diff --git a/packages/console/src/components/UnsavedChangesAlertModal/index.tsx b/packages/console/src/components/UnsavedChangesAlertModal/index.tsx index a68297123..f60a4cb8c 100644 --- a/packages/console/src/components/UnsavedChangesAlertModal/index.tsx +++ b/packages/console/src/components/UnsavedChangesAlertModal/index.tsx @@ -5,9 +5,16 @@ import { UNSAFE_NavigationContext, Navigator } from 'react-router-dom'; import ConfirmModal from '../ConfirmModal'; +/** + * The `usePrompt` and `useBlock` hooks are removed from react-router v6, as the developers think + * they are not ready to be shipped in v6. Reference: https://github.com/remix-run/react-router/issues/8139 + * Therefore we have to implement our own workaround to provide the same functionality, through `UNSAFE_NavigationContext`. + */ +type BlockFunction = (blocker: Blocker) => () => void; + type BlockerNavigator = Navigator & { location: Location; - block(blocker: Blocker): () => void; + block: BlockFunction; }; type Props = { @@ -27,10 +34,13 @@ const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => { return; } - const { - block, - location: { pathname }, - } = navigator as BlockerNavigator; + /** + * Props `block` and `location` are removed from `Navigator` type in react-router, for the same reason as above. + * So we have to define our own type `BlockerNavigator` to acquire these props that actually exist in `navigator` object. + */ + // eslint-disable-next-line no-restricted-syntax + const { block, location } = navigator as BlockerNavigator; + const { pathname } = location; const unblock = block((transition) => { const { diff --git a/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx b/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx index 3d269334b..7d0a3a124 100644 --- a/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx @@ -1,4 +1,10 @@ -import { Connector, ConnectorDto, ConnectorMetadata, ConnectorType } from '@logto/schemas'; +import { + arbitraryObjectGuard, + Connector, + ConnectorDto, + ConnectorMetadata, + ConnectorType, +} from '@logto/schemas'; import { useEffect, useMemo, useState } from 'react'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -60,28 +66,23 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop return; } - try { - const configJson = JSON.parse(config) as JSON; - setIsSubmitting(true); - const { metadata, ...reset } = await api - .patch(`/api/connectors/${connectorData.id}`, { - json: { config: configJson }, - }) - .json< - Connector & { - metadata: ConnectorMetadata; - } - >(); - onConnectorUpdated({ ...reset, ...metadata }); - toast.success(t('general.saved')); - } catch (error: unknown) { - if (error instanceof SyntaxError) { - toast.error(t('connector_details.save_error_json_parse_error')); - } else { - toast.error(t('errors.unexpected_error')); - } + const configJson = arbitraryObjectGuard.safeParse(config); + + if (!configJson.success) { + toast.error(t('connector_details.save_error_json_parse_error')); + + return; } + setIsSubmitting(true); + + const { metadata, ...reset } = await api + .patch(`/api/connectors/${connectorData.id}`, { json: { config: configJson.data } }) + .json(); + + onConnectorUpdated({ ...reset, ...metadata }); + toast.success(t('general.saved')); + setIsSubmitting(false); }; diff --git a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx index f3a12c29d..714be114d 100644 --- a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx @@ -1,4 +1,4 @@ -import { ConnectorType } from '@logto/schemas'; +import { ConnectorType, arbitraryObjectGuard } from '@logto/schemas'; import { phoneRegEx, emailRegEx } from '@logto/shared'; import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; @@ -56,25 +56,19 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props) const onSubmit = handleSubmit(async (formData) => { const { sendTo } = formData; + const result = arbitraryObjectGuard.safeParse(config); - try { - const configJson = JSON.parse(config) as JSON; - const data = { config: configJson, ...(isSms ? { phone: sendTo } : { email: sendTo }) }; + if (!result.success) { + toast.error(t('connector_details.save_error_json_parse_error')); - await api - .post(`/api/connectors/${connectorId}/test`, { - json: data, - }) - .json(); - - setShowTooltip(true); - } catch (error: unknown) { - if (error instanceof SyntaxError) { - toast.error(t('connector_details.save_error_json_parse_error')); - } else { - toast.error(t('errors.unexpected_error')); - } + return; } + + const data = { config: result.data, ...(isSms ? { phone: sendTo } : { email: sendTo }) }; + + await api.post(`/api/connectors/${connectorId}/test`, { json: data }).json(); + + setShowTooltip(true); }); return ( diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index a17647fce..44b5b1b53 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -1,4 +1,4 @@ -import { ConnectorDto, ConnectorType } from '@logto/schemas'; +import { ConnectorDto, ConnectorType, arbitraryObjectGuard } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import i18next from 'i18next'; import { Controller, FormProvider, useForm } from 'react-hook-form'; @@ -49,31 +49,28 @@ const Guide = ({ connector, onClose }: Props) => { return; } - try { - const config = JSON.parse(connectorConfigJson) as JSON; - await api - .patch(`/api/connectors/${connectorId}`, { - json: { config }, - }) - .json(); - await api - .patch(`/api/connectors/${connectorId}/enabled`, { - json: { enabled: true }, - }) - .json(); + const result = arbitraryObjectGuard.safeParse(connectorConfigJson); - await updateSettings({ - ...conditional(!isSocialConnector && { passwordlessConfigured: true }), - ...conditional(isSocialConnector && { socialSignInConfigured: true }), - }); + if (!result.success) { + toast.error(t('connector_details.save_error_json_parse_error')); - onClose(); - toast.success(t('general.saved')); - } catch (error: unknown) { - if (error instanceof SyntaxError) { - toast.error(t('connector_details.save_error_json_parse_error')); - } + return; } + + await api + .patch(`/api/connectors/${connectorId}`, { json: { config: result.data } }) + .json(); + await api + .patch(`/api/connectors/${connectorId}/enabled`, { json: { enabled: true } }) + .json(); + + await updateSettings({ + ...conditional(!isSocialConnector && { passwordlessConfigured: true }), + ...conditional(isSocialConnector && { socialSignInConfigured: true }), + }); + + onClose(); + toast.success(t('general.saved')); }); return ( diff --git a/packages/console/src/pages/UserDetails/components/UserSettings.tsx b/packages/console/src/pages/UserDetails/components/UserSettings.tsx index babaf51a2..c71931d28 100644 --- a/packages/console/src/pages/UserDetails/components/UserSettings.tsx +++ b/packages/console/src/pages/UserDetails/components/UserSettings.tsx @@ -1,4 +1,4 @@ -import { User } from '@logto/schemas'; +import { arbitraryObjectGuard, User } from '@logto/schemas'; import { Nullable } from '@silverhand/essentials'; import { useEffect } from 'react'; import { useForm, useController } from 'react-hook-form'; @@ -12,7 +12,6 @@ import TextInput from '@/components/TextInput'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import useApi from '@/hooks/use-api'; import * as detailsStyles from '@/scss/details.module.scss'; -import { safeParseJson } from '@/utilities/json'; import { uriValidator } from '@/utilities/validator'; import * as styles from '../index.module.scss'; @@ -62,11 +61,11 @@ const UserSettings = ({ userData, userFormData, isDeleted, onUserUpdated }: Prop return; } - const { customData: inputtedCustomData, name, avatar, roleNames } = formData; + const { customData: inputCustomData, name, avatar, roleNames } = formData; - const customData = inputtedCustomData ? safeParseJson(inputtedCustomData) : {}; + const result = arbitraryObjectGuard.safeParse(inputCustomData); - if (!customData) { + if (!result.success) { toast.error(t('user_details.custom_data_invalid')); return; @@ -76,7 +75,7 @@ const UserSettings = ({ userData, userFormData, isDeleted, onUserUpdated }: Prop name, avatar, roleNames, - customData, + customData: result.data, }; const updatedUser = await api diff --git a/packages/console/src/utilities/json.ts b/packages/console/src/utilities/json.ts deleted file mode 100644 index 5b4220ad4..000000000 --- a/packages/console/src/utilities/json.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ArbitraryObject } from '@logto/schemas'; - -export const safeParseJson = ( - value: string -): T | undefined => { - try { - return JSON.parse(value) as T; - } catch {} -}; diff --git a/packages/console/src/utilities/markdown.ts b/packages/console/src/utilities/markdown.ts deleted file mode 100644 index f665f51f6..000000000 --- a/packages/console/src/utilities/markdown.ts +++ /dev/null @@ -1,39 +0,0 @@ -type MarkdownWithYamlFrontmatter = { - metadata: string; -} & { - [K in keyof T]?: string; -}; - -/** - * A Yaml frontmatter is a series of variables that are defined at the top of the markdown file, - * that normally is not part of the text contents themselves, such as title, subtitle. - * Yaml frontmatter both starts and ends with three dashes (---), and valid Yaml syntax can be used - * in between the three dashes, to define the variables. - */ -export const parseMarkdownWithYamlFrontmatter = >( - markdown: string -): MarkdownWithYamlFrontmatter => { - const metaRegExp = new RegExp(/^---[\n\r](((?!---).|[\n\r])*)[\n\r]---$/m); - - // "rawYamlHeader" is the full matching string, including the --- and --- - // "yamlVariables" is the first capturing group, which is the string between the --- and --- - const [rawYamlHeader, yamlVariables] = metaRegExp.exec(markdown) ?? []; - - if (!rawYamlHeader || !yamlVariables) { - return { metadata: markdown }; - } - - const keyValues = yamlVariables.split('\n'); - - // Converts a list of string like ["key1: value1", "key2: value2"] to { key1: "value1", key2: "value2" } - const frontmatter = Object.fromEntries( - keyValues.map((keyValue) => { - const splitted = keyValue.split(':'); - const [key, value] = splitted; - - return [key ?? keyValue, value?.trim() ?? '']; - }) - ) as Record; - - return { ...frontmatter, metadata: markdown.replace(rawYamlHeader, '').trim() }; -}; diff --git a/packages/phrases/src/locales/en/translation/admin-console/errors.ts b/packages/phrases/src/locales/en/translation/admin-console/errors.ts index ecc51add1..f255405e7 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/errors.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/errors.ts @@ -6,6 +6,7 @@ const errors = { missing_total_number: 'Unable to find Total-Number in response headers', invalid_uri_format: 'Invalid URI format', invalid_origin_format: 'Invalid URI origin format', + invalid_error_message_format: 'The error message format is invalid.', required_field_missing: 'Please enter {{field}}', required_field_missing_plural: 'You have to enter at least one {{field}}', more_details: 'More details', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/errors.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/errors.ts index 2bcf79a2d..3ba4fc275 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/errors.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/errors.ts @@ -6,6 +6,7 @@ const errors = { missing_total_number: '无法从返回的头部信息中找到 Total-Number', invalid_uri_format: '无效的 URI 格式', invalid_origin_format: '无效的 URI origin 格式', + invalid_error_message_format: '非法的错误信息格式', required_field_missing: '请输入{{field}}', required_field_missing_plural: '至少需要输入一个{{field}}', more_details: '查看详情', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 420efc42c..25429ef4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -720,7 +720,7 @@ importers: react-markdown: ^8.0.0 react-modal: ^3.14.4 react-paginate: ^8.1.2 - react-router-dom: ^6.2.2 + react-router-dom: 6.2.2 react-syntax-highlighter: ^15.5.0 recharts: ^2.1.10 remark-gfm: ^3.0.1