741 lines
20 KiB
TypeScript
741 lines
20 KiB
TypeScript
import { ArrayField, createForm } from '@formily/core';
|
||
import {
|
||
createSchemaField,
|
||
FormProvider,
|
||
Schema,
|
||
SchemaOptionsContext,
|
||
useField,
|
||
useFieldSchema,
|
||
useForm,
|
||
} from '@formily/react';
|
||
import { uid } from '@formily/shared';
|
||
import constate from 'constate';
|
||
import { get } from 'lodash';
|
||
import React, { createContext } from 'react';
|
||
import { useState } from 'react';
|
||
import { useContext } from 'react';
|
||
import { useMemo } from 'react';
|
||
|
||
import { CodeOutlined } from '@ant-design/icons';
|
||
import Editor from '@monaco-editor/react';
|
||
|
||
import {
|
||
ArrayItems,
|
||
ArrayCollapse,
|
||
FormLayout,
|
||
FormItem as FormilyFormItem,
|
||
} from '@formily/antd';
|
||
|
||
import { Space, Card, Modal, Spin } from 'antd';
|
||
import { ArrayTable } from '../../schemas/array-table';
|
||
import { Action } from '../../schemas/action';
|
||
import { AddNew } from '../../schemas/add-new';
|
||
import { Calendar } from '../../schemas/calendar';
|
||
import { Cascader } from '../../schemas/cascader';
|
||
import { Checkbox } from '../../schemas/checkbox';
|
||
import { ColorSelect } from '../../schemas/color-select';
|
||
import {
|
||
DatabaseField,
|
||
DatabaseCollection,
|
||
} from '../../schemas/database-field';
|
||
import { DatePicker } from '../../schemas/date-picker';
|
||
import { Filter } from '../../schemas/filter';
|
||
import { Form } from '../../schemas/form';
|
||
import { ActionLogs } from '../../schemas/action-logs';
|
||
import { Grid } from '../../schemas/grid';
|
||
import { IconPicker } from '../../schemas/icon-picker';
|
||
import { Input } from '../../schemas/input';
|
||
import { InputNumber } from '../../schemas/input-number';
|
||
import { Markdown } from '../../schemas/markdown';
|
||
import { Kanban } from '../../schemas/kanban';
|
||
import { Menu } from '../../schemas/menu';
|
||
import { Password } from '../../schemas/password';
|
||
import { Radio } from '../../schemas/radio';
|
||
import { Select } from '../../schemas/select';
|
||
import {
|
||
CollectionFieldContext,
|
||
Table,
|
||
TableRowContext,
|
||
} from '../../schemas/table';
|
||
import { Tabs } from '../../schemas/tabs';
|
||
import { TimePicker } from '../../schemas/time-picker';
|
||
import { Upload } from '../../schemas/upload';
|
||
import { FormItem } from '../../schemas/form-item';
|
||
import { BlockItem } from '../../schemas/block-item';
|
||
import { CardItem } from '../../schemas/card-item';
|
||
import { DragAndDrop } from '../../schemas/drag-and-drop';
|
||
import { TreeSelect } from '../../schemas/tree-select';
|
||
import { Page } from '../../schemas/page';
|
||
import { Chart } from '../../schemas/chart';
|
||
import { useDesignableSwitchContext } from '../../constate/DesignableSwitch';
|
||
import { action } from '@formily/reactive';
|
||
import { ISchema, FormilyISchema } from '../../schemas';
|
||
import { Resource } from '../../resource';
|
||
import { useRequest } from 'ahooks';
|
||
import { CascaderOptionType } from 'antd/lib/cascader';
|
||
import {
|
||
useClient,
|
||
useCollectionContext,
|
||
useResourceRequest,
|
||
} from '../../constate';
|
||
import { i18n } from '../../i18n';
|
||
|
||
export const BlockContext = createContext({ dragRef: null });
|
||
|
||
const Div = (props) => <div {...props} />;
|
||
|
||
interface DesignableContextProps {
|
||
schema?: Schema;
|
||
refresh?: any;
|
||
}
|
||
|
||
export const useAsyncDataSource = (service: any) => (field: any) => {
|
||
field.loading = true;
|
||
const disableAsyncDataSource = field.componentProps.disableAsyncDataSource;
|
||
if (disableAsyncDataSource) {
|
||
return;
|
||
}
|
||
service(field).then(
|
||
action.bound((data: any) => {
|
||
field.dataSource = data;
|
||
field.loading = false;
|
||
}),
|
||
);
|
||
};
|
||
|
||
export const loadChinaRegionDataSource = function () {
|
||
const resource = useResourceRequest('china_regions');
|
||
return async (field: ArrayField) => {
|
||
if (field.readPretty) {
|
||
return [];
|
||
}
|
||
const maxLevel = field.componentProps.maxLevel || 3;
|
||
const { data } = await resource.list({
|
||
perPage: -1,
|
||
filter: {
|
||
level: 1,
|
||
},
|
||
});
|
||
console.log('loadChinaRegions', data, field.value);
|
||
return (
|
||
data?.map((item) => {
|
||
if (maxLevel !== 1) {
|
||
item.isLeaf = false;
|
||
}
|
||
return item;
|
||
}) || []
|
||
);
|
||
};
|
||
};
|
||
|
||
export const loadChinaRegionData = function () {
|
||
const resource = useResourceRequest('china_regions');
|
||
return (selectedOptions: CascaderOptionType[], field: ArrayField) => {
|
||
const maxLevel = field.componentProps.maxLevel || 3;
|
||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||
targetOption.loading = true;
|
||
resource
|
||
.list({
|
||
perPage: -1,
|
||
filter: {
|
||
parent_code: targetOption['code'],
|
||
},
|
||
})
|
||
.then((data) => {
|
||
targetOption.loading = false;
|
||
targetOption.children =
|
||
data?.data?.map((item) => {
|
||
if (maxLevel > item.level) {
|
||
item.isLeaf = false;
|
||
}
|
||
return item;
|
||
}) || [];
|
||
field.dataSource = [...field.dataSource];
|
||
});
|
||
};
|
||
};
|
||
|
||
const useChinaRegionFieldValue = (field: ArrayField) => {
|
||
if (field.readPretty) {
|
||
field.value = field?.value?.sort((a, b) => a.level - b.level);
|
||
}
|
||
console.log('useChinaRegionFieldValue', field);
|
||
};
|
||
|
||
const useAssociationResource = (options) => {
|
||
const { schema } = useDesignable();
|
||
const collectionField = useContext(CollectionFieldContext);
|
||
const { collection } = useCollectionContext();
|
||
const ctx = useContext(TableRowContext);
|
||
const associatedKey = ctx?.record?.id;
|
||
console.log(
|
||
'useAssociationResource',
|
||
collection,
|
||
collectionField,
|
||
schema['x-component-props'],
|
||
);
|
||
const { associatedName, resourceName } = schema['x-component-props'] || {};
|
||
const resource = useResourceRequest({
|
||
associatedName,
|
||
resourceName,
|
||
associatedKey,
|
||
});
|
||
const service = useRequest((params) => resource.list(params), {
|
||
manual: schema['x-component'] === 'Form',
|
||
formatResult: (data) => data?.data?.[0] || {},
|
||
onSuccess: options?.onSuccess,
|
||
});
|
||
return { resource, service, initialValues: service.data, ...service };
|
||
};
|
||
|
||
export const SchemaField = createSchemaField({
|
||
scope: {
|
||
t: i18n.t.bind(i18n),
|
||
Table,
|
||
Calendar,
|
||
Kanban,
|
||
useAsyncDataSource,
|
||
useClient,
|
||
useResourceRequest,
|
||
ChinaRegion: {
|
||
useFieldValue: useChinaRegionFieldValue,
|
||
loadData: loadChinaRegionData,
|
||
loadDataSource: loadChinaRegionDataSource,
|
||
},
|
||
Select,
|
||
Association: {
|
||
useResource: useAssociationResource,
|
||
},
|
||
},
|
||
components: {
|
||
Card,
|
||
Div,
|
||
Space,
|
||
Page,
|
||
Chart,
|
||
|
||
ArrayItems,
|
||
ArrayCollapse,
|
||
ArrayTable,
|
||
FormLayout,
|
||
TreeSelect,
|
||
FormilyFormItem,
|
||
|
||
DragAndDrop,
|
||
|
||
BlockItem,
|
||
CardItem,
|
||
FormItem,
|
||
|
||
Action,
|
||
AddNew,
|
||
Calendar,
|
||
Cascader,
|
||
Checkbox,
|
||
ColorSelect,
|
||
DatabaseField,
|
||
DatabaseCollection,
|
||
DatePicker,
|
||
Filter,
|
||
Form,
|
||
Grid,
|
||
IconPicker,
|
||
Input,
|
||
InputNumber,
|
||
Kanban,
|
||
Markdown,
|
||
Menu,
|
||
Password,
|
||
Radio,
|
||
Select,
|
||
Table,
|
||
Tabs,
|
||
TimePicker,
|
||
Upload,
|
||
|
||
ActionLogs,
|
||
},
|
||
});
|
||
|
||
export const DesignableContext = createContext<DesignableContextProps>({});
|
||
|
||
export function useSchema(path?: any) {
|
||
const { schema, refresh } = useContext(DesignableContext);
|
||
return { schema, refresh };
|
||
}
|
||
|
||
export function pathToArray(path): string[] {
|
||
if (Array.isArray(path)) {
|
||
return [...path];
|
||
}
|
||
|
||
if (typeof path === 'string') {
|
||
return path.split('.');
|
||
}
|
||
}
|
||
|
||
export function findPropertyByPath(schema: Schema, path?: any): Schema {
|
||
if (!path) {
|
||
return schema;
|
||
}
|
||
const arr = pathToArray(path);
|
||
let property = schema;
|
||
while (arr.length) {
|
||
const name = arr.shift();
|
||
if (!property.properties) {
|
||
console.warn('property does not exist.');
|
||
return null;
|
||
}
|
||
property = property.properties[name];
|
||
if (!property) {
|
||
console.warn('property does not exist.');
|
||
return null;
|
||
}
|
||
}
|
||
return property;
|
||
}
|
||
|
||
export function addPropertyBefore(
|
||
target: Schema,
|
||
data: ISchema | FormilyISchema,
|
||
) {
|
||
Object.keys(target.parent.properties).forEach((name) => {
|
||
if (name === target.name) {
|
||
target.parent.addProperty(data.name, data);
|
||
}
|
||
const property = target.parent.properties[name];
|
||
property.parent.removeProperty(property.name);
|
||
target.parent.addProperty(property.name, property.toJSON());
|
||
});
|
||
}
|
||
|
||
export function addPropertyAfter(
|
||
target: Schema,
|
||
data: ISchema | FormilyISchema,
|
||
) {
|
||
Object.keys(target.parent.properties).forEach((name) => {
|
||
const property = target.parent.properties[name];
|
||
property.parent.removeProperty(property.name);
|
||
target.parent.addProperty(property.name, property.toJSON());
|
||
if (name === target.name) {
|
||
target.parent.addProperty(data.name, data);
|
||
}
|
||
});
|
||
}
|
||
|
||
function setKeys(schema: ISchema | FormilyISchema, parentKey = null) {
|
||
if (!schema['key']) {
|
||
schema['key'] = uid();
|
||
}
|
||
if (parentKey) {
|
||
schema['parentKey'] = parentKey;
|
||
}
|
||
Object.keys(schema.properties || {}).forEach((name) => {
|
||
setKeys(schema.properties[name], schema['key']);
|
||
});
|
||
}
|
||
|
||
export function useSchemaComponent(component: string) {
|
||
if (!component) {
|
||
return null;
|
||
}
|
||
const options = useContext(SchemaOptionsContext);
|
||
return get(options.components, component);
|
||
}
|
||
|
||
export function useDesignable(path?: any) {
|
||
let { designable = true } = useDesignableSwitchContext();
|
||
const { schema = new Schema({}), refresh } = useContext(DesignableContext);
|
||
const schemaPath = path || useSchemaPath();
|
||
const fieldSchema = useFieldSchema();
|
||
let current;
|
||
let currentSchema = (current =
|
||
findPropertyByPath(schema, schemaPath) || ({} as Schema));
|
||
if (!currentSchema) {
|
||
currentSchema = fieldSchema;
|
||
}
|
||
if (Object.keys(currentSchema).length === 0) {
|
||
currentSchema = fieldSchema;
|
||
}
|
||
// console.log('useDesignable', { schema, schemaPath, currentSchema });
|
||
const options = useContext(SchemaOptionsContext);
|
||
let DesignableBar = get(
|
||
options.components,
|
||
currentSchema['x-designable-bar'],
|
||
);
|
||
if (!designable) {
|
||
DesignableBar = () => null;
|
||
}
|
||
if (!DesignableBar) {
|
||
DesignableBar = () => null;
|
||
}
|
||
return {
|
||
designable,
|
||
root: schema,
|
||
currentSchema: current,
|
||
DesignableBar,
|
||
schema: currentSchema,
|
||
refresh,
|
||
prepend: (property: ISchema | FormilyISchema, targetPath?: any): Schema => {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
if (!property.name) {
|
||
property.name = uid();
|
||
}
|
||
if (target['key']) {
|
||
property['parentKey'] = target['key'];
|
||
setKeys(property);
|
||
property['__prepend__'] = true;
|
||
// if (!property['key']) {
|
||
// property['key'] = uid();
|
||
// }
|
||
}
|
||
const properties = {};
|
||
properties[property.name] = property;
|
||
Object.keys(target.properties).forEach((name, index) => {
|
||
const current = target.properties[name];
|
||
current.parent.removeProperty(current.name);
|
||
properties[current.name] = current.toJSON();
|
||
});
|
||
// console.log({ properties }, target.properties);
|
||
target.setProperties(properties);
|
||
refresh();
|
||
return target.properties[property.name];
|
||
},
|
||
appendChild: (
|
||
property: ISchema | FormilyISchema,
|
||
targetPath?: any,
|
||
): Schema => {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
if (!property.name) {
|
||
property.name = uid();
|
||
}
|
||
if (target['key']) {
|
||
property['parentKey'] = target['key'];
|
||
// if (!property['key']) {
|
||
// property['key'] = uid();
|
||
// }
|
||
setKeys(property);
|
||
}
|
||
target.addProperty(property.name, property);
|
||
// BUG: 空 properties 时,addProperty 无反应。
|
||
const tmp = { name: uid() };
|
||
addPropertyAfter(target, tmp);
|
||
target.parent.removeProperty(tmp.name);
|
||
refresh();
|
||
return target.properties[property.name];
|
||
},
|
||
insertAfter: (
|
||
property: ISchema | FormilyISchema,
|
||
targetPath?: any,
|
||
): Schema => {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
if (!property.name) {
|
||
property.name = uid();
|
||
}
|
||
console.log('target.parentKey', target);
|
||
if (target['parentKey']) {
|
||
property['parentKey'] = target['parentKey'];
|
||
setKeys(property);
|
||
property['__insertAfter__'] = target['key'];
|
||
}
|
||
addPropertyAfter(target, property);
|
||
refresh();
|
||
return target.parent.properties[property.name];
|
||
},
|
||
insertBefore(property: ISchema | FormilyISchema, targetPath?: any): Schema {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
if (!property.name) {
|
||
property.name = uid();
|
||
}
|
||
if (target['parentKey']) {
|
||
property['parentKey'] = target['parentKey'];
|
||
setKeys(property);
|
||
property['__insertBefore__'] = target['key'];
|
||
}
|
||
addPropertyBefore(target, property);
|
||
refresh();
|
||
return target.parent.properties[property.name];
|
||
},
|
||
deepRemoveIfEmpty(targetPath) {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
const removed = [];
|
||
const remove = (s: Schema) => {
|
||
if (!s.parent) {
|
||
return;
|
||
}
|
||
s.parent.removeProperty(s.name);
|
||
removed.push(s);
|
||
if (s['x-component'] === 'Grid.Row') {
|
||
return;
|
||
}
|
||
if (Object.keys(s.parent.properties || {}).length === 0) {
|
||
remove(s.parent);
|
||
}
|
||
};
|
||
console.log({ removed });
|
||
if (Object.keys(target.properties || {}).length === 0) {
|
||
remove(target);
|
||
}
|
||
refresh();
|
||
return removed;
|
||
},
|
||
deepRemove(targetPath?: any) {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
const removed = [];
|
||
const remove = (s: Schema) => {
|
||
if (!s.parent) {
|
||
return;
|
||
}
|
||
s.parent.removeProperty(s.name);
|
||
removed.push(s);
|
||
if (s['x-component'] === 'Grid.Row') {
|
||
return;
|
||
}
|
||
if (Object.keys(s.parent.properties || {}).length === 0) {
|
||
remove(s.parent);
|
||
}
|
||
};
|
||
console.log({ removed });
|
||
remove(target);
|
||
refresh();
|
||
return removed;
|
||
},
|
||
remove(targetPath?: any) {
|
||
let target = currentSchema;
|
||
if (targetPath) {
|
||
target = findPropertyByPath(schema, targetPath);
|
||
}
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
target.parent.removeProperty(target.name);
|
||
refresh();
|
||
return target;
|
||
},
|
||
moveToBefore(path1, path2) {
|
||
const source = findPropertyByPath(schema, path1);
|
||
const property = source.toJSON() as ISchema;
|
||
const target = findPropertyByPath(schema, path2);
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
if (!property.name) {
|
||
property.name = uid();
|
||
}
|
||
if (target['parentKey']) {
|
||
property['parentKey'] = target['parentKey'];
|
||
setKeys(property);
|
||
property['__insertBefore__'] = target['key'];
|
||
}
|
||
addPropertyBefore(target, property);
|
||
refresh();
|
||
return target.parent.properties[property.name];
|
||
},
|
||
moveToAfter(path1, path2) {
|
||
const source = findPropertyByPath(schema, path1);
|
||
const property = source.toJSON() as ISchema;
|
||
const target = findPropertyByPath(schema, path2);
|
||
if (!target) {
|
||
console.error('target schema does not exist.');
|
||
return;
|
||
}
|
||
if (!property.name) {
|
||
property.name = uid();
|
||
}
|
||
if (target['parentKey']) {
|
||
property['parentKey'] = target['parentKey'];
|
||
setKeys(property);
|
||
property['__insertAfter__'] = target['key'];
|
||
}
|
||
addPropertyAfter(target, property);
|
||
|
||
refresh();
|
||
return target.parent.properties[property.name];
|
||
},
|
||
};
|
||
}
|
||
|
||
export function getSchemaPath(schema: Schema) {
|
||
const path = schema['x-path'] || [schema.name];
|
||
let parent = schema.parent;
|
||
while (parent) {
|
||
// if (!parent.name) {
|
||
// break;
|
||
// }
|
||
if (parent['x-path']) {
|
||
path.unshift(...parent['x-path']);
|
||
} else if (parent.name) {
|
||
path.unshift(parent.name);
|
||
}
|
||
parent = parent.parent;
|
||
}
|
||
// console.log('getSchemaPath', path, schema);
|
||
return [...path];
|
||
}
|
||
|
||
export function useSchemaPath() {
|
||
const schema = useFieldSchema();
|
||
const path = getSchemaPath(schema);
|
||
return [...path];
|
||
}
|
||
|
||
const FormValues = () => {
|
||
const form = useForm();
|
||
const [visible, setVisible] = useState(false);
|
||
return (
|
||
<>
|
||
<CodeOutlined onClick={() => setVisible(true)} />
|
||
<Modal
|
||
width={'50%'}
|
||
onOk={() => setVisible(false)}
|
||
onCancel={() => setVisible(false)}
|
||
visible={visible}
|
||
>
|
||
<Editor
|
||
height="60vh"
|
||
defaultLanguage="json"
|
||
value={JSON.stringify(form.values, null, 2)}
|
||
/>
|
||
{/* <pre>{JSON.stringify(schema.toJSON(), null, 2)}</pre> */}
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const CodePreview = ({ schema }) => {
|
||
const [visible, setVisible] = useState(false);
|
||
return (
|
||
<>
|
||
<CodeOutlined
|
||
style={{ position: 'relative', zIndex: 100 }}
|
||
onClick={() => setVisible(true)}
|
||
/>
|
||
<Modal
|
||
width={'50%'}
|
||
onOk={() => setVisible(false)}
|
||
onCancel={() => setVisible(false)}
|
||
visible={visible}
|
||
>
|
||
<Editor
|
||
height="60vh"
|
||
defaultLanguage="json"
|
||
value={JSON.stringify(schema.toJSON(), null, 2)}
|
||
/>
|
||
{/* <pre>{JSON.stringify(schema.toJSON(), null, 2)}</pre> */}
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export interface SchemaRendererProps {
|
||
schema: Schema | ISchema | FormilyISchema;
|
||
form?: any;
|
||
render?: any;
|
||
components?: any;
|
||
scope?: any;
|
||
debug?: boolean;
|
||
onlyRenderProperties?: boolean;
|
||
onRefresh?: any;
|
||
[key: string]: any;
|
||
}
|
||
|
||
export const SchemaRenderer = (props: SchemaRendererProps) => {
|
||
const { designable = true } = useDesignableSwitchContext();
|
||
const [, refresh] = useState(uid());
|
||
const form = useMemo(() => props.form || createForm(), []);
|
||
const schema = useMemo(() => {
|
||
if (Schema.isSchemaInstance(props.schema)) {
|
||
return props.schema;
|
||
}
|
||
let s = props.schema;
|
||
if (props.onlyRenderProperties) {
|
||
s = {
|
||
type: 'object',
|
||
properties: s.properties,
|
||
};
|
||
} else if (s.name) {
|
||
s = {
|
||
type: 'object',
|
||
properties: {
|
||
[s.name]: s,
|
||
},
|
||
};
|
||
}
|
||
return new Schema(s);
|
||
}, []);
|
||
// useEffect(() => {
|
||
// refresh(uid());
|
||
// }, [designable]);
|
||
console.log({ designable }, 'designable');
|
||
const defaultRender = ({ schema }) => (
|
||
<FormProvider form={form}>
|
||
<SchemaField
|
||
components={props.components}
|
||
scope={props.scope}
|
||
schema={schema}
|
||
/>
|
||
{props.debug && <CodePreview schema={schema} />}
|
||
{props.debug && <FormValues />}
|
||
</FormProvider>
|
||
);
|
||
return (
|
||
<DesignableContext.Provider
|
||
value={{
|
||
schema,
|
||
refresh: () => {
|
||
props.onRefresh && props.onRefresh(schema);
|
||
refresh(uid());
|
||
},
|
||
}}
|
||
>
|
||
<DesignableContext.Consumer>
|
||
{props.render || defaultRender}
|
||
</DesignableContext.Consumer>
|
||
</DesignableContext.Provider>
|
||
);
|
||
};
|