0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

Merge pull request #1592 from logto-io/charles-log-3658-remove-as

refactor: remove "as"
This commit is contained in:
Charles Zhao 2022-07-19 23:17:11 +08:00 committed by GitHub
commit 1afb6dd467
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 103 additions and 136 deletions

View file

@ -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 */

View file

@ -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": {

View file

@ -1,7 +1,11 @@
export type MultiTextInputError = {
required?: string;
inputs?: Record<number, string | undefined>;
};
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<typeof multiTextInputErrorGuard>;
export type MultiTextInputRule = {
required?: string;

View file

@ -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;
};

View file

@ -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%

View file

@ -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 {

View file

@ -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<Connector & { metadata: ConnectorMetadata }>();
onConnectorUpdated({ ...reset, ...metadata });
toast.success(t('general.saved'));
setIsSubmitting(false);
};

View file

@ -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 (

View file

@ -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<ConnectorDto>();
await api
.patch(`/api/connectors/${connectorId}/enabled`, {
json: { enabled: true },
})
.json<ConnectorDto>();
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<ConnectorDto>();
await api
.patch(`/api/connectors/${connectorId}/enabled`, { json: { enabled: true } })
.json<ConnectorDto>();
await updateSettings({
...conditional(!isSocialConnector && { passwordlessConfigured: true }),
...conditional(isSocialConnector && { socialSignInConfigured: true }),
});
onClose();
toast.success(t('general.saved'));
});
return (

View file

@ -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

View file

@ -1,9 +0,0 @@
import { ArbitraryObject } from '@logto/schemas';
export const safeParseJson = <T extends ArbitraryObject = ArbitraryObject>(
value: string
): T | undefined => {
try {
return JSON.parse(value) as T;
} catch {}
};

View file

@ -1,39 +0,0 @@
type MarkdownWithYamlFrontmatter<T> = {
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 = <T extends Record<string, string>>(
markdown: string
): MarkdownWithYamlFrontmatter<T> => {
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<string>(
keyValues.map((keyValue) => {
const splitted = keyValue.split(':');
const [key, value] = splitted;
return [key ?? keyValue, value?.trim() ?? ''];
})
) as Record<keyof T, string>;
return { ...frontmatter, metadata: markdown.replace(rawYamlHeader, '').trim() };
};

View file

@ -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',

View file

@ -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: '查看详情',

2
pnpm-lock.yaml generated
View file

@ -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