0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

fix(ui): fix no-restrict-syntax in ui (#1559)

* fix(ui): fix no-restrict-syntax in ui

fix no-restricted-syntax in ui

* fix(ui): cr update

cr update

* fix(ui): fix ts error

fix ts error
This commit is contained in:
simeng-li 2022-07-18 19:36:12 +08:00 committed by GitHub
parent 4532081426
commit 816ce9f903
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 70 additions and 59 deletions

View file

@ -63,6 +63,7 @@
"react-string-replace": "^1.0.0",
"react-timer-hook": "^3.0.5",
"stylelint": "^14.8.2",
"superstruct": "^0.16.0",
"typescript": "^4.6.2",
"use-debounced-loader": "^0.1.1"
},
@ -72,8 +73,7 @@
"eslintConfig": {
"extends": "@silverhand/react",
"rules": {
"complexity": "off",
"no-restricted-syntax": "off"
"complexity": "off"
}
},
"stylelint": {

View file

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import Dropdown, { DropdownItem } from '@/components/Dropdown';
import useSocial from '@/hooks/use-social';
import { ConnectorData } from '@/types';
import { isKeyOf } from '@/utils';
import * as styles from './index.module.scss';
@ -47,8 +48,7 @@ const SocialSignInDropdown = ({ isOpen, onClose, connectors, anchorRef }: Props)
>
{connectors.map((connector) => {
const { id, name, logo, logoDark } = connector;
const languageKey = Object.keys(name).find((key) => key === language) ?? 'en';
const localName = name[languageKey as Language];
const localName = isKeyOf(language, name) ? name[language] : name[Language.English];
return (
<DropdownItem

View file

@ -1,18 +1,16 @@
import { Optional } from '@silverhand/essentials';
import { conditional } from '@silverhand/essentials';
import { useCallback, useEffect, useContext, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { is } from 'superstruct';
import { registerWithSocial, bindSocialRelatedUser } from '@/apis/social';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import { LocalSignInMethod } from '@/types';
type LocationState = {
relatedUser?: string;
};
import { bindSocialStateGuard } from '@/types/guard';
const useBindSocial = () => {
const state = useLocation().state as Optional<LocationState>;
const { state } = useLocation();
const { experienceSettings } = useContext(PageContext);
const { result: registerResult, run: asyncRegisterWithSocial } = useApi(registerWithSocial);
const { result: bindUserResult, run: asyncBindSocialRelatedUser } = useApi(bindSocialRelatedUser);
@ -54,7 +52,7 @@ const useBindSocial = () => {
return {
localSignInMethods,
relatedUser: state?.relatedUser,
relatedUser: conditional(is(state, bindSocialStateGuard) && state.relatedUser),
registerWithSocial: createAccountHandler,
bindSocialRelatedUser: bindRelatedUserHandler,
};

View file

@ -1,9 +1,9 @@
import { useState, useCallback, useEffect, useRef, FormEvent } from 'react';
import { ErrorType } from '@/components/ErrorMessage';
import { entries } from '@/utils';
import { entries, fromEntries, Entries } from '@/utils';
const useForm = <T>(initialState: T) => {
const useForm = <T extends Record<string, unknown>>(initialState: T) => {
type ErrorState = {
[key in keyof T]?: ErrorType;
};
@ -19,12 +19,12 @@ const useForm = <T>(initialState: T) => {
const fieldValidationsRef = useRef<FieldValidations>({});
const validateForm = useCallback(() => {
const errors = entries(fieldValue).map<[keyof T, ErrorType | undefined]>(([key, value]) => [
const errors: Entries<ErrorState> = entries(fieldValue).map(([key, value]) => [
key,
fieldValidationsRef.current[key]?.(value),
]);
setFieldErrors(Object.fromEntries(errors) as ErrorState);
setFieldErrors(fromEntries(errors));
return errors.every(([, error]) => error === undefined);
}, [fieldValidationsRef, fieldValue]);
@ -37,8 +37,7 @@ const useForm = <T>(initialState: T) => {
return {
value: fieldValue[field],
error: fieldErrors[field],
onChange: ({ target }: FormEvent<HTMLInputElement>) => {
const { value } = target as HTMLInputElement;
onChange: ({ currentTarget: { value } }: FormEvent<HTMLInputElement>) => {
setFieldValue((previous) => ({ ...previous, [field]: value }));
},
};
@ -49,13 +48,13 @@ const useForm = <T>(initialState: T) => {
// Revalidate on Input change
useEffect(() => {
setFieldErrors((previous) => {
const errors = entries(fieldValue).map<[keyof T, ErrorType | undefined]>(([key, value]) => [
const errors: Entries<ErrorState> = entries(fieldValue).map(([key, value]) => [
key,
// Only validate field with existing errors
previous[key] && fieldValidationsRef.current[key]?.(value),
]);
return Object.fromEntries(errors) as ErrorState;
return fromEntries(errors);
});
}, [fieldValue, fieldValidationsRef]);

View file

@ -1,5 +1,4 @@
import { Language } from '@logto/phrases-ui';
import { AppearanceMode, ConnectorPlatform } from '@logto/schemas';
import { ConnectorPlatform } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials';
import i18next from 'i18next';
import { useEffect, useState } from 'react';
@ -7,19 +6,11 @@ import { useEffect, useState } from 'react';
import * as styles from '@/App.module.scss';
import { Context } from '@/hooks/use-page-context';
import initI18n from '@/i18n/init';
import { SignInExperienceSettingsResponse, SignInExperienceSettings, Platform } from '@/types';
import { SignInExperienceSettings, PreviewConfig } from '@/types';
import { parseQueryParameters } from '@/utils';
import { getPrimarySignInMethod, getSecondarySignInMethods } from '@/utils/sign-in-experience';
import { filterPreviewSocialConnectors } from '@/utils/social-connectors';
type PreviewConfig = {
signInExperience: SignInExperienceSettingsResponse;
language: Language;
mode: AppearanceMode.LightMode | AppearanceMode.DarkMode;
platform: Platform;
isNative: boolean;
};
const usePreview = (context: Context): [boolean, PreviewConfig?] => {
const [previewConfig, setPreviewConfig] = useState<PreviewConfig>();
const { setTheme, setExperienceSettings, setPlatform } = context;
@ -44,6 +35,8 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
}
if (event.data.sender === 'ac_preview') {
// #event.data should be guarded at the provider's side
// eslint-disable-next-line no-restricted-syntax
setPreviewConfig(event.data.config as PreviewConfig);
}
};

View file

@ -21,9 +21,7 @@ const useSocialSignInListener = () => {
if (parameters.connector) {
navigate(`/social/register/${parameters.connector}`, {
replace: true,
state: {
...(error.data as Record<string, unknown> | undefined),
},
state: error.data,
});
}
},

View file

@ -1,13 +1,12 @@
import { Nullable } from '@silverhand/essentials';
import { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useLocation } from 'react-router-dom';
import { is } from 'superstruct';
import NavBar from '@/components/NavBar';
import PasscodeValidation from '@/containers/PasscodeValidation';
import { PageContext } from '@/hooks/use-page-context';
import ErrorPage from '@/pages/ErrorPage';
import { UserFlow } from '@/types';
import { passcodeStateGuard, passcodeMethodGuard } from '@/types/guard';
import * as styles from './index.module.scss';
@ -16,30 +15,23 @@ type Parameters = {
method: string;
};
type StateType = Nullable<Record<string, string>>;
const Passcode = () => {
const { t } = useTranslation();
const { method, type } = useParams<Parameters>();
const state = useLocation().state as StateType;
const { state } = useLocation();
const invalidType = type !== 'sign-in' && type !== 'register';
const invalidMethod = method !== 'email' && method !== 'sms';
const { setToast } = useContext(PageContext);
useEffect(() => {
if (method && !state?.[method]) {
setToast(t(method === 'email' ? 'error.invalid_email' : 'error.invalid_phone'));
}
}, [method, setToast, state, t]);
const invalidMethod = !is(method, passcodeMethodGuard);
const invalidState = !is(state, passcodeStateGuard);
if (invalidType || invalidMethod) {
return <ErrorPage />;
}
const target = state?.[method];
const target = !invalidState && state[method];
if (!target) {
return <ErrorPage />;
return <ErrorPage title={method === 'email' ? 'error.invalid_email' : 'error.invalid_phone'} />;
}
return (

View file

@ -0,0 +1,12 @@
import * as s from 'superstruct';
export const bindSocialStateGuard = s.object({
relatedUser: s.optional(s.string()),
});
export const passcodeStateGuard = s.object({
email: s.optional(s.string()),
sms: s.optional(s.string()),
});
export const passcodeMethodGuard = s.union([s.literal('email'), s.literal('sms')]);

View file

@ -1,4 +1,5 @@
import { SignInExperience, ConnectorMetadata } from '@logto/schemas';
import { Language } from '@logto/phrases-ui';
import { SignInExperience, ConnectorMetadata, AppearanceMode } from '@logto/schemas';
export type UserFlow = 'sign-in' | 'register';
export type SignInMethod = 'username' | 'email' | 'sms' | 'social';
@ -34,3 +35,11 @@ export type SignInExperienceSettings = Omit<
export enum TermsOfUseModalMessage {
SHOW_DETAIL_MODAL = 'SHOW_DETAIL_MODAL',
}
export type PreviewConfig = {
signInExperience: SignInExperienceSettingsResponse;
language: Language;
mode: AppearanceMode.LightMode | AppearanceMode.DarkMode;
platform: Platform;
isNative: boolean;
};

View file

@ -18,6 +18,7 @@ export const countryCallingCodeMap: Record<string, CountryCode> = {
export const isValidCountryCode = (countryCode: string): countryCode is CountryCode => {
try {
// Use getCountryCallingCode method to guard the input's value is in CountryCode union type, if type not match exceptions are expected
// eslint-disable-next-line no-restricted-syntax
getCountryCallingCode(countryCode as CountryCode);
return true;

View file

@ -26,16 +26,21 @@ export const getSearchParameters = (parameters: string | URLSearchParams, key: s
return searchParameters.get(key) ?? undefined;
};
type Entries<T> = Array<
export type Entries<T> = Array<
{
[K in keyof T]: [K, T[K]];
}[keyof T]
>;
export const entries = <T>(object: T): Entries<T> => Object.entries(object) as Entries<T>;
export const entries = <T extends Record<string, unknown>>(object: T): Entries<T> =>
// eslint-disable-next-line no-restricted-syntax
Object.entries(object) as Entries<T>;
// eslint-disable-next-line @typescript-eslint/ban-types
export const inOperator = <K extends string, T extends object>(
key: K,
export const fromEntries = <T extends Record<string, unknown>>(entries: Entries<T>) =>
// eslint-disable-next-line no-restricted-syntax
Object.fromEntries(entries) as T;
export const isKeyOf = <T extends Record<string, unknown>>(
key: string | number | symbol,
object: T
): object is T & Record<K, unknown> => key in object;
): key is keyof T => key in object;

View file

@ -9,10 +9,12 @@ import { getSignInExperience } from '@/apis/settings';
import { SignInMethod, SignInExperienceSettingsResponse, SignInExperienceSettings } from '@/types';
import { filterSocialConnectors } from '@/utils/social-connectors';
import { entries } from '.';
export const getPrimarySignInMethod = (signInMethods: SignInMethods) => {
for (const [key, value] of Object.entries(signInMethods)) {
for (const [key, value] of entries(signInMethods)) {
if (value === 'primary') {
return key as keyof SignInMethods;
return key;
}
}
@ -20,9 +22,9 @@ export const getPrimarySignInMethod = (signInMethods: SignInMethods) => {
};
export const getSecondarySignInMethods = (signInMethods: SignInMethods) =>
Object.entries(signInMethods).reduce<SignInMethod[]>((methods, [key, value]) => {
entries(signInMethods).reduce<SignInMethod[]>((methods, [key, value]) => {
if (value === 'secondary') {
return [...methods, key as SignInMethod];
return [...methods, key];
}
return methods;

2
pnpm-lock.yaml generated
View file

@ -1225,6 +1225,7 @@ importers:
react-string-replace: ^1.0.0
react-timer-hook: ^3.0.5
stylelint: ^14.8.2
superstruct: ^0.16.0
typescript: ^4.6.2
use-debounced-loader: ^0.1.1
dependencies:
@ -1277,6 +1278,7 @@ importers:
react-string-replace: 1.0.0
react-timer-hook: 3.0.5_sfoxds7t5ydpegc3knd667wn6m
stylelint: 14.8.2
superstruct: 0.16.0
typescript: 4.6.2
use-debounced-loader: 0.1.1_react@17.0.2