feat: improve schema Initializer

This commit is contained in:
chenos 2022-02-17 23:56:39 +08:00
parent 78e89b76ef
commit b656f69565
49 changed files with 1756 additions and 346 deletions

View File

@ -5,17 +5,18 @@ import {
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
APIClientProvider, APIClientProvider,
AuthLayout, AuthLayout,
CollectionManagerProvider,
CollectionManagerShortcut, CollectionManagerShortcut,
compose, compose,
DesignableSwitch, DesignableSwitch,
DocumentTitleProvider, DocumentTitleProvider,
i18n, i18n,
PluginManagerProvider, PluginManagerProvider,
RemoteCollectionManagerProvider,
RouteSchemaComponent, RouteSchemaComponent,
RouteSwitch, RouteSwitch,
RouteSwitchProvider, RouteSwitchProvider,
SchemaComponentProvider, SchemaComponentProvider,
SchemaInitializerProvider,
SigninPage, SigninPage,
SignupPage, SignupPage,
SystemSettingsProvider, SystemSettingsProvider,
@ -38,13 +39,32 @@ const providers = [
SystemSettingsProvider, SystemSettingsProvider,
[ [
PluginManagerProvider, PluginManagerProvider,
{ components: { ACLShortcut, DesignableSwitch, CollectionManagerShortcut, SystemSettingsShortcut } }, {
components: {
ACLShortcut,
DesignableSwitch,
CollectionManagerShortcut,
SystemSettingsShortcut,
},
},
], ],
[SchemaComponentProvider, { components: { Link, NavLink } }], [SchemaComponentProvider, { components: { Link, NavLink } }],
CollectionManagerProvider, RemoteCollectionManagerProvider,
SchemaInitializerProvider,
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
[DocumentTitleProvider, { addonAfter: 'NocoBase' }], [DocumentTitleProvider, { addonAfter: 'NocoBase' }],
[RouteSwitchProvider, { components: { AuthLayout, AdminLayout, RouteSchemaComponent, SigninPage, SignupPage } }], [
RouteSwitchProvider,
{
components: {
AuthLayout,
AdminLayout,
RouteSchemaComponent,
SigninPage,
SignupPage,
},
},
],
]; ];
const App = compose(...providers)(() => { const App = compose(...providers)(() => {

View File

@ -2,6 +2,7 @@ import { ISchema } from '@formily/react';
const collection = { const collection = {
name: 'collections', name: 'collections',
targetKey: 'name',
filterTargetKey: 'name', filterTargetKey: 'name',
fields: [ fields: [
{ {
@ -42,6 +43,7 @@ export const roleCollectionsSchema: ISchema = {
type: 'void', type: 'void',
'x-decorator': 'ResourceActionProvider', 'x-decorator': 'ResourceActionProvider',
'x-decorator-props': { 'x-decorator-props': {
collection,
association: { association: {
sourceKey: 'name', sourceKey: 'name',
targetKey: 'name', targetKey: 'name',
@ -58,10 +60,6 @@ export const roleCollectionsSchema: ISchema = {
}, },
}, },
}, },
'x-component': 'CollectionProvider',
'x-component-props': {
collection,
},
properties: { properties: {
table1: { table1: {
type: 'void', type: 'void',

View File

@ -38,9 +38,7 @@ export const roleSchema: ISchema = {
type: 'void', type: 'void',
'x-decorator': 'ResourceActionProvider', 'x-decorator': 'ResourceActionProvider',
'x-decorator-props': { 'x-decorator-props': {
collection: { collection,
targetKey: 'name',
},
resourceName: 'roles', resourceName: 'roles',
request: { request: {
resource: 'roles', resource: 'roles',

View File

@ -16,6 +16,7 @@ import {
RouteSwitch, RouteSwitch,
RouteSwitchProvider, RouteSwitchProvider,
SchemaComponentProvider, SchemaComponentProvider,
SchemaInitializerProvider,
SigninPage, SigninPage,
SignupPage, SignupPage,
SystemSettingsProvider, SystemSettingsProvider,
@ -42,6 +43,7 @@ const providers = [
[SchemaComponentProvider, { components: { Link, NavLink } }], [SchemaComponentProvider, { components: { Link, NavLink } }],
CollectionManagerProvider, CollectionManagerProvider,
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
SchemaInitializerProvider,
[DocumentTitleProvider, { addonAfter: 'NocoBase' }], [DocumentTitleProvider, { addonAfter: 'NocoBase' }],
[RouteSwitchProvider, { components: { AuthLayout, AdminLayout, RouteSchemaComponent, SigninPage, SignupPage } }], [RouteSwitchProvider, { components: { AuthLayout, AdminLayout, RouteSchemaComponent, SigninPage, SignupPage } }],
]; ];

View File

@ -1,15 +1,54 @@
import { Spin } from 'antd';
import React from 'react'; import React from 'react';
import { useAPIClient, useRequest } from '../api-client';
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider'; import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
import { CollectionManagerContext } from './context'; import { CollectionManagerContext } from './context';
import * as defaultInterfaces from './interfaces';
import { CollectionManagerOptions } from './types'; import { CollectionManagerOptions } from './types';
export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => { export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
const { interfaces, collections } = props; const { service, interfaces, collections = [], refreshCM } = props;
return ( return (
<CollectionManagerContext.Provider value={{ interfaces, collections }}> <CollectionManagerContext.Provider
<CollectionManagerSchemaComponentProvider> value={{
{props.children} service,
</CollectionManagerSchemaComponentProvider> interfaces: { ...defaultInterfaces, ...interfaces },
collections,
refreshCM: async () => {
if (refreshCM) {
await refreshCM();
}
},
}}
>
<CollectionManagerSchemaComponentProvider>{props.children}</CollectionManagerSchemaComponentProvider>
</CollectionManagerContext.Provider> </CollectionManagerContext.Provider>
); );
}; };
export const RemoteCollectionManagerProvider = (props: any) => {
const api = useAPIClient();
const options = {
resource: 'collections',
action: 'list',
params: {
perPage: 999,
appends: ['fields', 'fields.uiSchema'],
},
};
const service = useRequest(options);
if (service.loading) {
return <Spin />;
}
return (
<CollectionManagerProvider
service={service}
collections={service?.data?.data}
refreshCM={async () => {
const { data } = await api.request(options);
service.mutate(data);
}}
{...props}
/>
);
};

View File

@ -1,23 +1,10 @@
import { DatabaseOutlined } from '@ant-design/icons'; import { DatabaseOutlined } from '@ant-design/icons';
import { ISchema, useForm } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { PluginManager } from '../plugin-manager'; import { PluginManager } from '../plugin-manager';
import { ActionContext, SchemaComponent, useActionContext } from '../schema-component'; import { ActionContext, SchemaComponent } from '../schema-component';
import { ConfigurationTable } from './Configuration'; import { AddFieldAction, ConfigurationTable, EditFieldAction } from './Configuration';
const useCloseAction = () => {
const { setVisible } = useActionContext();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -46,7 +33,7 @@ export const CollectionManagerShortcut = () => {
setVisible(true); setVisible(true);
}} }}
/> />
<SchemaComponent scope={{ useCloseAction }} schema={schema} components={{ ConfigurationTable }} /> <SchemaComponent schema={schema} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} />
</ActionContext.Provider> </ActionContext.Provider>
); );
}; };

View File

@ -0,0 +1,134 @@
import { PlusOutlined } from '@ant-design/icons';
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd';
import React, { useState } from 'react';
import { useAPIClient } from '../../api-client';
import { useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent } from '../../schema-component';
import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
const getSchema = (schema: IField, useAction): ISchema => {
if (!schema) {
return;
}
return {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
initialValue: {
...schema.default,
name: `f_${uid()}`,
},
},
title: 'Drawer Title',
properties: {
type: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
name: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
// @ts-ignore
...schema.properties,
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction,
},
},
},
},
},
},
},
};
};
export const AddFieldAction = () => {
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<Dropdown
overlay={
<Menu
onClick={(info) => {
const schema = getSchema(getInterface(info.key), '{{ useCreateActionAndRefreshCM }}');
setSchema(schema);
setVisible(true);
}}
>
<Menu.SubMenu title={'基本类型'}>
<Menu.Item key={'input'}>input</Menu.Item>
<Menu.Item key={'textarea'}>textarea</Menu.Item>
<Menu.Item key={'chinaRegion'}>China region</Menu.Item>
</Menu.SubMenu>
</Menu>
}
>
<Button icon={<PlusOutlined />} type={'primary'}>
</Button>
</Dropdown>
<SchemaComponent schema={schema} />
</ActionContext.Provider>
);
};
export const EditFieldAction = (props) => {
const record = useRecord();
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
const api = useAPIClient();
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<a
onClick={async () => {
const { data } = await api.resource('collections.fields', record.collectionName).get({
filterByTk: record.name,
appends: ['uiSchema'],
});
const schema = getSchema(
{
...getInterface(record.interface),
default: data?.data,
},
'{{ useUpdateActionAndRefreshCM }}',
);
setSchema(schema);
setVisible(true);
}}
>
</a>
<SchemaComponent schema={schema} />
</ActionContext.Provider>
);
};

View File

@ -5,9 +5,7 @@ import { collectionSchema } from './schemas/collections';
export const ConfigurationTable = () => { export const ConfigurationTable = () => {
return ( return (
<div> <div>
<SchemaComponent <SchemaComponent schema={collectionSchema} />
schema={collectionSchema}
/>
</div> </div>
); );
}; };

View File

@ -1 +1,3 @@
export * from './AddFieldAction';
export * from './ConfigurationTable'; export * from './ConfigurationTable';

View File

@ -20,6 +20,16 @@ const collection = {
required: true, required: true,
} as ISchema, } as ISchema,
}, },
{
type: 'string',
name: 'interface',
interface: 'input',
uiSchema: {
title: '字段类型',
type: 'string',
'x-component': 'Input',
},
},
{ {
type: 'string', type: 'string',
name: 'title', name: 'title',
@ -54,6 +64,7 @@ export const collectionFieldSchema: ISchema = {
sourceKey: 'name', sourceKey: 'name',
targetKey: 'name', targetKey: 'name',
}, },
collection,
request: { request: {
resource: 'collections.fields', resource: 'collections.fields',
action: 'list', action: 'list',
@ -65,10 +76,10 @@ export const collectionFieldSchema: ISchema = {
}, },
}, },
}, },
'x-component': 'CollectionProvider', // 'x-component': 'CollectionProvider',
'x-component-props': { // 'x-component-props': {
collection, // collection,
}, // },
properties: { properties: {
actions: { actions: {
type: 'void', type: 'void',
@ -82,57 +93,14 @@ export const collectionFieldSchema: ISchema = {
create: { create: {
type: 'void', type: 'void',
title: '创建', title: '创建',
'x-component': 'Action', 'x-component': 'AddFieldAction',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
}, },
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
title: 'Drawer Title',
properties: {
type: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
name: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useCreateAction }}',
}, },
}, },
}, },
}, table: {
},
},
},
},
},
},
table1: {
type: 'void', type: 'void',
'x-uid': 'input', 'x-uid': 'input',
'x-component': 'VoidTable', 'x-component': 'VoidTable',
@ -169,6 +137,18 @@ export const collectionFieldSchema: ISchema = {
}, },
}, },
column3: { column3: {
type: 'void',
'x-decorator': 'TableColumnDecorator',
'x-component': 'VoidTable.Column',
properties: {
interface: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column4: {
type: 'void', type: 'void',
title: 'Actions', title: 'Actions',
'x-component': 'VoidTable.Column', 'x-component': 'VoidTable.Column',
@ -183,60 +163,17 @@ export const collectionFieldSchema: ISchema = {
update: { update: {
type: 'void', type: 'void',
title: '编辑', title: '编辑',
'x-component': 'Action.Link', 'x-component': 'EditFieldAction',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
}, },
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ useValues }}',
},
title: 'Drawer Title',
properties: {
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
name: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-disabled': true,
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useUpdateAction }}',
},
},
},
},
},
},
},
}, },
delete: { delete: {
type: 'void', type: 'void',
title: '删除', title: '删除',
'x-component': 'Action.Link', 'x-component': 'Action.Link',
'x-component-props': { 'x-component-props': {
useAction: '{{ useDestroyAction }}', useAction: '{{ useDestroyActionAndRefreshCM }}',
}, },
}, },
}, },

