diff --git a/.gitignore b/.gitignore index 9132715ca..302bee1a0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ node_modules /packages/*/lib /packages/*/dist +# docs copied to admin console +/packages/console/**/*.mdx + # logs logs *.log* diff --git a/packages/console/package.json b/packages/console/package.json index 55ef5bbbf..338219ee0 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -8,7 +8,7 @@ "scripts": { "preinstall": "npx only-allow pnpm", "precommit": "lint-staged", - "copyfiles": "copyfiles -u 1 public/**/*.* dist", + "copyfiles": "copyfiles -u 2 \"../docs/**/*.mdx\" src/assets", "start": "pnpm copyfiles && parcel src/index.html", "dev": "pnpm copyfiles && PORT=5002 parcel src/index.html --public-url /console --no-cache --hmr-port 6002", "check": "tsc --noEmit", @@ -21,17 +21,40 @@ "@logto/phrases": "^0.1.0", "@logto/react": "^0.1.3", "@logto/schemas": "^0.1.0", + "@mdx-js/react": "^1.6.22", "@monaco-editor/react": "^4.3.1", + "@parcel/core": "^2.5.0", + "@parcel/transformer-mdx": "^2.5.0", + "@parcel/transformer-sass": "^2.5.0", + "@silverhand/eslint-config": "^0.10.2", + "@silverhand/eslint-config-react": "^0.10.3", "@silverhand/essentials": "^1.1.6", + "@silverhand/ts-config": "^0.10.2", + "@silverhand/ts-config-react": "^0.10.3", + "@tsconfig/docusaurus": "^1.0.5", + "@types/lodash.kebabcase": "^4.1.6", + "@types/mdx": "^2.0.1", + "@types/mdx-js__react": "^1.5.5", + "@types/react": "^17.0.14", + "@types/react-dom": "^17.0.9", + "@types/react-modal": "^3.13.1", "classnames": "^2.3.1", + "copyfiles": "^2.4.1", "csstype": "^3.0.11", "dnd-core": "^16.0.0", + "eslint": "^8.10.0", "i18next": "^21.6.12", "i18next-browser-languagedetector": "^6.1.3", "ky": "^0.30.0", + "lint-staged": "^12.0.0", "lodash.kebabcase": "^4.1.1", "monaco-editor": "^0.33.0", "nanoid": "^3.1.23", + "parcel": "^2.5.0", + "postcss": "^8.4.6", + "postcss-modules": "^4.3.0", + "prettier": "^2.3.2", + "process": "^0.11.10", "react": "^17.0.2", "react-dnd": "^16.0.0", "react-dnd-html5-backend": "^16.0.0", @@ -44,30 +67,13 @@ "react-paginate": "^8.1.2", "react-router-dom": "^6.2.2", "remark-gfm": "^3.0.1", - "swr": "^1.2.2", - "@parcel/core": "^2.3.2", - "@parcel/transformer-sass": "^2.3.2", - "@silverhand/eslint-config": "^0.10.2", - "@silverhand/eslint-config-react": "^0.10.3", - "@silverhand/ts-config": "^0.10.2", - "@silverhand/ts-config-react": "^0.10.3", - "@types/lodash.kebabcase": "^4.1.6", - "@types/react": "^17.0.14", - "@types/react-dom": "^17.0.9", - "@types/react-modal": "^3.13.1", - "copyfiles": "^2.4.1", - "eslint": "^8.10.0", - "lint-staged": "^12.0.0", - "parcel": "^2.3.2", - "postcss": "^8.4.6", - "postcss-modules": "^4.3.0", - "prettier": "^2.3.2", - "process": "^0.11.10", "stylelint": "^13.13.1", + "swr": "^1.2.2", "typescript": "^4.6.2" }, "alias": { - "@/*": "./src/$1" + "@/*": "./src/$1", + "@theme/*": "./src/mdx-components/$1" }, "eslintConfig": { "extends": "@silverhand/react" diff --git a/packages/console/public/get-started/application/react/en-us/index.json b/packages/console/public/get-started/application/react/en-us/index.json deleted file mode 100644 index 3c62211cb..000000000 --- a/packages/console/public/get-started/application/react/en-us/index.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"] -} diff --git a/packages/console/public/get-started/application/react/en-us/step1.md b/packages/console/public/get-started/application/react/en-us/step1.md deleted file mode 100644 index 83edd3ff2..000000000 --- a/packages/console/public/get-started/application/react/en-us/step1.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Install Logto SDK -subtitle: 1 step ---- -## Option 1: Install your SDK dependency - -Run the CLI command under your project's root directory. - -``` -// installation with npm -npm install @logto/react --save - -// installation with yarn -yarn add @logto/react - -// installation with pnpm -pnpm install @logto/react --save -``` - -## Option 2: Add script tag to your HTML - -``` - -``` - -## Option 3: Fork your own from github - -``` -git clone https://github.com/logto-io/js.git -``` - -``` -pnpm build -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/en-us/step2.md b/packages/console/public/get-started/application/react/en-us/step2.md deleted file mode 100644 index 277c5798d..000000000 --- a/packages/console/public/get-started/application/react/en-us/step2.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Initiate LogtoClient -subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ---- -Add the following code to your main html file. You may need client ID and authorization domain. - -```typescript -import { LogtoProvider, LogtoConfig } from '@logto/react'; -import React from 'react'; - -... - -const App = () => { - const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' } - - return ( - - - - } /> - } /> - - - - } - /> - - - - ); -}; -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/en-us/step3.md b/packages/console/public/get-started/application/react/en-us/step3.md deleted file mode 100644 index f8a4afc91..000000000 --- a/packages/console/public/get-started/application/react/en-us/step3.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Sign In -subtitle: 2 steps ---- -## Step 1: Setup your login - -The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI - -```redirectUris -Redirect URI -``` - -Add the following code to your web app - -```typescript -import React from "react"; -import { useLogto } from '@logto/react'; - -const SignInButton = () => { - const { signIn } = useLogto(); - const redirectUrl = window.location.origin + '/callback'; - - return ; -}; - -export default SignInButton; -``` - -## Step 2: Retrieve Auth Status - -```typescript -import React from "react"; -import { useLogto } from '@logto/react'; - -const App = () => { - const { isAuthenticated, signIn } = useLogto(); - - if !(isAuthenticated) { - return - } - - return <> - - - -}; -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/en-us/step4.md b/packages/console/public/get-started/application/react/en-us/step4.md deleted file mode 100644 index 0df80c5c8..000000000 --- a/packages/console/public/get-started/application/react/en-us/step4.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Sign Out -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. - -```postLogoutRedirectUris -Post sign out redirect URI -``` - -Add the following code to your web app - -```typescript -import React from "react"; -import { useLogto } from '@logto/react'; - -const SignOutButton = () => { - const { signOut } = useLogto(); - - return ( - - ); -}; - -export default SignOutButton; -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/en-us/step5.md b/packages/console/public/get-started/application/react/en-us/step5.md deleted file mode 100644 index 481ac231d..000000000 --- a/packages/console/public/get-started/application/react/en-us/step5.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Advanced Settings & Documentation Links -subtitle: 2 steps ---- -## Step 1: Advanced Settings - -Go to application details page and switch to advanced settings tab - -## Step 2: Now check out the documentation links below - -[- SDK Documentation](https://link-url-here.org) - -[- OIDC Documentation](https://link-url-here.org) - -[- Calling API to fetch accessToken](https://link-url-here.org) \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/zh-cn/index.json b/packages/console/public/get-started/application/react/zh-cn/index.json deleted file mode 100644 index 3c62211cb..000000000 --- a/packages/console/public/get-started/application/react/zh-cn/index.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"] -} diff --git a/packages/console/public/get-started/application/react/zh-cn/step1.md b/packages/console/public/get-started/application/react/zh-cn/step1.md deleted file mode 100644 index 2ae1000c8..000000000 --- a/packages/console/public/get-started/application/react/zh-cn/step1.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: 安装 Logto SDK -subtitle: 1 step ---- -## 方案1: 安装 SDK 依赖包 - -Run the CLI command under your project's root directory. - -``` -// installation with npm -npm install @logto/react --save - -// installation with yarn -yarn add @logto/react - -// installation with pnpm -pnpm install @logto/react --save -``` - -## 方案2: Add script tag to your HTML - -``` - -``` - -## 方案3: Fork your own from github - -``` -git clone https://github.com/logto-io/js.git -``` - -``` -pnpm build -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/zh-cn/step2.md b/packages/console/public/get-started/application/react/zh-cn/step2.md deleted file mode 100644 index 95b8356ac..000000000 --- a/packages/console/public/get-started/application/react/zh-cn/step2.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: 初始化 LogtoClient -subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ---- -Add the following code to your main html file. You may need client ID and authorization domain. - -```typescript -import { LogtoProvider, LogtoConfig } from '@logto/react'; -import React from 'react'; - -... - -const App = () => { - const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' } - - return ( - - - - } /> - } /> - - - - } - /> - - - - ); -}; -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/zh-cn/step3.md b/packages/console/public/get-started/application/react/zh-cn/step3.md deleted file mode 100644 index c59cad0e9..000000000 --- a/packages/console/public/get-started/application/react/zh-cn/step3.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: 登录 -subtitle: 2 steps ---- -## Step 1: Setup your login - -The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI - -```redirectUris -Redirect URI -``` - -Add the following code to your web app - -```typescript -import React from "react"; -import { useLogto } from '@logto/react'; - -const SignInButton = () => { - const { signIn } = useLogto(); - const redirectUrl = window.location.origin + '/callback'; - - return ; -}; - -export default SignInButton; -``` - -## Step 2: Retrieve Auth Status - -```typescript -import React from "react"; -import { useLogto } from '@logto/react'; - -const App = () => { - const { isAuthenticated, signIn } = useLogto(); - - if !(isAuthenticated) { - return - } - - return <> - - - -}; -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/zh-cn/step4.md b/packages/console/public/get-started/application/react/zh-cn/step4.md deleted file mode 100644 index 52dbb2f4b..000000000 --- a/packages/console/public/get-started/application/react/zh-cn/step4.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: 登出 -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. - -```postLogoutRedirectUris -Post sign out redirect URI -``` - -Add the following code to your web app - -```typescript -import React from "react"; -import { useLogto } from '@logto/react'; - -const SignOutButton = () => { - const { signOut } = useLogto(); - - return ( - - ); -}; - -export default SignOutButton; -``` \ No newline at end of file diff --git a/packages/console/public/get-started/application/react/zh-cn/step5.md b/packages/console/public/get-started/application/react/zh-cn/step5.md deleted file mode 100644 index bed6491ee..000000000 --- a/packages/console/public/get-started/application/react/zh-cn/step5.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: 高级设置以及相关文档链接 -subtitle: 2 steps ---- -## Step 1: Advanced Settings - -Go to application details page and switch to advanced settings tab - -## Step 2: Now check out the documentation links below - -[- SDK Documentation](https://link-url-here.org) - -[- OIDC Documentation](https://link-url-here.org) - -[- Calling API to fetch accessToken](https://link-url-here.org) \ No newline at end of file diff --git a/packages/console/src/mdx-components/TabItem/index.tsx b/packages/console/src/mdx-components/TabItem/index.tsx new file mode 100644 index 000000000..827acafad --- /dev/null +++ b/packages/console/src/mdx-components/TabItem/index.tsx @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { ReactNode } from 'react'; + +export type Props = { + children: ReactNode; + className?: string; + value: string; + label?: string; +}; + +const TabItem = ({ children, ...rest }: Props): JSX.Element => { + return ( +
+ {children} +
+ ); +}; + +export default TabItem; diff --git a/packages/console/src/mdx-components/Tabs/index.tsx b/packages/console/src/mdx-components/Tabs/index.tsx new file mode 100644 index 000000000..1f78b967e --- /dev/null +++ b/packages/console/src/mdx-components/Tabs/index.tsx @@ -0,0 +1,108 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Nullable } from '@silverhand/essentials'; +import React, { useState, isValidElement, type ReactElement, cloneElement } from 'react'; + +import type { Props as TabItemProps } from '../TabItem'; + +type Props = { + className?: string; + children: ReactElement; +}; + +// A very rough duck type, but good enough to guard against mistakes while +// allowing customization +function isTabItem(comp: ReactElement): comp is ReactElement { + return typeof comp.props.value !== 'undefined'; +} + +const Tabs = ({ className, children }: Props): JSX.Element => { + const verifiedChildren = React.Children.map(children, (child) => { + if (isValidElement(child) && isTabItem(child)) { + return child; + } + }); + + const values = + // Only pick keys that we recognize. MDX would inject some keys by default + verifiedChildren.map(({ props: { value, label } }) => ({ + value, + label, + })); + + const [selectedValue, setSelectedValue] = useState(); + const tabReferences: Array> = []; + + const handleTabChange = ( + event: React.FocusEvent | React.MouseEvent + ) => { + const newTab = event.currentTarget; + const newTabIndex = tabReferences.indexOf(newTab); + const newTabValue = values[newTabIndex]?.value; + + if (newTabValue !== selectedValue) { + setSelectedValue(newTabValue); + } + }; + + const handleKeydown = (event: React.KeyboardEvent) => { + // eslint-disable-next-line @silverhand/fp/no-let + let focusElement: Nullable = null; + + switch (event.key) { + case 'ArrowRight': { + const nextTab = tabReferences.indexOf(event.currentTarget) + 1; + // eslint-disable-next-line @silverhand/fp/no-mutation + focusElement = tabReferences[nextTab] ?? tabReferences[0] ?? null; + break; + } + + case 'ArrowLeft': { + const previousTab = tabReferences.indexOf(event.currentTarget) - 1; + // eslint-disable-next-line @silverhand/fp/no-mutation + focusElement = + tabReferences[previousTab] ?? tabReferences[tabReferences.length - 1] ?? null; + break; + } + default: + break; + } + + focusElement?.focus(); + }; + + return ( +
+
    + {values.map(({ value, label }) => ( +
  • tabReferences.concat(tabControl)} + role="tab" + tabIndex={selectedValue === value ? 0 : -1} + aria-selected={selectedValue === value} + onKeyDown={handleKeydown} + onFocus={handleTabChange} + onClick={handleTabChange} + > + {label ?? value} +
  • + ))} +
