tachybase_todo/packages/client/src/components/schema-renderer/index.tsx
2021-08-19 23:37:02 +08:00

656 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { Table } 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';
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 = async (field) => {
const maxLevel = field.componentProps.maxLevel || 3;
const resource = Resource.make('china_regions');
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 = (
selectedOptions: CascaderOptionType[],
field: ArrayField,
) => {
const maxLevel = field.componentProps.maxLevel || 3;
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const resource = Resource.make('china_regions');
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];
});
};
export const SchemaField = createSchemaField({
scope: {
Table,
Calendar,
Kanban,
useAsyncDataSource,
ChinaRegion: {
loadData: loadChinaRegionData,
loadDataSource: loadChinaRegionDataSource,
},
},
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']) {
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];
},
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 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>
);
};