View File

@ -1,9 +1,11 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { CollectionOptions } from '../../types';
import { collectionFieldSchema } from './collectionFields'; import { collectionFieldSchema } from './collectionFields';
const collection = { const collection: CollectionOptions = {
name: 'collections', name: 'collections',
filterTargetKey: 'name', filterTargetKey: 'name',
targetKey: 'name',
fields: [ fields: [
{ {
type: 'integer', type: 'integer',
@ -14,7 +16,7 @@ const collection = {
type: 'number', type: 'number',
'x-component': 'Input', 'x-component': 'Input',
required: true, required: true,
} as ISchema, },
}, },
{ {
type: 'string', type: 'string',
@ -25,7 +27,7 @@ const collection = {
type: 'string', type: 'string',
'x-component': 'Input', 'x-component': 'Input',
description: '使用英文', description: '使用英文',
} as ISchema, },
}, },
{ {
type: 'hasMany', type: 'hasMany',
@ -47,9 +49,7 @@ export const collectionSchema: ISchema = {
'x-collection': 'collections', 'x-collection': 'collections',
'x-decorator': 'ResourceActionProvider', 'x-decorator': 'ResourceActionProvider',
'x-decorator-props': { 'x-decorator-props': {
collection: { collection,
targetKey: 'name',
},
request: { request: {
resource: 'collections', resource: 'collections',
action: 'list', action: 'list',
@ -61,10 +61,10 @@ export const collectionSchema: ISchema = {
}, },
}, },
}, },
'x-component': 'CollectionProvider', // 'x-component': 'CollectionProvider',
'x-component-props': { // 'x-component-props': {
collection, // collection,
}, // },
properties: { properties: {
actions: { actions: {
type: 'void', type: 'void',
@ -113,7 +113,7 @@ export const collectionSchema: ISchema = {
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
useAction: '{{ useCreateAction }}', useAction: '{{ useCreateActionAndRefreshCM }}',
}, },
}, },
}, },
@ -201,7 +201,7 @@ export const collectionSchema: ISchema = {
'x-component': 'Action.Drawer', 'x-component': 'Action.Drawer',
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
useValues: '{{ useValues }}', useValues: '{{ useValuesFromRecord }}',
}, },
title: 'Drawer Title', title: 'Drawer Title',
properties: { properties: {
@ -230,7 +230,7 @@ export const collectionSchema: ISchema = {
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
useAction: '{{ useUpdateAction }}', useAction: '{{ useUpdateActionAndRefreshCM }}',
}, },
}, },
}, },
@ -244,7 +244,7 @@ export const collectionSchema: ISchema = {
title: '删除', title: '删除',
'x-component': 'Action.Link', 'x-component': 'Action.Link',
'x-component-props': { 'x-component-props': {
useAction: '{{ useDestroyAction }}', useAction: '{{ useDestroyActionAndRefreshCM }}',
}, },
}, },
}, },

