mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 23:49:27 +08:00
feat(client): add settings form
This commit is contained in:
parent
6608901596
commit
0c83c16391
@ -16,6 +16,7 @@ export * from './record-provider';
|
|||||||
export * from './route-switch';
|
export * from './route-switch';
|
||||||
export * from './schema-component';
|
export * from './schema-component';
|
||||||
export * from './schema-initializer';
|
export * from './schema-initializer';
|
||||||
|
export * from './settings-form';
|
||||||
export * from './system-settings';
|
export * from './system-settings';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
|
||||||
|
269
packages/client/src/settings-form/SettingsForm.tsx
Normal file
269
packages/client/src/settings-form/SettingsForm.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import { FormButtonGroup, FormDialog, FormDrawer, FormItem, FormLayout, Reset, Submit } from '@formily/antd';
|
||||||
|
import { createForm, Field, ObjectField, onFormValuesChange } from '@formily/core';
|
||||||
|
import {
|
||||||
|
FieldContext,
|
||||||
|
FormContext,
|
||||||
|
observer,
|
||||||
|
RecursionField,
|
||||||
|
Schema,
|
||||||
|
SchemaOptionsContext,
|
||||||
|
useField,
|
||||||
|
useFieldSchema,
|
||||||
|
useForm
|
||||||
|
} from '@formily/react';
|
||||||
|
import { Dropdown, Menu, Modal, Select, Switch } from 'antd';
|
||||||
|
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||||
|
import { SchemaComponentOptions, useAttach, useDesignable } from '..';
|
||||||
|
|
||||||
|
export interface SettingsFormContextProps {
|
||||||
|
field?: Field;
|
||||||
|
fieldSchema?: Schema;
|
||||||
|
dropdownVisible?: boolean;
|
||||||
|
setDropdownVisible?: (v: boolean) => void;
|
||||||
|
dn?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsFormContext = createContext<SettingsFormContextProps>(null);
|
||||||
|
|
||||||
|
export const useSettingsFormContext = () => {
|
||||||
|
return useContext(SettingsFormContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsForm: any = observer((props: any) => {
|
||||||
|
const dn = useDesignable();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
|
const settingsFormSchema = useMemo(() => new Schema(props.schema), []);
|
||||||
|
const form = useMemo(
|
||||||
|
() =>
|
||||||
|
createForm({
|
||||||
|
initialValues: fieldSchema.toJSON(),
|
||||||
|
effects(form) {
|
||||||
|
onFormValuesChange((form) => {
|
||||||
|
dn.patch(form.values);
|
||||||
|
console.log('form.values', form.values);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
||||||
|
return (
|
||||||
|
<SettingsFormContext.Provider value={{ dn, field, fieldSchema, dropdownVisible, setDropdownVisible }}>
|
||||||
|
<SchemaComponentOptions components={{ SettingsForm }}>
|
||||||
|
<FieldContext.Provider value={null}>
|
||||||
|
<FormContext.Provider value={form}>
|
||||||
|
<FieldContext.Provider value={f}>
|
||||||
|
<Dropdown
|
||||||
|
visible={dropdownVisible}
|
||||||
|
onVisibleChange={(visible) => setDropdownVisible(visible)}
|
||||||
|
overlayStyle={{ width: 200 }}
|
||||||
|
overlay={
|
||||||
|
<Menu>
|
||||||
|
{settingsFormSchema.mapProperties((s, key) => {
|
||||||
|
return <RecursionField name={key} schema={s} />;
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a>配置</a>
|
||||||
|
</Dropdown>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
</FormContext.Provider>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
</SettingsFormContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingsForm.Divider = () => {
|
||||||
|
return <Menu.Divider />;
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsForm.Remove = (props) => {
|
||||||
|
const field = useField();
|
||||||
|
const { dn, setDropdownVisible } = useSettingsFormContext();
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
setDropdownVisible(false);
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'Are you sure delete this task?',
|
||||||
|
content: 'Some descriptions',
|
||||||
|
okText: 'Yes',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: 'No',
|
||||||
|
...props.confirm,
|
||||||
|
onOk() {
|
||||||
|
dn.remove();
|
||||||
|
console.log('OK');
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.title}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsForm.Switch = observer(() => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
field.value = !field.value;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
{field.title} <Switch checked={!!field.value} />
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingsForm.Select = observer((props) => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Menu.Item onClick={() => !open && setOpen(true)}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
{field.title}
|
||||||
|
<Select
|
||||||
|
open={open}
|
||||||
|
onDropdownVisibleChange={(open) => setOpen(open)}
|
||||||
|
onSelect={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onChange={(value) => {
|
||||||
|
field.value = value;
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
options={field.dataSource}
|
||||||
|
style={{ width: '60%' }}
|
||||||
|
size={'small'}
|
||||||
|
bordered={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingsForm.Modal = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const options = useContext(SchemaOptionsContext);
|
||||||
|
const { setDropdownVisible } = useSettingsFormContext();
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
style={{ width: 200 }}
|
||||||
|
onClick={async () => {
|
||||||
|
setDropdownVisible(false);
|
||||||
|
const values = await FormDialog('Title', () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={options.scope} components={{ ...options.components, FormItem }}>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<RecursionField schema={fieldSchema} onlyRenderProperties />
|
||||||
|
</FormLayout>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
}).open({
|
||||||
|
initialValues: fieldSchema.type !== 'void' ? field.value : form.values,
|
||||||
|
});
|
||||||
|
if (fieldSchema.type !== 'void') {
|
||||||
|
form.setValues(
|
||||||
|
{
|
||||||
|
[fieldSchema.name]: values,
|
||||||
|
},
|
||||||
|
'deepMerge',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
form.setValues(values);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.title}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsForm.Drawer = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const field = useField<ObjectField>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const options = useContext(SchemaOptionsContext);
|
||||||
|
const { setDropdownVisible } = useSettingsFormContext();
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
style={{ width: 200 }}
|
||||||
|
onClick={async () => {
|
||||||
|
setDropdownVisible(false);
|
||||||
|
const values = await FormDrawer('Pop-up form', () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={options.scope} components={{ ...options.components, FormItem }}>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<RecursionField schema={fieldSchema} onlyRenderProperties />
|
||||||
|
<FormDrawer.Footer>
|
||||||
|
<FormButtonGroup align="right">
|
||||||
|
<Reset>Reset</Reset>
|
||||||
|
<Submit
|
||||||
|
onSubmit={() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Submit>
|
||||||
|
</FormButtonGroup>
|
||||||
|
</FormDrawer.Footer>
|
||||||
|
</FormLayout>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
}).open({
|
||||||
|
initialValues: fieldSchema.type !== 'void' ? field.value : form.values,
|
||||||
|
});
|
||||||
|
if (fieldSchema.type !== 'void') {
|
||||||
|
form.setValues(
|
||||||
|
{
|
||||||
|
[fieldSchema.name]: values,
|
||||||
|
},
|
||||||
|
'deepMerge',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
form.setValues(values);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.title}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsForm.SubMenu = () => {
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return (
|
||||||
|
<Menu.SubMenu title={field.title}>
|
||||||
|
{fieldSchema.mapProperties((schema, key) => {
|
||||||
|
return <RecursionField name={key} schema={schema} />;
|
||||||
|
})}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsForm.ItemGroup = () => {
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return (
|
||||||
|
<Menu.ItemGroup title={field.title}>
|
||||||
|
{fieldSchema.mapProperties((schema, key) => {
|
||||||
|
return <RecursionField name={key} schema={schema} />;
|
||||||
|
})}
|
||||||
|
</Menu.ItemGroup>
|
||||||
|
);
|
||||||
|
};
|
133
packages/client/src/settings-form/demos/demo1.tsx
Normal file
133
packages/client/src/settings-form/demos/demo1.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { ISchema, observer, useFieldSchema } from '@formily/react';
|
||||||
|
import { AntdSchemaComponentProvider, SchemaComponent, SchemaComponentProvider, SettingsForm } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
'x-component-props.switch': {
|
||||||
|
title: 'Switch',
|
||||||
|
'x-component': 'SettingsForm.Switch',
|
||||||
|
},
|
||||||
|
'x-component-props.select': {
|
||||||
|
title: 'Select',
|
||||||
|
'x-component': 'SettingsForm.Select',
|
||||||
|
enum: [
|
||||||
|
{ label: 'Option1', value: 'option1' },
|
||||||
|
{ label: 'Option2', value: 'option2' },
|
||||||
|
{ label: 'Option3', value: 'option3' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'Open Modal',
|
||||||
|
'x-component': 'SettingsForm.Modal',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
'x-component-props.title': {
|
||||||
|
title: '标题',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'Open Drawer',
|
||||||
|
'x-component': 'SettingsForm.Drawer',
|
||||||
|
properties: {
|
||||||
|
'x-component-props.title': {
|
||||||
|
title: '标题',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'ItemGroup',
|
||||||
|
'x-component': 'SettingsForm.ItemGroup',
|
||||||
|
properties: {
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Open Modal',
|
||||||
|
'x-component': 'SettingsForm.Modal',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: '标题',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submenu: {
|
||||||
|
type: 'void',
|
||||||
|
title: 'SubMenu',
|
||||||
|
'x-component': 'SettingsForm.SubMenu',
|
||||||
|
properties: {
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Open Modal',
|
||||||
|
'x-component': 'SettingsForm.Modal',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: '标题',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
'x-component': 'SettingsForm.Divider',
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
title: 'Delete',
|
||||||
|
'x-component': 'SettingsForm.Remove',
|
||||||
|
'x-component-props': {
|
||||||
|
confirm: {
|
||||||
|
title: 'Are you sure delete this task?',
|
||||||
|
content: 'Some descriptions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Hello = observer((props: any) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<pre>{JSON.stringify(props, null, 2)}</pre>
|
||||||
|
<pre>{JSON.stringify(fieldSchema.toJSON(), null, 2)}</pre>
|
||||||
|
<SettingsForm schema={schema} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Hello }}>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
hello: {
|
||||||
|
'x-component': 'Hello',
|
||||||
|
'x-component-props': {
|
||||||
|
title: 'abc',
|
||||||
|
switch: true,
|
||||||
|
select: 'option1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -5,4 +5,6 @@ group:
|
|||||||
path: /client
|
path: /client
|
||||||
---
|
---
|
||||||
|
|
||||||
# SettingsForm <Badge>待定</Badge>
|
# SettingsForm
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
||||||
|
1
packages/client/src/settings-form/index.ts
Normal file
1
packages/client/src/settings-form/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './SettingsForm';
|
Loading…
x
Reference in New Issue
Block a user