feat: 补充 ConfirmBox ui 控件, 并将 PickerContainer 改成 ConfirmBox 实现 (#5708)

* publish beta

* feat: 添加 ui ConfirmBox

* feat: 补充 confirmBox ui 控件, 并将 pickerContainer 改成 confirmBox 实现

* PickerContainer title 逻辑不变动

* 暴露 InputTableColumnProps

* 调整 ts 定义

* 升级 react-hook-form

* inputTable 补充数组本身的验证

* Combo 也支持内部数组的验证

* 调整内部验证

* 调整目录
This commit is contained in:
liaoxuezhi 2022-11-08 10:17:56 +08:00 committed by GitHub
parent 860c57eb0e
commit 723c6bf4eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 933 additions and 82 deletions

View File

@ -463,6 +463,8 @@ export const validateMessages: {
matchRegexp: 'validate.matchRegexp',
minLength: 'validate.minLength',
maxLength: 'validate.maxLength',
minLengthArray: 'validate.array.minLength',
maxLengthArray: 'validate.array.maxLength',
maximum: 'validate.maximum',
lt: 'validate.lt',
minimum: 'validate.minimum',
@ -525,10 +527,19 @@ export function validate(
});
if (!fn(values, value, ...args)) {
let msgRuleName = ruleName;
if (Array.isArray(value)) {
msgRuleName = `${ruleName}Array`;
}
errors.push({
rule: ruleName,
msg: filter(
__((messages && messages[ruleName]) || validateMessages[ruleName]),
__(
(messages && messages[ruleName]) ||
validateMessages[msgRuleName] ||
validateMessages[ruleName]
),
{
...[''].concat(args)
}

View File

@ -0,0 +1,206 @@
import React from 'react';
import {Layout, AsideNav, Spinner, NotFound} from 'amis-ui';
import {eachTree, TreeArray, TreeItem} from 'amis-core';
import {
HashRouter as Router,
Route,
Redirect,
Switch,
Link,
NavLink
} from 'react-router-dom';
const pages: TreeArray = [
{
label: '常规',
children: [
{
label: '按钮',
path: '/basic/button',
component: React.lazy(() => import('./basic/Button'))
}
]
},
{
label: '表单',
children: [
{
label: 'InputTable',
path: '/form/input-table',
component: React.lazy(() => import('./form/InputTable'))
},
{
label: 'Combo',
path: '/form/combo',
component: React.lazy(() => import('./form/Combo'))
}
]
},
{
label: '弹框',
children: [
{
label: 'PickContainer',
path: '/modal/pick-conatiner',
component: React.lazy(() => import('./modal/PickerContainer'))
},
{
label: 'ConfirmBox',
path: '/modal/confirm-box',
component: React.lazy(() => import('./modal/ConfirmBox'))
}
]
}
];
function getPath(path: string) {
return path ? (path[0] === '/' ? path : `/${path}`) : '';
}
function isActive(link: any, location: any) {
return !!(link.path && getPath(link.path) === location.pathname);
}
export function navigations2route(
navigations: any,
additionalProperties?: any
) {
let routes: any = [];
navigations.forEach((root: any) => {
root.children &&
eachTree(root.children, (item: any) => {
if (item.path && item.component) {
routes.push(
additionalProperties ? (
<Route
key={routes.length + 1}
path={item.path[0] === '/' ? item.path : `/${item.path}`}
render={(props: any) => (
<item.component {...additionalProperties} {...props} />
)}
/>
) : (
<Route
key={routes.length + 1}
path={item.path[0] === '/' ? item.path : `/${item.path}`}
component={item.component}
/>
)
);
}
});
});
return routes;
}
export default function App() {
function renderAside() {
return (
<AsideNav
navigations={pages.map((item: any) => ({
...item,
children: item.children
? item.children.map((item: any) => ({
...item,
className: 'is-top'
}))
: []
}))}
renderLink={({
link,
active,
toggleExpand,
classnames: cx,
depth
}: any) => {
let children = [];
if (link.children && link.children.length) {
children.push(
<span
key="expand-toggle"
className={cx('AsideNav-itemArrow')}
onClick={e => toggleExpand(link, e)}
></span>
);
}
link.badge &&
children.push(
<b
key="badge"
className={cx(
`AsideNav-itemBadge`,
link.badgeClassName || 'bg-info'
)}
>
{link.badge}
</b>
);
if (link.icon) {
children.push(
<i key="icon" className={cx(`AsideNav-itemIcon`, link.icon)} />
);
}
children.push(
<span className={cx('AsideNav-itemLabel')} key="label">
{link.label}
</span>
);
return link.path ? (
/^https?\:/.test(link.path) ? (
<a target="_blank" href={link.path} rel="noopener">
{children}
</a>
) : (
<Link
to={
getPath(link.path) ||
(link.children && getPath(link.children[0].path))
}
>
{children}
</Link>
)
) : (
<a onClick={link.children ? () => toggleExpand(link) : undefined}>
{children}
</a>
);
}}
isActive={(link: any) => isActive(link, location)}
/>
);
}
return (
<Router>
<Layout
header={
<div id="headerBar" className="box-shadow bg-dark">
<div className={`cxd-Layout-brand`}>amis-ui </div>
</div>
}
aside={renderAside()}
>
<React.Suspense
fallback={<Spinner overlay spinnerClassName="m-t-lg" size="lg" />}
>
<Switch>
{navigations2route(pages)}
<Route render={() => <NotFound description="Not found" />} />
</Switch>
</React.Suspense>
</Layout>
</Router>
);
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import {Button} from 'amis-ui';
export default function ButtonExamples() {
return (
<div className="wrapper">
<p> </p>
<Button>Button</Button>
</div>
);
}

View File

@ -0,0 +1,59 @@
import React from 'react';
import {Button, Combo, Form, Controller, InputBox} from 'amis-ui';
export default function ButtonExamples() {
const handleSubmit = React.useCallback((data: any) => {
console.log('submit', data);
}, []);
return (
<div className="wrapper">
<Form defaultValues={{items: [{a: 1, b: 2}]}} onSubmit={handleSubmit}>
{({control, onSubmit}) => {
return (
<>
<Combo
name="items"
control={control}
minLength={2}
maxLength={5}
itemRender={({control}) => (
<>
<Controller
name="key"
control={control}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
placeholder="Key"
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
<Controller
name="title"
control={control}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
placeholder="Title"
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
</>
)}
/>
<Button onClick={onSubmit}>Submit</Button>
</>
);
}}
</Form>
</div>
);
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import {Button, InputTable, Form, Controller, InputBox} from 'amis-ui';
export default function ButtonExamples() {
const handleSubmit = React.useCallback((data: any) => {
console.log('submit', data);
}, []);
return (
<div className="wrapper">
<Form defaultValues={{items: [{a: 1, b: 2}]}} onSubmit={handleSubmit}>
{({control, onSubmit}) => {
return (
<>
<InputTable
name="items"
control={control}
minLength={2}
maxLength={5}
columns={[
{
title: 'Name',
tdRender: ({control}) => {
return (
<Controller
name="key"
control={control}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
placeholder="Key"
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
);
}
}
]}
/>
<Button onClick={onSubmit}>Submit</Button>
</>
);
}}
</Form>
</div>
);
}

View File

@ -0,0 +1,73 @@
import React from 'react';
import {Button, ConfirmBox, Controller, Form, InputBox} from 'amis-ui';
export default function ButtonExamples() {
const [isShow, setIsShow] = React.useState(false);
const handleClick = React.useCallback(() => {
setIsShow(!isShow);
}, [isShow]);
const handleCancel = React.useCallback(() => {
setIsShow(false);
}, []);
// const beforeConfirm = React.useCallback(async () => {
// return false;
// }, []);
const handleConfirm = React.useCallback((data: any) => {
console.log('confirmed', data);
setIsShow(false);
}, []);
return (
<div className="wrapper">
<Button onClick={handleClick}>Open</Button>
<ConfirmBox
type="drawer"
size="md"
position="bottom"
onConfirm={handleConfirm}
show={isShow}
onCancel={handleCancel}
>
{({bodyRef}) => (
<Form ref={bodyRef}>
{({control}) => (
<>
<Controller
mode="horizontal"
label="A"
name="a"
control={control}
rules={{maxLength: 20}}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
<Controller
mode="horizontal"
label="B"
name="b"
control={control}
rules={{maxLength: 20}}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
</>
)}
</Form>
)}
</ConfirmBox>
</div>
);
}

View File

@ -0,0 +1,66 @@
import React from 'react';
import {PickerContainer, Button, Form, Controller, InputBox} from 'amis-ui';
export default function () {
const body = React.createRef<any>();
const beforeConfirm = React.useCallback(() => {
return body.current?.submit();
}, []);
const handleConfirm = React.useCallback((data: any) => {
console.log('confirmed', data);
}, []);
return (
<div className="wrapper">
<PickerContainer
beforeConfirm={beforeConfirm}
onConfirm={handleConfirm}
bodyRender={() => (
<Form ref={body}>
{({control}) => (
<>
<Controller
mode="horizontal"
label="A"
name="a"
control={control}
rules={{maxLength: 20}}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
<Controller
mode="horizontal"
label="B"
name="b"
control={control}
rules={{maxLength: 20}}
isRequired
render={({field, fieldState}) => (
<InputBox
{...field}
hasError={!!fieldState.error}
disabled={false}
/>
)}
/>
</>
)}
</Form>
)}
>
{({isOpened, onClick}) => (
<Button active={isOpened} onClick={onClick}>
Open
</Button>
)}
</PickerContainer>
</div>
);
}

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>amis-ui</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<link rel="stylesheet" href="../../examples/static/iconfont.css" />
<link
rel="stylesheet"
href="../../node_modules/@fortawesome/fontawesome-free/css/all.css"
/>
<link
rel="stylesheet"
href="../../node_modules/@fortawesome/fontawesome-free/css/v4-shims.css"
/>
<link rel="stylesheet" href="./scss/themes/cxd.scss" />
<link rel="stylesheet" href="./scss/helper.scss" />
<style>
.app-wrapper,
.schema-wrapper {
position: relative;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="root" class="app-wrapper"></div>
<script type="module">
import React from 'react';
import {createRoot} from 'react-dom/client';
import App from './examples/App';
export function bootstrap(mountTo, initalState) {
const root = createRoot(mountTo);
root.render(React.createElement(App));
}
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker();
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker();
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker();
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
}
return new editorWorker();
}
};
const initialState = {};
bootstrap(document.getElementById('root'), initialState);
</script>
</body>
</html>

View File

@ -57,7 +57,7 @@
"rc-input-number": "^7.3.9",
"rc-progress": "^3.1.4",
"react-color": "^2.19.3",
"react-hook-form": "7.30.0",
"react-hook-form": "7.39.0",
"react-json-view": "1.21.3",
"react-overlays": "5.1.1",
"react-textarea-autosize": "8.3.3",

View File

@ -73,7 +73,8 @@ import {
RegisterOptions,
useFieldArray,
UseFieldArrayProps,
UseFormReturn
UseFormReturn,
useFormState
} from 'react-hook-form';
import useSubForm from '../hooks/use-sub-form';
import Button from './Button';
@ -147,17 +148,64 @@ export function Combo({
minLength,
maxLength
}: ComboProps) {
// 看文档是支持的,但是传入报错,后面看看
// let rules2: any = {...rules};
const subForms = React.useRef<Record<any, UseFormReturn>>({});
const subFormRef = React.useCallback(
(subform: UseFormReturn | null, index: number) => {
if (subform) {
subForms.current[index] = subform;
} else {
delete subForms.current[index];
}
},
[subForms]
);
let finalRules: any = {...rules};
// if (isRequired) {
// rules2.required = true;
// }
if (isRequired) {
finalRules.required = true;
}
if (minLength) {
finalRules.minLength = minLength;
}
if (maxLength) {
finalRules.maxLength = maxLength;
}
finalRules.validate = React.useCallback(
async (items: Array<any>) => {
const map = subForms.current;
if (typeof rules?.validate === 'function') {
const result = await rules.validate(items);
if (result) {
return result;
}
}
for (let key of Object.keys(map)) {
const valid = await (function (methods) {
return new Promise<boolean>(resolve => {
methods.handleSubmit(
() => resolve(true),
() => resolve(false)
)();
});
})(map[key]);
if (!valid) {
return __('validateFailed');
}
}
},
[subForms]
);
const {fields, append, update, remove} = useFieldArray({
control,
name: name,
shouldUnregister: true
// rules: rules2
shouldUnregister: true,
rules: finalRules
});
function renderBody() {
@ -180,6 +228,7 @@ export function Combo({
itemRender={itemRender}
translate={__}
classnames={cx}
formRef={subFormRef}
/>
<a
onClick={() => remove(index)}
@ -215,6 +264,10 @@ export function Combo({
);
}
const {errors} = useFormState({
control
});
return wrap === false ? (
renderBody()
) : (
@ -226,8 +279,8 @@ export function Combo({
description={description}
mode={mode}
isRequired={isRequired}
hasError={false /*目前看来不支持,后续研究一下 */}
errors={undefined /*目前看来不支持,后续研究一下 */}
hasError={!!errors[name]?.message}
errors={errors[name]?.message as any}
>
{renderBody()}
</FormField>
@ -242,6 +295,7 @@ export interface ComboItemProps {
index: number;
translate: TranslateFn;
classnames: ClassNamesFn;
formRef: (form: UseFormReturn | null, index: number) => void;
}
export function ComboItem({
@ -250,9 +304,16 @@ export function ComboItem({
index,
translate,
update,
classnames: cx
classnames: cx,
formRef
}: ComboItemProps) {
const methods = useSubForm(value, translate, data => update(index, data));
React.useEffect(() => {
formRef?.(methods, index);
return () => {
formRef?.(null, index);
};
}, [methods]);
let child: any = itemRender(methods, index);
if (child?.type === React.Fragment) {

View File

@ -0,0 +1,145 @@
import React from 'react';
import Modal from './Modal';
import Button from './Button';
import Drawer from './Drawer';
import {localeable, LocaleProps, themeable, ThemeProps} from 'amis-core';
export interface ConfirmBoxProps extends LocaleProps, ThemeProps {
show?: boolean;
closeOnEsc?: boolean;
beforeConfirm?: (bodyRef?: any) => any;
onConfirm?: (data: any) => void;
onCancel?: () => void;
title?: string;
showTitle?: boolean;
showFooter?: boolean;
headerClassName?: string;
children?:
| JSX.Element
| ((methods: {
bodyRef: React.MutableRefObject<
| {
submit: () => Promise<Record<string, any>>;
}
| undefined
>;
}) => JSX.Element);
popOverContainer?: any;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
position?: 'top' | 'right' | 'bottom' | 'left';
resizable?: boolean;
type: 'dialog' | 'drawer';
}
export function ConfirmBox({
type,
size,
closeOnEsc,
show,
onCancel,
title,
showTitle,
headerClassName,
translate: __,
children,
showFooter,
onConfirm,
beforeConfirm,
popOverContainer,
position,
resizable,
classnames: cx
}: ConfirmBoxProps) {
const bodyRef = React.useRef<
{submit: () => Promise<Record<string, any>>} | undefined
>();
const handleConfirm = React.useCallback(async () => {
const ret = beforeConfirm
? await beforeConfirm?.(bodyRef.current)
: await bodyRef.current?.submit?.();
if (ret === false) {
return;
}
onConfirm?.(ret);
}, [onConfirm, beforeConfirm]);
function renderDialog() {
return (
<Modal
size={size}
closeOnEsc={closeOnEsc}
show={show}
onHide={onCancel!}
container={popOverContainer}
>
{showTitle !== false && title ? (
<Modal.Header onClose={onCancel} className={headerClassName}>
{title}
</Modal.Header>
) : null}
<Modal.Body>
{typeof children === 'function'
? children({
bodyRef: bodyRef
})
: children}
</Modal.Body>
{showFooter ?? true ? (
<Modal.Footer>
<Button onClick={onCancel}>{__('cancel')}</Button>
<Button onClick={handleConfirm} level="primary">
{__('confirm')}
</Button>
</Modal.Footer>
) : null}
</Modal>
);
}
function renderDrawer() {
return (
<Drawer
size={size}
closeOnEsc={closeOnEsc}
show={show}
onHide={onCancel!}
container={popOverContainer}
position={position}
resizable={resizable}
showCloseButton={false}
>
{showTitle !== false && title ? (
<div className={cx('Drawer-header', headerClassName)}>
<div className={cx('Drawer-title')}>{title}</div>
</div>
) : null}
<div className={cx('Drawer-body')}>
{typeof children === 'function'
? children({
bodyRef: bodyRef
})
: children}
</div>
{showFooter ?? true ? (
<div className={cx('Drawer-footer')}>
<Button onClick={handleConfirm} level="primary">
{__('confirm')}
</Button>
<Button onClick={onCancel}>{__('cancel')}</Button>
</div>
) : null}
</Drawer>
);
}
return type === 'drawer' ? renderDrawer() : renderDialog();
}
ConfirmBox.defaultProps = {
type: 'dialog' as 'dialog',
position: 'right' as 'right'
};
export default localeable(themeable(ConfirmBox));

View File

@ -2,7 +2,7 @@
* @file
*/
import React from 'react';
import {themeable, ThemeProps} from 'amis-core';
import {noop, themeable, ThemeProps} from 'amis-core';
import {useForm, UseFormReturn} from 'react-hook-form';
import {useValidationResolver} from '../hooks/use-validation-resolver';
import {localeable, LocaleProps} from 'amis-core';
@ -16,9 +16,9 @@ export type FormRef = React.MutableRefObject<
>;
export interface FormProps extends ThemeProps, LocaleProps {
defaultValues: any;
defaultValues?: any;
autoSubmit?: boolean;
onSubmit: (value: any) => void;
onSubmit?: (value: any) => void;
forwardRef?: FormRef;
children?: (
methods: UseFormReturn & {
@ -35,11 +35,11 @@ export function Form(props: FormProps) {
resolver: useValidationResolver(props.translate)
});
let onSubmit = React.useRef<(data: any) => void>(
methods.handleSubmit(props.onSubmit)
methods.handleSubmit(props.onSubmit || noop)
);
if (autoSubmit) {
onSubmit = React.useRef(
debounce(methods.handleSubmit(props.onSubmit), 250, {
debounce(methods.handleSubmit(props.onSubmit || noop), 250, {
leading: false,
trailing: true
})
@ -60,9 +60,12 @@ export function Form(props: FormProps) {
// 这个模式别的组件没见到过不知道后续会不会不允许
props.forwardRef.current = {
submit: () =>
new Promise<any>((resolve, reject) => {
new Promise<any>(resolve => {
methods.handleSubmit(
values => resolve(values),
values => {
props.onSubmit?.(values);
resolve(values);
},
() => resolve(false)
)();
})
@ -93,7 +96,14 @@ export function Form(props: FormProps) {
}
const ThemedForm = themeable(localeable(Form));
type ThemedFormProps = Omit<FormProps, keyof ThemeProps | keyof LocaleProps>;
type ThemedFormProps = Omit<
JSX.LibraryManagedAttributes<
typeof ThemedForm,
React.ComponentProps<typeof ThemedForm>
>,
'children'
> &
Pick<FormProps, 'children'>;
export default React.forwardRef((props: ThemedFormProps, ref: FormRef) => (
<ThemedForm {...props} forwardRef={ref} />

View File

@ -11,30 +11,29 @@ import {
Control,
useFieldArray,
UseFieldArrayProps,
UseFormReturn
UseFormReturn,
useFormState
} from 'react-hook-form';
import useSubForm from '../hooks/use-sub-form';
import Button from './Button';
import FormField, {FormFieldProps} from './FormField';
import {Icon} from './icons';
export interface InputTableColumnProps {
title?: string;
className?: string;
thRender?: () => JSX.Element;
tdRender: (methods: UseFormReturn, index: number) => JSX.Element | null;
}
export interface InputTabbleProps<T = any>
extends ThemeProps,
LocaleProps,
Omit<
FormFieldProps,
'children' | 'errors' | 'hasError' | 'isRequired' | 'className'
>,
Omit<FormFieldProps, 'children' | 'errors' | 'hasError' | 'className'>,
UseFieldArrayProps {
control: Control<any>;
fieldClassName?: string;
columns: Array<{
title?: string;
className?: string;
thRender?: () => JSX.Element;
tdRender: (methods: UseFormReturn, index: number) => JSX.Element | null;
}>;
columns: Array<InputTableColumnProps>;
/**
* label
@ -71,17 +70,78 @@ export function InputTable({
addButtonClassName,
scaffold,
minLength,
maxLength
maxLength,
isRequired,
rules
}: InputTabbleProps) {
const subForms = React.useRef<Record<any, UseFormReturn>>({});
const subFormRef = React.useCallback(
(subform: UseFormReturn | null, index: number) => {
if (subform) {
subForms.current[index] = subform;
} else {
delete subForms.current[index];
}
},
[subForms]
);
let finalRules: any = {...rules};
if (isRequired) {
finalRules.required = true;
}
if (minLength) {
finalRules.minLength = minLength;
}
if (maxLength) {
finalRules.maxLength = maxLength;
}
finalRules.validate = React.useCallback(
async (items: Array<any>) => {
const map = subForms.current;
if (typeof rules?.validate === 'function') {
const result = await rules.validate(items);
if (result) {
return result;
}
}
for (let key of Object.keys(map)) {
const valid = await (function (methods) {
return new Promise<boolean>(resolve => {
methods.handleSubmit(
() => resolve(true),
() => resolve(false)
)();
});
})(map[key]);
if (!valid) {
return __('validateFailed');
}
}
},
[subForms]
);
const {fields, append, update, remove} = useFieldArray({
control,
name: name
name: name,
rules: finalRules
});
if (!Array.isArray(columns)) {
columns = [];
}
const {errors} = useFormState({
control
});
function renderBody() {
return (
<div className={cx(`Table`, className)}>
@ -110,19 +170,17 @@ export function InputTable({
columns={columns}
translate={__}
classnames={cx}
formRef={subFormRef}
/>
<td key="operation">
<Button
level="link"
key="delete"
className={cx(
`Table-delBtn ${
removable === false ||
(minLength && fields.length <= minLength)
? 'is-disabled'
: ''
}`
)}
disabled={
removable === false ||
!!(minLength && fields.length <= minLength)
}
className={cx('Table-delBtn')}
onClick={() => remove(index)}
>
{__('delete')}
@ -170,8 +228,8 @@ export function InputTable({
labelClassName={labelClassName}
description={description}
mode={mode}
hasError={false /*目前看来不支持,后续研究一下 */}
errors={undefined /*目前看来不支持,后续研究一下 */}
hasError={!!errors[name]?.message}
errors={errors[name]?.message as any}
>
{renderBody()}
</FormField>
@ -189,6 +247,7 @@ export interface InputTableRowProps {
index: number;
translate: TranslateFn;
classnames: ClassNamesFn;
formRef: (form: UseFormReturn | null, index: number) => void;
}
export function InputTableRow({
@ -197,9 +256,16 @@ export function InputTableRow({
index,
translate,
update,
formRef,
classnames: cx
}: InputTableRowProps) {
const methods = useSubForm(value, translate, data => update(index, data));
React.useEffect(() => {
formRef?.(methods, index);
return () => {
formRef?.(null, index);
};
}, [methods]);
return (
<>

View File

@ -9,12 +9,12 @@ import {
} from 'amis-core';
import Modal from './Modal';
import Button from './Button';
import ConfirmBox, {ConfirmBoxProps} from './ConfirmBox';
export interface PickerContainerProps extends ThemeProps, LocaleProps {
title?: string;
showTitle?: boolean;
showFooter?: boolean;
headerClassName?: string;
export interface PickerContainerProps
extends ThemeProps,
LocaleProps,
Omit<ConfirmBoxProps, 'children' | 'type'> {
children: (props: {
onClick: (e: React.MouseEvent) => void;
setState: (state: any) => void;
@ -28,12 +28,6 @@ export interface PickerContainerProps extends ThemeProps, LocaleProps {
[propName: string]: any;
}) => JSX.Element | null;
value?: any;
beforeConfirm?: (bodyRef: any) => any;
onConfirm?: (value?: any) => void;
onCancel?: () => void;
popOverContainer?: any;
popOverClassName?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
onFocus?: () => void;
onClose?: () => void;
@ -99,7 +93,7 @@ export class PickerContainer extends React.Component<
}
@autobind
async confirm() {
async confirm(): Promise<any> {
const {onConfirm, beforeConfirm} = this.props;
const ret = await beforeConfirm?.(this.bodyRef.current);
@ -109,7 +103,7 @@ export class PickerContainer extends React.Component<
// beforeConfirm 返回 false 则阻止后续动作
if (ret === false) {
return;
return false;
} else if (isObject(ret)) {
state.value = ret;
}
@ -135,7 +129,8 @@ export class PickerContainer extends React.Component<
headerClassName,
translate: __,
size,
showFooter
showFooter,
closeOnEsc
} = this.props;
return (
<>
@ -145,36 +140,29 @@ export class PickerContainer extends React.Component<
setState: this.updateState
})}
<Modal
<ConfirmBox
type="dialog"
size={size}
closeOnEsc
closeOnEsc={closeOnEsc}
show={this.state.isOpened}
onHide={this.close}
onCancel={this.close}
title={title || __('Select.placeholder')}
showTitle={showTitle}
headerClassName={headerClassName}
showFooter={showFooter}
beforeConfirm={this.confirm}
>
{showTitle !== false ? (
<Modal.Header onClose={this.close} className={headerClassName}>
{__(title || 'Select.placeholder')}
</Modal.Header>
) : null}
<Modal.Body>
{popOverRender({
{() =>
popOverRender({
...(this.state as any),
ref: this.bodyRef,
setState: this.updateState,
onClose: this.close,
onChange: this.handleChange,
onConfirm: this.confirm
})}
</Modal.Body>
{showFooter ?? true ? (
<Modal.Footer>
<Button onClick={this.close}>{__('cancel')}</Button>
<Button onClick={this.confirm} level="primary">
{__('confirm')}
</Button>
</Modal.Footer>
) : null}
</Modal>
})!
}
</ConfirmBox>
</>
);
}

View File

@ -117,6 +117,8 @@ import Form from './Form';
import FormField, {Controller} from './FormField';
import Combo from './Combo';
import InputTable from './InputTable';
import type {InputTableColumnProps} from './InputTable';
import ConfirmBox from './ConfirmBox';
export {
NotFound,
@ -184,6 +186,7 @@ export {
SchemaVariableList,
VariableList,
PickerContainer,
ConfirmBox,
FormulaPicker,
InputJSONSchema,
withBadge,
@ -236,5 +239,6 @@ export {
FormField,
Controller,
Combo,
InputTable
InputTable,
InputTableColumnProps
};

View File

@ -23,6 +23,7 @@ export function useValidationResolver(__ = (str: string) => str) {
return React.useCallback<any>(
async (values: any, context: any, config: any) => {
const rules: any = {};
const customValidator: any = {};
const ruleKeys = Object.keys(validations);
for (let key of Object.keys(config.fields)) {
const field = config.fields[key];
@ -31,10 +32,27 @@ export function useValidationResolver(__ = (str: string) => str) {
if (field.required) {
rules[key].isRequired = true;
}
if (typeof field.validate === 'function') {
customValidator[key] = field.validate;
}
}
const errors = validateObject(values, rules, undefined, __);
for (let key of Object.keys(customValidator)) {
const validate = customValidator[key];
const result = await validate(values[key]);
if (typeof result === 'string') {
errors[key] = errors[key] || [];
errors[key].push({
rule: 'custom',
msg: result
});
}
}
return {
values,
errors: formatErrors(errors)

View File

@ -286,6 +286,10 @@ register('de-DE', {
'Kontrollieren Sie die Länge des Inhalts. Geben Sie nicht mehr als $1 Buchstaben ein.',
'validate.minimum': 'Der Eingabewert ist kleiner als der Mindestwert von $1.',
'validate.minLength': 'Geben Sie weitere Zeichen ein, mindestens $1.',
'validate.array.minLength':
'Bitte fügen Sie weitere Mitglieder hinzu, mindestens $1 Mitglieder',
'validate.array.maxLength':
'Bitte kontrollieren Sie die Anzahl der Mitglieder, die $1 nicht überschreiten darf',
'validate.notEmptyString': 'Geben Sie nicht nur Leerzeichen ein.',
'validate.isDateTimeSame':
'Der aktuelle Datumswert ist ungültig, bitte geben Sie denselben Datumswert wie $1 ein',

View File

@ -274,6 +274,9 @@ register('en-US', {
'Please control the content length, do not enter more than $1 letters',
'validate.minimum': 'The input value is lower than the minimum value of $1',
'validate.minLength': 'Please enter more, at least $1 characters.',
'validate.array.minLength': 'Please add more members, at least $1 members',
'validate.array.maxLength':
'Please control the number of members, which cannot exceed $1',
'validate.notEmptyString': 'Please do not enter all blank characters',
'validate.isDateTimeSame':
'The current date value is invalid, please enter the same date value as $1',

View File

@ -274,8 +274,10 @@ register('zh-CN', {
'validate.matchRegexp': '格式不正确, 请输入符合规则为 `${1|raw}` 的内容。',
'validate.maximum': '当前输入值超出最大值 $1',
'validate.maxLength': '请控制内容长度, 不要输入 $1 个以上字符',
'validate.array.maxLength': '请控制成员个数, 不能超过 $1 个',
'validate.minimum': '当前输入值低于最小值 $1',
'validate.minLength': '请输入更多的内容,至少输入 $1 个字符。',
'validate.array.minLength': '请添加更多的成员,成员数至少 $1 个。',
'validate.notEmptyString': '请不要全输入空白字符',
'validate.isDateTimeSame': '当前日期值不合法,请输入和 $1 相同的日期值',
'validate.isDateTimeBefore': '当前日期值不合法,请输入 $1 之前的日期值',

View File

@ -9,6 +9,6 @@
"../../node_modules/@types"
]
},
"include": ["src/**/*", "__tests__/**/*", "src/custom.d.ts"],
"include": ["src/**/*", "examples/**/*", "__tests__/**/*", "src/custom.d.ts"],
"references": [{"path": "../amis-core"}]
}