mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:36:44 +00:00
feat: improve collection manager (#1013)
* feat: 图形化管理数据表 * feat: 图形化管理数据表 * feat: 图形化管理数据表 * feat: 图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 完善图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat: 样式优化图形化管理数据表 * feat(collection-manager): add foreignKey Field and support relate field record foreignKey info through collection record into collections and foreignKey field record info fields * fix(collection-manager): if has through collection then don't create through collections record * fix(client/route-switch): skip sub routes * feat: 添加graphpostion * feat: 图形化collection新增表时刷新数据 * fix(collection-manager): refactor afterCreateForRelateField * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化collection存储位置 * feat: 图形化样式优化 * feat: styling * feat: 图形化样式优化 * feat: 图形化样式优化 * feat: 图形化数据表多语言完善 * feat: 图形化数据表多语言完善 * feat: improve code * feat: 图形化数据表连线样式修改 * feat: 图形化数据表样式修改 * feat: 图形化数据表样式修改 * feat: 图形化数据表样式修改 * feat: 图形化数据表样式修改 * fix(collection-manager): fix afterCreateForRelateField * feat: 样式优化 * feat: 样式优化 * feat: afterCreateForForeignKeyField * fix: timestamps: false * feat: 连线锚点优化 * fix(collection-manager): when del foreign key field, relate fields will be del too * fix: update package.json * fix: update package.json * feat: 文件名大小写 * feat: 连线锚点优化 * feat: 连线锚点通过计算得到样式优化 * feat: 连线锚点通过计算得到样式优化 * fix: fk * fix: remove index * feat: 连线hover时高亮 * fix: test error * feat: 初始化计算位置 * feat: 初始化时计算坐标位置 * feat: 初始化时计算坐标位置 * feat: improve code (#933) * fix: built in * feat: 没有关系字段时也要连线 * feat: 自关联也要连线 * fix: styling * feat: 滚动条问题 * feat: 拖拽优化 * feat: 画布paddig优化 * feat: 编辑时支持反向关联字段配置 * feat: 画布拖拽滚动优化 * feat: 画布拖拽滚动优化 * fix: reload * feat: 修复数据表新建重叠 * fix: refreshCM & refreshGM * feat: 修复表达式输入框显示异常 * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * fix: 消除代码警告 * fix: 消除代码警告 * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化(增量渲染) * feat: 渲染性能优化 * feat: 渲染性能优化 * feat: 外键生成在位置在前面 * feat: 限制表最多显示10个字段其余滚动 * feat: 移动表位置的连线重新计算最优位置 * fix: error * feat: 布局自动换行 * fix: test error * fix: xpipe.eq * fix: upgrade error * fix: upgrade error * feat: 选中表时只显示和目标表关联的表和连线 * feat: 连线优化 * fix: maxListenersExceededWarning * feat: 连线优化 * feat: powerby样式优化 * feat: 表筛选优化 * feat: 新建字段优化 * feat: 点击线高亮主外键和关联字段 * feat: 点击线高亮主外键和关联字段 * feat: 鼠标hover连线高亮主外键和关联字段 * fix(collection-manager): foreign key sorting should follow ID * fix(client/config-relation-field): set Relation field's ReverseField default value is false * feat: 卡片默认显示主外键和关联字段其余通过折叠展示且分组区分显示 * fix(client/collection-manager): don't display auto create through collections and foreign key only display in graph menu * feat: 样式优化 * feat: 添加字段时默认展开折叠 * feat: 样式优化 * feat: foreign field migration (#1001) * feat: 补充多语言 * feat: settings center tabs * feat: 主键判断primaryKey * fix(collection-manager): foreign key sorting should follow primaryKey * fix(client/block-select-collection): filter auto create through collections * fix(client/block-config-fields): filter isForeignKey fields * fix(client/configuration-table): relation fileds select collection filter auto create through * feat: 多对多连线高亮时全亮 * feat: 选中多对多中的一张表另一张表也显示 * feat: 连线mouseleave事件 * feat: 多语言更新 * feat: 计算新建表位置优化 * feat: 添加自动布局 * feat(client/configure-fields): categorize fields * fix(client/configure-fields): display foreign key fields * fix(client): package reference * fix: remove graph * fix: remove Co-authored-by: 唐小爱 <tangxiaoai@192.168.0.103> Co-authored-by: lyf-coder <lyf-coder@foxmail.com> Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: ChengLei Shao <chareice@live.com> Co-authored-by: mytharcher <mytharcher@gmail.com>
This commit is contained in:
parent
8f5a93bf63
commit
0e70e3848a
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
@ -4,41 +4,13 @@
|
|||||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug Server",
|
|
||||||
"cmd": "${workspaceRoot}",
|
|
||||||
"runtimeArgs": [
|
|
||||||
"-r", "dotenv/config",
|
|
||||||
"-r", "tsconfig-paths/register",
|
|
||||||
"-r", "ts-node/register"
|
|
||||||
],
|
|
||||||
"args": ["${workspaceRoot}/packages/app/server/src/index.ts", "start"],
|
|
||||||
"port": 9229,
|
|
||||||
"skipFiles": [
|
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"internalConsoleOptions": "neverOpen"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Jest Tests",
|
"name": "Debug Jest Tests",
|
||||||
"runtimeExecutable": "yarn",
|
"runtimeExecutable": "yarn",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["run", "--inspect-brk", "test", "--runInBand", "${fileBasenameNoExtension}"],
|
||||||
"run",
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"--inspect-brk",
|
|
||||||
"test",
|
|
||||||
"--runInBand",
|
|
||||||
// could be any single file path to debug
|
|
||||||
"${workspaceFolder}/packages/plugins/workflow/src/__tests__/instructions/parallel.test.ts"
|
|
||||||
],
|
|
||||||
"port": 9229,
|
|
||||||
"skipFiles": [
|
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen"
|
"internalConsoleOptions": "neverOpen"
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ export const AddCollectionField = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AddFieldAction = (props) => {
|
export const AddFieldAction = (props) => {
|
||||||
const { scope, getContainer, item: record, children } = props;
|
const { scope, getContainer, item: record, children,trigger } = props;
|
||||||
const { getInterface } = useCollectionManager();
|
const { getInterface } = useCollectionManager();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [schema, setSchema] = useState({});
|
const [schema, setSchema] = useState({});
|
||||||
@ -154,6 +154,7 @@ export const AddFieldAction = (props) => {
|
|||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
getPopupContainer={getContainer}
|
getPopupContainer={getContainer}
|
||||||
|
trigger={trigger}
|
||||||
overlay={
|
overlay={
|
||||||
<Menu
|
<Menu
|
||||||
style={{
|
style={{
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import { createForm, Field } from '@formily/core';
|
||||||
|
import { FieldContext, FormContext, observer, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { Options, Result } from 'ahooks/lib/useRequest/src/types';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { AsyncDataProvider, useAsyncData, useRequest } from '../..';
|
||||||
|
import { useAttach } from '@formily/react/lib/hooks/useAttach';
|
||||||
|
import { TableProps } from 'antd';
|
||||||
|
import { CollectionFieldsTableArray } from './CollectionFieldsTableArray';
|
||||||
|
|
||||||
|
type TableVoidProps = TableProps<any> & {
|
||||||
|
request?: any;
|
||||||
|
useSelectedRowKeys?: any;
|
||||||
|
useDataSource?: (
|
||||||
|
options?: Options<any, any> & { uid?: string },
|
||||||
|
props?: any,
|
||||||
|
) => Result<any, any> & { state?: any; setState?: any };
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDefSelectedRowKeys = () => {
|
||||||
|
const result = useAsyncData();
|
||||||
|
return [result?.state?.selectedRowKeys, (selectedRowKeys) => result?.setState?.({ selectedRowKeys })];
|
||||||
|
};
|
||||||
|
const useDef = (options, props) => {
|
||||||
|
const { request, dataSource } = props;
|
||||||
|
if (request) {
|
||||||
|
return useRequest(request(props), options);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: dataSource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CollectionFieldsTable: React.FC<TableVoidProps> = observer((props) => {
|
||||||
|
const { rowKey = 'id', useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const form = useMemo(() => createForm(), []);
|
||||||
|
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
|
||||||
|
const result = useDataSource(
|
||||||
|
{
|
||||||
|
uid: fieldSchema['x-uid'],
|
||||||
|
onSuccess(data) {
|
||||||
|
form.setValues({
|
||||||
|
[fieldSchema.name]: data?.data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<AsyncDataProvider value={result}>
|
||||||
|
<FormContext.Provider value={form}>
|
||||||
|
<FieldContext.Provider value={f}>
|
||||||
|
<CollectionFieldsTableArray
|
||||||
|
{...props}
|
||||||
|
rowKey={rowKey}
|
||||||
|
loading={result?.['loading']}
|
||||||
|
useSelectedRowKeys={useSelectedRowKeys}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
</FormContext.Provider>
|
||||||
|
</AsyncDataProvider>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,220 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { ArrayField, Field } from '@formily/core';
|
||||||
|
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { Table, TableColumnProps } from 'antd';
|
||||||
|
import { default as classNames } from 'classnames';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { RecordIndexProvider, RecordProvider, useCollectionManager, useRequest, useSchemaInitializer } from '../..';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const isColumnComponent = (schema: Schema) => {
|
||||||
|
return schema['x-component']?.endsWith('.Column') > -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTableColumns = () => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
|
||||||
|
const columns = schema
|
||||||
|
.reduceProperties((buf, s) => {
|
||||||
|
if (isColumnComponent(s)) {
|
||||||
|
return buf.concat([s]);
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
.map((s: Schema) => {
|
||||||
|
return {
|
||||||
|
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
|
||||||
|
dataIndex: s.name,
|
||||||
|
key: s.name,
|
||||||
|
render: (v, record) => {
|
||||||
|
const index = field.value?.indexOf(record);
|
||||||
|
// console.log((Date.now() - start) / 1000);
|
||||||
|
return (
|
||||||
|
<RecordIndexProvider index={index}>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<RecursionField schema={s} name={index} onlyRenderProperties />
|
||||||
|
</RecordProvider>
|
||||||
|
</RecordIndexProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} as TableColumnProps<any>;
|
||||||
|
});
|
||||||
|
if (!exists) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
return columns.concat({
|
||||||
|
title: render(),
|
||||||
|
dataIndex: 'TABLE_COLUMN_INITIALIZER',
|
||||||
|
key: 'TABLE_COLUMN_INITIALIZER',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const components = {
|
||||||
|
body: {
|
||||||
|
row: (props) => {
|
||||||
|
return <tr {...props} />;
|
||||||
|
},
|
||||||
|
cell: (props) => (
|
||||||
|
<td
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
props.className,
|
||||||
|
css`
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDef = () => {
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
return [selectedRowKeys, setSelectedRowKeys];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDefDataSource = (options, props) => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
return useRequest(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: field.value,
|
||||||
|
});
|
||||||
|
}, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupColumns = [
|
||||||
|
{
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type CategorizeKey = 'primaryAndForeignKey' | 'relation' | 'systemInfo' | 'basic';
|
||||||
|
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
|
||||||
|
const CategorizeKeyNameMap = new Map<CategorizeKey, string>([
|
||||||
|
['primaryAndForeignKey', 'Primary key & Foreign key fields'],
|
||||||
|
['relation', 'Relation fields'],
|
||||||
|
['systemInfo', 'System fields'],
|
||||||
|
['basic', 'General fields'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
interface CategorizeDataItem {
|
||||||
|
key: CategorizeKey;
|
||||||
|
name: string;
|
||||||
|
data: Array<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const columns = useTableColumns();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getInterface } = useCollectionManager();
|
||||||
|
const {
|
||||||
|
showIndex = true,
|
||||||
|
useSelectedRowKeys = useDef,
|
||||||
|
useDataSource = useDefDataSource,
|
||||||
|
onChange,
|
||||||
|
...others
|
||||||
|
} = props;
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
|
||||||
|
|
||||||
|
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
|
||||||
|
useDataSource({
|
||||||
|
onSuccess(data) {
|
||||||
|
field.value = data?.data || [];
|
||||||
|
// categorize field
|
||||||
|
const categorizeMap = new Map<CategorizeKey, any>();
|
||||||
|
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
|
||||||
|
let fieldArr = categorizeMap.get(categorizeKey);
|
||||||
|
if (!fieldArr) {
|
||||||
|
fieldArr = [];
|
||||||
|
}
|
||||||
|
fieldArr.push(val);
|
||||||
|
categorizeMap.set(categorizeKey, fieldArr);
|
||||||
|
};
|
||||||
|
field.value.forEach((item) => {
|
||||||
|
const itemInterface = getInterface(item?.interface);
|
||||||
|
if (item?.primaryKey || item.isForeignKey) {
|
||||||
|
addCategorizeVal('primaryAndForeignKey', item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = itemInterface?.group as CategorizeKey;
|
||||||
|
switch (group) {
|
||||||
|
case 'systemInfo':
|
||||||
|
case 'relation':
|
||||||
|
addCategorizeVal(group, item);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
addCategorizeVal('basic', item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tmpData: Array<CategorizeDataItem> = [];
|
||||||
|
sortKeyArr.forEach((key) => {
|
||||||
|
if (categorizeMap.get(key)?.length > 0) {
|
||||||
|
tmpData.push({
|
||||||
|
key,
|
||||||
|
name: t(CategorizeKeyNameMap.get(key)),
|
||||||
|
data: categorizeMap.get(key),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setCategorizeData(tmpData);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const restProps = {
|
||||||
|
rowSelection: props.rowSelection
|
||||||
|
? {
|
||||||
|
type: 'checkbox',
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange(selectedRowKeys: any[]) {
|
||||||
|
setSelectedRowKeys(selectedRowKeys);
|
||||||
|
},
|
||||||
|
...props.rowSelection,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRowKey = (record: any) => {
|
||||||
|
return field.value?.indexOf?.(record);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
|
||||||
|
debugger;
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
{...others}
|
||||||
|
{...restProps}
|
||||||
|
components={components}
|
||||||
|
showHeader={true}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={record.data}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
.ant-table {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
showHeader={false}
|
||||||
|
loading={props?.loading}
|
||||||
|
columns={groupColumns}
|
||||||
|
dataSource={categorizeData}
|
||||||
|
pagination={false}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender,
|
||||||
|
defaultExpandedRowKeys: sortKeyArr,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -11,6 +11,7 @@ import { AddSubFieldAction } from './AddSubFieldAction';
|
|||||||
import { FieldSummary } from './components/FieldSummary';
|
import { FieldSummary } from './components/FieldSummary';
|
||||||
import { EditSubFieldAction } from './EditSubFieldAction';
|
import { EditSubFieldAction } from './EditSubFieldAction';
|
||||||
import { collectionSchema } from './schemas/collections';
|
import { collectionSchema } from './schemas/collections';
|
||||||
|
import { CollectionFieldsTable } from ".";
|
||||||
|
|
||||||
const useAsyncDataSource = (service: any) => (field: any) => {
|
const useAsyncDataSource = (service: any) => (field: any) => {
|
||||||
field.loading = true;
|
field.loading = true;
|
||||||
@ -174,10 +175,12 @@ export const ConfigurationTable = () => {
|
|||||||
const { collections = [] } = useCollectionManager();
|
const { collections = [] } = useCollectionManager();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const loadCollections = async (field: any) => {
|
const loadCollections = async (field: any) => {
|
||||||
return collections?.map((item: any) => ({
|
return collections
|
||||||
label: compile(item.title),
|
?.filter((item) => !(item.autoCreate && item.isThrough))
|
||||||
value: item.name,
|
.map((item: any) => ({
|
||||||
}));
|
label: compile(item.title),
|
||||||
|
value: item.name,
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
const ctx = useContext(SchemaComponentContext);
|
const ctx = useContext(SchemaComponentContext);
|
||||||
return (
|
return (
|
||||||
@ -189,6 +192,7 @@ export const ConfigurationTable = () => {
|
|||||||
AddSubFieldAction,
|
AddSubFieldAction,
|
||||||
EditSubFieldAction,
|
EditSubFieldAction,
|
||||||
FieldSummary,
|
FieldSummary,
|
||||||
|
CollectionFieldsTable
|
||||||
}}
|
}}
|
||||||
scope={{
|
scope={{
|
||||||
useDestroySubField,
|
useDestroySubField,
|
||||||
|
@ -6,6 +6,7 @@ export * from './ConfigurationTable';
|
|||||||
export * from './EditFieldAction';
|
export * from './EditFieldAction';
|
||||||
export * from './interfaces';
|
export * from './interfaces';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './CollectionFieldsTable';
|
||||||
|
|
||||||
registerValidateFormats({
|
registerValidateFormats({
|
||||||
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
|
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
|
||||||
|
@ -71,7 +71,7 @@ export const collectionFieldSchema: ISchema = {
|
|||||||
resource: 'collections.fields',
|
resource: 'collections.fields',
|
||||||
action: 'list',
|
action: 'list',
|
||||||
params: {
|
params: {
|
||||||
pageSize: 50,
|
paginate: false,
|
||||||
filter: {
|
filter: {
|
||||||
'interface.$not': null,
|
'interface.$not': null,
|
||||||
},
|
},
|
||||||
@ -123,7 +123,7 @@ export const collectionFieldSchema: ISchema = {
|
|||||||
table: {
|
table: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-uid': 'input',
|
'x-uid': 'input',
|
||||||
'x-component': 'Table.Void',
|
'x-component': 'CollectionFieldsTable',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
rowKey: 'name',
|
rowKey: 'name',
|
||||||
rowSelection: {
|
rowSelection: {
|
||||||
|
@ -63,6 +63,11 @@ export const collectionSchema: ISchema = {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
filter: {
|
filter: {
|
||||||
inherit: false,
|
inherit: false,
|
||||||
|
options: {
|
||||||
|
// filter auto create through collections
|
||||||
|
autoCreate: { $not: true },
|
||||||
|
isThrough: { $not: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
sort: ['sort'],
|
sort: ['sort'],
|
||||||
appends: [],
|
appends: [],
|
||||||
|
@ -78,7 +78,7 @@ export const reverseFieldProperties: Record<string, ISchema> = {
|
|||||||
properties: {
|
properties: {
|
||||||
autoCreateReverseField: {
|
autoCreateReverseField: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: true,
|
default: false,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
'x-content': '{{t("Create inverse field in the target collection")}}',
|
'x-content': '{{t("Create inverse field in the target collection")}}',
|
||||||
|
@ -121,6 +121,10 @@ export default {
|
|||||||
"Configure columns": "Configure columns",
|
"Configure columns": "Configure columns",
|
||||||
"Edit field": "Edit field",
|
"Edit field": "Edit field",
|
||||||
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
||||||
|
"Primary key & Foreign key fields": "Primary key & Foreign key fields",
|
||||||
|
"Relation fields": "Relation fields",
|
||||||
|
"System fields": "System fields",
|
||||||
|
"General fields": "General fields",
|
||||||
"Basic": "Basic",
|
"Basic": "Basic",
|
||||||
"Single line text": "Single line text",
|
"Single line text": "Single line text",
|
||||||
"Long text": "Long text",
|
"Long text": "Long text",
|
||||||
|
@ -125,6 +125,10 @@ export default {
|
|||||||
"Configure columns": "配置字段",
|
"Configure columns": "配置字段",
|
||||||
"Edit field": "编辑字段",
|
"Edit field": "编辑字段",
|
||||||
"Configure fields of {{title}}": "「{{title}}」的字段配置",
|
"Configure fields of {{title}}": "「{{title}}」的字段配置",
|
||||||
|
"Primary key & Foreign key fields": "主外键字段",
|
||||||
|
"Relation fields": "关系字段",
|
||||||
|
"System fields": "系统字段",
|
||||||
|
"General fields": "普通字段",
|
||||||
"Basic": "基本类型",
|
"Basic": "基本类型",
|
||||||
"Single line text": "单行文本",
|
"Single line text": "单行文本",
|
||||||
"Long text": "多行文本",
|
"Long text": "多行文本",
|
||||||
|
@ -14,7 +14,7 @@ import { useCompile } from '../schema-component';
|
|||||||
import { BlockTemplatesPane } from '../schema-templates';
|
import { BlockTemplatesPane } from '../schema-templates';
|
||||||
import { SystemSettingsPane } from '../system-settings';
|
import { SystemSettingsPane } from '../system-settings';
|
||||||
|
|
||||||
const SettingsCenterContext = createContext<any>({});
|
export const SettingsCenterContext = createContext<any>({});
|
||||||
|
|
||||||
const PluginCard = (props) => {
|
const PluginCard = (props) => {
|
||||||
const history = useHistory<any>();
|
const history = useHistory<any>();
|
||||||
|
@ -190,7 +190,7 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
padding: 10px 50px;
|
padding: 0px 50px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -74,7 +74,7 @@ export const useTableColumnInitializerFields = () => {
|
|||||||
const { name, fields = [] } = useCollection();
|
const { name, fields = [] } = useCollection();
|
||||||
const { getInterface } = useCollectionManager();
|
const { getInterface } = useCollectionManager();
|
||||||
return fields
|
return fields
|
||||||
.filter((field) => field?.interface && field?.interface !== 'subTable')
|
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const interfaceConfig = getInterface(field.interface);
|
const interfaceConfig = getInterface(field.interface);
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -157,7 +157,7 @@ export const useFormItemInitializerFields = (options?: any) => {
|
|||||||
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
?.filter((field) => field?.interface)
|
?.filter((field) => field?.interface && !field?.isForeignKey)
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
const interfaceConfig = getInterface(field.interface);
|
const interfaceConfig = getInterface(field.interface);
|
||||||
|
|
||||||
@ -433,7 +433,13 @@ export const useCollectionDataSourceItems = (componentName) => {
|
|||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: t('Select collection'),
|
title: t('Select collection'),
|
||||||
children: collections
|
children: collections
|
||||||
?.filter((item) => !item.inherit)
|
?.filter((item) => {
|
||||||
|
if(item.inherit){
|
||||||
|
return false
|
||||||
|
}else{
|
||||||
|
return !(item?.isThrough && item?.autoCreate);
|
||||||
|
}
|
||||||
|
})
|
||||||
?.map((item, index) => {
|
?.map((item, index) => {
|
||||||
const templates = getTemplatesByCollection(item.name).filter((template) => {
|
const templates = getTemplatesByCollection(item.name).filter((template) => {
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,14 @@ import Database from '@nocobase/database';
|
|||||||
|
|
||||||
export function afterCreateForForeignKeyField(db: Database) {
|
export function afterCreateForForeignKeyField(db: Database) {
|
||||||
function generateFkOptions(collectionName: string, foreignKey: string) {
|
function generateFkOptions(collectionName: string, foreignKey: string) {
|
||||||
const M = db.getModel(collectionName);
|
const collection = db.getCollection(collectionName);
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
throw new Error('collection not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const M = collection.model;
|
||||||
|
|
||||||
const attr = M.rawAttributes[foreignKey];
|
const attr = M.rawAttributes[foreignKey];
|
||||||
if (!attr) {
|
if (!attr) {
|
||||||
throw new Error(`${collectionName}.${foreignKey} does not exists`);
|
throw new Error(`${collectionName}.${foreignKey} does not exists`);
|
||||||
@ -50,23 +57,39 @@ export function afterCreateForForeignKeyField(db: Database) {
|
|||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
if (instance.type !== values.type) {
|
if (instance.type !== values.type) {
|
||||||
throw new Error(`fk type invalid`);
|
throw new Error(`fk type invalid`);
|
||||||
}
|
}
|
||||||
instance.set('sort',1);
|
instance.set('sort', 1);
|
||||||
instance.set('isForeignKey', true);
|
instance.set('isForeignKey', true);
|
||||||
await instance.save({ transaction });
|
await instance.save({ transaction });
|
||||||
} else {
|
} else {
|
||||||
await r.create({
|
const creatInstance = await r.create({
|
||||||
values: {
|
values: {
|
||||||
isForeignKey: true,
|
isForeignKey: true,
|
||||||
sort:1,
|
|
||||||
...values,
|
...values,
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
// SortField#setSortValue instance._previousDataValues[scopeKey] judgment cause create set sort:1 invalid, need update
|
||||||
|
creatInstance.set('sort', 1);
|
||||||
|
await creatInstance.save({ transaction });
|
||||||
}
|
}
|
||||||
|
// update ID sort:0
|
||||||
|
await r.update({
|
||||||
|
filter: {
|
||||||
|
collectionName,
|
||||||
|
options:{
|
||||||
|
primaryKey: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
sort: 0,
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return async (model, { transaction, context }) => {
|
return async (model, { transaction, context }) => {
|
||||||
@ -74,7 +97,9 @@ export function afterCreateForForeignKeyField(db: Database) {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, interface: interfaceType, collectionName, target, through, foreignKey, otherKey } = model.get();
|
const { type, interface: interfaceType, collectionName, target, through, foreignKey, otherKey } = model.get();
|
||||||
|
|
||||||
// foreign key in target collection
|
// foreign key in target collection
|
||||||
if (['oho', 'o2m'].includes(interfaceType)) {
|
if (['oho', 'o2m'].includes(interfaceType)) {
|
||||||
const values = generateFkOptions(target, foreignKey);
|
const values = generateFkOptions(target, foreignKey);
|
||||||
@ -86,6 +111,7 @@ export function afterCreateForForeignKeyField(db: Database) {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// foreign key in source collection
|
// foreign key in source collection
|
||||||
else if (['obo', 'm2o'].includes(interfaceType)) {
|
else if (['obo', 'm2o'].includes(interfaceType)) {
|
||||||
const values = generateFkOptions(collectionName, foreignKey);
|
const values = generateFkOptions(collectionName, foreignKey);
|
||||||
@ -94,6 +120,7 @@ export function afterCreateForForeignKeyField(db: Database) {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// foreign key in through collection
|
// foreign key in through collection
|
||||||
else if (['linkTo', 'm2m'].includes(interfaceType)) {
|
else if (['linkTo', 'm2m'].includes(interfaceType)) {
|
||||||
if (type !== 'belongsToMany') {
|
if (type !== 'belongsToMany') {
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Migration } from '@nocobase/server';
|
||||||
|
import { afterCreateForForeignKeyField } from '../hooks/afterCreateForForeignKeyField';
|
||||||
|
|
||||||
|
export default class DropForeignKeysMigration extends Migration {
|
||||||
|
async up() {
|
||||||
|
const result = await this.app.version.satisfies('<0.8.0');
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = await this.app.db.sequelize.transaction();
|
||||||
|
const callback = afterCreateForForeignKeyField(this.app.db);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fields = await this.app.db.getCollection('fields').repository.find({
|
||||||
|
filter: {
|
||||||
|
interface: {
|
||||||
|
$in: ['oho', 'o2m', 'obo', 'm2o', 'linkTo', 'm2m'],
|
||||||
|
},
|
||||||
|
collectionName: {
|
||||||
|
$not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
try {
|
||||||
|
await callback(field, {
|
||||||
|
transaction,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('collection not found')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -83,6 +83,7 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// after migrate
|
// after migrate
|
||||||
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
|
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user