feat: support subTable field

* feat: add linkTo and subTable fields

* add subTable field component

* improve sub table

* bugfix
This commit is contained in:
chenos 2020-12-20 12:52:15 +08:00 committed by GitHub
parent 3054ddb13b
commit ecab106c3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 700 additions and 47 deletions

View File

@ -60,6 +60,100 @@ export default {
showInForm: true, showInForm: true,
}, },
}, },
{
interface: 'boolean',
title: '是/否',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'select',
title: '下拉',
dataSource: [
{value: 'value1', label: '选项1'},
{value: 'value2', label: '选项2'},
{value: 'value3', label: '选项3'},
],
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'multipleSelect',
title: '下拉多选',
dataSource: [
{value: 'value1', label: '选项1'},
{value: 'value2', label: '选项2'},
{value: 'value3', label: '选项3'},
],
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'radio',
title: '单选框',
dataSource: [
{value: 'value1', label: '选项1'},
{value: 'value2', label: '选项2'},
{value: 'value3', label: '选项3'},
],
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'checkboxes',
title: '多选框',
dataSource: [
{value: 'value1', label: '选项1'},
{value: 'value2', label: '选项2'},
{value: 'value3', label: '选项3'},
],
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'subTable',
title: '子表格',
children: [
{
interface: 'string',
title: '标题',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'textarea',
title: '内容',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
],
component: {
// showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{ {
interface: 'datetime', interface: 'datetime',
title: '日期', title: '日期',
@ -78,6 +172,24 @@ export default {
showInForm: true, showInForm: true,
}, },
}, },
{
interface: 'createdBy',
title: '创建人',
component: {
showInTable: true,
showInDetail: true,
// showInForm: true,
},
},
{
interface: 'updatedBy',
title: '更新人',
component: {
showInTable: true,
showInDetail: true,
// showInForm: true,
},
},
{ {
interface: 'createdAt', interface: 'createdAt',
title: '创建日期', title: '创建日期',

View File

@ -6,9 +6,9 @@ import associated from '../../../actions/src/middlewares/associated';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
const sync = { const sync = {
force: true, force: false,
alter: { alter: {
drop: true, drop: false,
}, },
}; };
@ -59,7 +59,7 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
}); });
api.resourcer.use(async (ctx: actions.Context, next) => { api.resourcer.use(async (ctx: actions.Context, next) => {
const { resourceName } = ctx.action.params; const { resourceName, fields = {} } = ctx.action.params;
const table = ctx.db.getTable(resourceName); const table = ctx.db.getTable(resourceName);
// ctx.state.developerMode = {[Op.not]: null}; // ctx.state.developerMode = {[Op.not]: null};
ctx.state.developerMode = false; ctx.state.developerMode = false;
@ -67,8 +67,8 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
ctx.action.setParam('filter.developerMode', ctx.state.developerMode); ctx.action.setParam('filter.developerMode', ctx.state.developerMode);
} }
if (table) { if (table) {
const except = []; const except = fields.except || [];
const appends = []; const appends = fields.appends || [];
for (const [name, field] of table.getFields()) { for (const [name, field] of table.getFields()) {
if (field.options.hidden) { if (field.options.hidden) {
except.push(field.options.name); except.push(field.options.name);

View File

@ -173,17 +173,19 @@ api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-use
await Collection.import(table.getOptions(), { update: true, migrate: false }); await Collection.import(table.getOptions(), { update: true, migrate: false });
} }
await Page.import(data); await Page.import(data);
await User.findOrCreate({ const user = await User.findOne({
where: { where: {
username: "admin", username: "admin",
}, },
defaults: { });
if (!user) {
await User.create({
nickname: "超级管理员", nickname: "超级管理员",
password: "admin", password: "admin",
username: "admin", username: "admin",
token: "38979f07e1fca68fb3d2", token: "38979f07e1fca68fb3d2",
}, });
}); }
await database.getModel('collections').import(require('./collections/example').default); await database.getModel('collections').import(require('./collections/example').default);
await database.close(); await database.close();
})(); })();

View File

@ -0,0 +1,29 @@
import React from 'react'
import { connect } from '@formily/react-schema-renderer'
import moment from 'moment'
import { Select } from 'antd'
import {
mapStyledProps,
mapTextComponent,
compose,
isStr,
isArr
} from '../shared'
function DrawerSelectComponent(props) {
console.log(props);
return (
<>
<Select>
<Select.Option value={'aaa'}>aaa</Select.Option>
</Select>
</>
);
}
export const DrawerSelect = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent,
})(DrawerSelectComponent)
export default DrawerSelect

