mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(console): inspire me (#3360)
This commit is contained in:
parent
fa85b7d0eb
commit
b1b5200876
33 changed files with 465 additions and 141 deletions
1
.github/workflows/integration-test.yml
vendored
1
.github/workflows/integration-test.yml
vendored
|
@ -58,6 +58,7 @@ jobs:
|
|||
env:
|
||||
INTEGRATION_TEST: true
|
||||
IS_CLOUD: ${{ contains(matrix.target, 'cloud') && '1' || '0' }}
|
||||
PATH_BASED_MULTI_TENANCY: ${{ contains(matrix.target, 'cloud') && '1' || '0' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
15
packages/console/src/assets/images/light-bulb.svg
Normal file
15
packages/console/src/assets/images/light-bulb.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.0723 5.45703C15.3365 4.8519 14.475 4.41865 13.5504 4.18891C12.6259 3.95916 11.6617 3.9387 10.7283 4.12903C9.4854 4.38009 8.34509 4.99469 7.45204 5.89486C6.559 6.79502 5.95347 7.94018 5.7123 9.18503C5.53617 10.1184 5.56818 11.0792 5.80602 11.9987C6.04387 12.9183 6.4817 13.7741 7.0883 14.505C7.65124 15.1403 7.97392 15.9526 8.0003 16.801V19.201C8.0003 19.8376 8.25315 20.448 8.70324 20.8981C9.15333 21.3482 9.76378 21.601 10.4003 21.601H13.6003C14.2368 21.601 14.8473 21.3482 15.2974 20.8981C15.7474 20.448 16.0003 19.8376 16.0003 19.201V16.953C16.0271 16.0164 16.3713 15.1166 16.9763 14.401C18.0364 13.0897 18.5361 11.4129 18.3668 9.73513C18.1976 8.05738 17.373 6.51422 16.0723 5.44103V5.45703ZM14.4003 19.201C14.4003 19.4132 14.316 19.6167 14.166 19.7667C14.016 19.9167 13.8125 20.001 13.6003 20.001H10.4003C10.1881 20.001 9.98464 19.9167 9.83461 19.7667C9.68458 19.6167 9.6003 19.4132 9.6003 19.201V18.401H14.4003V19.201Z" fill="url(#paint0_linear_691_82122)"/>
|
||||
<path d="M16.0721 5.45703C15.3363 4.8519 14.4747 4.41865 13.5502 4.18891C12.6256 3.95916 11.6615 3.9387 10.7281 4.12903C9.48515 4.38009 8.34484 4.99469 7.4518 5.89486C6.55875 6.79502 5.95323 7.94018 5.71205 9.18503C5.53593 10.1184 5.56793 11.0792 5.80578 11.9987C6.04363 12.9183 6.48146 13.7741 7.08805 14.505C7.651 15.1403 7.97368 15.9526 8.00005 16.801V19.201C8.00005 19.8376 8.25291 20.448 8.703 20.8981C9.15308 21.3482 9.76353 21.601 10.4001 21.601H13.6001C14.2366 21.601 14.847 21.3482 15.2971 20.8981C15.7472 20.448 16.0001 19.8376 16.0001 19.201V16.953C16.0269 16.0164 16.371 15.1166 16.9761 14.401C18.0361 13.0897 18.5359 11.4129 18.3666 9.73513C18.1973 8.05738 17.3727 6.51422 16.0721 5.44103V5.45703ZM14.4001 19.201C14.4001 19.4132 14.3158 19.6167 14.1657 19.7667C14.0157 19.9167 13.8122 20.001 13.6001 20.001H10.4001C10.1879 20.001 9.9844 19.9167 9.83437 19.7667C9.68434 19.6167 9.60005 19.4132 9.60005 19.201V18.401H14.4001V19.201ZM15.7361 13.409C14.9317 14.3631 14.4623 15.5547 14.4001 16.801H12.8001V14.401C12.8001 14.1889 12.7158 13.9854 12.5657 13.8353C12.4157 13.6853 12.2122 13.601 12.0001 13.601C11.7879 13.601 11.5844 13.6853 11.4344 13.8353C11.2843 13.9854 11.2001 14.1889 11.2001 14.401V16.801H9.60005C9.57894 15.5755 9.12561 14.3968 8.32005 13.473C7.78856 12.8362 7.43116 12.0726 7.2827 11.2565C7.13423 10.4405 7.19977 9.59984 7.47292 8.81666C7.74607 8.03347 8.21751 7.33443 8.84127 6.78769C9.46503 6.24096 10.2198 5.86519 11.0321 5.69703C11.7301 5.55331 12.4514 5.56683 13.1435 5.73661C13.8357 5.90639 14.4814 6.22817 15.0337 6.67859C15.586 7.12902 16.0311 7.69677 16.3366 8.34065C16.6421 8.98452 16.8004 9.68835 16.8001 10.401C16.806 11.4969 16.4297 12.5606 15.7361 13.409Z" fill="#191C1D"/>
|
||||
<rect x="4.61646" y="2.2019" width="3" height="1.5" rx="0.75" transform="rotate(45 4.61646 2.2019)" fill="#191C1D"/>
|
||||
<rect x="1.30005" y="8.2019" width="3" height="1.5" rx="0.75" fill="#191C1D"/>
|
||||
<rect x="19.429" y="8.2019" width="3" height="1.5" rx="0.75" fill="#191C1D"/>
|
||||
<rect x="11.429" y="3" width="3" height="1.5" rx="0.75" transform="rotate(-90 11.429 3)" fill="#191C1D"/>
|
||||
<rect width="3" height="1.5" rx="0.75" transform="matrix(0.707107 -0.707107 -0.707107 -0.707107 18.7126 5.2019)" fill="#191C1D"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_691_82122" x1="5.60034" y1="12.8005" x2="18.3992" y2="12.8005" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF930F"/>
|
||||
<stop offset="1" stop-color="#FFF95B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -1,4 +1,4 @@
|
|||
import type { UserAssetsResponse, AllowedUploadMimeType } from '@logto/schemas';
|
||||
import type { AllowedUploadMimeType, UserAssets } from '@logto/schemas';
|
||||
import { maxUploadFileSize } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
@ -66,9 +66,7 @@ const FileUploader = ({ maxSize, allowedMimeTypes, limitDescription, onCompleted
|
|||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
const { url } = await api
|
||||
.post('api/user-assets', { body: formData })
|
||||
.json<UserAssetsResponse>();
|
||||
const { url } = await api.post('api/user-assets', { body: formData }).json<UserAssets>();
|
||||
|
||||
onCompleted(url);
|
||||
} catch {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import type { KeyboardEventHandler, ReactNode } from 'react';
|
||||
import type { KeyboardEventHandler, ReactElement, ReactNode } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type DangerousRaw from '../DangerousRaw';
|
||||
import * as styles from './Radio.module.scss';
|
||||
|
||||
const Check = () => (
|
||||
|
@ -18,7 +19,7 @@ const Check = () => (
|
|||
export type Props = {
|
||||
className?: string;
|
||||
value: string;
|
||||
title?: AdminConsoleKey;
|
||||
title?: AdminConsoleKey | ReactElement<typeof DangerousRaw>;
|
||||
name?: string;
|
||||
children?: ReactNode;
|
||||
isChecked?: boolean;
|
||||
|
@ -85,7 +86,7 @@ const Radio = ({
|
|||
{children}
|
||||
{type === 'plain' && <div className={styles.indicator} />}
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
{title && t(title)}
|
||||
{title && (typeof title === 'string' ? t(title) : title)}
|
||||
{isDisabled && disabledLabel && (
|
||||
<div className={classNames(styles.indicator, styles.disabledLabel)}>
|
||||
{t(disabledLabel)}
|
||||
|
|
12
packages/console/src/hooks/use-user-assets-service.ts
Normal file
12
packages/console/src/hooks/use-user-assets-service.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { UserAssetsServiceStatus } from '@logto/schemas';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
const useUserAssetsService = () => {
|
||||
const { data } = useSWRImmutable<UserAssetsServiceStatus>('api/user-assets/service-status');
|
||||
|
||||
return {
|
||||
isReady: data?.status === 'ready',
|
||||
};
|
||||
};
|
||||
|
||||
export default useUserAssetsService;
|
|
@ -1,11 +1,11 @@
|
|||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
|
||||
import type { Option } from './types';
|
||||
import type { CardSelectorOption } from './types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
value: string;
|
||||
options: Option[];
|
||||
options: CardSelectorOption[];
|
||||
onChange: (value: string) => void;
|
||||
optionClassName?: string;
|
||||
};
|
||||
|
|
|
@ -29,6 +29,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.tag {
|
||||
font: var(--font-body-3);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.trailingTag {
|
||||
margin-left: _.unit(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-text-link);
|
||||
|
|
|
@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import type { Option } from '../types';
|
||||
import type { MultiCardSelectorOption } from '../types';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
options: Option[];
|
||||
options: MultiCardSelectorOption[];
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
isNotAllowEmpty?: boolean;
|
||||
|
@ -39,7 +39,7 @@ const MultiCardSelector = ({
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.selector, className)}>
|
||||
{options.map(({ icon, title, value }) => (
|
||||
{options.map(({ icon, title, value, tag, trailingTag }) => (
|
||||
<div
|
||||
key={value}
|
||||
role="button"
|
||||
|
@ -57,7 +57,15 @@ const MultiCardSelector = ({
|
|||
})}
|
||||
>
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
{t(title)}
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
{typeof title === 'string' ? t(title) : title}
|
||||
{trailingTag && (
|
||||
<span className={classNames(styles.tag, styles.trailingTag)}>{t(trailingTag)}</span>
|
||||
)}
|
||||
</div>
|
||||
{tag && <span className={styles.tag}>{t(tag)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export type { Option } from './types';
|
||||
export type { CardSelectorOption, MultiCardSelectorOption } from './types';
|
||||
export { default as CardSelector } from './CardSelector';
|
||||
export { default as MultiCardSelector } from './MultiCardSelector';
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
|
||||
export type Option = {
|
||||
import type DangerousRaw from '@/components/DangerousRaw';
|
||||
|
||||
export type CardSelectorOption = {
|
||||
icon?: ReactNode;
|
||||
title: AdminConsoleKey;
|
||||
title: AdminConsoleKey | ReactElement<typeof DangerousRaw>;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type MultiCardSelectorOption = CardSelectorOption & {
|
||||
tag?: AdminConsoleKey;
|
||||
trailingTag?: AdminConsoleKey;
|
||||
};
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import type { OnboardingSieConfig } from '../types';
|
||||
import { Authentication } from '../types';
|
||||
|
||||
export const reservationLink = 'https://calendly.com/logto/30min';
|
||||
export const logtoBlogLink =
|
||||
'https://www.notion.so/silverhand/About-Logto-Cloud-Preview-ca316bc05f2a4b9188047da014124434?pvs=4';
|
||||
|
||||
export const defaultOnboardingSieConfig: OnboardingSieConfig = {
|
||||
color: '#5D34F2',
|
||||
identifier: SignInIdentifier.Email,
|
||||
authentications: [Authentication.Password],
|
||||
};
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import type { Option as SelectorOption } from '@/onboarding/components/CardSelector';
|
||||
import type {
|
||||
CardSelectorOption,
|
||||
MultiCardSelectorOption,
|
||||
} from '@/onboarding/components/CardSelector';
|
||||
|
||||
import { CompanySize, Reason, Title } from '../../types';
|
||||
|
||||
export const titleOptions: SelectorOption[] = [
|
||||
export const titleOptions: MultiCardSelectorOption[] = [
|
||||
{ title: 'cloud.about.title_options.developer', value: Title.Developer },
|
||||
{ title: 'cloud.about.title_options.team_lead', value: Title.TeamLead },
|
||||
{ title: 'cloud.about.title_options.ceo', value: Title.Ceo },
|
||||
|
@ -11,7 +14,7 @@ export const titleOptions: SelectorOption[] = [
|
|||
{ title: 'cloud.about.title_options.others', value: Title.Others },
|
||||
];
|
||||
|
||||
export const companySizeOptions: SelectorOption[] = [
|
||||
export const companySizeOptions: CardSelectorOption[] = [
|
||||
{ title: 'cloud.about.company_options.size_1', value: CompanySize.Scale1 },
|
||||
{ title: 'cloud.about.company_options.size_1_49', value: CompanySize.Scale2 },
|
||||
{ title: 'cloud.about.company_options.size_50_199', value: CompanySize.Scale3 },
|
||||
|
@ -19,7 +22,7 @@ export const companySizeOptions: SelectorOption[] = [
|
|||
{ title: 'cloud.about.company_options.size_1000_plus', value: CompanySize.Scale5 },
|
||||
];
|
||||
|
||||
export const reasonOptions: SelectorOption[] = [
|
||||
export const reasonOptions: MultiCardSelectorOption[] = [
|
||||
{ title: 'cloud.about.reason_options.adoption', value: Reason.Adoption },
|
||||
{ title: 'cloud.about.reason_options.replacement', value: Reason.Replacement },
|
||||
{ title: 'cloud.about.reason_options.evaluation', value: Reason.Evaluation },
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.inspire {
|
||||
margin-top: _.unit(3);
|
||||
display: flex;
|
||||
padding: _.unit(4) _.unit(5);
|
||||
border-radius: 12px;
|
||||
background-color: var(--color-base);
|
||||
align-items: center;
|
||||
|
||||
.inspireContent {
|
||||
margin-right: _.unit(6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.inspireTitle {
|
||||
font: var(--font-title-2);
|
||||
margin-bottom: _.unit(1);
|
||||
}
|
||||
|
||||
.inspireDescription {
|
||||
font: var(--font-body-2);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
&:not(:disabled):not(:active):hover {
|
||||
background: var(--color-layer-1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Bulb from '@/assets/images/bulb.svg';
|
||||
import LightBulb from '@/assets/images/light-bulb.svg';
|
||||
import Button from '@/components/Button';
|
||||
import useConnectorGroups from '@/hooks/use-connector-groups';
|
||||
import type { OnboardingSieConfig } from '@/onboarding/types';
|
||||
|
||||
import { randomSieConfigTemplate } from '../../sie-config-templates';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
onInspired: (template: OnboardingSieConfig) => void;
|
||||
};
|
||||
|
||||
const InspireMe = ({ onInspired }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [isButtonHover, setIsButtonHover] = useState(false);
|
||||
const BulbIcon = isButtonHover ? LightBulb : Bulb;
|
||||
const [lastTemplateIndex, setLastTemplateIndex] = useState<number>();
|
||||
const { data: connectorData, error } = useConnectorGroups();
|
||||
const availableSocialTargets = error
|
||||
? []
|
||||
: connectorData
|
||||
?.filter(({ type }) => type === ConnectorType.Social)
|
||||
.map(({ target }) => target) ?? [];
|
||||
|
||||
const handleInspire = () => {
|
||||
const { template, templateIndex } = randomSieConfigTemplate(
|
||||
lastTemplateIndex,
|
||||
availableSocialTargets
|
||||
);
|
||||
setLastTemplateIndex(templateIndex);
|
||||
onInspired(template);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.inspire}>
|
||||
<div className={styles.inspireContent}>
|
||||
<div className={styles.inspireTitle}>{t('cloud.sie.inspire.title')}</div>
|
||||
<div className={styles.inspireDescription}>{t('cloud.sie.inspire.description')}</div>
|
||||
</div>
|
||||
<Button
|
||||
icon={<BulbIcon />}
|
||||
className={styles.button}
|
||||
title="cloud.sie.inspire.inspire_me"
|
||||
onMouseEnter={() => {
|
||||
setIsButtonHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsButtonHover(false);
|
||||
}}
|
||||
onClick={handleInspire}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InspireMe;
|
|
@ -0,0 +1,40 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
|
||||
import ConnectorLogo from '@/components/ConnectorLogo';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import useConnectorGroups from '@/hooks/use-connector-groups';
|
||||
import type { MultiCardSelectorOption } from '@/onboarding/components/CardSelector';
|
||||
import { MultiCardSelector } from '@/onboarding/components/CardSelector';
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
|
||||
const SocialSelector = ({ value, onChange }: Props) => {
|
||||
const { data: connectorData, error } = useConnectorGroups();
|
||||
|
||||
if (!connectorData || error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectorOptions: MultiCardSelectorOption[] = connectorData
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
.map((item) => {
|
||||
return {
|
||||
icon: <ConnectorLogo size="small" data={item} />,
|
||||
title: (
|
||||
<DangerousRaw>
|
||||
<UnnamedTrans resource={item.name} />
|
||||
</DangerousRaw>
|
||||
),
|
||||
value: item.target,
|
||||
tag: 'general.trial',
|
||||
};
|
||||
});
|
||||
|
||||
return <MultiCardSelector options={connectorOptions} value={value} onChange={onChange} />;
|
||||
};
|
||||
|
||||
export default SocialSelector;
|
|
@ -2,52 +2,20 @@
|
|||
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
padding: 0 _.unit(17);
|
||||
|
||||
.configWrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
margin-right: _.unit(4);
|
||||
padding: 0 _.unit(4) _.unit(6) 0;
|
||||
}
|
||||
|
||||
.config {
|
||||
background-color: var(--color-layer-1);
|
||||
border-radius: 8px;
|
||||
padding: _.unit(12);
|
||||
min-width: 430px;
|
||||
margin-right: _.unit(8);
|
||||
|
||||
.title {
|
||||
margin-top: _.unit(6);
|
||||
font: var(--font-title-1);
|
||||
}
|
||||
|
||||
.inspire {
|
||||
margin-top: _.unit(3);
|
||||
display: flex;
|
||||
padding: _.unit(4) _.unit(5);
|
||||
border-radius: 12px;
|
||||
background-color: var(--color-base);
|
||||
align-items: center;
|
||||
|
||||
.inspireContent {
|
||||
margin-right: _.unit(6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.inspireTitle {
|
||||
font: var(--font-title-2);
|
||||
margin-bottom: _.unit(1);
|
||||
}
|
||||
|
||||
.inspireDescription {
|
||||
font: var(--font-body-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cardFieldHeadline {
|
||||
margin-bottom: _.unit(2);
|
||||
}
|
||||
|
@ -59,8 +27,9 @@
|
|||
|
||||
.preview {
|
||||
flex: 1;
|
||||
margin-bottom: _.unit(6);
|
||||
min-height: 580px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,36 @@
|
|||
import type { SignInExperience as SignInExperienceType } from '@logto/schemas';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Bulb from '@/assets/images/bulb.svg';
|
||||
import Tools from '@/assets/images/tools.svg';
|
||||
import Button from '@/components/Button';
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
import FormField from '@/components/FormField';
|
||||
import ImageUploader from '@/components/ImageUploader';
|
||||
import OverlayScrollbar from '@/components/OverlayScrollbar';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useUserAssetsService from '@/hooks/use-user-assets-service';
|
||||
import ActionBar from '@/onboarding/components/ActionBar';
|
||||
import { CardSelector, MultiCardSelector } from '@/onboarding/components/CardSelector';
|
||||
import { defaultOnboardingSieConfig } from '@/onboarding/constants';
|
||||
import * as pageLayout from '@/onboarding/scss/layout.module.scss';
|
||||
import { OnboardingPage } from '@/onboarding/types';
|
||||
import { Authentication, OnboardingPage } from '@/onboarding/types';
|
||||
import type { OnboardingSieConfig } from '@/onboarding/types';
|
||||
import { getOnboardingPage } from '@/onboarding/utils';
|
||||
import { uriValidator } from '@/utils/validator';
|
||||
|
||||
import InspireMe from './components/InspireMe';
|
||||
import Preview from './components/Preview';
|
||||
import SocialSelector from './components/SocialSelector';
|
||||
import * as styles from './index.module.scss';
|
||||
import { authenticationOptions, identifierOptions } from './options';
|
||||
import { defaultOnboardingSieConfig } from './sie-config-templates';
|
||||
import { parser } from './utils';
|
||||
|
||||
const SignInExperience = () => {
|
||||
|
@ -33,21 +39,35 @@ const SignInExperience = () => {
|
|||
const { data: signInExperience, mutate } = useSWR<SignInExperienceType, RequestError>(
|
||||
'api/sign-in-exp'
|
||||
);
|
||||
|
||||
const api = useApi();
|
||||
const { isReady: isUserAssetsServiceReady } = useUserAssetsService();
|
||||
|
||||
const {
|
||||
reset,
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, isDirty },
|
||||
getValues,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty, errors },
|
||||
} = useForm<OnboardingSieConfig>({ defaultValues: defaultOnboardingSieConfig });
|
||||
|
||||
const updateAuthenticationConfig = useCallback(() => {
|
||||
const identifier = getValues('identifier');
|
||||
|
||||
if (identifier === SignInIdentifier.Username) {
|
||||
setValue('authentications', [Authentication.Password]);
|
||||
}
|
||||
}, [getValues, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (signInExperience) {
|
||||
reset(parser.signInExperienceToOnboardSieConfig(signInExperience));
|
||||
updateAuthenticationConfig();
|
||||
}
|
||||
}, [reset, signInExperience]);
|
||||
}, [reset, signInExperience, updateAuthenticationConfig]);
|
||||
|
||||
const onboardingSieConfig = watch();
|
||||
|
||||
|
@ -57,13 +77,7 @@ const SignInExperience = () => {
|
|||
}
|
||||
}, [onboardingSieConfig, signInExperience]);
|
||||
|
||||
const handleInspireMe = () => {
|
||||
// TODO @xiaoyijun
|
||||
reset(defaultOnboardingSieConfig);
|
||||
console.log('on inspire me');
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
const submit = (onSuccess: () => void) => async (formData: OnboardingSieConfig) => {
|
||||
if (!signInExperience) {
|
||||
return;
|
||||
}
|
||||
|
@ -75,46 +89,44 @@ const SignInExperience = () => {
|
|||
.json<SignInExperienceType>();
|
||||
|
||||
void mutate(updatedData);
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(getOnboardingPage(OnboardingPage.AboutUser), { replace: true });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSubmit();
|
||||
toast.success(t('general.saved'));
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
await onSubmit();
|
||||
navigate(getOnboardingPage(OnboardingPage.Congrats), { replace: true });
|
||||
onSuccess();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={pageLayout.page}>
|
||||
<div className={styles.content}>
|
||||
<OverlayScrollbar
|
||||
options={{ scrollbars: { autoHide: 'scroll', autoHideDelay: 500 } }}
|
||||
className={styles.configWrapper}
|
||||
>
|
||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.config}>
|
||||
<Tools />
|
||||
<div className={styles.title}>{t('cloud.sie.title')}</div>
|
||||
<div className={styles.inspire}>
|
||||
<div className={styles.inspireContent}>
|
||||
<div className={styles.inspireTitle}>{t('cloud.sie.inspire.title')}</div>
|
||||
<div className={styles.inspireDescription}>
|
||||
{t('cloud.sie.inspire.description')}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
icon={<Bulb />}
|
||||
title="cloud.sie.inspire.inspire_me"
|
||||
onClick={handleInspireMe}
|
||||
/>
|
||||
</div>
|
||||
<FormField title="cloud.sie.logo_field">TBD</FormField>
|
||||
<InspireMe
|
||||
onInspired={(template) => {
|
||||
reset(template);
|
||||
updateAuthenticationConfig();
|
||||
}}
|
||||
/>
|
||||
<FormField title="cloud.sie.logo_field">
|
||||
{isUserAssetsServiceReady ? (
|
||||
<Controller
|
||||
name="logo"
|
||||
control={control}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<ImageUploader name={name} value={value ?? ''} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
size="large"
|
||||
{...register('logo', {
|
||||
validate: (value) =>
|
||||
!value || uriValidator(value) || t('errors.invalid_uri_format'),
|
||||
})}
|
||||
hasError={Boolean(errors.logo)}
|
||||
errorMessage={errors.logo?.message}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField title="cloud.sie.color_field">
|
||||
<Controller
|
||||
name="color"
|
||||
|
@ -136,7 +148,10 @@ const SignInExperience = () => {
|
|||
name={name}
|
||||
value={value}
|
||||
options={identifierOptions}
|
||||
onChange={onChange}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
updateAuthenticationConfig();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -155,7 +170,11 @@ const SignInExperience = () => {
|
|||
isNotAllowEmpty
|
||||
className={styles.authnSelector}
|
||||
value={value}
|
||||
options={authenticationOptions}
|
||||
options={authenticationOptions.filter(
|
||||
({ value }) =>
|
||||
onboardingSieConfig.identifier !== SignInIdentifier.Username ||
|
||||
value === Authentication.Password
|
||||
)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
|
@ -166,16 +185,23 @@ const SignInExperience = () => {
|
|||
title="cloud.sie.social_field"
|
||||
headlineClassName={styles.cardFieldHeadline}
|
||||
>
|
||||
TBD
|
||||
<Controller
|
||||
name="socialTargets"
|
||||
control={control}
|
||||
defaultValue={defaultOnboardingSieConfig.socialTargets}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SocialSelector value={value ?? []} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
<Preview
|
||||
className={styles.preview}
|
||||
signInExperience={previewSieConfig}
|
||||
isLivePreviewDisabled={isDirty}
|
||||
/>
|
||||
</div>
|
||||
<Preview
|
||||
className={styles.preview}
|
||||
signInExperience={previewSieConfig}
|
||||
isLivePreviewDisabled={isDirty}
|
||||
/>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
|
||||
<ActionBar step={3}>
|
||||
<div className={styles.continueActions}>
|
||||
|
@ -183,16 +209,34 @@ const SignInExperience = () => {
|
|||
type="outline"
|
||||
title="general.save"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSave}
|
||||
onClick={async () => {
|
||||
await handleSubmit(
|
||||
submit(() => {
|
||||
toast.success(t('general.saved'));
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
title="cloud.sie.finish_and_done"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleNext}
|
||||
onClick={async () => {
|
||||
await handleSubmit(
|
||||
submit(() => {
|
||||
navigate(getOnboardingPage(OnboardingPage.Congrats), { replace: true });
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button title="general.back" disabled={isSubmitting} onClick={handleBack} />
|
||||
<Button
|
||||
title="general.back"
|
||||
disabled={isSubmitting}
|
||||
onClick={() => {
|
||||
navigate(getOnboardingPage(OnboardingPage.AboutUser), { replace: true });
|
||||
}}
|
||||
/>
|
||||
</ActionBar>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,10 +5,13 @@ import Keyboard from '@/assets/images/keyboard.svg';
|
|||
import Label from '@/assets/images/label.svg';
|
||||
import Lock from '@/assets/images/lock.svg';
|
||||
import Mobile from '@/assets/images/mobile.svg';
|
||||
import type { Option as SelectorOption } from '@/onboarding/components/CardSelector';
|
||||
import type {
|
||||
MultiCardSelectorOption,
|
||||
CardSelectorOption,
|
||||
} from '@/onboarding/components/CardSelector';
|
||||
import { Authentication } from '@/onboarding/types';
|
||||
|
||||
export const identifierOptions: SelectorOption[] = [
|
||||
export const identifierOptions: CardSelectorOption[] = [
|
||||
{
|
||||
icon: <Envelop />,
|
||||
title: 'sign_in_exp.sign_up_and_sign_in.identifiers_email',
|
||||
|
@ -26,7 +29,7 @@ export const identifierOptions: SelectorOption[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export const authenticationOptions: SelectorOption[] = [
|
||||
export const authenticationOptions: MultiCardSelectorOption[] = [
|
||||
{
|
||||
icon: <Lock />,
|
||||
title: 'sign_in_exp.sign_up_and_sign_in.sign_in.password_auth',
|
||||
|
@ -36,5 +39,6 @@ export const authenticationOptions: SelectorOption[] = [
|
|||
icon: <Keyboard />,
|
||||
title: 'sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth',
|
||||
value: Authentication.VerificationCode,
|
||||
trailingTag: 'general.cap_limit',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import type { OnboardingSieConfig } from '@/onboarding/types';
|
||||
import { Authentication } from '@/onboarding/types';
|
||||
|
||||
const assetsUrl =
|
||||
'https://logtodev.blob.core.windows.net/public-blobs/admin/BY4BCq8GvfBF/2023/03/10';
|
||||
|
||||
export const defaultOnboardingSieConfig: OnboardingSieConfig = {
|
||||
color: '#5D34F2',
|
||||
identifier: SignInIdentifier.Email,
|
||||
authentications: [Authentication.Password],
|
||||
};
|
||||
|
||||
const configTemplate1: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/tVCAHjAB/logo1.png`,
|
||||
color: '#19BEFD',
|
||||
identifier: SignInIdentifier.Email,
|
||||
authentications: [Authentication.Password, Authentication.VerificationCode],
|
||||
};
|
||||
|
||||
const configTemplate2: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/kvCN1Z2a/logo2.png`,
|
||||
color: '#F47346',
|
||||
identifier: SignInIdentifier.Phone,
|
||||
authentications: [Authentication.Password, Authentication.VerificationCode],
|
||||
};
|
||||
|
||||
const configTemplate3: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/IcI0snBP/logo3.png`,
|
||||
color: '#FF5449',
|
||||
identifier: SignInIdentifier.Username,
|
||||
authentications: [Authentication.Password],
|
||||
};
|
||||
|
||||
const configTemplate4: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/7UQyvuFc/logo4.png`,
|
||||
color: '#CA4E96',
|
||||
identifier: SignInIdentifier.Email,
|
||||
authentications: [Authentication.Password],
|
||||
};
|
||||
|
||||
const configTemplate5: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/zB2merH1/logo5.png`,
|
||||
color: '#F07EFF',
|
||||
identifier: SignInIdentifier.Email,
|
||||
authentications: [Authentication.VerificationCode],
|
||||
};
|
||||
|
||||
const configTemplate6: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/CX51jxXS/logo6.png`,
|
||||
color: '#9E65F8',
|
||||
identifier: SignInIdentifier.Phone,
|
||||
authentications: [Authentication.VerificationCode],
|
||||
};
|
||||
|
||||
const configTemplate7: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/uLoMzrlz/logo7.png`,
|
||||
color: '#FF5449',
|
||||
identifier: SignInIdentifier.Email,
|
||||
authentications: [Authentication.Password, Authentication.VerificationCode],
|
||||
};
|
||||
|
||||
const configTemplate8: OnboardingSieConfig = {
|
||||
logo: `${assetsUrl}/dIz8UHEh/logo8.png`,
|
||||
color: '#5D34F2',
|
||||
identifier: SignInIdentifier.Phone,
|
||||
authentications: [Authentication.VerificationCode],
|
||||
};
|
||||
|
||||
const sieConfigTemplates: OnboardingSieConfig[] = [
|
||||
configTemplate1,
|
||||
configTemplate2,
|
||||
configTemplate3,
|
||||
configTemplate4,
|
||||
configTemplate5,
|
||||
configTemplate6,
|
||||
configTemplate7,
|
||||
configTemplate8,
|
||||
];
|
||||
|
||||
export const randomSieConfigTemplate = (
|
||||
lastTemplateIndex: number | undefined,
|
||||
availableSocialTargets: string[]
|
||||
) => {
|
||||
// Get random template
|
||||
const randomIndex = Math.floor(Math.random() * sieConfigTemplates.length);
|
||||
const index =
|
||||
randomIndex === lastTemplateIndex ? (randomIndex + 1) % sieConfigTemplates.length : randomIndex;
|
||||
const template = sieConfigTemplates[index] ?? configTemplate1;
|
||||
|
||||
// Take the first two after shuffling.
|
||||
const socialTargets = availableSocialTargets
|
||||
.slice()
|
||||
.sort(() => 0.5 - Math.random())
|
||||
.slice(0, 2);
|
||||
|
||||
return {
|
||||
template: {
|
||||
...template,
|
||||
socialTargets,
|
||||
},
|
||||
templateIndex: index,
|
||||
};
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import type { SignInExperience } from '@logto/schemas';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
import type { OnboardingSieConfig } from '@/onboarding/types';
|
||||
import { Authentication } from '@/onboarding/types';
|
||||
|
@ -9,8 +10,10 @@ const signInExperienceToOnboardSieConfig = (
|
|||
): OnboardingSieConfig => {
|
||||
const {
|
||||
color: { primaryColor },
|
||||
branding: { logoUrl: logo },
|
||||
signIn: { methods: signInMethods },
|
||||
signUp: { identifiers: signUpIdentifiers },
|
||||
socialSignInConnectorTargets,
|
||||
} = signInExperience;
|
||||
|
||||
const identifier =
|
||||
|
@ -28,9 +31,11 @@ const signInExperienceToOnboardSieConfig = (
|
|||
}, []);
|
||||
|
||||
return {
|
||||
logo,
|
||||
color: primaryColor,
|
||||
identifier,
|
||||
authentications,
|
||||
socialTargets: socialSignInConnectorTargets,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -38,16 +43,19 @@ const onboardSieConfigToSignInExperience = (
|
|||
config: OnboardingSieConfig,
|
||||
basedConfig: SignInExperience
|
||||
): SignInExperience => {
|
||||
const { color: onboardConfigColor, identifier, authentications } = config;
|
||||
const { color: baseColorConfig } = basedConfig;
|
||||
const { logo, color: onboardConfigColor, identifier, authentications, socialTargets } = config;
|
||||
const { color: baseColorConfig, branding: baseBranding } = basedConfig;
|
||||
|
||||
const isPasswordSetup = authentications.includes(Authentication.Password);
|
||||
const isVerificationCodeSetup =
|
||||
authentications.includes(Authentication.VerificationCode) &&
|
||||
identifier !== SignInIdentifier.Username;
|
||||
const isVerificationCodeSetup = identifier !== SignInIdentifier.Username;
|
||||
|
||||
const signInExperience = {
|
||||
const signInExperience: SignInExperience = {
|
||||
...basedConfig,
|
||||
branding: {
|
||||
...baseBranding,
|
||||
logoUrl: conditional(logo?.length && logo),
|
||||
darkLogoUrl: conditional(logo?.length && logo),
|
||||
},
|
||||
color: {
|
||||
...baseColorConfig,
|
||||
primaryColor: onboardConfigColor,
|
||||
|
@ -67,6 +75,7 @@ const onboardSieConfigToSignInExperience = (
|
|||
},
|
||||
],
|
||||
},
|
||||
socialSignInConnectorTargets: socialTargets ?? [],
|
||||
};
|
||||
|
||||
return signInExperience;
|
||||
|
|
|
@ -2,11 +2,11 @@ import Building from '@/assets/images/building.svg';
|
|||
import Cloud from '@/assets/images/cloud.svg';
|
||||
import Database from '@/assets/images/database.svg';
|
||||
import Pizza from '@/assets/images/pizza.svg';
|
||||
import type { Option as SelectorOption } from '@/onboarding/components/CardSelector';
|
||||
import type { CardSelectorOption } from '@/onboarding/components/CardSelector';
|
||||
|
||||
import { DeploymentType, Project } from '../../types';
|
||||
|
||||
export const projectOptions: SelectorOption[] = [
|
||||
export const projectOptions: CardSelectorOption[] = [
|
||||
{
|
||||
icon: <Pizza />,
|
||||
title: 'cloud.welcome.project_options.personal',
|
||||
|
@ -19,7 +19,7 @@ export const projectOptions: SelectorOption[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export const deploymentTypeOptions: SelectorOption[] = [
|
||||
export const deploymentTypeOptions: CardSelectorOption[] = [
|
||||
{
|
||||
icon: <Database />,
|
||||
title: 'cloud.welcome.deployment_type_options.open_source',
|
||||
|
|
|
@ -72,7 +72,9 @@ export enum Authentication {
|
|||
}
|
||||
|
||||
export type OnboardingSieConfig = {
|
||||
logo?: string;
|
||||
color: string;
|
||||
identifier: SignInIdentifier;
|
||||
authentications: Authentication[];
|
||||
socialTargets?: string[];
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
|
||||
import { generateStandardId } from '@logto/core-kit';
|
||||
import type { UserAssetsResponse } from '@logto/schemas';
|
||||
import type { UserAssets } from '@logto/schemas';
|
||||
import {
|
||||
userAssetsGuard,
|
||||
userAssetsServiceStatusGuard,
|
||||
|
@ -81,7 +81,7 @@ export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: Ro
|
|||
publicUrl: storageProviderConfig.publicUrl,
|
||||
});
|
||||
|
||||
const result: UserAssetsResponse = {
|
||||
const result: UserAssets = {
|
||||
url,
|
||||
};
|
||||
|
||||
|
|
|
@ -104,6 +104,12 @@ describe('smoke testing for cloud', () => {
|
|||
});
|
||||
|
||||
it('can complete the sie configuration process and enter the congrats page', async () => {
|
||||
// Wait for the sie config to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select username as the identifier
|
||||
await expect(page).toClick('div[role=radio]:has(input[name=identifier][value=username])');
|
||||
|
||||
// Click the finish button
|
||||
await expect(page).toClick('div[class$=continueActions] button:last-child');
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -50,6 +50,7 @@ const general = {
|
|||
try_now: 'Try Now',
|
||||
multiple_form_field: '(Multiple)',
|
||||
cap_limit: 'Cap limit',
|
||||
trial: 'Trail',
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -51,6 +51,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -50,6 +50,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -51,6 +51,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -50,6 +50,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -51,6 +51,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -50,6 +50,7 @@ const general = {
|
|||
try_now: 'Try Now', // UNTRANSLATED
|
||||
multiple_form_field: '(Multiple)', // UNTRANSLATED
|
||||
cap_limit: 'Cap limit', // UNTRANSLATED
|
||||
trial: 'Trail', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default general;
|
||||
|
|
|
@ -24,10 +24,10 @@ export const userAssetsServiceStatusGuard = z.object({
|
|||
maxUploadFileSize: z.number().optional(),
|
||||
});
|
||||
|
||||
export type UserAssetsServiceStatusResponse = z.infer<typeof userAssetsServiceStatusGuard>;
|
||||
export type UserAssetsServiceStatus = z.infer<typeof userAssetsServiceStatusGuard>;
|
||||
|
||||
export const userAssetsGuard = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type UserAssetsResponse = z.infer<typeof userAssetsGuard>;
|
||||
export type UserAssets = z.infer<typeof userAssetsGuard>;
|
||||
|
|
Loading…
Reference in a new issue