mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
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:
parent
860c57eb0e
commit
723c6bf4eb
@ -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)
|
||||
}
|
||||
|
206
packages/amis-ui/examples/App.tsx
Normal file
206
packages/amis-ui/examples/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
11
packages/amis-ui/examples/basic/Button.tsx
Normal file
11
packages/amis-ui/examples/basic/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
59
packages/amis-ui/examples/form/Combo.tsx
Normal file
59
packages/amis-ui/examples/form/Combo.tsx
Normal 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>
|
||||
);
|
||||
}
|
50
packages/amis-ui/examples/form/InputTable.tsx
Normal file
50
packages/amis-ui/examples/form/InputTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
73
packages/amis-ui/examples/modal/ConfirmBox.tsx
Normal file
73
packages/amis-ui/examples/modal/ConfirmBox.tsx
Normal 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>
|
||||
);
|
||||
}
|
66
packages/amis-ui/examples/modal/PickerContainer.tsx
Normal file
66
packages/amis-ui/examples/modal/PickerContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
74
packages/amis-ui/index.html
Normal file
74
packages/amis-ui/index.html
Normal 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>
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
145
packages/amis-ui/src/components/ConfirmBox.tsx
Normal file
145
packages/amis-ui/src/components/ConfirmBox.tsx
Normal 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));
|
@ -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} />
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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 之前的日期值',
|
||||
|
@ -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"}]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user