View File

@ -1,6 +1,6 @@
import { Result } from 'ahooks/lib/useRequest/src/types'; import { Result } from 'ahooks/lib/useRequest/src/types';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useRecord } from '..'; import { CollectionProvider, useRecord } from '..';
import { useAPIClient, useRequest } from '../api-client'; import { useAPIClient, useRequest } from '../api-client';
export const ResourceActionContext = createContext<Result<any, any>>(null); export const ResourceActionContext = createContext<Result<any, any>>(null);
@ -18,18 +18,20 @@ const CollectionResourceActionProvider = (props) => {
const api = useAPIClient(); const api = useAPIClient();
const service = useRequest(request, { const service = useRequest(request, {
uid, uid,
refreshDeps: [request], // refreshDeps: [request],
}); });
const resource = api.resource(request.resource); const resource = api.resource(request.resource);
return ( return (
<ResourceContext.Provider value={{ type: 'collection', resource, collection }}> <ResourceContext.Provider value={{ type: 'collection', resource, collection }}>
<ResourceActionContext.Provider value={service}>{props.children}</ResourceActionContext.Provider> <ResourceActionContext.Provider value={service}>
<CollectionProvider collection={collection}>{props.children}</CollectionProvider>
</ResourceActionContext.Provider>
</ResourceContext.Provider> </ResourceContext.Provider>
); );
}; };
const AssociationResourceActionProvider = (props) => { const AssociationResourceActionProvider = (props) => {
const { association, request, uid } = props; const { collection, association, request, uid } = props;
const api = useAPIClient(); const api = useAPIClient();
const record = useRecord(); const record = useRecord();
const resourceOf = record[association.sourceKey]; const resourceOf = record[association.sourceKey];
@ -37,13 +39,15 @@ const AssociationResourceActionProvider = (props) => {
{ resourceOf, ...request }, { resourceOf, ...request },
{ {
uid, uid,
refreshDeps: [request, resourceOf], // refreshDeps: [request, resourceOf],
}, },
); );
const resource = api.resource(request.resource, resourceOf); const resource = api.resource(request.resource, resourceOf);
return ( return (
<ResourceContext.Provider value={{ type: 'association', resource, association }}> <ResourceContext.Provider value={{ type: 'association', resource, association }}>
<ResourceActionContext.Provider value={service}>{props.children}</ResourceActionContext.Provider> <ResourceActionContext.Provider value={service}>
<CollectionProvider collection={collection}>{props.children}</CollectionProvider>
</ResourceActionContext.Provider>
</ResourceContext.Provider> </ResourceContext.Provider>
); );
}; };

View File

@ -1,4 +1,5 @@
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { useCollectionManager } from '.';
import { useRequest } from '../api-client'; import { useRequest } from '../api-client';
import { useRecord } from '../record-provider'; import { useRecord } from '../record-provider';
import { useActionContext } from '../schema-component'; import { useActionContext } from '../schema-component';
@ -60,10 +61,43 @@ export const useDestroyAction = () => {
}; };
}; };
export const useValues = (options) => { export const useValuesFromRecord = (options) => {
const record = useRecord(); const record = useRecord();
return useRequest(() => Promise.resolve({ data: record }), { return useRequest(() => Promise.resolve({ data: record }), {
...options, ...options,
refreshDeps: [record], refreshDeps: [record],
}); });
}; };
export const useCreateActionAndRefreshCM = () => {
const { run } = useCreateAction();
const { refreshCM } = useCollectionManager();
return {
async run() {
await run();
await refreshCM();
},
};
};
export const useUpdateActionAndRefreshCM = () => {
const { run } = useUpdateAction();
const { refreshCM } = useCollectionManager();
return {
async run() {
await run();
await refreshCM();
},
};
};
export const useDestroyActionAndRefreshCM = () => {
const { run } = useDestroyAction();
const { refreshCM } = useCollectionManager();
return {
async run() {
await run();
await refreshCM();
},
};
};

View File

@ -11,7 +11,6 @@ export const useCollectionField = () => {
const ctx = useContext(CollectionFieldContext); const ctx = useContext(CollectionFieldContext);
const resourceName = `${ctx?.collectinName || collection?.name}.${ctx.name}`; const resourceName = `${ctx?.collectinName || collection?.name}.${ctx.name}`;
const resource = api?.resource(resourceName, record[ctx.sourceKey]); const resource = api?.resource(resourceName, record[ctx.sourceKey]);
console.log({ resourceName });
return { return {
...ctx, ...ctx,
resource, resource,

View File

@ -1,11 +1,19 @@
import { clone } from '@formily/shared';
import { useContext } from 'react'; import { useContext } from 'react';
import { CollectionManagerContext } from '../context'; import { CollectionManagerContext } from '../context';
export const useCollectionManager = () => { export const useCollectionManager = () => {
const { collections } = useContext(CollectionManagerContext); const { refreshCM, service, interfaces, collections } = useContext(CollectionManagerContext);
return { return {
service,
interfaces,
collections,
refreshCM: () => refreshCM?.(),
get(name: string) { get(name: string) {
return collections?.find((collection) => collection.name === name); return collections?.find((collection) => collection.name === name);
}, },
getInterface(name) {
return interfaces[name] ? clone(interfaces[name]) : null;
},
}; };
}; };

View File

@ -0,0 +1,73 @@
import { uid } from '@formily/shared';
import { IField } from './types';
export const chinaRegion: IField = {
name: 'chinaRegion',
type: 'object',
group: 'choices',
order: 7,
title: '{{t("China region")}}',
isAssociation: true,
default: {
interface: 'chinaRegion',
type: 'belongsToMany',
target: 'chinaRegions',
targetKey: 'code',
// name,
uiSchema: {
type: 'array',
// title,
'x-component': 'Cascader',
'x-component-props': {
changeOnSelectLast: false,
labelInValue: true,
maxLevel: 3,
fieldNames: {
label: 'name',
value: 'code',
children: 'children',
},
},
},
},
initialize: (values: any) => {
if (!values.through) {
values.through = `t_${uid()}`;
}
if (!values.foreignKey) {
values.foreignKey = `f_${uid()}`;
}
if (!values.otherKey) {
values.otherKey = `f_${uid()}`;
}
if (!values.sourceKey) {
values.sourceKey = 'id';
}
if (!values.targetKey) {
values.targetKey = 'id';
}
},
properties: {
'uiSchema.x-component-props.maxLevel': {
type: 'number',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
title: '{{t("Select level")}}',
default: 3,
enum: [
{ value: 1, label: '{{t("Province")}}' },
{ value: 2, label: '{{t("City")}}' },
{ value: 3, label: '{{t("Area")}}' },
{ value: 4, label: '{{t("Street")}}' },
{ value: 5, label: '{{t("Village")}}' },
],
},
'uiSchema.x-component-props.changeOnSelectLast': {
type: 'boolean',
'x-component': 'Checkbox',
'x-content': '{{t("Must select to the last level")}}',
'x-decorator': 'FormItem',
},
},
operators: [{ label: '{{t("is")}}', value: 'code.in' }],
};

View File

@ -0,0 +1,4 @@
export * from './chinaRegion';
export * from './input';
export * from './textarea';

View File

@ -0,0 +1,28 @@
import { IField } from './types';
export const input: IField = {
name: 'string',
type: 'object',
group: 'basic',
order: 1,
title: '{{t("Single line text")}}',
sortable: true,
default: {
interface: 'input',
type: 'string',
uiSchema: {
type: 'string',
'x-component': 'Input',
},
},
properties: {
},
operators: [
{ label: '{{t("contains")}}', value: '$includes', selected: true },
{ label: '{{t("does not contain")}}', value: '$notIncludes' },
{ label: '{{t("is")}}', value: 'eq' },
{ label: '{{t("is not")}}', value: 'ne' },
{ label: '{{t("is empty")}}', value: '$null', noValue: true },
{ label: '{{t("is not empty")}}', value: '$notNull', noValue: true },
],
};

View File

@ -0,0 +1,198 @@
import { ISchema } from '@formily/react';
export const type: ISchema = {
type: 'string',
title: '{{t("Storage type")}}',
required: true,
'x-disabled': true,
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: 'Boolean', value: 'boolean' },
{ label: 'String', value: 'string' },
{ label: 'Text', value: 'text' },
{ label: 'Integer', value: 'integer' },
{ label: 'Float', value: 'float' },
{ label: 'Decimal', value: 'decimal' },
{ label: 'Date', value: 'date' },
{ label: 'DateOnly', value: 'dateonly' },
{ label: 'Time', value: 'time' },
{ label: 'Virtual', value: 'virtual' },
{ label: 'JSON', value: 'json' },
{ label: 'Password', value: 'password' },
{ label: 'HasOne', value: 'hasOne' },
{ label: 'HasMany', value: 'hasMany' },
{ label: 'BelongsTo', value: 'belongsTo' },
{ label: 'BelongsToMany', value: 'belongsToMany' },
],
};
export const dateTimeProps: { [key: string]: ISchema } = {
'uiSchema.x-component-props.dateFormat': {
type: 'string',
title: '{{t("Date format")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'YYYY-MM-DD',
enum: [
{
label: '{{t("Year/Month/Day")}}',
value: 'YYYY/MM/DD',
},
{
label: '{{t("Year-Month-Day")}}',
value: 'YYYY-MM-DD',
},
{
label: '{{t("Day/Month/Year")}}',
value: 'DD/MM/YYYY',
},
],
},
'uiSchema.x-component-props.showTime': {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Show time")}}',
'x-reactions': [
`{{(field) => {
field.query('..[].timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none';
});
}}}`,
],
},
'uiSchema.x-component-props.timeFormat': {
type: 'string',
title: '{{t("Time format")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'HH:mm:ss',
enum: [
{
label: '{{t("12 hour")}}',
value: 'hh:mm:ss a',
},
{
label: '{{t("24 hour")}}',
value: 'HH:mm:ss',
},
],
},
};
export const dataSource: ISchema = {
type: 'array',
title: '{{t("Options")}}',
'x-decorator': 'FormItem',
'x-component': 'ArrayTable',
'x-component-props': {
pagination: false,
// scroll: { x: '100%' },
},
items: {
type: 'object',
properties: {
column1: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { width: 50, title: '', align: 'center' },
properties: {
sort: {
type: 'void',
'x-component': 'ArrayTable.SortHandle',
},
},
},
column2: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '{{t("Option value")}}' },
'x-hidden': true,
properties: {
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
column3: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '{{t("Option label")}}' },
properties: {
label: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
column4: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { title: '{{t("Color")}}' },
properties: {
color: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'ColorSelect',
},
},
},
column5: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: '',
dataIndex: 'operations',
fixed: 'right',
},
properties: {
item: {
type: 'void',
'x-component': 'FormItem',
properties: {
remove: {
type: 'void',
'x-component': 'ArrayTable.Remove',
},
},
},
},
},
},
},
properties: {
add: {
type: 'void',
'x-component': 'ArrayTable.Addition',
'x-component-props': {
randomValue: true,
},
title: "{{t('Add option')}}",
},
},
};
export const defaultProps = {
'uiSchema.title': {
type: 'string',
title: '{{t("Field display name")}}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '{{t("Field name")}}',
required: true,
// 'x-disabled': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
},
type,
};

View File

@ -0,0 +1,19 @@
import { IField } from './types';
export const textarea: IField = {
name: 'textarea',
type: 'object',
group: 'basic',
order: 2,
title: '{{t("Long text")}}',
default: {
interface: 'textarea',
type: 'text',
// name,
uiSchema: {
type: 'string',
'x-component': 'Input.TextArea',
},
},
properties: {},
};

View File

@ -0,0 +1,12 @@
import { ISchema } from '@formily/react';
interface IDefault {
type: string;
uiSchema?: ISchema;
[key: string]: any;
}
export interface IField extends ISchema {
default?: IDefault;
[key: string]: any;
}

View File

@ -1,14 +1,24 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
export interface CollectionManagerOptions { export interface CollectionManagerOptions {
service?: any;
interfaces?: any; interfaces?: any;
collections?: any[]; collections?: any[];
refreshCM?: () => Promise<void>;
}
export interface FieldOptions {
type: string;
interface?: string;
uiSchema?: ISchema;
[key: string]: any;
} }
export interface CollectionOptions { export interface CollectionOptions {
name?: string; name?: string;
filterTargetKey?: string; filterTargetKey?: string;
fields?: any[]; targetKey?: string;
fields?: FieldOptions[];
} }
export interface ICollectionProviderProps { export interface ICollectionProviderProps {

View File

@ -1,4 +1,4 @@
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ActionContext } from './context'; import { ActionContext } from './context';
@ -23,7 +23,7 @@ export const ActionLink: ComposedAction = observer((props: any) => {
> >
{field.title} {field.title}
</Link> </Link>
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties /> {props.children}
</ActionContext.Provider> </ActionContext.Provider>
); );
}); });

View File

@ -1,4 +1,4 @@
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { observer, useField, useFieldSchema } from '@formily/react';
import { Button } from 'antd'; import { Button } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
import ActionContainer from './Action.Container'; import ActionContainer from './Action.Container';
@ -28,7 +28,7 @@ export const Action: ComposedAction = observer((props: any) => {
> >
{field.title} {field.title}
</Button> </Button>
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties /> {props.children}
</ActionContext.Provider> </ActionContext.Provider>
); );
}); });

View File

@ -1,58 +0,0 @@
import { observer } from '@formily/react';
import React from 'react';
import { SchemaInitializer } from '../../../schema-initializer';
const useTableColumnInitializerFields = () => {
const fields = [
{
title: 'Field1',
name: 'field1',
component: ArrayTableColumnInitializer,
},
{
title: 'Field2',
name: 'field2',
component: ArrayTableColumnInitializer,
},
].filter((field) => field.component);
return fields;
};
export const TableColumnInitializeButton = observer((props: any) => {
return (
<SchemaInitializer.Button
insertPosition={'beforeEnd'}
items={[
{
type: 'item',
title: 'Display fields',
children: useTableColumnInitializerFields(),
},
]}
>
Configure columns
</SchemaInitializer.Button>
);
});
export const ArrayTableColumnInitializer = SchemaInitializer.itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
onClick={() => {
insert({
type: 'void',
title: 'Name',
'x-component': 'ArrayTable.Column',
properties: {
name: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
});
}}
/>
);
});

View File

@ -0,0 +1,41 @@
import { observer, useFieldSchema } from '@formily/react';
import React from 'react';
import { createDesignable, useDesignable } from '../..';
import { useAPIClient } from '../../../api-client';
export const TableActionDesignable = observer((props) => {
const fieldSchema = useFieldSchema();
const api = useAPIClient();
const { refresh } = useDesignable();
return (
<div>
<span
onClick={() => {
const spaceSchema = fieldSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'Space') {
return schema;
}
return buf;
}, null);
const dn = createDesignable({
current: spaceSchema,
refresh,
api,
});
dn.loadAPIClientEvents();
dn.insertBeforeEnd({
type: 'void',
title: '{{ t("Delete") }}',
'x-component': 'Action.Link',
'x-component-props': {
useAction: '{{ useDestroyAction }}',
},
});
}}
>
Edit
</span>
{props.children}
</div>
);
});

View File

@ -0,0 +1,4 @@
import React from 'react';
export const TableActionDesignableBar = () => {
return <div>Edit</div>;
};

View File

@ -0,0 +1,14 @@
import { observer, useFieldSchema } from '@formily/react';
import React from 'react';
import { useComponent } from '../..';
export const TableColumnActionBar = observer((props) => {
const fieldSchema = useFieldSchema();
const ActionInitializer = useComponent(fieldSchema['x-action-initializer']);
return (
<div>
{ActionInitializer && <ActionInitializer />}
{props.children}
</div>
);
});

View File

@ -1,4 +1,5 @@
export * from './ArrayTable'; export * from './ArrayTable';
export * from './Initializer'; export * from './TableColumnActionBar';
export * from './TableColumnDecorator'; export * from './TableColumnDecorator';

View File

@ -1,9 +1,18 @@
import React from 'react'; import React from 'react';
import { useDesignable } from '../..';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
export const BlockItem: React.FC<any> = (props) => { export const BlockItem: React.FC<any> = (props) => {
const { remove } = useDesignable();
return ( return (
<SortableItem className={'nb-block-item'} style={{ position: 'relative' }}> <SortableItem className={'nb-block-item'} style={{ position: 'relative' }}>
<a
onClick={() => {
remove();
}}
>
</a>
{props.children} {props.children}
</SortableItem> </SortableItem>
); );

View File

@ -6,8 +6,6 @@ import cls from 'classnames';
import React, { createContext, useContext, useState } from 'react'; import React, { createContext, useContext, useState } from 'react';
import { useComponent } from '../..'; import { useComponent } from '../..';
import { DndContext } from '../../common/dnd-context'; import { DndContext } from '../../common/dnd-context';
import { AddGridBlockItem } from './AddBlockItem';
import { AddGridFormItem } from './AddFormItem';
const GridRowContext = createContext(null); const GridRowContext = createContext(null);
const GridColContext = createContext(null); const GridColContext = createContext(null);
@ -209,6 +207,3 @@ Grid.Col = observer((props: any) => {
</div> </div>
); );
}); });
Grid.AddFormItem = AddGridFormItem;
Grid.AddBlockItem = AddGridBlockItem;

View File

@ -1,6 +1,15 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Form, FormItem, Grid, Input, Markdown, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import {
Form,
FormItem,
Grid,
Input,
Markdown,
SchemaComponent,
SchemaComponentProvider,
SchemaInitializerProvider
} from '@nocobase/client';
import React from 'react'; import React from 'react';
const schema: ISchema = { const schema: ISchema = {
@ -8,7 +17,7 @@ const schema: ISchema = {
name: 'grid1', name: 'grid1',
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-component': 'Grid', 'x-component': 'Grid',
'x-item-initializer': 'Grid.AddFormItem', 'x-item-initializer': 'AddFormItem',
'x-uid': uid(), 'x-uid': uid(),
properties: { properties: {
row1: { row1: {
@ -56,7 +65,9 @@ const schema: ISchema = {
export default function App() { export default function App() {
return ( return (
<SchemaComponentProvider components={{ Markdown, Form, Grid, Input, FormItem }}> <SchemaComponentProvider components={{ Markdown, Form, Grid, Input, FormItem }}>
<SchemaInitializerProvider>
<SchemaComponent schema={schema} /> <SchemaComponent schema={schema} />
</SchemaInitializerProvider>
</SchemaComponentProvider> </SchemaComponentProvider>
); );
} }

View File

@ -9,6 +9,7 @@ import {
Markdown, Markdown,
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
SchemaInitializerProvider,
VoidTable VoidTable
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
@ -29,7 +30,7 @@ const schema: ISchema = {
grid: { grid: {
type: 'void', type: 'void',
'x-component': 'Grid', 'x-component': 'Grid',
'x-item-initializer': 'Grid.AddBlockItem', 'x-item-initializer': 'AddBlockItem',
'x-uid': uid(), 'x-uid': uid(),
properties: {}, properties: {},
}, },
@ -39,7 +40,9 @@ const schema: ISchema = {
export default function App() { export default function App() {
return ( return (
<SchemaComponentProvider components={{ BlockItem, Block, Grid, CardItem, Markdown, Form, VoidTable }}> <SchemaComponentProvider components={{ BlockItem, Block, Grid, CardItem, Markdown, Form, VoidTable }}>
<SchemaInitializerProvider>
<SchemaComponent schema={schema} /> <SchemaComponent schema={schema} />
</SchemaInitializerProvider>
</SchemaComponentProvider> </SchemaComponentProvider>
); );
} }

View File

@ -475,7 +475,7 @@ export function useDesignable() {
update(key); update(key);
refresh(); refresh();
}, },
remove(schema: any, options?: RemoveOptions) { remove(schema?: any, options?: RemoveOptions) {
dn.remove(schema, options); dn.remove(schema, options);
}, },
insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions) { insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions) {

View File

@ -0,0 +1,260 @@
import { TableOutlined } from '@ant-design/icons';
import { ISchema } from '@formily/react';
import { clone } from '@formily/shared';
import React from 'react';
import { SchemaInitializer } from '../../';
import { CollectionOptions, useCollectionManager } from '../../../collection-manager';
const collection: CollectionOptions = {
name: 'collections',
filterTargetKey: 'name',
targetKey: 'name',
fields: [
{
type: 'integer',
name: 'title',
interface: 'input',
uiSchema: {
title: '数据表名称',
type: 'number',
'x-component': 'Input',
required: true,
},
},
{
type: 'string',
name: 'name',
interface: 'input',
uiSchema: {
title: '数据表标识',
type: 'string',
'x-component': 'Input',
description: '使用英文',
},
},
{
type: 'hasMany',
name: 'fields',
target: 'fields',
collectionName: 'collections',
sourceKey: 'name',
targetKey: 'name',
uiSchema: {},
},
],
};
export const collectionSchema: ISchema = {
type: 'void',
'x-collection': 'collections',
'x-component': 'CardItem',
'x-decorator': 'ResourceActionProvider',
'x-decorator-props': {
collection,
request: {
resource: 'collections',
action: 'list',
params: {
pageSize: 5,
filter: {},
sort: ['sort'],
appends: [],
},
},
},
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-action-initializer': 'AddActionButton',
properties: {
delete: {
type: 'void',
title: '删除',
'x-component': 'Action',
},
create: {
type: 'void',
title: '创建',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
title: 'Drawer Title',
properties: {
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
name: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useCreateAction }}',
},
},
},
},
},
},
},
},
},
},
table1: {
type: 'void',
'x-component': 'VoidTable',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useDataSource: '{{ useDataSourceFromRAC }}',
},
'x-column-initializer': 'AddTableColumn',
properties: {
column3: {
type: 'void',
title: 'Actions',
'x-component': 'VoidTable.Column',
properties: {
actions: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
view: {
type: 'void',
title: '配置字段',
'x-component': 'Action.Link',
'x-component-props': {},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
title: 'Drawer Title',
properties: {},
},
},
},
update: {
type: 'void',
title: '编辑',
'x-component': 'Action.Link',
'x-component-props': {
type: 'primary',
},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ useValues }}',
},
title: 'Drawer Title',
properties: {
title: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
name: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-disabled': true,
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: 'Cancel',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useUpdateAction }}',
},
},
},
},
},
},
},
},
delete: {
type: 'void',
title: '删除',
'x-component': 'Action.Link',
'x-component-props': {
useAction: '{{ useDestroyAction }}',
},
},
},
},
},
},
},
},
},
};
const itemWrap = SchemaInitializer.itemWrap;
export const FormBlockInitializerItem = itemWrap((props) => {
const { insert } = props;
const { collections } = useCollectionManager();
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
onClick={() => {
insert(clone(collectionSchema));
}}
items={[
{
type: 'itemGroup',
title: 'select a data source',
children: collections?.map((item) => {
return {
type: 'item',
name: item.name,
title: item.title,
};
}),
},
]}
>
Form
</SchemaInitializer.Item>
);
});

View File

@ -0,0 +1,135 @@
import { TableOutlined } from '@ant-design/icons';
import { ISchema } from '@formily/react';
import { clone } from '@formily/shared';
import React from 'react';
import { SchemaInitializer } from '../..';
import { CollectionOptions, useCollectionManager } from '../../../collection-manager';
const collection: CollectionOptions = {
name: 'collections',
filterTargetKey: 'name',
targetKey: 'name',
fields: [
{
type: 'integer',
name: 'title',
interface: 'input',
uiSchema: {
title: '数据表名称',
type: 'number',
required: true,
'x-component': 'Input',
},
},
{
type: 'string',
name: 'name',
interface: 'input',
uiSchema: {
title: '数据表标识',
type: 'string',
description: '使用英文',
'x-component': 'Input',
},
},
{
type: 'hasMany',
name: 'fields',
target: 'fields',
collectionName: 'collections',
sourceKey: 'name',
targetKey: 'name',
uiSchema: {},
},
],
};
export const collectionSchema: ISchema = {
type: 'void',
'x-collection': 'collections',
'x-component': 'CardItem',
'x-decorator': 'ResourceActionProvider',
'x-decorator-props': {
collection,
request: {
resource: 'collections',
action: 'list',
params: {
pageSize: 5,
filter: {},
sort: ['sort'],
appends: [],
},
},
},
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-action-initializer': 'TableActionInitializer',
properties: {},
},
table1: {
type: 'void',
'x-component': 'VoidTable',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useDataSource: '{{ useDataSourceFromRAC }}',
},
'x-column-initializer': 'TableColumnInitializer',
properties: {
actions: {
type: 'void',
title: 'Actions',
'x-decorator': 'TableColumnActionBar',
'x-component': 'VoidTable.Column',
'x-action-initializer': 'TableColumnActionInitializer',
properties: {
actions: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {},
},
},
},
},
},
},
};
const itemWrap = SchemaInitializer.itemWrap;
export const TableBlockInitializerItem = itemWrap((props) => {
const { insert } = props;
const { collections } = useCollectionManager();
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
onClick={() => {
insert(clone(collectionSchema));
}}
items={[
{
type: 'itemGroup',
title: 'select a data source',
children: collections?.map((item) => {
return {
type: 'item',
name: item.name,
title: item.title,
};
}),
},
]}
>
Table
</SchemaInitializer.Item>
);
});

View File

@ -1,8 +1,10 @@
import { FormOutlined, TableOutlined } from '@ant-design/icons'; import { FormOutlined } from '@ant-design/icons';
import { ISchema, observer } from '@formily/react'; import { ISchema, observer } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import React from 'react'; import React from 'react';
import { SchemaInitializer } from '../../../schema-initializer'; import { SchemaInitializer } from '../..';
import { FormBlockInitializerItem } from './FormBlockInitializerItem';
import { TableBlockInitializerItem } from './TableBlockInitializerItem';
const gridRowColWrap = (schema: ISchema) => { const gridRowColWrap = (schema: ISchema) => {
return { return {
@ -22,87 +24,6 @@ const gridRowColWrap = (schema: ISchema) => {
const itemWrap = SchemaInitializer.itemWrap; const itemWrap = SchemaInitializer.itemWrap;
const TableBlockInitializerItem = itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
onClick={() => {
insert({
type: 'void',
'x-component': 'VoidTable',
'x-decorator': 'CardItem',
});
}}
items={[
{
type: 'itemGroup',
title: 'select a data source',
children: [
{
type: 'item',
name: 'users',
title: 'Users',
},
{
type: 'item',
name: 'posts',
title: 'Posts',
},
],
},
]}
>
Table
</SchemaInitializer.Item>
);
});
const FormBlockInitializerItem = itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<FormOutlined />}
onClick={() => {
insert({
type: 'void',
'x-uid': uid(),
'x-component': 'Form',
'x-decorator': 'CardItem',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-item-initializer': 'Grid.AddFormItem',
'x-uid': uid(),
},
},
});
}}
items={[
{
type: 'itemGroup',
title: 'select a data source',
children: [
{
type: 'item',
name: 'users',
title: 'Users',
},
{
type: 'item',
name: 'posts',
title: 'Posts',
},
],
},
]}
>
Form
</SchemaInitializer.Item>
);
});
const TestInitializerItem = itemWrap((props) => { const TestInitializerItem = itemWrap((props) => {
const { insert } = props; const { insert } = props;
return ( return (
@ -193,7 +114,7 @@ const TestInitializerItem = itemWrap((props) => {
); );
}); });
export const AddGridBlockItem = observer((props: any) => { export const BlockInitializer = observer((props: any) => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
wrap={gridRowColWrap} wrap={gridRowColWrap}
@ -213,11 +134,6 @@ export const AddGridBlockItem = observer((props: any) => {
title: 'Form', title: 'Form',
component: FormBlockInitializerItem, component: FormBlockInitializerItem,
}, },
{
type: 'item',
title: 'Test',
component: TestInitializerItem,
},
], ],
}, },
]} ]}

