mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(console): support input form fields in get-started steps
This commit is contained in:
parent
09703896f3
commit
3ddc177f8e
17 changed files with 294 additions and 144 deletions
|
@ -6,7 +6,7 @@ subtitle: 2 steps
|
|||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
```multitextinput
|
||||
```redirectUris
|
||||
Redirect URI
|
||||
```
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ subtitle: 1 steps
|
|||
---
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
```multitextinput
|
||||
```postLogoutRedirectUris
|
||||
Post sign out redirect URI
|
||||
```
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ subtitle: 2 steps
|
|||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
```multitextinput
|
||||
```redirectUris
|
||||
Redirect URI
|
||||
```
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ subtitle: 1 steps
|
|||
---
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
```multitextinput
|
||||
```postLogoutRedirectUris
|
||||
Post sign out redirect URI
|
||||
```
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DangerousRaw from '../DangerousRaw';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
title: I18nKey;
|
||||
title: I18nKey | ReactElement<typeof DangerousRaw>;
|
||||
children: ReactNode;
|
||||
isRequired?: boolean;
|
||||
className?: string;
|
||||
|
@ -18,7 +19,7 @@ const FormField = ({ title, children, isRequired, className }: Props) => {
|
|||
return (
|
||||
<div className={classNames(styles.field, className)}>
|
||||
<div className={styles.headline}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
<div className={styles.title}>{typeof title === 'string' ? t(title) : title}</div>
|
||||
{isRequired && <div className={styles.required}>{t('admin_console.form.required')}</div>}
|
||||
</div>
|
||||
{children}
|
||||
|
|
|
@ -128,7 +128,7 @@ const ApplicationDetails = () => {
|
|||
required: t('application_details.redirect_uri_required'),
|
||||
pattern: {
|
||||
regex: noSpaceRegex,
|
||||
message: t('application_details.no_space_in_uri'),
|
||||
message: t('errors.no_space_in_uri'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
|
@ -153,7 +153,7 @@ const ApplicationDetails = () => {
|
|||
validate: createValidatorForRhf({
|
||||
pattern: {
|
||||
regex: noSpaceRegex,
|
||||
message: t('application_details.no_space_in_uri'),
|
||||
message: t('errors.no_space_in_uri'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Application, ApplicationType, Setting } from '@logto/schemas';
|
|||
import React, { useState } from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
|
@ -11,11 +10,10 @@ import ModalLayout from '@/components/ModalLayout';
|
|||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import GetStarted from '@/pages/GetStarted';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { applicationTypeI18nKey, SupportedJavascriptLibraries } from '@/types/applications';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
|
||||
import LibrarySelector from '../LibrarySelector';
|
||||
import GetStartedModal from '../GetStartedModal';
|
||||
import TypeDescription from '../TypeDescription';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -31,7 +29,7 @@ type Props = {
|
|||
|
||||
const CreateForm = ({ onClose }: Props) => {
|
||||
const [createdApp, setCreatedApp] = useState<Application>();
|
||||
const [isQuickStartGuideOpen, setIsQuickStartGuideOpen] = useState(false);
|
||||
const [isGetStartedModalOpen, setIsGetStartedModalOpen] = useState(false);
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
|
@ -48,7 +46,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
const isGetStartedSkipped = setting?.adminConsole.applicationSkipGetStarted;
|
||||
|
||||
const closeModal = () => {
|
||||
setIsQuickStartGuideOpen(false);
|
||||
setIsGetStartedModalOpen(false);
|
||||
onClose?.(createdApp);
|
||||
};
|
||||
|
||||
|
@ -63,10 +61,29 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
if (isGetStartedSkipped) {
|
||||
closeModal();
|
||||
} else {
|
||||
setIsQuickStartGuideOpen(true);
|
||||
setIsGetStartedModalOpen(true);
|
||||
}
|
||||
});
|
||||
|
||||
const onComplete = async (data: GetStartedForm) => {
|
||||
if (!createdApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const application = await api
|
||||
.patch(`/api/applications/${createdApp.id}`, {
|
||||
json: {
|
||||
oidcClientMetadata: {
|
||||
redirectUris: data.redirectUris.filter(Boolean),
|
||||
postLogoutRedirectUris: data.postLogoutRedirectUris.filter(Boolean),
|
||||
},
|
||||
},
|
||||
})
|
||||
.json<Application>();
|
||||
setCreatedApp(application);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
title="applications.create"
|
||||
|
@ -108,17 +125,12 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
</FormField>
|
||||
</form>
|
||||
{!isGetStartedSkipped && createdApp && (
|
||||
<Modal isOpen={isQuickStartGuideOpen} className={modalStyles.fullScreen}>
|
||||
<GetStarted
|
||||
bannerComponent={<LibrarySelector />}
|
||||
title={createdApp.name}
|
||||
subtitle="applications.get_started.header_description"
|
||||
type="application"
|
||||
defaultSubtype={SupportedJavascriptLibraries.React}
|
||||
onClose={closeModal}
|
||||
onComplete={closeModal}
|
||||
/>
|
||||
</Modal>
|
||||
<GetStartedModal
|
||||
appName={createdApp.name}
|
||||
isOpen={isGetStartedModalOpen}
|
||||
onClose={closeModal}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
)}
|
||||
</ModalLayout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import GetStarted from '@/pages/GetStarted';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { SupportedJavascriptLibraries } from '@/types/applications';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
|
||||
import LibrarySelector from '../LibrarySelector';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onComplete: (data: GetStartedForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
|
||||
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
|
||||
<GetStarted
|
||||
bannerComponent={<LibrarySelector />}
|
||||
title={appName}
|
||||
subtitle="applications.get_started.header_description"
|
||||
type="application"
|
||||
defaultSubtype={SupportedJavascriptLibraries.React}
|
||||
onClose={onClose}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export default GetStartedModal;
|
|
@ -31,6 +31,7 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 0 0 56px;
|
||||
height: 56px;
|
||||
background: var(--color-neutral-variant-90);
|
||||
border-radius: _.unit(2);
|
||||
padding: 0 _.unit(4);
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import React, { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react.js';
|
||||
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import FormField from '@/components/FormField';
|
||||
import MultiTextInput from '@/components/MultiTextInput';
|
||||
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
import { noSpaceRegex } from '@/utilities/regex';
|
||||
|
||||
type Props = PropsWithChildren<CodeProps> & { onError: () => void };
|
||||
|
||||
const CodeComponentRenderer = ({ className, children, onError }: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<GetStartedForm>();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [, codeBlockType] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
const content = String(children);
|
||||
|
||||
/** Code block types defined in markdown. E.g.
|
||||
* ```typescript
|
||||
* some code
|
||||
* ```
|
||||
* These two custom code block types should be replaced with `MultiTextInput` component:
|
||||
* 'redirectUris' and 'postLogoutRedirectUris'
|
||||
*/
|
||||
const isMultilineInput =
|
||||
codeBlockType === 'redirectUris' || codeBlockType === 'postLogoutRedirectUris';
|
||||
|
||||
const firstErrorKey = Object.keys(errors)[0];
|
||||
const isFirstErrorField = firstErrorKey && firstErrorKey === codeBlockType;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstErrorField) {
|
||||
onError();
|
||||
}
|
||||
}, [isFirstErrorField, onError]);
|
||||
|
||||
if (isMultilineInput) {
|
||||
return (
|
||||
<FormField isRequired title={<DangerousRaw>{content}</DangerousRaw>}>
|
||||
<Controller
|
||||
name={codeBlockType}
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
validate: createValidatorForRhf({
|
||||
required: t('errors.required_field_missing_plural', { field: content }),
|
||||
pattern: {
|
||||
regex: noSpaceRegex,
|
||||
message: t('errors.no_space_in_uri'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<div ref={ref}>
|
||||
<MultiTextInput
|
||||
value={value}
|
||||
error={convertRhfErrorMessage(error?.message)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
return <CodeEditor isReadonly language={codeBlockType} value={content} />;
|
||||
};
|
||||
|
||||
export default CodeComponentRenderer;
|
|
@ -6,7 +6,7 @@
|
|||
flex-direction: column;
|
||||
scroll-margin: _.unit(5);
|
||||
|
||||
.cardHeader {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -48,6 +48,16 @@
|
|||
justify-content: flex-end;
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.expanded {
|
||||
max-height: 9999px;
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card + .card {
|
||||
|
|
|
@ -1,41 +1,65 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import React, { forwardRef, Ref } from 'react';
|
||||
import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react.js';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
|
||||
import Tick from '@/icons/Tick';
|
||||
import { StepMetadata } from '@/types/get-started';
|
||||
|
||||
import CodeComponentRenderer from '../CodeComponentRenderer';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export type StepMetadata = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
metadata: string; // Markdown formatted string
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: StepMetadata;
|
||||
index: number;
|
||||
isCompleted: boolean;
|
||||
isExpanded: boolean;
|
||||
isActive: boolean;
|
||||
isComplete: boolean;
|
||||
isFinalStep: boolean;
|
||||
onComplete?: () => void;
|
||||
onNext?: () => void;
|
||||
onToggle?: () => void;
|
||||
};
|
||||
|
||||
const Step = (
|
||||
{ data, index, isCompleted, isExpanded, isFinalStep, onComplete, onNext, onToggle }: Props,
|
||||
ref?: Ref<HTMLDivElement>
|
||||
) => {
|
||||
const Step = ({ data, index, isActive, isComplete, isFinalStep, onNext }: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { title, subtitle, metadata } = data;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToStep = useCallback(() => {
|
||||
ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}, []);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
setIsExpanded(true);
|
||||
scrollToStep();
|
||||
}, [scrollToStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
scrollToStep();
|
||||
}
|
||||
}, [isExpanded, scrollToStep]);
|
||||
|
||||
const memoizedComponents = useMemo(
|
||||
() => ({
|
||||
code: ({ ...props }: PropsWithChildren<CodeProps>) => (
|
||||
<CodeComponentRenderer {...props} onError={onError} />
|
||||
),
|
||||
}),
|
||||
[onError]
|
||||
);
|
||||
|
||||
// Steps in get-started must have "title" declared in the Yaml header of the markdown source file
|
||||
if (!title) {
|
||||
|
@ -43,18 +67,22 @@ const Step = (
|
|||
}
|
||||
|
||||
// TODO: add more styles to markdown renderer
|
||||
// TODO: render form and input fields in steps
|
||||
return (
|
||||
<Card key={title} ref={ref} className={styles.card}>
|
||||
<div className={styles.cardHeader} onClick={onToggle}>
|
||||
<div
|
||||
className={styles.header}
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.index,
|
||||
isExpanded && styles.active,
|
||||
isCompleted && styles.completed
|
||||
isActive && styles.active,
|
||||
isComplete && styles.completed
|
||||
)}
|
||||
>
|
||||
{isCompleted ? <Tick /> : index + 1}
|
||||
{isComplete ? <Tick /> : index + 1}
|
||||
</div>
|
||||
<CardTitle
|
||||
size="medium"
|
||||
|
@ -64,38 +92,21 @@ const Step = (
|
|||
<Spacer />
|
||||
<IconButton>{isExpanded ? <ArrowUp /> : <ArrowDown />}</IconButton>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<>
|
||||
<ReactMarkdown
|
||||
className={styles.markdownContent}
|
||||
components={{
|
||||
code: ({ className, children }) => {
|
||||
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
const content = String(children);
|
||||
|
||||
return <CodeEditor isReadonly language={language} value={content} />;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{metadata}
|
||||
</ReactMarkdown>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
type="primary"
|
||||
title={`general.${isFinalStep ? 'done' : 'next'}`}
|
||||
onClick={() => {
|
||||
if (isFinalStep) {
|
||||
onComplete?.();
|
||||
} else {
|
||||
onNext?.();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
|
||||
<ReactMarkdown className={styles.markdownContent} components={memoizedComponents}>
|
||||
{metadata}
|
||||
</ReactMarkdown>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
htmlType={isFinalStep ? 'submit' : 'button'}
|
||||
type="primary"
|
||||
title={`general.${isFinalStep ? 'done' : 'next'}`}
|
||||
onClick={conditional(!isFinalStep && onNext)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Step);
|
||||
export default Step;
|
||||
|
|
|
@ -3,10 +3,9 @@ import { useMemo } from 'react';
|
|||
// eslint-disable-next-line node/file-extension-in-import
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
import { StepMetadata } from '@/types/get-started';
|
||||
import { parseMarkdownWithYamlFrontmatter } from '@/utilities/markdown';
|
||||
|
||||
import { StepMetadata } from '../components/Step';
|
||||
|
||||
type DocumentFileNames = {
|
||||
files: string[];
|
||||
};
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
import { AdminConsoleKey } from '@logto/phrases';
|
||||
import { Nullable } from '@silverhand/essentials';
|
||||
import React, {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { cloneElement, isValidElement, PropsWithChildren, ReactNode, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
|
@ -16,6 +8,7 @@ import DangerousRaw from '@/components/DangerousRaw';
|
|||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import Close from '@/icons/Close';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
|
||||
import Step from './components/Step';
|
||||
import { GetStartedType, useGetStartedSteps } from './hooks';
|
||||
|
@ -31,8 +24,7 @@ type Props = PropsWithChildren<{
|
|||
defaultSubtype?: string;
|
||||
bannerComponent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
onComplete?: () => void;
|
||||
onToggleSteps?: () => void;
|
||||
onComplete?: (data: GetStartedForm) => Promise<void>;
|
||||
}>;
|
||||
|
||||
const onClickFetchSampleProject = (projectName: string) => {
|
||||
|
@ -48,23 +40,22 @@ const GetStarted = ({
|
|||
bannerComponent,
|
||||
onClose,
|
||||
onComplete,
|
||||
onToggleSteps,
|
||||
}: Props) => {
|
||||
const [subtype, setSubtype] = useState(defaultSubtype);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState<number>(-1);
|
||||
const steps = useGetStartedSteps(type, subtype) ?? [];
|
||||
const methods = useForm<GetStartedForm>({ reValidateMode: 'onBlur' });
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
} = methods;
|
||||
|
||||
const stepReferences = useRef<Array<Nullable<HTMLDivElement>>>(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
Array.from<null>({ length: steps.length }).fill(null)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeStepIndex > -1) {
|
||||
const activeStepRef = stepReferences.current[activeStepIndex];
|
||||
activeStepRef?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
}, [activeStepIndex, stepReferences]);
|
||||
void onComplete?.(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
@ -79,7 +70,7 @@ const GetStarted = ({
|
|||
{subtype && (
|
||||
<Button
|
||||
type="outline"
|
||||
title="admin_console.applications.get_started.get_sample_file"
|
||||
title="admin_console.get_started.get_sample_file"
|
||||
onClick={() => {
|
||||
onClickFetchSampleProject(subtype);
|
||||
}}
|
||||
|
@ -87,40 +78,35 @@ const GetStarted = ({
|
|||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{isValidElement(bannerComponent) &&
|
||||
cloneElement(bannerComponent, {
|
||||
className: styles.banner,
|
||||
onChange: setSubtype,
|
||||
onToggle: () => {
|
||||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
{steps.map((step, index) => {
|
||||
const isFinalStep = index === steps.length - 1;
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={onSubmit}>
|
||||
{isValidElement(bannerComponent) &&
|
||||
cloneElement(bannerComponent, {
|
||||
className: styles.banner,
|
||||
onChange: setSubtype,
|
||||
onToggle: () => {
|
||||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
{steps.map((step, index) => {
|
||||
const isFinalStep = index === steps.length - 1;
|
||||
|
||||
return (
|
||||
<Step
|
||||
key={step.title}
|
||||
ref={(element) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
stepReferences.current[index] = element;
|
||||
}}
|
||||
data={step}
|
||||
index={index}
|
||||
isCompleted={activeStepIndex > index}
|
||||
isExpanded={activeStepIndex === index}
|
||||
isFinalStep={isFinalStep}
|
||||
onComplete={onComplete}
|
||||
onNext={() => {
|
||||
setActiveStepIndex(index + 1);
|
||||
}}
|
||||
onToggle={() => {
|
||||
setActiveStepIndex(index);
|
||||
onToggleSteps?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Step
|
||||
key={step.title}
|
||||
data={step}
|
||||
index={index}
|
||||
isActive={activeStepIndex === index}
|
||||
isComplete={activeStepIndex > index}
|
||||
isFinalStep={isFinalStep}
|
||||
onNext={() => {
|
||||
setActiveStepIndex(index + 1);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
11
packages/console/src/types/get-started.ts
Normal file
11
packages/console/src/types/get-started.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export type StepMetadata = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
metadata: string; // Markdown formatted string
|
||||
};
|
||||
|
||||
export type GetStartedForm = {
|
||||
redirectUris: string[];
|
||||
postLogoutRedirectUris: string[];
|
||||
connectorConfigJson: string;
|
||||
};
|
|
@ -51,6 +51,9 @@ const translation = {
|
|||
unknown_server_error: 'Unknown server error occurred.',
|
||||
empty: 'No Data',
|
||||
missing_total_number: 'Unable to find Total-Number in response headers.',
|
||||
no_space_in_uri: 'Space is not allowed in URI',
|
||||
required_field_missing: 'Please enter {{field}}',
|
||||
required_field_missing_plural: 'You have to enter at least one {{field}}',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: 'Overview',
|
||||
|
@ -103,7 +106,6 @@ const translation = {
|
|||
get_started: {
|
||||
header_description:
|
||||
'Follow a step by step guide to integrate your application or get a sample configured with your account settings',
|
||||
get_sample_file: 'Get a sample file',
|
||||
title: 'Congratulations! The application has been created successfully.',
|
||||
subtitle:
|
||||
'Now follow the steps below to finish your app settings. Please select the JS library to continue.',
|
||||
|
@ -138,7 +140,6 @@ const translation = {
|
|||
application_deleted: 'The application {{name}} deleted.',
|
||||
save_success: 'Saved!',
|
||||
redirect_uri_required: 'You have to enter at least one redirect URI.',
|
||||
no_space_in_uri: 'Space is not allowed in URI',
|
||||
},
|
||||
api_resources: {
|
||||
title: 'API Resources',
|
||||
|
@ -211,6 +212,9 @@ const translation = {
|
|||
more_options: 'MORE OPTIONS',
|
||||
connector_deleted: 'The connector has been deleted.',
|
||||
},
|
||||
get_started: {
|
||||
get_sample_file: 'Get a sample file',
|
||||
},
|
||||
users: {
|
||||
title: 'User Management',
|
||||
subtitle:
|
||||
|
|
|
@ -53,6 +53,9 @@ const translation = {
|
|||
unknown_server_error: '服务器发生未知错误。',
|
||||
empty: '没有数据',
|
||||
missing_total_number: '无法从返回的头部信息中找到 Total-Number。',
|
||||
no_space_in_uri: 'URI 中不能包含空格',
|
||||
required_field_missing: '请输入{{field}}',
|
||||
required_field_missing_plural: '{{field}}不能全部为空',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: '概览',
|
||||
|
@ -105,7 +108,6 @@ const translation = {
|
|||
get_started: {
|
||||
header_description:
|
||||
'参考如下教程,将 Logto 集成到您的应用中。您也可以点击右侧链接,获取我们为您准备好的示范工程。',
|
||||
get_sample_file: '获取示范工程',
|
||||
title: '恭喜!您的应用已成功创建。',
|
||||
subtitle: '请参考以下步骤完成您的应用设置。首先,请选择您要使用的 Javascript 框架:',
|
||||
description_by_library: '本教程向您演示如何在 {{library}} 应用中集成 Logto 登录功能',
|
||||
|
@ -138,7 +140,6 @@ const translation = {
|
|||
application_deleted: 'The application {{name}} deleted.',
|
||||
save_success: 'Saved!',
|
||||
redirect_uri_required: 'You have to enter at least one redirect URI.',
|
||||
no_space_in_uri: 'Space is not allowed in URI',
|
||||
},
|
||||
api_resources: {
|
||||
title: 'API Resources',
|
||||
|
@ -211,6 +212,9 @@ const translation = {
|
|||
more_options: '更多选项',
|
||||
connector_deleted: '成功删除连接器。',
|
||||
},
|
||||
get_started: {
|
||||
get_sample_file: '获取示例工程',
|
||||
},
|
||||
users: {
|
||||
title: '用户管理',
|
||||
subtitle: '管理已注册用户, 创建新用户,编辑用户资料。',
|
||||
|
|
Loading…
Add table
Reference in a new issue