mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:36:44 +00:00
feat: support subTable field
* feat: add linkTo and subTable fields * add subTable field component * improve sub table * bugfix
This commit is contained in:
parent
3054ddb13b
commit
ecab106c3c
@ -60,6 +60,100 @@ export default {
|
||||
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',
|
||||
title: '日期',
|
||||
@ -78,6 +172,24 @@ export default {
|
||||
showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'createdBy',
|
||||
title: '创建人',
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
// showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'updatedBy',
|
||||
title: '更新人',
|
||||
component: {
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
// showInForm: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'createdAt',
|
||||
title: '创建日期',
|
||||
|
@ -6,9 +6,9 @@ import associated from '../../../actions/src/middlewares/associated';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
const sync = {
|
||||
force: true,
|
||||
force: false,
|
||||
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) => {
|
||||
const { resourceName } = ctx.action.params;
|
||||
const { resourceName, fields = {} } = ctx.action.params;
|
||||
const table = ctx.db.getTable(resourceName);
|
||||
// ctx.state.developerMode = {[Op.not]: null};
|
||||
ctx.state.developerMode = false;
|
||||
@ -67,8 +67,8 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
|
||||
ctx.action.setParam('filter.developerMode', ctx.state.developerMode);
|
||||
}
|
||||
if (table) {
|
||||
const except = [];
|
||||
const appends = [];
|
||||
const except = fields.except || [];
|
||||
const appends = fields.appends || [];
|
||||
for (const [name, field] of table.getFields()) {
|
||||
if (field.options.hidden) {
|
||||
except.push(field.options.name);
|
||||
|
@ -173,17 +173,19 @@ api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-use
|
||||
await Collection.import(table.getOptions(), { update: true, migrate: false });
|
||||
}
|
||||
await Page.import(data);
|
||||
await User.findOrCreate({
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
username: "admin",
|
||||
},
|
||||
defaults: {
|
||||
});
|
||||
if (!user) {
|
||||
await User.create({
|
||||
nickname: "超级管理员",
|
||||
password: "admin",
|
||||
username: "admin",
|
||||
token: "38979f07e1fca68fb3d2",
|
||||
},
|
||||
});
|
||||
}
|
||||
await database.getModel('collections').import(require('./collections/example').default);
|
||||
await database.close();
|
||||
})();
|
||||
|
@ -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
|
@ -14,6 +14,8 @@ import { Range } from './range'
|
||||
import { Rating } from './rating'
|
||||
import { Upload } from './upload'
|
||||
import { Filter } from './filter'
|
||||
import { DrawerSelect } from './drawer-select'
|
||||
import { SubTable } from './sub-table'
|
||||
|
||||
export const setup = () => {
|
||||
registerFormFields({
|
||||
@ -43,5 +45,7 @@ export const setup = () => {
|
||||
rating: Rating,
|
||||
upload: Upload,
|
||||
filter: Filter,
|
||||
drawerSelect: DrawerSelect,
|
||||
subTable: SubTable,
|
||||
})
|
||||
}
|
||||
|
91
packages/app/src/components/form.fields/sub-table/Form.tsx
Normal file
91
packages/app/src/components/form.fields/sub-table/Form.tsx
Normal 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>
|
||||
);
|
||||
});
|
118
packages/app/src/components/form.fields/sub-table/Table.tsx
Normal file
118
packages/app/src/components/form.fields/sub-table/Table.tsx
Normal 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>
|
||||
);
|
||||
}
|
20
packages/app/src/components/form.fields/sub-table/index.tsx
Normal file
20
packages/app/src/components/form.fields/sub-table/index.tsx
Normal 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
|
@ -17,15 +17,16 @@ export function Details(props: any) {
|
||||
associatedKey,
|
||||
resourceKey,
|
||||
} = props;
|
||||
const { actions = [], fields = [] } = props.schema;
|
||||
const { data = {}, loading, refresh } = useRequest(() => {
|
||||
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
||||
return api.resource(name).get({
|
||||
resourceKey,
|
||||
associatedKey,
|
||||
'fields[appends]': fields.filter(field => get(field, 'interface') === 'subTable').map(field => field.name).join(',')
|
||||
});
|
||||
});
|
||||
console.log(props);
|
||||
const { actions = [], fields = [] } = props.schema;
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<Actions
|
||||
@ -40,7 +41,7 @@ export function Details(props: any) {
|
||||
<Descriptions bordered column={1}>
|
||||
{fields.map((field: any) => {
|
||||
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)}/>
|
||||
</Descriptions.Item>
|
||||
)
|
||||
|
@ -1,10 +1,11 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { Tag, Popover } from 'antd';
|
||||
import { Tag, Popover, Table } from 'antd';
|
||||
import Icon from '@/components/icons';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { fields2columns } from '../SortableTable';
|
||||
|
||||
const InterfaceTypes = new Map<string, any>();
|
||||
|
||||
@ -27,6 +28,12 @@ function getFieldComponent(type) {
|
||||
|
||||
export function StringField(props: any) {
|
||||
const { value } = props;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return (
|
||||
<>{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({
|
||||
string: StringField,
|
||||
textarea: TextareaField,
|
||||
@ -173,6 +193,7 @@ registerFieldComponents({
|
||||
icon: IconField,
|
||||
createdBy: RealtionField,
|
||||
updatedBy: RealtionField,
|
||||
subTable: SubTableField,
|
||||
});
|
||||
|
||||
export default function Field(props: any) {
|
||||
|
@ -59,9 +59,7 @@ export async function getApp() {
|
||||
app.resourcer.registerActionHandlers({...actions.associate, ...actions.common});
|
||||
app.registerPlugin('collections', [plugin]);
|
||||
await app.loadPlugins();
|
||||
await app.database.sync({
|
||||
force: true,
|
||||
});
|
||||
await app.database.sync();
|
||||
// 表配置信息存到数据库里
|
||||
// const tables = app.database.getTables([]);
|
||||
// for (const table of tables) {
|
||||
|
@ -50,4 +50,80 @@ describe('models.field', () => {
|
||||
{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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -238,6 +238,11 @@ export default {
|
||||
list: {
|
||||
sort: 'sort',
|
||||
},
|
||||
get: {
|
||||
fields: {
|
||||
appends: ['children'],
|
||||
},
|
||||
},
|
||||
},
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
|
@ -90,6 +90,36 @@ export default {
|
||||
"target": "timeFormat",
|
||||
"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",
|
||||
"target": "component.showInForm",
|
||||
@ -158,7 +188,6 @@ export default {
|
||||
type: 'virtual',
|
||||
name: 'precision',
|
||||
title: '精度',
|
||||
defaultValue: 1,
|
||||
dataSource: [
|
||||
{value: 1, label: '1'},
|
||||
{value: 0.1, label: '1.0'},
|
||||
@ -169,6 +198,7 @@ export default {
|
||||
component: {
|
||||
type: 'number',
|
||||
showInForm: true,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -176,7 +206,6 @@ export default {
|
||||
type: 'virtual',
|
||||
name: 'dateFormat',
|
||||
title: '日期格式',
|
||||
defaultValue: 'YYYY/MM/DD',
|
||||
dataSource: [
|
||||
{value: 'YYYY/MM/DD', label: '年/月/日'},
|
||||
{value: 'YYYY-MM-DD', label: '年-月-日'},
|
||||
@ -185,6 +214,7 @@ export default {
|
||||
component: {
|
||||
type: 'string',
|
||||
showInForm: true,
|
||||
default: 'YYYY/MM/DD',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -192,10 +222,10 @@ export default {
|
||||
type: 'virtual',
|
||||
name: 'showTime',
|
||||
title: '显示时间',
|
||||
defaultValue: false,
|
||||
component: {
|
||||
type: 'boolean',
|
||||
showInForm: true,
|
||||
default: false,
|
||||
"x-linkages": [
|
||||
{
|
||||
"type": "value:visible",
|
||||
@ -210,7 +240,6 @@ export default {
|
||||
type: 'virtual',
|
||||
name: 'timeFormat',
|
||||
title: '时间格式',
|
||||
defaultValue: 'HH:mm:ss',
|
||||
dataSource: [
|
||||
{ value: 'HH:mm:ss', label: '24小时制' },
|
||||
{ value: 'hh:mm:ss a', label: '12小时制' },
|
||||
@ -218,6 +247,7 @@ export default {
|
||||
component: {
|
||||
type: 'string',
|
||||
showInForm: true,
|
||||
default: 'HH:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -234,18 +264,80 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'linkTo',
|
||||
multiple: true,
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
title: '子字段',
|
||||
target: 'fields',
|
||||
foreignKey: 'parent_id',
|
||||
sourceKey: 'id',
|
||||
interface: 'string',
|
||||
type: 'virtual',
|
||||
name: 'target',
|
||||
title: '要关联的数据表',
|
||||
component: {
|
||||
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',
|
||||
type: 'virtual',
|
||||
|
@ -55,7 +55,7 @@ export default {
|
||||
required: true,
|
||||
dataSource: [
|
||||
{ label: '详情数据', value: 'details' },
|
||||
{ label: '相关数据', value: 'association', disabled: true },
|
||||
{ label: '相关数据', value: 'association' },
|
||||
{ label: '模块组合', value: 'module', disabled: true },
|
||||
],
|
||||
component: {
|
||||
@ -78,7 +78,7 @@ export default {
|
||||
name: 'association',
|
||||
title: '相关数据表',
|
||||
component: {
|
||||
type: 'string',
|
||||
type: 'drawerSelect',
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
},
|
||||
|
@ -26,6 +26,7 @@ export default {
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
title: '视图名称',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'string',
|
||||
className: 'drag-visible',
|
||||
@ -51,9 +52,10 @@ export default {
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
title: '视图类型',
|
||||
required: true,
|
||||
dataSource: [
|
||||
{ label: '表格', value: 'table' },
|
||||
{ label: '表单', value: 'form' },
|
||||
// { label: '表单', value: 'form' },
|
||||
{ label: '看板', value: 'kanban', disabled: true },
|
||||
{ label: '日历', value: 'calendar', disabled: true },
|
||||
{ label: '地图', value: 'map', disabled: true },
|
||||
@ -63,6 +65,7 @@ export default {
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
default: 'table',
|
||||
"x-linkages": [
|
||||
{
|
||||
"type": "value:visible",
|
||||
@ -79,6 +82,7 @@ export default {
|
||||
title: '筛选数据',
|
||||
developerMode: false,
|
||||
mode: 'replace',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'filter',
|
||||
showInForm: true,
|
||||
@ -89,13 +93,15 @@ export default {
|
||||
type: 'string',
|
||||
name: 'template',
|
||||
title: '模板',
|
||||
required: true,
|
||||
dataSource: [
|
||||
{ label: '表单', value: 'DrawerForm' },
|
||||
// { label: '表单', value: 'DrawerForm' },
|
||||
{ label: '常规表格', value: 'Table' },
|
||||
{ label: '简易表格', value: 'SimpleTable' },
|
||||
],
|
||||
component: {
|
||||
type: 'string',
|
||||
default: 'Table',
|
||||
showInTable: true,
|
||||
showInDetail: true,
|
||||
showInForm: true,
|
||||
|
@ -5,4 +5,11 @@ export default async function (model: FieldModel, options: any = {}) {
|
||||
if (migrate) {
|
||||
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')}});
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,51 @@
|
||||
import FieldModel from '../models/field';
|
||||
import * as types from '../interfaces/types';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default async function (model: FieldModel) {
|
||||
// if (model.get('interface')) {
|
||||
// model.setInterface(model.get('interface'));
|
||||
// }
|
||||
export default async function (model: FieldModel, options) {
|
||||
// 生成随机 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,13 +270,13 @@ export const time = {
|
||||
// });
|
||||
export const subTable = {
|
||||
title: '子表格',
|
||||
disabled: true,
|
||||
// disabled: true,
|
||||
options: {
|
||||
interface: 'subTable',
|
||||
type: 'hasMany',
|
||||
// fields: [],
|
||||
component: {
|
||||
type: 'table',
|
||||
type: 'subTable',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -324,14 +324,14 @@ export const subTable = {
|
||||
|
||||
export const linkTo = {
|
||||
title: '关联数据',
|
||||
disabled: true,
|
||||
// disabled: true,
|
||||
options: {
|
||||
interface: 'linkTo',
|
||||
multiple: true, // 可能影响 type
|
||||
type: 'belongsToMany',
|
||||
// name,
|
||||
// target: '关联表', // 用户会输入
|
||||
filterable: true,
|
||||
filterable: false,
|
||||
component: {
|
||||
type: 'drawerSelect',
|
||||
},
|
||||
|
@ -61,9 +61,9 @@ export class CollectionModel extends BaseModel {
|
||||
const associationTableNames = [];
|
||||
for (const [key, association] of table.getAssociations()) {
|
||||
// TODO:是否需要考虑重载的情况?(暂时是跳过处理)
|
||||
if (!this.database.isDefined(association.options.target)) {
|
||||
continue;
|
||||
}
|
||||
// if (!this.database.isDefined(association.options.target)) {
|
||||
// continue;
|
||||
// }
|
||||
associationTableNames.push(association.options.target);
|
||||
}
|
||||
if (associationTableNames.length) {
|
||||
@ -131,7 +131,7 @@ export class CollectionModel extends BaseModel {
|
||||
data = _.cloneDeep(data);
|
||||
// @ts-ignore
|
||||
const { update } = options;
|
||||
let collection;
|
||||
let collection: CollectionModel;
|
||||
if (data.name) {
|
||||
collection = await this.findOne({
|
||||
...options,
|
||||
@ -159,6 +159,7 @@ export class CollectionModel extends BaseModel {
|
||||
continue;
|
||||
}
|
||||
const Model = this.database.getModel(key);
|
||||
const ids = [];
|
||||
for (const index in data[key]) {
|
||||
let model;
|
||||
const item = data[key][index];
|
||||
@ -189,6 +190,14 @@ export class CollectionModel extends BaseModel {
|
||||
collection_name: collection.name,
|
||||
}, options);
|
||||
}
|
||||
if (model) {
|
||||
ids.push(model.id);
|
||||
}
|
||||
}
|
||||
if (collection.get('internal')) {
|
||||
await collection.updateAssociations({
|
||||
[key]: ids,
|
||||
});
|
||||
}
|
||||
}
|
||||
return collection;
|
||||
|
@ -4,6 +4,8 @@ import { FieldOptions } from '@nocobase/database';
|
||||
import * as types from '../interfaces/types';
|
||||
import { merge } from '../utils';
|
||||
import { BuildOptions } from 'sequelize';
|
||||
import { SaveOptions, Utils } from 'sequelize';
|
||||
import { generateCollectionName } from './collection';
|
||||
|
||||
export function generateFieldName(title?: string): string {
|
||||
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];
|
||||
// @ts-ignore
|
||||
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
|
||||
super(data, options);
|
||||
// console.log(data);
|
||||
}
|
||||
|
||||
generateName() {
|
||||
|
@ -79,6 +79,10 @@ const transforms = {
|
||||
if (interfaceType === 'multipleSelect') {
|
||||
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)) {
|
||||
prop.enum = field.get('dataSource');
|
||||
}
|
||||
@ -94,9 +98,14 @@ const transforms = {
|
||||
if (!get(field.component, 'showInDetail')) {
|
||||
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({
|
||||
...field.toJSON(),
|
||||
...field.options,
|
||||
...props,
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
|
Loading…
Reference in New Issue
Block a user