diff --git a/packages/console/src/components/MultilineInput/index.module.scss b/packages/console/src/components/MultilineInput/index.module.scss new file mode 100644 index 000000000..64585df2b --- /dev/null +++ b/packages/console/src/components/MultilineInput/index.module.scss @@ -0,0 +1,17 @@ +@use '@/scss/underscore' as _; + +.multilineInput { + > :not(:first-child) { + margin-top: _.unit(2); + } + + .deletableInput { + display: flex; + align-items: center; + + .textField { + @include _.form-text-field; + margin-right: _.unit(3); + } + } +} diff --git a/packages/console/src/components/MultilineInput/index.tsx b/packages/console/src/components/MultilineInput/index.tsx new file mode 100644 index 000000000..95c03a884 --- /dev/null +++ b/packages/console/src/components/MultilineInput/index.tsx @@ -0,0 +1,71 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import * as textButtonStyles from '@/components/TextButton/index.module.scss'; +import Minus from '@/icons/Minus'; + +import IconButton from '../IconButton'; +import TextInput from '../TextInput'; +import * as styles from './index.module.scss'; + +type Props = { + value: string[]; + onChange: (value: string[]) => void; +}; + +const MultilineInput = ({ value, onChange }: Props) => { + const { t } = useTranslation(undefined, { + keyPrefix: 'general', + }); + + const fields = useMemo(() => { + if (value.length === 0) { + return ['']; + } + + return value; + }, [value]); + + const handleAdd = () => { + onChange([...fields, '']); + }; + + const handleRemove = (index: number) => { + onChange(fields.filter((_, i) => i !== index)); + }; + + const handleInputChange = (event: React.FormEvent, index: number) => { + onChange(fields.map((value, i) => (i === index ? event.currentTarget.value : value))); + }; + + return ( +
+ {fields.map((fieldValue, fieldIndex) => ( + // eslint-disable-next-line react/no-array-index-key +
+ { + handleInputChange(event, fieldIndex); + }} + /> + {fields.length > 1 && ( + { + handleRemove(fieldIndex); + }} + > + + + )} +
+ ))} +
+ {t('add_another')} +
+
+ ); +}; + +export default MultilineInput; diff --git a/packages/console/src/components/TextButton/index.module.scss b/packages/console/src/components/TextButton/index.module.scss index 1d7810c0b..0b883d07d 100644 --- a/packages/console/src/components/TextButton/index.module.scss +++ b/packages/console/src/components/TextButton/index.module.scss @@ -7,6 +7,7 @@ padding: _.unit(0.5) _.unit(1); border-radius: _.unit(1); text-decoration: none; + cursor: pointer; svg { fill: var(--color-primary); diff --git a/packages/console/src/icons/Minus.tsx b/packages/console/src/icons/Minus.tsx new file mode 100644 index 000000000..384d8789c --- /dev/null +++ b/packages/console/src/icons/Minus.tsx @@ -0,0 +1,12 @@ +import React, { SVGProps } from 'react'; + +const Minus = (props: SVGProps) => ( + + + +); + +export default Minus; diff --git a/packages/console/src/pages/ApplicationDetails/index.module.scss b/packages/console/src/pages/ApplicationDetails/index.module.scss index 78f62ca6d..857b91e12 100644 --- a/packages/console/src/pages/ApplicationDetails/index.module.scss +++ b/packages/console/src/pages/ApplicationDetails/index.module.scss @@ -1,16 +1,21 @@ @use '@/scss/underscore' as _; .container { - > *:not(:first-child) { + display: flex; + flex-direction: column; + height: 100%; + + > :not(:first-child) { margin-top: _.unit(4); } } .container .header { - padding: _.unit(8); + flex: 0; display: flex; align-items: center; justify-content: space-between; + padding: _.unit(8); > *:not(:first-child) { margin-left: _.unit(6); @@ -19,24 +24,21 @@ .metadata { flex: 1; - > div { - display: flex; - align-items: center; - - &:not(:first-child) { - margin-top: _.unit(2); - } - - > *:not(:first-child) { - margin-left: _.unit(2); - } - } - .name { font: var(--font-title-large); color: var(--color-component-text); } + .details { + display: flex; + align-items: center; + margin-top: _.unit(2); + + > :not(:first-child) { + margin-left: _.unit(2); + } + } + .type { background-color: var(--color-neutral-90); padding: _.unit(0.5) _.unit(2); @@ -61,32 +63,46 @@ } .container .body { - > :not(:first-child) { - margin-top: _.unit(6); - } + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; - .tabContent { - form { - >:not(:first-child) { - margin-top: _.unit(6); + .form { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: auto; + + > *:not(:first-child) { + margin-top: _.unit(6); + } + + .fields { + flex: 1; + border-bottom: 1px solid var(--color-border); + padding: _.unit(6) 0; + overflow: auto; + + > div { + @include _.form-text-field; } - .fields { - border-bottom: 1px solid var(--color-border); - padding-bottom: _.unit(6); + .textField { + @include _.form-text-field; + } - > div { - @include _.form-text-field; - } - - .copy { - @include _.form-text-field; + .listFields { + > *:not(:first-child) { + margin-top: _.unit(1); } } + } - .submit { - text-align: right; - } + .submit { + flex: 0; + text-align: right; } } } diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index c70f2335a..974ae9508 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -1,6 +1,6 @@ import { Application } from '@logto/schemas'; -import React, { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; +import React, { useEffect, useMemo } from 'react'; +import { useController, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useLocation, useParams } from 'react-router-dom'; import useSWR from 'swr'; @@ -11,6 +11,7 @@ import Card from '@/components/Card'; import CopyToClipboard from '@/components/CopyToClipboard'; import FormField from '@/components/FormField'; import ImagePlaceholder from '@/components/ImagePlaceholder'; +import MultilineInput from '@/components/MultilineInput'; import TabNav, { TabNavLink } from '@/components/TabNav'; import TextInput from '@/components/TextInput'; import { RequestError } from '@/hooks/use-api'; @@ -18,6 +19,7 @@ import { applicationTypeI18nKey } from '@/types/applications'; import * as styles from './index.module.scss'; +// TODO LOG-1908: OidcConfig in Application Details type OidcConfig = { authorization_endpoint: string; userinfo_endpoint: string; @@ -27,45 +29,118 @@ type OidcConfig = { const ApplicationDetails = () => { const { id } = useParams(); const location = useLocation(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { data, error } = useSWR(id && `/api/applications/${id}`); + // TODO LOG-1908: OidcConfig in Application Details const { data: oidcConfig, error: fetchOidcConfigError } = useSWR( '/oidc/.well-known/openid-configuration' ); - const isLoading = !data && !error && !fetchOidcConfigError; - const dataFetched = data && oidcConfig; + const isLoading = !data && !error && !oidcConfig && !fetchOidcConfigError; - const { handleSubmit, register, reset } = useForm({ - defaultValues: data, - }); - - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - - const isAdvancedSettings = location.pathname.includes('advanced-settings'); + const { control, handleSubmit, register, reset } = useForm(); useEffect(() => { if (!data) { return; } + reset(data); }, [data, reset]); + const { + field: { value: redirectUris, onChange: onRedirectUriChange }, + } = useController({ + control, + name: 'oidcClientMetadata.redirectUris', + defaultValue: [], + }); + + const { + field: { value: postSignOutRedirectUris, onChange: onPostSignOutRedirectUriChange }, + } = useController({ + control, + name: 'oidcClientMetadata.postLogoutRedirectUris', + defaultValue: [], + }); + const onSubmit = handleSubmit((formData) => { console.log(formData); }); + const isAdvancedSettings = location.pathname.includes('advanced-settings'); + + const SettingsPage = useMemo(() => { + return ( + oidcConfig && ( + <> + + + + + + + + + + + { + onRedirectUriChange(value); + }} + /> + + + { + onPostSignOutRedirectUriChange(value); + }} + /> + + + ) + ); + }, [ + oidcConfig, + onPostSignOutRedirectUriChange, + onRedirectUriChange, + postSignOutRedirectUris, + redirectUris, + register, + ]); + + const AdvancedSettingsPage = useMemo(() => { + return ( + oidcConfig && ( + <> + + + + + + + + ) + ); + }, [oidcConfig]); + return (
{t('application_details.back_to_applications')} {isLoading &&
loading
} {error &&
{`error occurred: ${error.metadata.code}`}
} - {dataFetched && ( + {data && oidcConfig && ( <>
{data.name}
-
+
{t(`${applicationTypeI18nKey[data.type]}.title`)}
App ID
@@ -85,54 +160,18 @@ const ApplicationDetails = () => { {t('application_details.advanced_settings')} -
-
-
- {!isAdvancedSettings && ( - <> - - - - - - - - - - - )} - {isAdvancedSettings && ( - <> - - - - - - - - )} -
-
-
-
-
+
+
+ {isAdvancedSettings ? AdvancedSettingsPage : SettingsPage} +
+
+
+
)} diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 993cfefb0..33c65d2b8 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -1,6 +1,7 @@ const translation = { general: { placeholder: 'Placeholder', + add_another: '+ Add Another', }, sign_in: { action: 'Sign In', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 31fd5b8da..930c5726e 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -3,6 +3,7 @@ import en from './en'; const translation = { general: { placeholder: '占位符', + add_another: '+ Add Another', }, sign_in: { action: '登录', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf5895004..a8eab6d9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10108,12 +10108,6 @@ packages: hasBin: true dev: false - /nanoid/3.3.0: - resolution: {integrity: sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - /nanoid/3.3.1: resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -11514,7 +11508,7 @@ packages: resolution: {integrity: sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.0 + nanoid: 3.3.1 picocolors: 1.0.0 source-map-js: 1.0.2 dev: true