mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(console): implement radio input and form
This commit is contained in:
parent
2196b217fd
commit
295314e693
14 changed files with 263 additions and 5 deletions
|
@ -31,6 +31,7 @@
|
|||
"pnpm": ">=6"
|
||||
},
|
||||
"alias": {
|
||||
"html-parse-stringify": "html-parse-stringify/dist/html-parse-stringify.module.js"
|
||||
"html-parse-stringify": "html-parse-stringify/dist/html-parse-stringify.module.js",
|
||||
"react-hook-form": "react-hook-form/dist/index.esm.mjs"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"lodash.kebabcase": "^4.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.27.1",
|
||||
"react-i18next": "^11.15.4",
|
||||
"react-modal": "^3.14.4",
|
||||
"react-router-dom": "^6.2.2",
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
--color-neutral-90: #e0e3e3;
|
||||
--color-on-secondary-container: #201c00;
|
||||
--color-component-caption: #747778;
|
||||
--color-component-text: #191c1d;
|
||||
--color-outline: #78767f;
|
||||
--color-table-row-selected: rgba(0, 0, 0, 2%);
|
||||
}
|
||||
|
@ -40,9 +41,11 @@ $font-family: 'SF UI Text', 'SF Pro Display', sans-serif;
|
|||
.web {
|
||||
--font-title-medium: 500 16px/24px #{$font-family};
|
||||
--font-heading-small: 600 24px/32px #{$font-family};
|
||||
--font-heading: 600 16px/24px #{$font-family};
|
||||
--font-body: normal 16px/22px #{$font-family};
|
||||
--font-small-text: normal 12px/16px #{$font-family};
|
||||
--font-caption: normal 14px/20px #{$font-family};
|
||||
--font-caption-bold: 600 14px/20px #{$font-family};
|
||||
--font-button: 500 14px/20px #{$font-family};
|
||||
--font-subhead-2: 500 14px/20px #{$font-family};
|
||||
}
|
||||
|
|
21
packages/console/src/components/FormField/index.module.scss
Normal file
21
packages/console/src/components/FormField/index.module.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: _.unit(1);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.title {
|
||||
font: var(--font-caption-bold);
|
||||
color: var(--color-component-text);
|
||||
}
|
||||
|
||||
.required {
|
||||
font: var(--font-body);
|
||||
color: var(--color-component-caption);
|
||||
}
|
||||
}
|
27
packages/console/src/components/FormField/index.tsx
Normal file
27
packages/console/src/components/FormField/index.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
title: I18nKey;
|
||||
children: ReactNode;
|
||||
isRequired?: boolean;
|
||||
};
|
||||
|
||||
const FormField = ({ title, children, isRequired }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.headline}>
|
||||
<div className={styles.title}>{t(title)}</div>
|
||||
{isRequired && <div className={styles.required}>{t('admin_console.form.required')}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormField;
|
52
packages/console/src/components/RadioGroup/Radio.tsx
Normal file
52
packages/console/src/components/RadioGroup/Radio.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { forwardRef, ReactNode } from 'react';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Check = () => (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="18" height="18" rx="9" fill="#4F37F9" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.31476 13.858L5.13295 10.441C4.95568 10.253 4.95568 9.947 5.13295 9.757L5.77568 9.074C5.95295 8.886 6.24113 8.886 6.4184 9.074L8.63657 11.466L13.5811 6.141C13.7584 5.953 14.0465 5.953 14.2238 6.141L14.8665 6.825C15.0438 7.013 15.0438 7.32 14.8665 7.507L8.95748 13.858C8.78021 14.046 8.49203 14.046 8.31476 13.858Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// https://github.com/yannickcr/eslint-plugin-react/issues/2856
|
||||
/* eslint-disable react/require-default-props */
|
||||
export type Props = {
|
||||
value: string;
|
||||
title: string;
|
||||
name?: string;
|
||||
children?: ReactNode;
|
||||
isChecked?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/* eslint-enable react/require-default-props */
|
||||
|
||||
const Radio = forwardRef<HTMLInputElement, Props>(
|
||||
({ value, title, name, children, isChecked, onClick }, reference) => {
|
||||
return (
|
||||
<div className={classNames(styles.radio, isChecked && styles.checked)} onClick={onClick}>
|
||||
<input
|
||||
ref={reference}
|
||||
readOnly
|
||||
type="radio"
|
||||
name={name}
|
||||
value={value}
|
||||
checked={isChecked}
|
||||
/>
|
||||
<div className={classNames(styles.headline, !children && styles.center)}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<Check />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Radio;
|
67
packages/console/src/components/RadioGroup/index.module.scss
Normal file
67
packages/console/src/components/RadioGroup/index.module.scss
Normal file
|
@ -0,0 +1,67 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.radioGroup {
|
||||
display: flex;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: _.unit(3);
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: 0%;
|
||||
}
|
||||
|
||||
|
||||
> .radio {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
padding: _.unit(5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: _.unit(4);
|
||||
border: 1px solid var(--color-neutral-90);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: _.unit(6);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-color: var(--color-primary);
|
||||
outline: 1px solid var(--color-primary);
|
||||
|
||||
svg {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.headline {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&.center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-left: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font: var(--font-heading);
|
||||
color: var(--color-component-text);
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
34
packages/console/src/components/RadioGroup/index.tsx
Normal file
34
packages/console/src/components/RadioGroup/index.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { Children, cloneElement, isValidElement, ReactNode } from 'react';
|
||||
|
||||
import Radio, { Props as RadioProps } from './Radio';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
const RadioGroup = ({ name, children, value, onChange }: Props) => {
|
||||
return (
|
||||
<div className={styles.radioGroup}>
|
||||
{Children.map(children, (child) => {
|
||||
if (!isValidElement(child) || child.type !== Radio) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return cloneElement<RadioProps>(child, {
|
||||
name,
|
||||
isChecked: value === child.props.value,
|
||||
onClick: () => {
|
||||
onChange?.(child.props.value);
|
||||
},
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioGroup;
|
||||
export { default as Radio } from './Radio';
|
|
@ -16,3 +16,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: _.unit(8);
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import { ApplicationType } from '@logto/schemas';
|
||||
import React, { SVGProps } from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import FormField from '@/components/FormField';
|
||||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -14,19 +18,44 @@ const Close = (props: SVGProps<SVGSVGElement>) => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
type FormData = {
|
||||
type: ApplicationType;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const Create = ({ onClose }: Props) => {
|
||||
const CreateForm = ({ onClose }: Props) => {
|
||||
const { handleSubmit, control } = useForm<FormData>();
|
||||
const {
|
||||
field: { onChange, value },
|
||||
} = useController({ name: 'type', control });
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.headline}>
|
||||
<CardTitle title="applications.create" subtitle="applications.subtitle" />
|
||||
<Close onClick={onClose} />
|
||||
</div>
|
||||
<form className={styles.form} onSubmit={onSubmit}>
|
||||
<FormField title="admin_console.applications.select_application_type">
|
||||
<RadioGroup name="application_type" value={value} onChange={onChange}>
|
||||
<Radio title="Native" value={ApplicationType.Native} />
|
||||
<Radio title="Single Page Application" value={ApplicationType.SPA} />
|
||||
<Radio title="Tranditional Web" value={ApplicationType.Traditional} />
|
||||
</RadioGroup>
|
||||
</FormField>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Create;
|
||||
export default CreateForm;
|
|
@ -14,7 +14,7 @@ import * as modalStyles from '@/scss/modal.module.scss';
|
|||
import { RequestError } from '@/swr';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
import Create from './components/Create';
|
||||
import CreateForm from './components/CreateForm';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Applications = () => {
|
||||
|
@ -38,7 +38,7 @@ const Applications = () => {
|
|||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
>
|
||||
<Create
|
||||
<CreateForm
|
||||
onClose={() => {
|
||||
setIsCreateFormOpen(false);
|
||||
}}
|
||||
|
|
|
@ -19,6 +19,9 @@ const translation = {
|
|||
copying: 'Copying',
|
||||
copied: 'Copied',
|
||||
},
|
||||
form: {
|
||||
required: 'Required',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: 'Overview',
|
||||
resource_management: 'Resource Management',
|
||||
|
@ -44,6 +47,7 @@ const translation = {
|
|||
'Setup a mobile, single page or traditional application to use Logto for authentication.',
|
||||
create: 'Create Application',
|
||||
application_name: 'Application Name',
|
||||
select_application_type: 'Select an application type',
|
||||
client_id: 'Client ID',
|
||||
type: {
|
||||
native: 'Native App',
|
||||
|
|
|
@ -21,6 +21,9 @@ const translation = {
|
|||
copying: '拷贝中',
|
||||
copied: '已拷贝',
|
||||
},
|
||||
form: {
|
||||
required: '必填',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: '概览',
|
||||
resource_management: '资源管理',
|
||||
|
@ -46,6 +49,7 @@ const translation = {
|
|||
'Setup a mobile, single page or traditional application to use Logto for authentication.',
|
||||
create: 'Create Application',
|
||||
application_name: 'Application Name',
|
||||
select_application_type: 'Select an application type',
|
||||
client_id: 'Client ID',
|
||||
type: {
|
||||
native: 'Native App',
|
||||
|
|
|
@ -45,6 +45,7 @@ importers:
|
|||
prettier: ^2.3.2
|
||||
react: ^17.0.2
|
||||
react-dom: ^17.0.2
|
||||
react-hook-form: ^7.27.1
|
||||
react-i18next: ^11.15.4
|
||||
react-modal: ^3.14.4
|
||||
react-router-dom: ^6.2.2
|
||||
|
@ -61,6 +62,7 @@ importers:
|
|||
lodash.kebabcase: 4.1.1
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
react-hook-form: 7.27.1_react@17.0.2
|
||||
react-i18next: 11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5
|
||||
react-modal: 3.14.4_react-dom@17.0.2+react@17.0.2
|
||||
react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2
|
||||
|
@ -11216,6 +11218,15 @@ packages:
|
|||
scheduler: 0.20.2
|
||||
dev: false
|
||||
|
||||
/react-hook-form/7.27.1_react@17.0.2:
|
||||
resolution: {integrity: sha512-N3a7A6zIQ8DJeThisVZGtOUabTbJw+7DHJidmB9w8m3chckv2ZWKb5MHps9d2pPJqmCDoWe53Bos56bYmJms5w==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17
|
||||
dependencies:
|
||||
react: 17.0.2
|
||||
dev: false
|
||||
|
||||
/react-i18next/11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5:
|
||||
resolution: {integrity: sha512-jKJNAcVcbPGK+yrTcXhLblgPY16n6NbpZZL3Mk8nswj1v3ayIiUBVDU09SgqnT+DluyQBS97hwSvPU5yVFG0yg==}
|
||||
peerDependencies:
|
||||
|
|
Loading…
Reference in a new issue