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:
commit
1afb6dd467
15 changed files with 103 additions and 136 deletions
|
@ -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 */
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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%
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {}
|
||||
};
|
|
@ -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() };
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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
2
pnpm-lock.yaml
generated
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue