feat: add collections table/form

This commit is contained in:
chenos 2021-09-14 16:28:11 +08:00
parent 63ecfaa2ea
commit 37347cb6b2
5 changed files with 766 additions and 5 deletions

View File

@ -0,0 +1,756 @@
import { SchemaRenderer } from '../../../';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { FormItem } from '@formily/antd';
import { action } from '@formily/reactive';
import {
useCollectionContext,
useCollectionsContext,
} from '../../../constate/Collections';
import { Button, Drawer, Menu, Dropdown, Space } from 'antd';
import { PlusOutlined, DownOutlined } from '@ant-design/icons';
import cls from 'classnames';
import { uid, isValid, clone } from '@formily/shared';
import { Resource } from '../../../resource';
import { TableRowContext, useTable } from '../../../schemas/table';
import { useRequest } from 'ahooks';
import { VisibleContext } from '../../../context';
import { connect, FormProvider, observer, useForm } from '@formily/react';
import { DescriptionsContext } from '../../../schemas/form';
import { createContext } from 'react';
import { ISchema } from '../../../';
import { createForm, Field } from '@formily/core';
import { SchemaField } from '../../../schemas';
import {
interfaces,
options,
} from '../../../schemas/database-field/interfaces';
export const RoleContext = createContext(null);
function VisibleProvider(props) {
const ctx = useContext(TableRowContext);
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
{props.children}
</VisibleContext.Provider>
);
}
const useResource = () => {
const resource = Resource.make('collections');
return {
resource,
};
};
const useCollectionsResource = () => {
const descriptionsContext = useContext(DescriptionsContext);
console.log('descriptionsContext.service', descriptionsContext.service);
const resource = Resource.make('collections');
return {
resource,
};
};
class ActionPermissionResource extends Resource {
save(options?: any) {
console.log('ActionPermissionResource.save');
return Promise.resolve({});
}
}
const useActionPermissionSubmit = () => {
const form = useForm();
const role = useContext(RoleContext);
const resource = Resource.make({
resourceName: 'roles',
resourceKey: role.name,
});
return {
async run() {
await resource.save(form.values);
console.log('useActionPermissionSubmit', form.values?.actionPermissions);
},
};
};
const useActionPermissionResource = ({ onSuccess }) => {
const role = useContext(RoleContext);
console.log('RoleContext', role);
// const { props } = useTable();
const ctx = useContext(TableRowContext);
const resource = ActionPermissionResource.make({
resourceName: 'action_permissions',
});
const service = useRequest(
(params?: any) => {
return resource.list({
...params,
filter: {
role_name: role.name,
collection_name: ctx.record.name,
},
appends: ['fields'],
});
},
{
formatResult: (result) => result?.data,
onSuccess(data) {
console.log('actionPermissions', data);
onSuccess({
actionPermissions: data.map((permission) => {
const item: any = {};
Object.keys(permission).forEach((key) => {
if (isValid(permission[key])) {
item[key] = permission[key];
}
});
item.fields = permission?.fields.map((field) => field.key) || [];
return item;
}),
});
},
manual: true,
},
);
const [visible] = useContext(VisibleContext);
useEffect(() => {
if (visible) {
service.run({});
}
}, [visible]);
return { resource, service, initialValues: service.data, ...service };
};
const useDetailsResource = ({ onSuccess }) => {
const { props } = useTable();
const ctx = useContext(TableRowContext);
const resource = Resource.make({
resourceName: 'collections',
resourceKey: ctx.record[props.rowKey],
});
const service = useRequest(
(params?: any) => {
return resource.get({ ...params });
},
{
formatResult: (result) => result?.data,
onSuccess,
manual: true,
},
);
const [visible] = useContext(VisibleContext);
useEffect(() => {
if (visible) {
service.run({});
}
}, [visible]);
return { resource, service, initialValues: service.data, ...service };
};
const useFieldsResource = () => {
const { props } = useTable();
const ctx = useContext(TableRowContext);
class FieldResource extends Resource {
list(options) {
return super.list({
...options,
filter: { state: 1, collection_name: ctx.record[props.rowKey] },
});
}
}
const resource = FieldResource.make('fields');
return {
resource,
};
};
const fieldInterfaces = [];
for (const [key, schema] of interfaces) {
fieldInterfaces.push({
value: key,
label: schema.title,
});
}
const collectionSchema: ISchema = {
type: 'array',
'x-decorator': 'VisibleProvider',
'x-component': 'Table',
default: [],
'x-component-props': {
rowKey: 'key',
dragSort: true,
showIndex: true,
refreshRequestOnChange: true,
pagination: {
pageSize: 10,
},
defaultAppends: ['uiSchema'],
useResource: useFieldsResource,
collectionName: 'fields',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Table.ActionBar',
properties: {
[uid()]: {
type: 'void',
name: 'action1',
title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-component': 'Action',
'x-component-props': {
icon: 'DeleteOutlined',
confirm: {
title: '删除数据',
content: '删除后无法恢复,确定要删除吗?',
},
useAction: '{{ Table.useTableDestroyAction }}',
},
},
[uid()]: {
type: 'void',
title: '添加',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component': 'CreateFieldButton',
'x-component-props': {
type: 'primary',
icon: 'PlusOutlined',
},
},
},
},
column1: {
type: 'void',
title: '字段名称',
'x-component': 'Table.Column',
properties: {
'uiSchema.title': {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column2: {
type: 'void',
title: '字段标识',
'x-component': 'Table.Column',
properties: {
name: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column3: {
type: 'void',
title: '字段类型',
'x-component': 'Table.Column',
properties: {
interface: {
type: 'string',
'x-component': 'Select',
'x-read-pretty': true,
// @ts-ignore
enum: fieldInterfaces,
},
},
},
[uid()]: {
type: 'void',
title: '操作',
'x-component': 'Table.Column',
'x-component-props': {
width: 160,
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Group',
'x-component-props': {
type: 'link',
},
properties: {
[uid()]: {
type: 'void',
title: '编辑',
'x-component': 'EditFieldButton',
'x-component-props': {
type: 'link',
useAction() {
return {
async run() {
alert('abc');
},
};
},
},
'x-action-type': 'update',
},
[uid()]: {
type: 'void',
title: '删除',
'x-component': 'Action',
'x-action-type': 'destroy',
'x-component-props': {
type: 'link',
confirm: {
title: '删除数据',
content: '删除后无法恢复,确定要删除吗?',
},
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
},
},
},
},
};
function FieldConfigTitle() {
const ctx = useContext(TableRowContext);
return <>{`配置「${ctx.record.title}」表字段`}</>;
}
const schema: ISchema = {
type: 'void',
name: 'action',
'x-component': 'Action',
'x-component-props': {
tooltip: '数据表配置',
className: 'nb-database-config',
icon: 'DatabaseOutlined',
type: 'primary',
},
properties: {
modal1: {
type: 'void',
title: '数据表配置',
'x-component': 'Action.Drawer',
properties: {
table: {
type: 'array',
// 'x-decorator': 'CardItem',
'x-component': 'Table',
default: [],
'x-component-props': {
rowKey: 'name',
dragSort: true,
showIndex: true,
refreshRequestOnChange: true,
pagination: {
pageSize: 10,
},
useResource,
collectionName: 'collections',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Table.ActionBar',
properties: {
[uid()]: {
type: 'void',
name: 'action1',
title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-component': 'Action',
'x-component-props': {
icon: 'DeleteOutlined',
confirm: {
title: '删除数据',
content: '删除后无法恢复,确定要删除吗?',
},
useAction: '{{ Table.useTableDestroyAction }}',
},
},
[uid()]: {
type: 'void',
title: '创建数据表',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component': 'Action',
'x-component-props': {
type: 'primary',
icon: 'PlusOutlined',
},
properties: {
modal: {
type: 'void',
title: '创建数据表',
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
'x-component-props': {
useOkAction: '{{ Table.useTableCreateAction }}',
},
properties: {
title: {
type: 'string',
title: '数据表名称',
'x-component': 'Input',
'x-decorator': 'FormilyFormItem',
},
},
},
},
},
},
},
column1: {
type: 'void',
title: '数据表名称',
'x-component': 'Table.Column',
properties: {
title: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column2: {
type: 'void',
title: '数据表标识',
'x-component': 'Table.Column',
properties: {
name: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
[uid()]: {
type: 'void',
title: '操作',
'x-component': 'Table.Column',
'x-component-props': {
width: 160,
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Group',
'x-component-props': {
type: 'link',
},
properties: {
[uid()]: {
type: 'void',
title: '配置字段',
'x-component': 'Action',
'x-component-props': {
type: 'link',
},
'x-action-type': 'view',
properties: {
[uid()]: {
type: 'void',
title: <FieldConfigTitle />,
// 'x-decorator': 'RoleProvider',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
[uid()]: collectionSchema,
},
},
},
},
[uid()]: {
type: 'void',
title: '编辑',
'x-component': 'Action',
'x-component-props': {
type: 'link',
},
'x-action-type': 'update',
properties: {
[uid()]: {
type: 'void',
title: '编辑数据表',
'x-decorator': 'Form',
'x-decorator-props': {
useResource: useDetailsResource,
},
'x-component': 'Action.Drawer',
'x-component-props': {
useOkAction: '{{ Table.useTableUpdateAction }}',
},
properties: {
title: {
type: 'string',
title: '数据表名称',
'x-component': 'Input',
'x-decorator': 'FormilyFormItem',
},
},
},
},
},
[uid()]: {
type: 'void',
title: '删除',
'x-component': 'Action',
'x-action-type': 'destroy',
'x-component-props': {
type: 'link',
confirm: {
title: '删除数据',
content: '删除后无法恢复,确定要删除吗?',
},
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
},
},
},
},
},
},
},
},
};
function CreateFieldButton() {
const { refresh } = useCollectionsContext();
const ctx = useContext(TableRowContext);
const { service } = useTable();
const [visible, setVisible] = useState(false);
const form = useMemo(() => createForm(), []);
const [properties, setProperties] = useState({});
const { collections = [], loading } = useCollectionsContext();
const loadCollections = async (field: any) => {
return collections.map((item: any) => ({
label: item.title,
value: item.name,
}));
};
const loadCollectionFields = async (field: Field) => {
const target = field.query('....target').get('value');
const f = field.query('....target').take();
console.log('loadCollectionFields', f, field);
const collection = collections?.find((item) => item.name === target);
if (!collection) {
return [];
}
return collection?.generalFields
?.filter((item) => item?.uiSchema?.title)
?.map((item) => ({
label: item?.uiSchema?.title || item.name,
value: item.name,
}));
};
const menu = (
<Menu
onClick={(info) => {
console.log('click', info.key);
const schema = interfaces.get(info.key);
form.setValues({
...schema.default,
collection_name: ctx.record.name,
key: uid(),
name: uid(),
interface: info.key,
});
setProperties(clone(schema.properties));
setVisible(true);
}}
>
{options.map(
(option, groupIndex) =>
option.children.length > 0 && (
<Menu.SubMenu key={groupIndex} title={option.label}>
{option.children.map((item) => (
<Menu.Item style={{ minWidth: 120 }} key={item.name}>
{item.title}
</Menu.Item>
))}
</Menu.SubMenu>
),
)}
</Menu>
);
return (
<>
<Dropdown overlay={menu} overlayClassName={'all-fields'}>
<Button type={'primary'} icon={<PlusOutlined />}>
<DownOutlined />
</Button>
</Dropdown>
<Drawer
title={'添加字段'}
width={'50%'}
visible={visible}
onClose={() => setVisible(false)}
footer={
<Space style={{ float: 'right' }}>
<Button
onClick={() => {
setVisible(false);
}}
>
</Button>
<Button
type={'primary'}
onClick={async () => {
await Resource.make('fields').save(form.values);
setVisible(false);
await service.refresh();
await refresh();
}}
>
</Button>
</Space>
}
>
<FormProvider form={form}>
<SchemaField
scope={{
loadCollections,
loadCollectionFields,
}}
schema={{
type: 'object',
properties,
}}
/>
</FormProvider>
</Drawer>
</>
);
}
function EditFieldButton() {
const { refresh } = useCollectionsContext();
const { service } = useTable();
const ctx = useContext(TableRowContext);
const [visible, setVisible] = useState(false);
const form = useMemo(() => createForm(), []);
const schema = interfaces.get(ctx.record.interface);
const { collections = [], loading } = useCollectionsContext();
const loadCollections = async (field: any) => {
return collections.map((item: any) => ({
label: item.title,
value: item.name,
}));
};
const loadCollectionFields = async (field: Field) => {
const target = field.query('....target').get('value');
const f = field.query('....target').take();
console.log('loadCollectionFields', f, field);
const collection = collections?.find((item) => item.name === target);
if (!collection) {
return [];
}
return collection?.generalFields
?.filter((item) => item?.uiSchema?.title)
?.map((item) => ({
label: item?.uiSchema?.title || item.name,
value: item.name,
}));
};
return (
<>
<Button
type={'link'}
onClick={() => {
setVisible(true);
form.setValues(clone(ctx.record));
}}
>
</Button>
<Drawer
title={'编辑字段'}
width={'50%'}
visible={visible}
onClose={() => setVisible(false)}
footer={
<Space style={{ float: 'right' }}>
<Button
onClick={() => {
setVisible(false);
}}
>
</Button>
<Button
type={'primary'}
onClick={async () => {
await Resource.make('fields').save(form.values, {
resourceKey: ctx.record.key,
});
setVisible(false);
await service.refresh();
await refresh();
}}
>
</Button>
</Space>
}
>
<FormProvider form={form}>
<SchemaField
scope={{
loadCollections,
loadCollectionFields,
}}
schema={{
type: 'object',
properties: clone(schema.properties),
}}
/>
</FormProvider>
</Drawer>
</>
);
}
export const Collections = () => {
return (
<SchemaRenderer
components={{
VisibleProvider,
CreateFieldButton,
EditFieldButton,
}}
schema={schema}
/>
);
};
export default Collections;

View File

@ -36,6 +36,7 @@ import {
} from '../../constate';
import { uid } from '@formily/shared';
import { Permissions } from './Permissions';
import { Collections } from './Collections'
import { More } from './More';
import { UserInfo } from './UserInfo';
import {
@ -160,7 +161,7 @@ function LayoutWithMenu(props: LayoutWithMenuProps) {
/>
</MenuSelectedKeysContext.Provider>
<DesignableToggle />
<Database />
<Collections />
<Permissions />
<More />
<UserInfo />

View File

@ -1,13 +1,10 @@
import { Model, ModelCtor } from '@nocobase/database';
import { actions, middlewares } from '@nocobase/actions';
import { sort } from '@nocobase/actions/src/actions/common';
import { cloneDeep, omit } from 'lodash';
export const create = async (ctx: actions.Context, next: actions.Next) => {
await actions.common.create(ctx, async () => {});
const { associated } = ctx.action.params;
await ctx.body.generateReverseField();
await associated.migrate();
// console.log('associated.migrate');
await ctx.body.migrate();
await next();
}

View File

@ -42,6 +42,11 @@ export class Field extends Model {
return props;
}
async migrate() {
const collection = await this.getCollection();
await collection.migrate()
}
async getNestedFields() {
const fields = await this.getChildren();
const items = [];

View File

@ -96,4 +96,6 @@ export default async function (this: Application, options = {}) {
this.resourcer.registerActionHandler('collections.fields:create', create);
this.resourcer.registerActionHandler('collections:findAll', findAll);
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);
this.resourcer.registerActionHandler('fields:create', create);
this.resourcer.registerActionHandler('collections:create', createOrUpdate);
}