View File

@ -0,0 +1,84 @@
import { observer, useFieldSchema } from '@formily/react';
import { Switch } from 'antd';
import React from 'react';
import { SchemaInitializer } from '../..';
import { useDesignable } from '../../../schema-component';
const useCurrentActionSchema = (action: string) => {
const fieldSchema = useFieldSchema();
const { remove } = useDesignable();
const schema = fieldSchema.reduceProperties((buf, s) => {
if (s['x-action'] === action) {
return s;
}
return buf;
});
return {
schema,
exists: !!schema,
remove() {
schema && remove(schema);
},
};
};
const InitializeAction = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentActionSchema(item.schema['x-action']);
return (
<SchemaInitializer.Item
onClick={() => {
if (exists) {
return remove();
}
insert({
type: 'void',
'x-component': 'Action',
...item.schema,
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{item.title} <Switch size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
});
export const DetailsActionInitializer = observer((props: any) => {
return (
<SchemaInitializer.Button
insertPosition={'beforeEnd'}
style={{ marginLeft: 8 }}
items={[
{
type: 'itemGroup',
title: 'Enable actions',
children: [
{
type: 'item',
title: 'Create',
component: InitializeAction,
schema: {
title: 'Create',
'x-action': 'posts:create',
'x-align': 'left',
},
},
{
type: 'item',
title: 'Update',
component: InitializeAction,
schema: {
title: 'Update',
'x-action': 'posts:update',
},
},
],
},
]}
>
Configure actions
</SchemaInitializer.Button>
);
});

View File

@ -2,8 +2,8 @@ import { ISchema, observer, Schema, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Switch } from 'antd'; import { Switch } from 'antd';
import React from 'react'; import React from 'react';
import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer'; import { SchemaInitializer, SchemaInitializerItemOptions } from '../..';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../../schema-component';
const useFormItemInitializerFields = () => { const useFormItemInitializerFields = () => {
return [ return [
@ -122,7 +122,7 @@ const InitializeTextFormItem = itemWrap((props) => {
); );
}); });
export const AddGridFormItem = observer((props: any) => { export const FormItemInitializer = observer((props: any) => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
wrap={gridRowColWrap} wrap={gridRowColWrap}

View File

@ -0,0 +1,142 @@
import { FormOutlined } from '@ant-design/icons';
import { ISchema, observer } from '@formily/react';
import { uid } from '@formily/shared';
import React from 'react';
import { SchemaInitializer } from '../..';
const gridRowColWrap = (schema: ISchema) => {
return {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[schema.name || uid()]: schema,
},
},
},
};
};
const itemWrap = SchemaInitializer.itemWrap;
const TestInitializerItem = itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<FormOutlined />}
onClick={() => {
insert({
type: 'void',
name: uid(),
'x-decorator': 'CardItem',
'x-component': 'Grid',
'x-uid': uid(),
properties: {
row1: {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': uid(),
properties: {
col11: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
block1: {
type: 'void',
title: '1',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
block2: {
type: 'void',
title: '2',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
col12: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
block3: {
type: 'void',
title: '3',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
},
},
row2: {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': uid(),
properties: {
col21: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
block4: {
type: 'void',
title: '4',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
block5: {
type: 'void',
title: '5',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
},
},
},
});
}}
>
Test
</SchemaInitializer.Item>
);
});
export const RecordBlockInitializer = observer((props: any) => {
return (
<SchemaInitializer.Button
wrap={gridRowColWrap}
insertPosition={'beforeEnd'}
items={[
{
type: 'itemGroup',
title: 'Data blocks',
children: [
{
type: 'item',
title: 'Form',
component: TestInitializerItem,
},
{
type: 'item',
title: 'Details',
component: TestInitializerItem,
},
],
},
]}
>
Add block
</SchemaInitializer.Button>
);
});

View File

@ -0,0 +1,96 @@
import { observer, useFieldSchema } from '@formily/react';
import { Switch } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../..';
import { useDesignable } from '../../../schema-component';
const useCurrentActionSchema = (action: string) => {
const fieldSchema = useFieldSchema();
const { remove } = useDesignable();
const schema = fieldSchema.reduceProperties((buf, s) => {
if (s['x-action'] === action) {
return s;
}
return buf;
});
return {
schema,
exists: !!schema,
remove() {
schema && remove(schema);
},
};
};
const InitializeAction = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentActionSchema(item.schema['x-action']);
return (
<SchemaInitializer.Item
onClick={() => {
if (exists) {
return remove();
}
insert({
type: 'void',
'x-component': 'Action',
...item.schema,
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{item.title}
<Switch style={{ marginLeft: 20 }} size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
});
export const TableActionInitializer = observer((props: any) => {
const { t } = useTranslation();
return (
<SchemaInitializer.Button
insertPosition={'beforeEnd'}
style={{ marginLeft: 8 }}
items={[
{
type: 'itemGroup',
title: t('Enable actions'),
children: [
{
type: 'item',
title: t('Filter'),
component: InitializeAction,
schema: {
title: '{{ t("Filter") }}',
'x-action': 'filter',
'x-align': 'left',
},
},
{
type: 'item',
title: t('Add new'),
component: InitializeAction,
schema: {
title: '{{ t("Add new") }}',
'x-action': 'create',
},
},
{
type: 'item',
title: t('Delete'),
component: InitializeAction,
schema: {
title: '{{ t("Delete") }}',
'x-action': 'destroy',
},
},
],
},
]}
>
{t('Configure actions')}
</SchemaInitializer.Button>
);
});

View File

@ -0,0 +1,145 @@
import { observer, Schema, useFieldSchema } from '@formily/react';
import { Switch } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../..';
import { useAPIClient } from '../../../api-client';
import { createDesignable, useDesignable } from '../../../schema-component';
const useCurrentActionSchema = (action: string) => {
const fieldSchema = useFieldSchema();
const { remove } = useDesignable();
const findActionSchema = (schema: Schema) => {
return schema.reduceProperties((buf, s) => {
if (s['x-action'] === action) {
return s;
}
const c = findActionSchema(s);
if (c) {
return c;
}
return buf;
});
};
const schema = findActionSchema(fieldSchema);
return {
schema,
exists: !!schema,
remove() {
schema && remove(schema);
},
};
};
const InitializeAction = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentActionSchema(item.schema['x-action']);
return (
<SchemaInitializer.Item
onClick={() => {
console.log('InitializeAction', insert);
if (exists) {
return remove();
}
insert({
type: 'void',
'x-component': 'Action.Link',
...item.schema,
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{item.title} <Switch style={{ marginLeft: 20 }} size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
});
export const TableColumnActionInitializer = observer((props: any) => {
const fieldSchema = useFieldSchema();
const api = useAPIClient();
const { refresh } = useDesignable();
const { t } = useTranslation();
return (
<SchemaInitializer.Button
insertPosition={'beforeEnd'}
insert={(schema) => {
const spaceSchema = fieldSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'Space') {
return schema;
}
return buf;
}, null);
if (!spaceSchema) {
return;
}
const dn = createDesignable({
api,
refresh,
current: spaceSchema,
});
dn.loadAPIClientEvents();
dn.insertBeforeEnd(schema);
}}
items={[
{
type: 'itemGroup',
title: t('Enable actions'),
children: [
{
type: 'item',
title: t('View'),
component: InitializeAction,
schema: {
title: '{{ t("View") }}',
type: 'void',
'x-action': 'view',
'x-component': 'Action.Link',
'x-component-props': {},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
title: '{{ t("View record") }}',
properties: {},
},
},
},
},
{
type: 'item',
title: t('Edit'),
component: InitializeAction,
schema: {
title: '{{ t("Edit") }}',
type: 'void',
'x-action': 'update',
'x-component': 'Action.Link',
'x-component-props': {},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
title: '{{ t("Edit record") }}',
properties: {},
},
},
}
},
{
type: 'item',
title: t('Delete'),
component: InitializeAction,
schema: {
title: '{{ t("Delete") }}',
'x-action': 'destroy',
},
},
],
},
]}
>
Configure
</SchemaInitializer.Button>
);
});

View File

@ -0,0 +1,99 @@
import { observer, Schema, useFieldSchema } from '@formily/react';
import { Switch } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer, SchemaInitializerItemOptions } from '../..';
import { useCollection, useDesignable } from '../../..';
const useTableColumnInitializerFields = () => {
const { name, fields } = useCollection();
return fields
.filter((field) => field?.uiSchema?.title)
.map((field) => {
return {
type: 'item',
title: field?.uiSchema?.title,
schema: {
name: field.name,
'x-collection-field': `${name}.${field.name}`,
'x-component': 'CollectionField',
},
component: ColumnInitializerItem,
} as SchemaInitializerItemOptions;
});
};
const useCurrentColumnSchema = (path: string) => {
const fieldSchema = useFieldSchema();
const { remove } = useDesignable();
const findFieldSchema = (schema: Schema) => {
return schema.reduceProperties((buf, s) => {
if (s['x-collection-field'] === path) {
return s;
}
const child = findFieldSchema(s);
if (child) {
return child;
}
return buf;
});
};
const schema = findFieldSchema(fieldSchema);
return {
schema,
exists: !!schema,
remove() {
schema && remove(schema.parent);
},
};
};
const ColumnInitializerItem = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentColumnSchema(item.schema['x-collection-field']);
return (
<SchemaInitializer.Item
onClick={() => {
if (exists) {
return remove();
}
insert({
type: 'void',
'x-decorator': 'TableColumnDecorator',
'x-component': 'VoidTable.Column',
properties: {
[item.schema.name]: {
'x-read-pretty': true,
...item.schema,
},
},
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{item.title} <Switch style={{ marginLeft: 20 }} size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
});
/**
*
*/
export const TableColumnInitializer = observer((props: any) => {
const { t } = useTranslation();
return (
<SchemaInitializer.Button
insertPosition={'beforeEnd'}
items={[
{
type: 'itemGroup',
title: t('Display fields'),
children: useTableColumnInitializerFields(),
},
]}
>
{t('Configure columns')}
</SchemaInitializer.Button>
);
});

View File

@ -0,0 +1,8 @@
export * from './BlockInitializer';
export * from './DetailsActionInitializer';
export * from './FormItemInitializer';
export * from './RecordBlockInitializer';
export * from './TableActionInitializer';
export * from './TableColumnActionInitializer';
export * from './TableColumnInitializer';

View File

@ -16,10 +16,16 @@ export const SchemaInitializerItemContext = createContext(null);
export const SchemaInitializer = () => null; export const SchemaInitializer = () => null;
SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => { SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
const { wrap = defaultWrap, items = [], insertPosition, dropdown, ...others } = props; const { insert, wrap = defaultWrap, items = [], insertPosition, dropdown, ...others } = props;
const { insertAdjacent, findComponent } = useDesignable(); let { insertAdjacent, findComponent } = useDesignable();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const insertSchema = (schema) => {
if (props.insert) {
props.insert(wrap(schema));
} else {
insertAdjacent(insertPosition, wrap(schema));
}
};
const menu = ( const menu = (
<Menu> <Menu>
{items?.map((item, indexA) => { {items?.map((item, indexA) => {
@ -36,18 +42,10 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
index: indexA, index: indexA,
item, item,
info: item, info: item,
insert: (schema) => { insert: insertSchema,
insertAdjacent(insertPosition, wrap(schema));
},
}} }}
> >
<Component <Component {...item} item={item} insert={insertSchema} />
{...item}
item={item}
insert={(schema) => {
insertAdjacent(insertPosition, wrap(schema));
}}
/>
</SchemaInitializerItemContext.Provider> </SchemaInitializerItemContext.Provider>
) )
); );
@ -68,18 +66,10 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
index: indexB, index: indexB,
item: child, item: child,
info: child, info: child,
insert: (schema) => { insert: insertSchema,
insertAdjacent(insertPosition, wrap(schema));
},
}} }}
> >
<Component <Component {...child} item={child} insert={insertSchema} />
{...child}
item={child}
insert={(schema) => {
insertAdjacent(insertPosition, wrap(schema));
}}
/>
</SchemaInitializerItemContext.Provider> </SchemaInitializerItemContext.Provider>
) )
); );
@ -91,6 +81,8 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
</Menu> </Menu>
); );
console.log('others', others);
return ( return (
<Dropdown <Dropdown
visible={visible} visible={visible}

View File

@ -0,0 +1,7 @@
import React from 'react';
import { SchemaComponentOptions } from '../schema-component';
import * as initializers from './Initializers';
export const SchemaInitializerProvider: React.FC = (props) => {
return <SchemaComponentOptions components={initializers}>{props.children}</SchemaComponentOptions>;
};

View File

@ -1,3 +1,4 @@
export * from './SchemaInitializer'; export * from './SchemaInitializer';
export * from './SchemaInitializerProvider';
export * from './types'; export * from './types';

View File

@ -2,6 +2,7 @@ import { ISchema } from '@formily/react';
import { ButtonProps, DropDownProps, MenuItemProps } from 'antd'; import { ButtonProps, DropDownProps, MenuItemProps } from 'antd';
export interface SchemaInitializerButtonProps extends ButtonProps { export interface SchemaInitializerButtonProps extends ButtonProps {
insert?: (s: ISchema) => void;
wrap?: (s: ISchema) => ISchema; wrap?: (s: ISchema) => ISchema;
insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
items?: SchemaInitializerItemOptions[]; items?: SchemaInitializerItemOptions[];

View File

@ -54,12 +54,12 @@ export class UiRoutesStoragePlugin extends Plugin {
grid1: { grid1: {
type: 'void', type: 'void',
'x-component': 'Grid', 'x-component': 'Grid',
'x-item-initializer': 'Grid.AddBlockItem', 'x-item-initializer': 'BlockInitializer',
properties: {}, properties: {},
}, },
}, },
} },
} },
}, },
item8: { item8: {
type: 'void', type: 'void',