View File

@ -14,6 +14,8 @@ import { Range } from './range'
import { Rating } from './rating' import { Rating } from './rating'
import { Upload } from './upload' import { Upload } from './upload'
import { Filter } from './filter' import { Filter } from './filter'
import { DrawerSelect } from './drawer-select'
import { SubTable } from './sub-table'
export const setup = () => { export const setup = () => {
registerFormFields({ registerFormFields({
@ -43,5 +45,7 @@ export const setup = () => {
rating: Rating, rating: Rating,
upload: Upload, upload: Upload,
filter: Filter, filter: Filter,
drawerSelect: DrawerSelect,
subTable: SubTable,
}) })
} }

View File

@ -0,0 +1,91 @@
import React, { useState, useEffect, useImperativeHandle, forwardRef, useRef } from 'react';
import { Button, Drawer } from 'antd';
import { Tooltip, Input } from 'antd';
import {
SchemaForm,
SchemaMarkupField as Field,
createFormActions,
createAsyncFormActions,
Submit,
Reset,
FormButtonGroup,
registerFormFields,
FormValidator,
setValidationLanguage,
} from '@formily/antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { useRequest } from 'umi';
import api from '@/api-client';
import { Spin } from '@nocobase/client';
export default forwardRef((props: any, ref) => {
console.log(props);
const {
target,
onFinish,
} = props;
const { data: schema = {}, loading } = useRequest(() => api.resource(target).getView({
resourceKey: 'form'
}));
const [visible, setVisible] = useState(false);
const [data, setData] = useState({});
const [title, setTitle] = useState('创建子字段');
const [index, setIndex] = useState();
useImperativeHandle(ref, () => ({
setVisible,
setData,
setTitle,
setIndex,
}));
const actions = createFormActions();
console.log({onFinish});
const { fields = {} } = schema;
if (loading) {
return <Spin/>;
}
return (
<Drawer
{...props}
destroyOnClose
visible={visible}
width={'40%'}
onClose={() => {
setVisible(false);
}}
title={title}
footer={[
<Button type={'primary'} onClick={async () => {
const { values = {} } = await actions.submit();
setVisible(false);
onFinish && onFinish(values, index);
}}></Button>
]}
>
<SchemaForm
colon={true}
layout={'vertical'}
initialValues={data}
actions={actions}
schema={{
type: 'object',
properties: fields,
}}
expressionScope={{
text(...args: any[]) {
return React.createElement('span', {}, ...args)
},
tooltip(title: string, offset = 3) {
return (
<Tooltip title={title}>
<QuestionCircleOutlined
style={{ margin: '0 3px', cursor: 'default', marginLeft: offset }}
/>
</Tooltip>
);
},
}}
>
</SchemaForm>
</Drawer>
);
});

View File

@ -0,0 +1,118 @@
import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { Table as AntdTable, Button, Space, Popconfirm } from 'antd';
import { Actions } from '@/components/actions';
import ViewFactory from '@/components/views';
import { useRequest } from 'umi';
import api from '@/api-client';
import { components, fields2columns } from '@/components/views/SortableTable';
import Form from './Form';
import { Spin } from '@nocobase/client';
import maxBy from 'lodash/maxBy';
export interface SimpleTableProps {
schema?: any;
activeTab?: any;
resourceName: string;
associatedName?: string;
associatedKey?: string;
[key: string]: any;
}
export function generateIndex(): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export default function Table(props: SimpleTableProps) {
console.log(props);
const drawerRef = useRef<any>();
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onTableChange = (selectedRowKeys: React.ReactText[]) => {
setSelectedRowKeys(selectedRowKeys);
}
const tableProps: any = {};
tableProps.rowSelection = {
selectedRowKeys,
onChange: onTableChange,
}
const { rowKey = '__id', fields = [] } = props;
const { target = 'fields', value = [], onChange } = props;
const { data: schema = {}, loading } = useRequest(() => api.resource(target).getView({
resourceKey: 'simple'
}));
if (loading) {
return <Spin/>
}
return (
<div>
<div>
<Space style={{marginBottom: 14}}>
<Button type={'primary'} onClick={() => {
drawerRef.current.setVisible(true);
drawerRef.current.setIndex(undefined);
drawerRef.current.setData({});
drawerRef.current.setTitle('创建子字段');
}}></Button>
<Popconfirm title="确认删除吗?" onConfirm={() => {
console.log({selectedRowKeys})
const newValues = value.filter(item => selectedRowKeys.indexOf(item.__id) === -1);
onChange(newValues);
}}>
<Button></Button>
</Popconfirm>
</Space>
</div>
<Form target={target} onFinish={(values, index: number) => {
console.log(values);
const newVaules = [...value];
if (typeof index === 'undefined') {
newVaules.push({...values, __id: generateIndex()})
} else {
newVaules[index] = values;
}
onChange(newVaules);
console.log(newVaules);
}} ref={drawerRef}/>
<AntdTable
rowKey={rowKey}
// loading={loading}
columns={fields2columns(schema.fields||[])}
dataSource={value}
onChange={(pagination, filters, sorter, extra) => {
}}
components={components({
data: {
list: value.map((item, index) => {
if (item.__id) {
return item;
};
return {...item, __id: generateIndex()};
}),
},
mutate: (values) => {
onChange(values.list);
console.log(values);
},
rowKey,
onMoved: async ({resourceKey, target}) => {
}
})}
onRow={(record, index) => ({
onClick: () => {
console.log(record);
drawerRef.current.setVisible(true);
drawerRef.current.setIndex(index);
drawerRef.current.setData(record);
drawerRef.current.setTitle('编辑子字段');
}
})}
pagination={false}
{...tableProps}
/>
</div>
);
}

View File

@ -0,0 +1,20 @@
import React, { useRef } from 'react'
import { connect } from '@formily/react-schema-renderer'
import moment from 'moment'
import { Select, Button, Table as AntdTable } from 'antd'
import {
mapStyledProps,
mapTextComponent,
compose,
isStr,
isArr
} from '../shared'
import ViewFactory from '@/components/views';
import Table from './Table';
export const SubTable = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent,
})(Table)
export default SubTable