+
+ {verifiedChildren.map((tabItem) => + cloneElement(tabItem, { + key: tabItem.props.value, + }) + )} +
+
+ ); +}; + +export default Tabs; diff --git a/packages/console/src/pages/Applications/components/CreateForm/index.tsx b/packages/console/src/pages/Applications/components/CreateForm/index.tsx index 9fe1fbee5..cce5c64fc 100644 --- a/packages/console/src/pages/Applications/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Applications/components/CreateForm/index.tsx @@ -10,9 +10,9 @@ import RadioGroup, { Radio } from '@/components/RadioGroup'; import TextInput from '@/components/TextInput'; import useApi from '@/hooks/use-api'; import { applicationTypeI18nKey } from '@/types/applications'; -import { GetStartedForm } from '@/types/get-started'; +import { GuideForm } from '@/types/guide'; -import GetStartedModal from '../GetStartedModal'; +import GuideModal from '../GuideModal'; import TypeDescription from '../TypeDescription'; import * as styles from './index.module.scss'; @@ -57,7 +57,7 @@ const CreateForm = ({ onClose }: Props) => { setIsGetStartedModalOpen(true); }); - const onComplete = async (data: GetStartedForm) => { + const onComplete = async (data: GuideForm) => { if (!createdApp) { return; } @@ -118,7 +118,7 @@ const CreateForm = ({ onClose }: Props) => { {createdApp && ( - void; - onComplete: (data: GetStartedForm) => Promise; + onComplete: (data: GuideForm) => Promise; }; -const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => ( +const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => ( - } title={appName} subtitle="applications.get_started.header_description" - type="application" defaultSubtype={SupportedJavascriptLibraries.React} onClose={onClose} onComplete={onComplete} @@ -29,4 +28,4 @@ const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => ( ); -export default GetStartedModal; +export default GuideModal; diff --git a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx index 21d375fed..e1e8489e1 100644 --- a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx @@ -11,7 +11,7 @@ import UnnamedTrans from '@/components/UnnamedTrans'; import { RequestError } from '@/hooks/use-api'; import * as modalStyles from '@/scss/modal.module.scss'; -import GetStartedModal from '../GetStartedModal'; +import GuideModal from '../GuideModal'; import * as styles from './index.module.scss'; type Props = { @@ -101,7 +101,7 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { )} {activeConnector && ( - void; - onComplete?: (data: GetStartedForm) => Promise; + onComplete?: (data: GuideForm) => Promise; }; const onClickFetchSampleProject = (name: string) => { @@ -34,7 +34,7 @@ const onClickFetchSampleProject = (name: string) => { window.open(sampleUrl, '_blank'); }; -const GetStartedModal = ({ connector, isOpen, onClose }: Props) => { +const GuideModal = ({ connector, isOpen, onClose }: Props) => { const api = useApi(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { @@ -48,7 +48,7 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => { const isSocialConnector = connectorType !== ConnectorType.SMS && connectorType !== ConnectorType.Email; const [activeStepIndex, setActiveStepIndex] = useState(0); - const methods = useForm({ reValidateMode: 'onBlur' }); + const methods = useForm({ reValidateMode: 'onBlur' }); const { control, formState: { isSubmitting }, @@ -126,9 +126,7 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => { title="Enter your json here" subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit." index={0} - isActive={activeStepIndex === 0} - isComplete={activeStepIndex > 0} - isFinalStep={isSocialConnector} + activeIndex={activeStepIndex} buttonHtmlType="submit" > { {!isSocialConnector && ( 1} + activeIndex={activeStepIndex} buttonHtmlType="button" + buttonText="general.done" onNext={onClose} > @@ -163,4 +160,4 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => { ); }; -export default GetStartedModal; +export default GuideModal; diff --git a/packages/console/src/pages/GetStarted/components/CodeComponentRenderer/index.tsx b/packages/console/src/pages/GetStarted/components/CodeComponentRenderer/index.tsx deleted file mode 100644 index c96a3cc11..000000000 --- a/packages/console/src/pages/GetStarted/components/CodeComponentRenderer/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -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 & { onError: () => void }; - -const CodeComponentRenderer = ({ className, children, onError }: Props) => { - const { - control, - formState: { errors }, - } = useFormContext(); - - const ref = useRef(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 ( - {content}}> - ( -
- -
- )} - /> -
- ); - } - - return ; -}; - -export default CodeComponentRenderer; diff --git a/packages/console/src/pages/GetStarted/hooks/index.ts b/packages/console/src/pages/GetStarted/hooks/index.ts deleted file mode 100644 index 775b87294..000000000 --- a/packages/console/src/pages/GetStarted/hooks/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import i18next from 'i18next'; -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'; - -type DocumentFileNames = { - files: string[]; -}; - -export type GetStartedType = 'application' | 'connector'; - -/** - * Fetch the markdown files for the given type and subtype. - * @param type 'application' or 'connector' - * @param subtype Application library name or connector name - * @returns List of step metadata including Yaml frontmatter and markdown content - */ -export const useGetStartedSteps = (type: GetStartedType, subtype?: string) => { - const subPath = subtype ? `/${subtype}` : ''; - const publicPath = useMemo( - () => `/console/get-started/${type}${subPath}/${i18next.language}`.toLowerCase(), - [type, subPath] - ); - - const { data: jsonData } = useSWRImmutable(`${publicPath}/index.json`); - const { data: steps } = useSWRImmutable( - jsonData, - async ({ files }: DocumentFileNames) => - Promise.all( - files.map(async (fileName) => { - const response = await fetch(`${publicPath}/${fileName}`); - const markdownFile = await response.text(); - - return parseMarkdownWithYamlFrontmatter(markdownFile); - }) - ) - ); - - return steps; -}; diff --git a/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx b/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx new file mode 100644 index 000000000..00d0f3364 --- /dev/null +++ b/packages/console/src/pages/Guide/components/MultiTextInputField/index.tsx @@ -0,0 +1,64 @@ +import React, { useRef } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import DangerousRaw from '@/components/DangerousRaw'; +import FormField from '@/components/FormField'; +import MultiTextInput from '@/components/MultiTextInput'; +import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils'; +import { GuideForm } from '@/types/guide'; +import { noSpaceRegex } from '@/utilities/regex'; + +type Props = { + name: 'redirectUris' | 'postLogoutRedirectUris'; + title: string; + onError?: () => void; +}; + +const MultiTextInputField = ({ name, title, onError }: Props) => { + const { + control, + formState: { errors }, + } = useFormContext(); + + const ref = useRef(null); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const firstErrorKey = Object.keys(errors)[0]; + const isFirstErrorField = firstErrorKey && firstErrorKey === name; + + if (isFirstErrorField) { + ref.current?.scrollIntoView({ block: 'center', behavior: 'smooth' }); + onError?.(); + } + + return ( + {title}}> + ( +
+ +
+ )} + /> +
+ ); +}; + +export default MultiTextInputField; diff --git a/packages/console/src/pages/GetStarted/components/Step/index.module.scss b/packages/console/src/pages/Guide/components/Step/index.module.scss similarity index 100% rename from packages/console/src/pages/GetStarted/components/Step/index.module.scss rename to packages/console/src/pages/Guide/components/Step/index.module.scss diff --git a/packages/console/src/pages/GetStarted/components/Step/index.tsx b/packages/console/src/pages/Guide/components/Step/index.tsx similarity index 62% rename from packages/console/src/pages/GetStarted/components/Step/index.tsx rename to packages/console/src/pages/Guide/components/Step/index.tsx index 075adcec4..d77e6b789 100644 --- a/packages/console/src/pages/GetStarted/components/Step/index.tsx +++ b/packages/console/src/pages/Guide/components/Step/index.tsx @@ -1,16 +1,7 @@ +import { I18nKey } from '@logto/phrases'; import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; -import React, { - cloneElement, - isValidElement, - PropsWithChildren, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { CodeProps } from 'react-markdown/lib/ast-to-react.js'; +import React, { PropsWithChildren, useEffect, useRef, useState } from 'react'; import Button from '@/components/Button'; import Card from '@/components/Card'; @@ -21,16 +12,15 @@ import Spacer from '@/components/Spacer'; import { ArrowDown, ArrowUp } from '@/icons/Arrow'; import Tick from '@/icons/Tick'; -import CodeComponentRenderer from '../CodeComponentRenderer'; import * as styles from './index.module.scss'; type Props = PropsWithChildren<{ title: string; subtitle?: string; index: number; - isActive: boolean; - isComplete: boolean; - isFinalStep: boolean; + activeIndex: number; + invalidIndex?: number; + buttonText?: I18nKey; buttonHtmlType: 'submit' | 'button'; onNext?: () => void; }>; @@ -40,46 +30,30 @@ const Step = ({ title, subtitle, index, - isActive, - isComplete, - isFinalStep, - buttonHtmlType, + activeIndex, + invalidIndex, + buttonText = 'general.next', + buttonHtmlType = 'button', onNext, }: Props) => { const [isExpanded, setIsExpanded] = useState(false); + const isActive = index === activeIndex; + const isComplete = index < activeIndex; + const isInvalid = index === invalidIndex; const ref = useRef(null); - const scrollToStep = useCallback(() => { - ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); - }, []); - - const onError = useCallback(() => { - setIsExpanded(true); - scrollToStep(); - }, [scrollToStep]); - useEffect(() => { - if (isActive) { + if (isActive || isInvalid) { setIsExpanded(true); } - }, [isActive]); + }, [isActive, isInvalid]); useEffect(() => { if (isExpanded) { - scrollToStep(); + ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); } - }, [isExpanded, scrollToStep]); + }, [isExpanded]); - const memoizedComponents = useMemo( - () => ({ - code: ({ ...props }: PropsWithChildren) => ( - - ), - }), - [onError] - ); - - // TODO: add more styles to markdown renderer return (
{isExpanded ? : }
- {isValidElement(children) && cloneElement(children, { components: memoizedComponents })} + {children}
diff --git a/packages/console/src/pages/GetStarted/index.module.scss b/packages/console/src/pages/Guide/index.module.scss similarity index 95% rename from packages/console/src/pages/GetStarted/index.module.scss rename to packages/console/src/pages/Guide/index.module.scss index 198e301c7..9537a41d7 100644 --- a/packages/console/src/pages/GetStarted/index.module.scss +++ b/packages/console/src/pages/Guide/index.module.scss @@ -39,6 +39,10 @@ .banner { margin-bottom: _.unit(6); } + + h1 { + display: none; + } } } diff --git a/packages/console/src/pages/GetStarted/index.tsx b/packages/console/src/pages/Guide/index.tsx similarity index 52% rename from packages/console/src/pages/GetStarted/index.tsx rename to packages/console/src/pages/Guide/index.tsx index 4d1192653..68d48715d 100644 --- a/packages/console/src/pages/GetStarted/index.tsx +++ b/packages/console/src/pages/Guide/index.tsx @@ -1,31 +1,52 @@ import { AdminConsoleKey } from '@logto/phrases'; -import React, { cloneElement, isValidElement, PropsWithChildren, ReactNode, useState } from 'react'; +import { MDXProvider } from '@mdx-js/react'; +import i18next from 'i18next'; +import { MDXProps } from 'mdx/types'; +import React, { + cloneElement, + isValidElement, + lazy, + LazyExoticComponent, + PropsWithChildren, + ReactNode, + Suspense, + useState, +} from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import ReactMarkdown from 'react-markdown'; import Button from '@/components/Button'; 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 Close from '@/icons/Close'; -import { GetStartedForm } from '@/types/get-started'; +import { GuideForm } from '@/types/guide'; +import MultiTextInputField from './components/MultiTextInputField'; import Step from './components/Step'; -import { GetStartedType, useGetStartedSteps } from './hooks'; import * as styles from './index.module.scss'; +const Guides: Record JSX.Element>> = { + react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react/index.mdx')), + 'react_zh-cn': lazy( + async () => + import( + '@/assets/i18n/zh-cn/docusaurus-plugin-content-docs/current/tutorial/integrate-sdk/react/index.mdx' + ) + ), +}; + type Props = PropsWithChildren<{ title: string; subtitle?: AdminConsoleKey; - type: GetStartedType; /** `subtype` can be an actual type of an application or connector. * e.g. React, Angular, Vue, etc. for application. Or Github, WeChat, etc. for connector. */ defaultSubtype?: string; bannerComponent?: ReactNode; onClose?: () => void; - onComplete?: (data: GetStartedForm) => Promise; + onComplete?: (data: GuideForm) => Promise; }>; const onClickFetchSampleProject = (projectName: string) => { @@ -33,19 +54,23 @@ const onClickFetchSampleProject = (projectName: string) => { window.open(sampleUrl, '_blank'); }; -const GetStarted = ({ +const Guide = ({ title, subtitle, - type, - defaultSubtype, + defaultSubtype = '', bannerComponent, onClose, onComplete, }: Props) => { const [subtype, setSubtype] = useState(defaultSubtype); - const [activeStepIndex, setActiveStepIndex] = useState(-1); - const steps = useGetStartedSteps(type, subtype) ?? []; - const methods = useForm({ reValidateMode: 'onBlur' }); + const [activeStepIndex, setActiveStepIndex] = useState(-1); + const [invalidStepIndex, setInvalidStepIndex] = useState(-1); + + const locale = i18next.language; + const guideKey = `${subtype}_${locale}`.toLowerCase(); + const GuideComponent = Guides[guideKey] ?? Guides[subtype]; + + const methods = useForm({ mode: 'onSubmit', reValidateMode: 'onChange' }); const { formState: { isSubmitting }, handleSubmit, @@ -89,32 +114,32 @@ const GetStarted = ({ setActiveStepIndex(0); }, })} - {steps.map(({ title, subtitle, metadata }, index) => { - if (!title) { - return null; - } - const isFinalStep = index === steps.length - 1; + { + const [, language] = /language-(\w+)/.exec(className ?? '') ?? []; - return ( - index} - isFinalStep={isFinalStep} - buttonHtmlType={isFinalStep ? 'submit' : 'button'} - onNext={() => { - setActiveStepIndex(index + 1); - }} - > - {metadata && ( - {metadata} - )} - - ); - })} + return ; + }, + MultiTextInputField, + Step, + }} + > + Loading...}> + {GuideComponent && ( + { + setActiveStepIndex(nextIndex); + }} + onError={(invalidIndex: number) => { + setInvalidStepIndex(invalidIndex); + }} + /> + )} + + @@ -122,4 +147,4 @@ const GetStarted = ({ ); }; -export default GetStarted; +export default Guide; diff --git a/packages/console/src/types/applications.ts b/packages/console/src/types/applications.ts index 609bc408f..0cfaf24c9 100644 --- a/packages/console/src/types/applications.ts +++ b/packages/console/src/types/applications.ts @@ -7,7 +7,7 @@ export const applicationTypeI18nKey = Object.freeze({ } as const); export enum SupportedJavascriptLibraries { - Angular = 'Angular', - React = 'React', - Vue = 'Vue', + Angular = 'angular', + React = 'react', + Vue = 'vue', } diff --git a/packages/console/src/types/get-started.ts b/packages/console/src/types/get-started.ts deleted file mode 100644 index b610af3e9..000000000 --- a/packages/console/src/types/get-started.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type StepMetadata = { - title?: string; - subtitle?: string; - metadata: string; // Markdown formatted string -}; - -export type GetStartedForm = { - redirectUris: string[]; - postLogoutRedirectUris: string[]; - connectorConfigJson: string; -}; diff --git a/packages/console/src/types/guide.ts b/packages/console/src/types/guide.ts new file mode 100644 index 000000000..10418f263 --- /dev/null +++ b/packages/console/src/types/guide.ts @@ -0,0 +1,5 @@ +export type GuideForm = { + redirectUris: string[]; + postLogoutRedirectUris: string[]; + connectorConfigJson: string; +}; diff --git a/packages/docs/docs/tutorial/integrate-sdk/react/_step-1.mdx b/packages/docs/docs/tutorial/integrate-sdk/react/_step-1.mdx deleted file mode 100644 index c8a6df8f9..000000000 --- a/packages/docs/docs/tutorial/integrate-sdk/react/_step-1.mdx +++ /dev/null @@ -1,45 +0,0 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -## Install SDK - - - - -```bash -npm i @logto/react -``` - - - - -```bash -yarn add @logto/react -``` - - - - -```bash -pnpm add @logto/react -``` - - - - - -```html -