View File

@ -17,15 +17,16 @@ export function Details(props: any) {
associatedKey, associatedKey,
resourceKey, resourceKey,
} = props; } = props;
const { actions = [], fields = [] } = props.schema;
const { data = {}, loading, refresh } = useRequest(() => { const { data = {}, loading, refresh } = useRequest(() => {
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName; const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
return api.resource(name).get({ return api.resource(name).get({
resourceKey, resourceKey,
associatedKey, associatedKey,
'fields[appends]': fields.filter(field => get(field, 'interface') === 'subTable').map(field => field.name).join(',')
}); });
}); });
console.log(props); console.log(props);
const { actions = [], fields = [] } = props.schema;
return ( return (
<Card bordered={false}> <Card bordered={false}>
<Actions <Actions
@ -40,7 +41,7 @@ export function Details(props: any) {
<Descriptions bordered column={1}> <Descriptions bordered column={1}>
{fields.map((field: any) => { {fields.map((field: any) => {
return ( return (
<Descriptions.Item labelStyle={{minWidth: 200}} label={field.title||field.name}> <Descriptions.Item labelStyle={{minWidth: 200, maxWidth: 300, width: 300}} label={field.title||field.name}>
<Field viewType={'descriptions'} schema={field} value={get(data, field.name)}/> <Field viewType={'descriptions'} schema={field} value={get(data, field.name)}/>
</Descriptions.Item> </Descriptions.Item>
) )

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import moment from 'moment'; import moment from 'moment';
import { Tag, Popover } from 'antd'; import { Tag, Popover, Table } from 'antd';
import Icon from '@/components/icons'; import Icon from '@/components/icons';
import get from 'lodash/get'; import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { fields2columns } from '../SortableTable';
const InterfaceTypes = new Map<string, any>(); const InterfaceTypes = new Map<string, any>();
@ -27,6 +28,12 @@ function getFieldComponent(type) {
export function StringField(props: any) { export function StringField(props: any) {
const { value } = props; const { value } = props;
if (!value) {
return null;
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return ( return (
<>{value}</> <>{value}</>
); );
@ -157,6 +164,19 @@ export function RealtionField(props: any) {
); );
} }
export function SubTableField(props: any) {
const { schema: { children }, value } = props;
console.log(value);
if (!Array.isArray(value)) {
return null;
}
return (
<div>
<Table columns={fields2columns(children)} dataSource={value} pagination={false}/>
</div>
);
}
registerFieldComponents({ registerFieldComponents({
string: StringField, string: StringField,
textarea: TextareaField, textarea: TextareaField,
@ -173,6 +193,7 @@ registerFieldComponents({
icon: IconField, icon: IconField,
createdBy: RealtionField, createdBy: RealtionField,
updatedBy: RealtionField, updatedBy: RealtionField,
subTable: SubTableField,
}); });
export default function Field(props: any) { export default function Field(props: any) {

View File

@ -59,9 +59,7 @@ export async function getApp() {
app.resourcer.registerActionHandlers({...actions.associate, ...actions.common}); app.resourcer.registerActionHandlers({...actions.associate, ...actions.common});
app.registerPlugin('collections', [plugin]); app.registerPlugin('collections', [plugin]);
await app.loadPlugins(); await app.loadPlugins();
await app.database.sync({ await app.database.sync();
force: true,
});
// 表配置信息存到数据库里 // 表配置信息存到数据库里
// const tables = app.database.getTables([]); // const tables = app.database.getTables([]);
// for (const table of tables) { // for (const table of tables) {

View File

@ -50,4 +50,80 @@ describe('models.field', () => {
{label: 'xx', value: 'xx'}, {label: 'xx', value: 'xx'},
]); ]);
}); });
it.only('sub table field', async () => {
const [Collection, Field] = app.database.getModels(['collections', 'fields']);
const options = {
title: 'tests',
name: 'tests',
fields: [
{
interface: 'subTable',
title: '子表格',
name: 'subs',
children: [
{
interface: 'string',
title: '名称',
name: 'name',
},
],
},
],
};
const collection = await Collection.create(options);
await collection.updateAssociations(options);
const field = await Field.findOne({
where: {
title: '子表格',
},
});
await field.createChild({
interface: 'string',
title: '名称',
name: 'title',
});
const Test = app.database.getModel('tests');
const Sub = app.database.getModel('subs');
// console.log(Test.associations);
// console.log(Sub.rawAttributes);
const test = await Test.create({});
const sub = await test.createSub({name: 'name1', title: 'title1'});
expect(sub.toJSON()).toMatchObject({name: 'name1', title: 'title1'})
});
it('sub table field', async () => {
const [Collection, Field] = app.database.getModels(['collections', 'fields']);
// @ts-ignore
const options = {
title: 'tests',
name: 'tests',
fields: [
{
interface: 'subTable',
title: '子表格',
// name: 'subs',
children: [
{
interface: 'string',
title: '名称',
// name: 'name',
},
],
},
],
};
const collection = await Collection.create(options);
await collection.updateAssociations(options);
const field = await Field.findOne({
where: {
title: '子表格',
},
});
await field.createChild({
interface: 'string',
title: '名称',
name: 'title',
});
});
}); });

View File

@ -238,6 +238,11 @@ export default {
list: { list: {
sort: 'sort', sort: 'sort',
}, },
get: {
fields: {
appends: ['children'],
},
},
}, },
component: { component: {
type: 'drawerSelect', type: 'drawerSelect',

View File

@ -90,6 +90,36 @@ export default {
"target": "timeFormat", "target": "timeFormat",
"condition": "{{ ['time'].indexOf($self.value) !== -1 }}" "condition": "{{ ['time'].indexOf($self.value) !== -1 }}"
}, },
{
"type": "value:visible",
"target": "multiple",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "target",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "labelField",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "createable",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "children",
"condition": "{{ ['subTable'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "component.showInTable",
"condition": "{{ ['subTable'].indexOf($self.value) === -1 }}"
},
{ {
"type": "value:visible", "type": "value:visible",
"target": "component.showInForm", "target": "component.showInForm",
@ -158,7 +188,6 @@ export default {
type: 'virtual', type: 'virtual',
name: 'precision', name: 'precision',
title: '精度', title: '精度',
defaultValue: 1,
dataSource: [ dataSource: [
{value: 1, label: '1'}, {value: 1, label: '1'},
{value: 0.1, label: '1.0'}, {value: 0.1, label: '1.0'},
@ -169,6 +198,7 @@ export default {
component: { component: {
type: 'number', type: 'number',
showInForm: true, showInForm: true,
default: 1,
}, },
}, },
{ {
@ -176,7 +206,6 @@ export default {
type: 'virtual', type: 'virtual',
name: 'dateFormat', name: 'dateFormat',
title: '日期格式', title: '日期格式',
defaultValue: 'YYYY/MM/DD',
dataSource: [ dataSource: [
{value: 'YYYY/MM/DD', label: '年/月/日'}, {value: 'YYYY/MM/DD', label: '年/月/日'},
{value: 'YYYY-MM-DD', label: '年-月-日'}, {value: 'YYYY-MM-DD', label: '年-月-日'},
@ -185,6 +214,7 @@ export default {
component: { component: {
type: 'string', type: 'string',
showInForm: true, showInForm: true,
default: 'YYYY/MM/DD',
}, },
}, },
{ {
@ -192,10 +222,10 @@ export default {
type: 'virtual', type: 'virtual',
name: 'showTime', name: 'showTime',
title: '显示时间', title: '显示时间',
defaultValue: false,
component: { component: {
type: 'boolean', type: 'boolean',
showInForm: true, showInForm: true,
default: false,
"x-linkages": [ "x-linkages": [
{ {
"type": "value:visible", "type": "value:visible",
@ -210,7 +240,6 @@ export default {
type: 'virtual', type: 'virtual',
name: 'timeFormat', name: 'timeFormat',
title: '时间格式', title: '时间格式',
defaultValue: 'HH:mm:ss',
dataSource: [ dataSource: [
{ value: 'HH:mm:ss', label: '24小时制' }, { value: 'HH:mm:ss', label: '24小时制' },
{ value: 'hh:mm:ss a', label: '12小时制' }, { value: 'hh:mm:ss a', label: '12小时制' },
@ -218,6 +247,7 @@ export default {
component: { component: {
type: 'string', type: 'string',
showInForm: true, showInForm: true,
default: 'HH:mm:ss',
}, },
}, },
{ {
@ -234,18 +264,80 @@ export default {
}, },
}, },
{ {
interface: 'linkTo', interface: 'string',
multiple: true, type: 'virtual',
type: 'hasMany', name: 'target',
name: 'children', title: '要关联的数据表',
title: '子字段',
target: 'fields',
foreignKey: 'parent_id',
sourceKey: 'id',
component: { component: {
type: 'drawerSelect', type: 'drawerSelect',
showInDetail: true,
showInForm: true,
}, },
}, },
{
interface: 'string',
type: 'virtual',
name: 'labelField',
title: '要关联的字段',
component: {
type: 'drawerSelect',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'multiple',
title: '允许添加多条记录',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'createable',
title: '允许直接在关联的数据表内新建数据',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'subTable',
type: 'hasMany',
name: 'children',
target: 'fields',
sourceKey: 'id',
foreignKey: 'parent_id',
title: '子表格字段',
// visible: true,
component: {
type: 'subTable',
default: [],
// showInTable: true,
// showInDetail: true,
showInForm: true,
},
},
// {
// interface: 'linkTo',
// multiple: true,
// type: 'hasMany',
// name: 'children',
// title: '子字段',
// target: 'fields',
// foreignKey: 'parent_id',
// sourceKey: 'id',
// component: {
// type: 'drawerSelect',
// },
// },
{ {
interface: 'textarea', interface: 'textarea',
type: 'virtual', type: 'virtual',

View File

@ -55,7 +55,7 @@ export default {
required: true, required: true,
dataSource: [ dataSource: [
{ label: '详情数据', value: 'details' }, { label: '详情数据', value: 'details' },
{ label: '相关数据', value: 'association', disabled: true }, { label: '相关数据', value: 'association' },
{ label: '模块组合', value: 'module', disabled: true }, { label: '模块组合', value: 'module', disabled: true },
], ],
component: { component: {
@ -78,7 +78,7 @@ export default {
name: 'association', name: 'association',
title: '相关数据表', title: '相关数据表',
component: { component: {
type: 'string', type: 'drawerSelect',
showInDetail: true, showInDetail: true,
showInForm: true, showInForm: true,
}, },

View File

@ -26,6 +26,7 @@ export default {
type: 'string', type: 'string',
name: 'title', name: 'title',
title: '视图名称', title: '视图名称',
required: true,
component: { component: {
type: 'string', type: 'string',
className: 'drag-visible', className: 'drag-visible',
@ -51,9 +52,10 @@ export default {
type: 'string', type: 'string',
name: 'type', name: 'type',
title: '视图类型', title: '视图类型',
required: true,
dataSource: [ dataSource: [
{ label: '表格', value: 'table' }, { label: '表格', value: 'table' },
{ label: '表单', value: 'form' }, // { label: '表单', value: 'form' },
{ label: '看板', value: 'kanban', disabled: true }, { label: '看板', value: 'kanban', disabled: true },
{ label: '日历', value: 'calendar', disabled: true }, { label: '日历', value: 'calendar', disabled: true },
{ label: '地图', value: 'map', disabled: true }, { label: '地图', value: 'map', disabled: true },
@ -63,6 +65,7 @@ export default {
showInTable: true, showInTable: true,
showInDetail: true, showInDetail: true,
showInForm: true, showInForm: true,
default: 'table',
"x-linkages": [ "x-linkages": [
{ {
"type": "value:visible", "type": "value:visible",
@ -79,6 +82,7 @@ export default {
title: '筛选数据', title: '筛选数据',
developerMode: false, developerMode: false,
mode: 'replace', mode: 'replace',
defaultValue: {},
component: { component: {
type: 'filter', type: 'filter',
showInForm: true, showInForm: true,
@ -89,13 +93,15 @@ export default {
type: 'string', type: 'string',
name: 'template', name: 'template',
title: '模板', title: '模板',
required: true,
dataSource: [ dataSource: [
{ label: '表单', value: 'DrawerForm' }, // { label: '表单', value: 'DrawerForm' },
{ label: '常规表格', value: 'Table' }, { label: '常规表格', value: 'Table' },
{ label: '简易表格', value: 'SimpleTable' }, { label: '简易表格', value: 'SimpleTable' },
], ],
component: { component: {
type: 'string', type: 'string',
default: 'Table',
showInTable: true, showInTable: true,
showInDetail: true, showInDetail: true,
showInForm: true, showInForm: true,

View File

@ -5,4 +5,11 @@ export default async function (model: FieldModel, options: any = {}) {
if (migrate) { if (migrate) {
await model.migrate(options); await model.migrate(options);
} }
if (model.get('collection_name') && model.get('parent_id')) {
const parent = await model.getParent({
...options,
});
const Collection = model.database.getModel('collections');
await Collection.load({...options, where: {name: parent.get('collection_name')}});
}
} }

View File

@ -1,11 +1,51 @@
import FieldModel from '../models/field'; import FieldModel from '../models/field';
import * as types from '../interfaces/types';
import _ from 'lodash'; import _ from 'lodash';
export default async function (model: FieldModel) { export default async function (model: FieldModel, options) {
// if (model.get('interface')) {
// model.setInterface(model.get('interface'));
// }
// 生成随机 name 要放最后 // 生成随机 name 要放最后
model.generateNameIfNull(); // model.generateNameIfNull();
const Collection = model.database.getModel('collections');
if (model.get('interface') === 'subTable') {
const target = model.get('target');
if (target) {
const collection = await Collection.findOne({
...options,
where: {
name: target,
},
});
if (!collection) {
await Collection.create({
name: target,
internal: true,
developerMode: true,
}, options);
}
await Collection.load({...options, where: {name: model.get('name')}})
}
}
// 如果 collection_name 不存在
if (!model.get('collection_name') && model.get('parent_id')) {
const parent = await model.getParent({
...options,
});
const target = parent.get('target');
if (target) {
const collection = await Collection.findOne({
...options,
where: {
name: target,
},
});
if (!collection) {
await Collection.create({
name: target,
internal: true,
developerMode: true,
}, options);
}
await Collection.load({...options, where: {name: parent.get('name')}})
model.set('collection_name', target);
}
}
} }

View File

@ -270,13 +270,13 @@ export const time = {
// }); // });
export const subTable = { export const subTable = {
title: '子表格', title: '子表格',
disabled: true, // disabled: true,
options: { options: {
interface: 'subTable', interface: 'subTable',
type: 'hasMany', type: 'hasMany',
// fields: [], // fields: [],
component: { component: {
type: 'table', type: 'subTable',
}, },
}, },
}; };
@ -324,14 +324,14 @@ export const subTable = {
export const linkTo = { export const linkTo = {
title: '关联数据', title: '关联数据',
disabled: true, // disabled: true,
options: { options: {
interface: 'linkTo', interface: 'linkTo',
multiple: true, // 可能影响 type multiple: true, // 可能影响 type
type: 'belongsToMany', type: 'belongsToMany',
// name, // name,
// target: '关联表', // 用户会输入 // target: '关联表', // 用户会输入
filterable: true, filterable: false,
component: { component: {
type: 'drawerSelect', type: 'drawerSelect',
}, },

View File

@ -61,9 +61,9 @@ export class CollectionModel extends BaseModel {
const associationTableNames = []; const associationTableNames = [];
for (const [key, association] of table.getAssociations()) { for (const [key, association] of table.getAssociations()) {
// TODO是否需要考虑重载的情况暂时是跳过处理 // TODO是否需要考虑重载的情况暂时是跳过处理
if (!this.database.isDefined(association.options.target)) { // if (!this.database.isDefined(association.options.target)) {
continue; // continue;
} // }
associationTableNames.push(association.options.target); associationTableNames.push(association.options.target);
} }
if (associationTableNames.length) { if (associationTableNames.length) {
@ -131,7 +131,7 @@ export class CollectionModel extends BaseModel {
data = _.cloneDeep(data); data = _.cloneDeep(data);
// @ts-ignore // @ts-ignore
const { update } = options; const { update } = options;
let collection; let collection: CollectionModel;
if (data.name) { if (data.name) {
collection = await this.findOne({ collection = await this.findOne({
...options, ...options,
@ -159,6 +159,7 @@ export class CollectionModel extends BaseModel {
continue; continue;
} }
const Model = this.database.getModel(key); const Model = this.database.getModel(key);
const ids = [];
for (const index in data[key]) { for (const index in data[key]) {
let model; let model;
const item = data[key][index]; const item = data[key][index];
@ -189,6 +190,14 @@ export class CollectionModel extends BaseModel {
collection_name: collection.name, collection_name: collection.name,
}, options); }, options);
} }
if (model) {
ids.push(model.id);
}
}
if (collection.get('internal')) {
await collection.updateAssociations({
[key]: ids,
});
} }
} }
return collection; return collection;

View File

@ -4,6 +4,8 @@ import { FieldOptions } from '@nocobase/database';
import * as types from '../interfaces/types'; import * as types from '../interfaces/types';
import { merge } from '../utils'; import { merge } from '../utils';
import { BuildOptions } from 'sequelize'; import { BuildOptions } from 'sequelize';
import { SaveOptions, Utils } from 'sequelize';
import { generateCollectionName } from './collection';
export function generateFieldName(title?: string): string { export function generateFieldName(title?: string): string {
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`; return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
@ -23,10 +25,21 @@ export class FieldModel extends BaseModel {
let args = [options, data]; let args = [options, data];
// @ts-ignore // @ts-ignore
data = merge(...args); data = merge(...args);
if (['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
if (!data.name) {
data.name = generateFieldName();
data.target = generateCollectionName();
}
if (!data.target) {
data.target = ['hasOne', 'belongsTo'].includes(data.type) ? Utils.pluralize(data.name) : data.name;
}
}
if (!data.name) {
data.name = generateFieldName();
}
} }
// @ts-ignore // @ts-ignore
super(data, options); super(data, options);
// console.log(data);
} }
generateName() { generateName() {

View File

@ -79,6 +79,10 @@ const transforms = {
if (interfaceType === 'multipleSelect') { if (interfaceType === 'multipleSelect') {
set(prop, 'x-component-props.mode', 'multiple'); set(prop, 'x-component-props.mode', 'multiple');
} }
if (interfaceType === 'subTable' && field.get('target')) {
set(prop, 'x-component-props.target', field.get('target'));
// resourceName
}
if (['radio', 'select', 'multipleSelect', 'checkboxes'].includes(interfaceType)) { if (['radio', 'select', 'multipleSelect', 'checkboxes'].includes(interfaceType)) {
prop.enum = field.get('dataSource'); prop.enum = field.get('dataSource');
} }
@ -94,9 +98,14 @@ const transforms = {
if (!get(field.component, 'showInDetail')) { if (!get(field.component, 'showInDetail')) {
continue; continue;
} }
const props = {};
if (field.get('interface') === 'subTable') {
const children = await field.getChildren();
props['children'] = children.map(child => ({...child.toJSON(), dataIndex: child.name.split('.')}))
}
arr.push({ arr.push({
...field.toJSON(), ...field.toJSON(),
...field.options, ...props,
}); });
} }
return arr; return arr;