feat: tree collection (#1561)
* feat: adjacency list * fix: error * feat: collection extender * Revert "feat: collection extender" This reverts commit a942eee7699724fe831a98bae3f67a42db91a28b. * fix: registerBuiltInListeners * chore: getAllNodeIds # Conflicts: # packages/plugins/acl/src/server.ts * fix: get list data tree ids # Conflicts: # packages/plugins/acl/src/__tests__/list-action.test.ts * feat: suport add child * feat: demo3 * feat: suport add child * feat: support add child * feat: support add child * fix: tree true * feat: suport expend and collapse * feat: support expend and collapse * feat: support expend and collapse * feat: table block of the selector supports tree table * feat: expand and collapse are only displayed in the tree table * fix: when the tree table is closed and opened, it needs to be refreshed to take effect * fix: test * refactor: add child is hidden when the tree table is closed * refactor: tree table filter the children field * refactor: tree table filter the children field * refactor: tree table filter the children field * refactor: expand and collapse button does not support modify name and icon * refactor: parent cannot be modified when adding child * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: filter out the node itself when selecting parent * refactor: filter out the node itself when selecting parent * refactor: tree collection locale * refactor: tree collection locale * fix: parameter exception when creating data selector * fix: translation * refactor: tree collection locale * feat: data selector Support tree table * fix: failed to uncheck when multiple data selectors are selected * fix: open or disabled the tree table, and add child does not respond immediately * feat: data selector Support tree table * fix: can not hide add child button * fix: improve code * fix: tree table * fix: dynamic children column --------- Co-authored-by: Chareice <chareice@live.com> Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
parent
cd2340ecd0
commit
a9e5e14429
@ -10,9 +10,6 @@
|
|||||||
"@nocobase/database": "0.9.1-alpha.2",
|
"@nocobase/database": "0.9.1-alpha.2",
|
||||||
"@nocobase/resourcer": "0.9.1-alpha.2"
|
"@nocobase/resourcer": "0.9.1-alpha.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"@nocobase/test": "0.9.1-alpha.2"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/nocobase/nocobase.git",
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MockServer, mockServer } from '@nocobase/test';
|
|
||||||
import { registerActions } from '@nocobase/actions';
|
import { registerActions } from '@nocobase/actions';
|
||||||
|
import { MockServer, mockServer } from './';
|
||||||
|
|
||||||
describe('get action', () => {
|
describe('get action', () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
@ -10,9 +10,7 @@ describe('get action', () => {
|
|||||||
let Profile;
|
let Profile;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = mockServer({
|
app = mockServer();
|
||||||
dataWrapping: false,
|
|
||||||
});
|
|
||||||
registerActions(app);
|
registerActions(app);
|
||||||
|
|
||||||
PostTag = app.collection({
|
PostTag = app.collection({
|
||||||
|
@ -129,3 +129,201 @@ describe('list action', () => {
|
|||||||
expect(response.body.count).toEqual(0);
|
expect(response.body.count).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('list-tree', () => {
|
||||||
|
let app;
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = actionMockServer();
|
||||||
|
registerActions(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be tree', async () => {
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
name: '1',
|
||||||
|
__index: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '1-1',
|
||||||
|
__index: '0.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '1-1-1',
|
||||||
|
__index: '0.children.0.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '1-1-1-1',
|
||||||
|
__index: '0.children.0.children.0.children.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '2',
|
||||||
|
__index: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '2-1',
|
||||||
|
__index: '1.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '2-1-1',
|
||||||
|
__index: '1.children.0.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '2-1-1-1',
|
||||||
|
__index: '1.children.0.children.0.children.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const db = app.db;
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'categories',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
treeParent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
treeChildren: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await db.getRepository('categories').create({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.agent()
|
||||||
|
.resource('categories')
|
||||||
|
.list({
|
||||||
|
tree: true,
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
sort: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
expect(response.body.rows).toMatchObject(values);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.only('should be tree', async () => {
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
name: '1',
|
||||||
|
__index: '0',
|
||||||
|
children2: [
|
||||||
|
{
|
||||||
|
name: '1-1',
|
||||||
|
__index: '0.children2.0',
|
||||||
|
children2: [
|
||||||
|
{
|
||||||
|
name: '1-1-1',
|
||||||
|
__index: '0.children2.0.children2.0',
|
||||||
|
children2: [
|
||||||
|
{
|
||||||
|
name: '1-1-1-1',
|
||||||
|
__index: '0.children2.0.children2.0.children2.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '2',
|
||||||
|
__index: '1',
|
||||||
|
children2: [
|
||||||
|
{
|
||||||
|
name: '2-1',
|
||||||
|
__index: '1.children2.0',
|
||||||
|
children2: [
|
||||||
|
{
|
||||||
|
name: '2-1-1',
|
||||||
|
__index: '1.children2.0.children2.0',
|
||||||
|
children2: [
|
||||||
|
{
|
||||||
|
name: '2-1-1-1',
|
||||||
|
__index: '1.children2.0.children2.0.children2.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const db = app.db;
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'categories',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
foreignKey: 'cid',
|
||||||
|
treeParent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children2',
|
||||||
|
foreignKey: 'cid',
|
||||||
|
treeChildren: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await db.getRepository('categories').create({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.agent()
|
||||||
|
.resource('categories')
|
||||||
|
.list({
|
||||||
|
tree: true,
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
sort: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
expect(response.body.rows).toMatchObject(values);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ActionParams } from '@nocobase/resourcer';
|
import { assign } from '@nocobase/utils';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import { getRepositoryFromParams } from '../utils';
|
import { getRepositoryFromParams } from '../utils';
|
||||||
|
|
||||||
@ -22,9 +22,20 @@ function totalPage(total, pageSize): number {
|
|||||||
return Math.ceil(total / pageSize);
|
return Math.ceil(total / pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findArgs(params: ActionParams) {
|
function findArgs(ctx: Context) {
|
||||||
const { fields, filter, appends, except, sort } = params;
|
const resourceName = ctx.action.resourceName;
|
||||||
return { filter, fields, appends, except, sort };
|
const params = ctx.action.params;
|
||||||
|
if (params.tree) {
|
||||||
|
const [collectionName, associationName] = resourceName.split('.');
|
||||||
|
const collection = ctx.db.getCollection(resourceName);
|
||||||
|
// tree collection 或者关系表是 tree collection
|
||||||
|
if (collection.options.tree && !(associationName && collectionName === collection.name)) {
|
||||||
|
const foreignKey = collection.treeParentField?.foreignKey || 'parentId';
|
||||||
|
assign(params, { filter: { [foreignKey]: null } }, { filter: 'andMerge' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { tree, fields, filter, appends, except, sort } = params;
|
||||||
|
return { tree, filter, fields, appends, except, sort };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listWithPagination(ctx: Context) {
|
async function listWithPagination(ctx: Context) {
|
||||||
@ -34,7 +45,7 @@ async function listWithPagination(ctx: Context) {
|
|||||||
|
|
||||||
const [rows, count] = await repository.findAndCount({
|
const [rows, count] = await repository.findAndCount({
|
||||||
context: ctx,
|
context: ctx,
|
||||||
...findArgs(ctx.action.params),
|
...findArgs(ctx),
|
||||||
...pageArgsToLimitArgs(parseInt(String(page)), parseInt(String(pageSize))),
|
...pageArgsToLimitArgs(parseInt(String(page)), parseInt(String(pageSize))),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,7 +61,7 @@ async function listWithPagination(ctx: Context) {
|
|||||||
async function listWithNonPaged(ctx: Context) {
|
async function listWithNonPaged(ctx: Context) {
|
||||||
const repository = getRepositoryFromParams(ctx);
|
const repository = getRepositoryFromParams(ctx);
|
||||||
|
|
||||||
const rows = await repository.find({ context: ctx, ...findArgs(ctx.action.params) });
|
const rows = await repository.find({ context: ctx, ...findArgs(ctx) });
|
||||||
|
|
||||||
ctx.body = rows;
|
ctx.body = rows;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { useCollection } from '../collection-manager';
|
|||||||
import { RecordProvider, useRecord } from '../record-provider';
|
import { RecordProvider, useRecord } from '../record-provider';
|
||||||
import { useDesignable } from '../schema-component';
|
import { useDesignable } from '../schema-component';
|
||||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
import { useActionContext } from '../schema-component';
|
||||||
|
|
||||||
export const FormBlockContext = createContext<any>({});
|
export const FormBlockContext = createContext<any>({});
|
||||||
|
|
||||||
@ -88,6 +89,18 @@ export const useFormBlockContext = () => {
|
|||||||
|
|
||||||
export const useFormBlockProps = () => {
|
export const useFormBlockProps = () => {
|
||||||
const ctx = useFormBlockContext();
|
const ctx = useFormBlockContext();
|
||||||
|
const record = useRecord();
|
||||||
|
const { fieldSchema } = useActionContext();
|
||||||
|
const { addChild } = fieldSchema?.['x-component-props']||{};
|
||||||
|
useEffect(() => {
|
||||||
|
if (addChild) {
|
||||||
|
ctx.form.query('parent').take((field) => {
|
||||||
|
field.disabled = true;
|
||||||
|
field.value = new Proxy({ ...record }, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ctx.form.setInitialValues(ctx.service?.data?.data);
|
ctx.form.setInitialValues(ctx.service?.data?.data);
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { ArrayField, createForm } from '@formily/core';
|
import { ArrayField, createForm } from '@formily/core';
|
||||||
import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
|
import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { useCollectionManager } from '../collection-manager';
|
import { useCollectionManager } from '../collection-manager';
|
||||||
|
import { SchemaComponentOptions, useFixedSchema } from '../schema-component';
|
||||||
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
||||||
import { useFixedSchema } from '../schema-component';
|
|
||||||
|
|
||||||
export const TableBlockContext = createContext<any>({});
|
export const TableBlockContext = createContext<any>({});
|
||||||
|
|
||||||
const InternalTableBlockProvider = (props) => {
|
const InternalTableBlockProvider = (props) => {
|
||||||
const { params, showIndex, dragSort, rowKey } = props;
|
const { params, showIndex, dragSort, rowKey, childrenColumnName } = props;
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { resource, service } = useBlockRequestContext();
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
const [expandFlag, setExpandFlag] = useState(false);
|
||||||
// if (service.loading) {
|
// if (service.loading) {
|
||||||
// return <Spin />;
|
// return <Spin />;
|
||||||
// }
|
// }
|
||||||
@ -26,6 +27,9 @@ const InternalTableBlockProvider = (props) => {
|
|||||||
showIndex,
|
showIndex,
|
||||||
dragSort,
|
dragSort,
|
||||||
rowKey,
|
rowKey,
|
||||||
|
expandFlag,
|
||||||
|
childrenColumnName,
|
||||||
|
setExpandFlag: () => setExpandFlag(!expandFlag),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RenderChildrenWithAssociationFilter {...props} />
|
<RenderChildrenWithAssociationFilter {...props} />
|
||||||
@ -81,21 +85,44 @@ export const useAssociationNames = (collection) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TableBlockProvider = (props) => {
|
export const TableBlockProvider = (props) => {
|
||||||
|
const resourceName = props.resource;
|
||||||
const params = { ...props.params };
|
const params = { ...props.params };
|
||||||
const appends = useAssociationNames(props.collection);
|
const appends = useAssociationNames(props.collection);
|
||||||
const form = useMemo(() => createForm(), []);
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { getCollection, getCollectionField } = useCollectionManager();
|
||||||
|
const collection = getCollection(props.collection);
|
||||||
|
const { treeTable } = fieldSchema?.['x-decorator-props']||{};
|
||||||
if (props.dragSort) {
|
if (props.dragSort) {
|
||||||
params['sort'] = ['sort'];
|
params['sort'] = ['sort'];
|
||||||
}
|
}
|
||||||
|
let childrenColumnName = 'children';
|
||||||
|
if (collection.tree && treeTable !== false) {
|
||||||
|
if (resourceName.includes('.')) {
|
||||||
|
const f = getCollectionField(resourceName);
|
||||||
|
if (f?.treeChildren) {
|
||||||
|
childrenColumnName = f.name;
|
||||||
|
params['tree'] = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const f = collection.fields.find(f => f.treeChildren);
|
||||||
|
if (f) {
|
||||||
|
childrenColumnName = f.name;
|
||||||
|
}
|
||||||
|
params['tree'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!Object.keys(params).includes('appends')) {
|
if (!Object.keys(params).includes('appends')) {
|
||||||
params['appends'] = appends;
|
params['appends'] = appends;
|
||||||
}
|
}
|
||||||
|
const form = useMemo(() => createForm(), [treeTable]);
|
||||||
return (
|
return (
|
||||||
<FormContext.Provider value={form}>
|
<SchemaComponentOptions scope={{ treeTable }}>
|
||||||
<BlockProvider {...props} params={params}>
|
<FormContext.Provider value={form}>
|
||||||
<InternalTableBlockProvider {...props} params={params} />
|
<BlockProvider {...props} params={params}>
|
||||||
</BlockProvider>
|
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
|
||||||
</FormContext.Provider>
|
</BlockProvider>
|
||||||
|
</FormContext.Provider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,6 +147,7 @@ export const useTableBlockProps = () => {
|
|||||||
}
|
}
|
||||||
}, [ctx?.service?.loading]);
|
}, [ctx?.service?.loading]);
|
||||||
return {
|
return {
|
||||||
|
childrenColumnName: ctx.childrenColumnName,
|
||||||
loading: ctx?.service?.loading,
|
loading: ctx?.service?.loading,
|
||||||
showIndex: ctx.showIndex,
|
showIndex: ctx.showIndex,
|
||||||
dragSort: ctx.dragSort,
|
dragSort: ctx.dragSort,
|
||||||
@ -132,6 +160,7 @@ export const useTableBlockProps = () => {
|
|||||||
}
|
}
|
||||||
: false,
|
: false,
|
||||||
onRowSelectionChange(selectedRowKeys) {
|
onRowSelectionChange(selectedRowKeys) {
|
||||||
|
console.log(selectedRowKeys);
|
||||||
ctx.field.data = ctx?.field?.data || {};
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { ArrayField } from '@formily/core';
|
import { ArrayField } from '@formily/core';
|
||||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import React, { createContext, useContext, useEffect } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import { useCollectionManager } from '../collection-manager';
|
import { useCollectionManager } from '../collection-manager';
|
||||||
import { RecordProvider, useRecord } from '../record-provider';
|
import { RecordProvider, useRecord } from '../record-provider';
|
||||||
|
import { SchemaComponentOptions } from '../schema-component';
|
||||||
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
||||||
import { useFormBlockContext } from './FormBlockProvider';
|
|
||||||
|
|
||||||
export const TableSelectorContext = createContext<any>({});
|
export const TableSelectorContext = createContext<any>({});
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ const InternalTableSelectorProvider = (props) => {
|
|||||||
const { params, rowKey, extraFilter } = props;
|
const { params, rowKey, extraFilter } = props;
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { resource, service } = useBlockRequestContext();
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
const [expandFlag, setExpandFlag] = useState(false);
|
||||||
// if (service.loading) {
|
// if (service.loading) {
|
||||||
// return <Spin />;
|
// return <Spin />;
|
||||||
// }
|
// }
|
||||||
@ -26,6 +27,10 @@ const InternalTableSelectorProvider = (props) => {
|
|||||||
params,
|
params,
|
||||||
extraFilter,
|
extraFilter,
|
||||||
rowKey,
|
rowKey,
|
||||||
|
expandFlag,
|
||||||
|
setExpandFlag: () => {
|
||||||
|
setExpandFlag(!expandFlag);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RenderChildrenWithAssociationFilter {...props} />
|
<RenderChildrenWithAssociationFilter {...props} />
|
||||||
@ -99,19 +104,29 @@ const useAssociationNames = (collection) => {
|
|||||||
|
|
||||||
export const TableSelectorProvider = (props) => {
|
export const TableSelectorProvider = (props) => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const ctx = useFormBlockContext();
|
|
||||||
const { getCollectionJoinField, getCollectionFields } = useCollectionManager();
|
const { getCollectionJoinField, getCollectionFields } = useCollectionManager();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { getCollection } = useCollectionManager();
|
||||||
|
const collection = getCollection(props.collection);
|
||||||
|
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
||||||
const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField');
|
const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField');
|
||||||
// const value = ctx.form.query(collectionFieldSchema?.name).value();
|
|
||||||
const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']);
|
const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']);
|
||||||
|
|
||||||
const params = { ...props.params };
|
const params = { ...props.params };
|
||||||
const appends = useAssociationNames(props.collection);
|
const appends = useAssociationNames(props.collection);
|
||||||
if (props.dragSort) {
|
if (props.dragSort) {
|
||||||
params['sort'] = ['sort'];
|
params['sort'] = ['sort'];
|
||||||
}
|
}
|
||||||
|
if (collection.tree && treeTable !== false) {
|
||||||
|
params['tree'] = true;
|
||||||
|
if (collectionFieldSchema.name === 'parent') {
|
||||||
|
params.filter = {
|
||||||
|
...(params.filter ?? {}),
|
||||||
|
id: record.id && {
|
||||||
|
$ne: record.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!Object.keys(params).includes('appends')) {
|
if (!Object.keys(params).includes('appends')) {
|
||||||
params['appends'] = appends;
|
params['appends'] = appends;
|
||||||
}
|
}
|
||||||
@ -181,9 +196,11 @@ export const TableSelectorProvider = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BlockProvider {...props} params={params}>
|
<SchemaComponentOptions scope={{ treeTable }}>
|
||||||
<InternalTableSelectorProvider {...props} params={params} extraFilter={extraFilter} />
|
<BlockProvider {...props} params={params}>
|
||||||
</BlockProvider>
|
<InternalTableSelectorProvider {...props} params={params} extraFilter={extraFilter} />
|
||||||
|
</BlockProvider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -128,12 +128,12 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource)
|
|||||||
export const useCreateActionProps = () => {
|
export const useCreateActionProps = () => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { field, resource, __parent } = useBlockRequestContext();
|
const { field, resource, __parent } = useBlockRequestContext();
|
||||||
const { visible, setVisible } = useActionContext();
|
const { visible, setVisible, fieldSchema } = useActionContext();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
const { fields, getField } = useCollection();
|
const { fields, getField, getTreeParentField } = useCollection();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const filterByTk = useFilterByTk();
|
const filterByTk = useFilterByTk();
|
||||||
const currentRecord = useRecord();
|
const currentRecord = useRecord();
|
||||||
@ -148,11 +148,17 @@ export const useCreateActionProps = () => {
|
|||||||
overwriteValues,
|
overwriteValues,
|
||||||
skipValidator,
|
skipValidator,
|
||||||
} = actionSchema?.['x-action-settings'] ?? {};
|
} = actionSchema?.['x-action-settings'] ?? {};
|
||||||
|
const { addChild } = fieldSchema?.['x-component-props'];
|
||||||
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser });
|
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser });
|
||||||
if (!skipValidator) {
|
if (!skipValidator) {
|
||||||
await form.submit();
|
await form.submit();
|
||||||
}
|
}
|
||||||
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
|
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
|
||||||
|
if (addChild) {
|
||||||
|
const treeParentField = getTreeParentField();
|
||||||
|
values[treeParentField?.name ?? 'parent'] = currentRecord;
|
||||||
|
values[treeParentField?.foreignKey ?? 'parentId'] = currentRecord.id;
|
||||||
|
}
|
||||||
actionField.data = field.data || {};
|
actionField.data = field.data || {};
|
||||||
actionField.data.loading = true;
|
actionField.data.loading = true;
|
||||||
try {
|
try {
|
||||||
|
@ -84,7 +84,7 @@ const getSchema = (schema, category, compile): ISchema => {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
useAction: () => useCreateCollection(),
|
useAction: () => useCreateCollection(schema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -188,16 +188,19 @@ const useDefaultCollectionFields = (values) => {
|
|||||||
return defaults;
|
return defaults;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCreateCollection = () => {
|
const useCreateCollection = (schema?: any) => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { refreshCM } = useCollectionManager();
|
const { refreshCM } = useCollectionManager();
|
||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
const { refresh } = useResourceActionContext();
|
const { refresh } = useResourceActionContext();
|
||||||
const { resource } = useResourceContext();
|
const { resource, collection } = useResourceContext();
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
await form.submit();
|
await form.submit();
|
||||||
const values = cloneDeep(form.values);
|
const values = cloneDeep(form.values);
|
||||||
|
if (schema?.events?.beforeSubmit) {
|
||||||
|
schema.events.beforeSubmit(values);
|
||||||
|
}
|
||||||
const fields = useDefaultCollectionFields(values);
|
const fields = useDefaultCollectionFields(values);
|
||||||
if (values.autoCreateReverseField) {
|
if (values.autoCreateReverseField) {
|
||||||
} else {
|
} else {
|
||||||
|
@ -28,9 +28,13 @@ export const useCollection = () => {
|
|||||||
...collection,
|
...collection,
|
||||||
resource,
|
resource,
|
||||||
getField(name: SchemaKey): CollectionFieldOptions {
|
getField(name: SchemaKey): CollectionFieldOptions {
|
||||||
const fields = totalFields;
|
const fields = totalFields as any[];
|
||||||
return fields?.find((field) => field.name === name);
|
return fields?.find((field) => field.name === name);
|
||||||
},
|
},
|
||||||
|
getTreeParentField() {
|
||||||
|
const fields = totalFields;
|
||||||
|
return fields?.find((field) => field.treeParent);
|
||||||
|
},
|
||||||
fields: totalFields,
|
fields: totalFields,
|
||||||
getPrimaryKey: () => {
|
getPrimaryKey: () => {
|
||||||
if (collection.targetKey || collection.filterTargetKey) {
|
if (collection.targetKey || collection.filterTargetKey) {
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
recordPickerSelector,
|
recordPickerSelector,
|
||||||
recordPickerViewer,
|
recordPickerViewer,
|
||||||
relationshipType,
|
relationshipType,
|
||||||
reverseFieldProperties,
|
reverseFieldProperties
|
||||||
} from './properties';
|
} from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './calendar';
|
export * from './calendar';
|
||||||
export * from './general';
|
export * from './general';
|
||||||
|
export * from './tree';
|
||||||
|
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import { getConfigurableProperties } from './properties';
|
||||||
|
import { ICollectionTemplate } from './types';
|
||||||
|
|
||||||
|
export const tree: ICollectionTemplate = {
|
||||||
|
name: 'tree',
|
||||||
|
title: '{{t("Tree collection")}}',
|
||||||
|
order: 3,
|
||||||
|
color: 'blue',
|
||||||
|
default: {
|
||||||
|
tree: 'adjacencyList',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
interface: 'integer',
|
||||||
|
name: 'parentId',
|
||||||
|
type: 'bigInt',
|
||||||
|
isForeignKey: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Parent ID")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'm2o',
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
treeParent: true,
|
||||||
|
uiSchema: {
|
||||||
|
title: '{{t("Parent")}}',
|
||||||
|
'x-component': 'RecordPicker',
|
||||||
|
'x-component-props': {
|
||||||
|
// mode: 'tags',
|
||||||
|
multiple: false,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'id',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'o2m',
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
treeChildren: true,
|
||||||
|
uiSchema: {
|
||||||
|
title: '{{t("Children")}}',
|
||||||
|
'x-component': 'RecordPicker',
|
||||||
|
'x-component-props': {
|
||||||
|
// mode: 'tags',
|
||||||
|
multiple: true,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'id',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
beforeSubmit(values) {
|
||||||
|
if (Array.isArray(values?.fields)) {
|
||||||
|
values?.fields.map((f) => {
|
||||||
|
f.target = values.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'moreOptions'),
|
||||||
|
};
|
@ -9,6 +9,7 @@ export interface ICollectionTemplate {
|
|||||||
order?: number;
|
order?: number;
|
||||||
/** 默认配置 */
|
/** 默认配置 */
|
||||||
default?: CollectionOptions;
|
default?: CollectionOptions;
|
||||||
|
events?: any;
|
||||||
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
|
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
|
||||||
configurableProperties?: Record<string, ISchema>;
|
configurableProperties?: Record<string, ISchema>;
|
||||||
/** 当前模板可用的字段类型 */
|
/** 当前模板可用的字段类型 */
|
||||||
@ -48,4 +49,5 @@ interface CollectionOptions {
|
|||||||
inherits?: string | string[];
|
inherits?: string | string[];
|
||||||
/* 字段列表 */
|
/* 字段列表 */
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export interface CollectionOptions {
|
|||||||
sortable?: any;
|
sortable?: any;
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
inherits?: string[];
|
inherits?: string[];
|
||||||
|
tree?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICollectionProviderProps {
|
export interface ICollectionProviderProps {
|
||||||
|
@ -108,6 +108,11 @@ export default {
|
|||||||
"Add block": "Add block",
|
"Add block": "Add block",
|
||||||
"Add new": "Add new",
|
"Add new": "Add new",
|
||||||
"Add record": "Add record",
|
"Add record": "Add record",
|
||||||
|
'Add child':'Add child',
|
||||||
|
'Collapse all':'Collapse all',
|
||||||
|
'Expand all':'Expand all',
|
||||||
|
'Expand/Collapse':'Expand/Collapse',
|
||||||
|
"Tree table":"Tree table",
|
||||||
"Custom field display name": "Custom field display name",
|
"Custom field display name": "Custom field display name",
|
||||||
"Display fields": "Display collection fields",
|
"Display fields": "Display collection fields",
|
||||||
"Edit record": "Edit record",
|
"Edit record": "Edit record",
|
||||||
|
@ -42,6 +42,11 @@ export default {
|
|||||||
"Category name":"分類名",
|
"Category name":"分類名",
|
||||||
"Delete category":"分類の削除",
|
"Delete category":"分類の削除",
|
||||||
"Collection category":"Collection category",
|
"Collection category":"Collection category",
|
||||||
|
'Add child':'サブレコードの追加',
|
||||||
|
'Collapse all':'すべて閉じる',
|
||||||
|
'Expand all':'すべて展開',
|
||||||
|
'Expand/Collapse':'展開と終了',
|
||||||
|
"Tree table":"ツリーテーブル",
|
||||||
"Visible":"表示",
|
"Visible":"表示",
|
||||||
"Read only":"読み取り専用(編集不可)",
|
"Read only":"読み取り専用(編集不可)",
|
||||||
"Easy reading":"読み取り専用(読取りモード)",
|
"Easy reading":"読み取り専用(読取りモード)",
|
||||||
|
@ -58,6 +58,15 @@ export default {
|
|||||||
"Sort":"排序",
|
"Sort":"排序",
|
||||||
"Categories":"数据表类别",
|
"Categories":"数据表类别",
|
||||||
"Category name":"分类名称",
|
"Category name":"分类名称",
|
||||||
|
'Add child':'添加子记录',
|
||||||
|
'Collapse all':'全部收起',
|
||||||
|
'Expand all':'全部展开',
|
||||||
|
'Expand/Collapse':'展开/折叠',
|
||||||
|
'Tree collection': '树结构表',
|
||||||
|
"Tree table":"树表格",
|
||||||
|
'Parent ID': '父记录ID',
|
||||||
|
'Parent': '父记录',
|
||||||
|
'Children': '子记录',
|
||||||
"Roles & Permissions": "角色和权限",
|
"Roles & Permissions": "角色和权限",
|
||||||
"Edit profile": "个人资料",
|
"Edit profile": "个人资料",
|
||||||
"Change password": "修改密码",
|
"Change password": "修改密码",
|
||||||
|
@ -50,6 +50,7 @@ export const ActionDesigner = (props) => {
|
|||||||
const actionType = fieldSchema['x-action'] ?? '';
|
const actionType = fieldSchema['x-action'] ?? '';
|
||||||
const isLinkageAction = Object.keys(useFormBlockContext()).length > 0 && Object.keys(useRecord()).length > 0;
|
const isLinkageAction = Object.keys(useFormBlockContext()).length > 0 && Object.keys(useRecord()).length > 0;
|
||||||
const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create';
|
const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create';
|
||||||
|
const isSupportEditButton = fieldSchema['x-action'] !== 'expandAll';
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const schemaUid = uid();
|
const schemaUid = uid();
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
@ -85,6 +86,7 @@ export const ActionDesigner = (props) => {
|
|||||||
title: t('Button title'),
|
title: t('Button title'),
|
||||||
default: fieldSchema.title,
|
default: fieldSchema.title,
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
|
'x-visible': isSupportEditButton,
|
||||||
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
@ -93,6 +95,7 @@ export const ActionDesigner = (props) => {
|
|||||||
title: t('Button icon'),
|
title: t('Button icon'),
|
||||||
default: fieldSchema?.['x-component-props']?.icon,
|
default: fieldSchema?.['x-component-props']?.icon,
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
|
'x-visible': isSupportEditButton,
|
||||||
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
@ -114,27 +117,25 @@ export const ActionDesigner = (props) => {
|
|||||||
} as ISchema
|
} as ISchema
|
||||||
}
|
}
|
||||||
onSubmit={({ title, icon, type }) => {
|
onSubmit={({ title, icon, type }) => {
|
||||||
if (title) {
|
fieldSchema.title = title;
|
||||||
fieldSchema.title = title;
|
field.title = title;
|
||||||
field.title = title;
|
field.componentProps.icon = icon;
|
||||||
field.componentProps.icon = icon;
|
field.componentProps.danger = type === 'danger';
|
||||||
field.componentProps.danger = type === 'danger';
|
field.componentProps.type = type;
|
||||||
field.componentProps.type = type;
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
fieldSchema['x-component-props'].icon = icon;
|
||||||
fieldSchema['x-component-props'].icon = icon;
|
fieldSchema['x-component-props'].danger = type === 'danger';
|
||||||
fieldSchema['x-component-props'].danger = type === 'danger';
|
fieldSchema['x-component-props'].type = type;
|
||||||
fieldSchema['x-component-props'].type = type;
|
dn.emit('patch', {
|
||||||
dn.emit('patch', {
|
schema: {
|
||||||
schema: {
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
['x-uid']: fieldSchema['x-uid'],
|
title,
|
||||||
title,
|
'x-component-props': {
|
||||||
'x-component-props': {
|
...fieldSchema['x-component-props'],
|
||||||
...fieldSchema['x-component-props'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
dn.refresh();
|
});
|
||||||
}
|
dn.refresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />}
|
{isLinkageAction && <SchemaSettings.LinkageRules collectionName={name} />}
|
||||||
|
@ -12,6 +12,17 @@ import { useFieldNames } from './useFieldNames';
|
|||||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||||
|
|
||||||
const RecordPickerContext = createContext(null);
|
const RecordPickerContext = createContext(null);
|
||||||
|
function flatData(data) {
|
||||||
|
let newArr = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const children = data[i]['children'];
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
newArr.push(...flatData(children));
|
||||||
|
}
|
||||||
|
newArr.push({ ...data[i] });
|
||||||
|
}
|
||||||
|
return newArr;
|
||||||
|
}
|
||||||
|
|
||||||
const useTableSelectorProps = () => {
|
const useTableSelectorProps = () => {
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
@ -28,11 +39,12 @@ const useTableSelectorProps = () => {
|
|||||||
},
|
},
|
||||||
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const scopeRows = field.value || [];
|
const scopeRows = flatData(field.value) || [];
|
||||||
const allSelectedRows = rcSelectRows || [];
|
const allSelectedRows = rcSelectRows || [];
|
||||||
const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey || 'id');
|
const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey || 'id');
|
||||||
const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey || 'id');
|
const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey || 'id');
|
||||||
const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey || 'id']);
|
const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey || 'id']);
|
||||||
|
console.log(unionSelectedRows, unionSelectedRowKeys);
|
||||||
setSelectedRows?.(unionSelectedRows);
|
setSelectedRows?.(unionSelectedRows);
|
||||||
onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows);
|
onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows);
|
||||||
} else {
|
} else {
|
||||||
@ -92,7 +104,7 @@ export const InputRecordPicker: React.FC<any> = (props) => {
|
|||||||
setOptions(opts);
|
setOptions(opts);
|
||||||
setSelectedRows(opts);
|
setSelectedRows(opts);
|
||||||
}
|
}
|
||||||
}, [value,fieldNames?.label]);
|
}, [value, fieldNames?.label]);
|
||||||
|
|
||||||
const getValue = () => {
|
const getValue = () => {
|
||||||
if (multiple == null) return null;
|
if (multiple == null) return null;
|
||||||
|
@ -10,9 +10,15 @@ import { default as classNames, default as cls } from 'classnames';
|
|||||||
import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DndContext, useDesignable } from '../..';
|
import { DndContext, useDesignable } from '../..';
|
||||||
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
|
import {
|
||||||
|
RecordIndexProvider,
|
||||||
|
RecordProvider,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useTableBlockContext,
|
||||||
|
useTableSelectorContext
|
||||||
|
} from '../../../';
|
||||||
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
|
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
|
||||||
import { isCollectionFieldComponent, isColumnComponent } from './utils';
|
import { extractIndex, getIdsWithChildren, isCollectionFieldComponent, isColumnComponent } from './utils';
|
||||||
|
|
||||||
const useTableColumns = () => {
|
const useTableColumns = () => {
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
@ -106,7 +112,7 @@ const TableIndex = (props) => {
|
|||||||
const { index } = props;
|
const { index } = props;
|
||||||
return (
|
return (
|
||||||
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }}>
|
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }}>
|
||||||
{index + 1}
|
{index}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -162,10 +168,16 @@ export const Table: any = observer((props: any) => {
|
|||||||
required,
|
required,
|
||||||
...others
|
...others
|
||||||
} = { ...others1, ...others2 } as any;
|
} = { ...others1, ...others2 } as any;
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
|
||||||
|
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
|
||||||
|
const { expandFlag } = ctx;
|
||||||
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
|
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
|
||||||
const paginationProps = usePaginationProps(pagination1, pagination2);
|
const paginationProps = usePaginationProps(pagination1, pagination2);
|
||||||
const requiredValidator = field.required || required;
|
const requiredValidator = field.required || required;
|
||||||
|
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
|
||||||
|
const [expandedKeys, setExpandesKeys] = useState([]);
|
||||||
|
const [allIncludesChildren, setAllIncludesChildren] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
field.setValidator((value) => {
|
field.setValidator((value) => {
|
||||||
if (requiredValidator) {
|
if (requiredValidator) {
|
||||||
@ -174,6 +186,26 @@ export const Table: any = observer((props: any) => {
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}, [requiredValidator]);
|
}, [requiredValidator]);
|
||||||
|
// useEffect(() => {
|
||||||
|
// const data = field.value;
|
||||||
|
// field.value = null;
|
||||||
|
// field.value = data;
|
||||||
|
// }, [treeTable]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (treeTable !== false) {
|
||||||
|
const keys = getIdsWithChildren(field.value?.slice());
|
||||||
|
setAllIncludesChildren(keys);
|
||||||
|
}
|
||||||
|
}, [field.value]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (expandFlag) {
|
||||||
|
setExpandesKeys(allIncludesChildren);
|
||||||
|
} else {
|
||||||
|
setExpandesKeys([]);
|
||||||
|
}
|
||||||
|
}, [expandFlag]);
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
@ -275,7 +307,10 @@ export const Table: any = observer((props: any) => {
|
|||||||
const current = props?.pagination?.current;
|
const current = props?.pagination?.current;
|
||||||
const pageSize = props?.pagination?.pageSize || 20;
|
const pageSize = props?.pagination?.pageSize || 20;
|
||||||
if (current) {
|
if (current) {
|
||||||
index = index + (current - 1) * pageSize;
|
index = index + (current - 1) * pageSize + 1;
|
||||||
|
}
|
||||||
|
if (record.__index) {
|
||||||
|
index = extractIndex(record.__index);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -284,6 +319,7 @@ export const Table: any = observer((props: any) => {
|
|||||||
css`
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
float: left;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
@ -419,6 +455,13 @@ export const Table: any = observer((props: any) => {
|
|||||||
tableLayout={'auto'}
|
tableLayout={'auto'}
|
||||||
scroll={scroll}
|
scroll={scroll}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
expandable={{
|
||||||
|
onExpand: (flag, record) => {
|
||||||
|
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
|
||||||
|
setExpandesKeys(newKeys);
|
||||||
|
},
|
||||||
|
expandedRowKeys: expandedKeys,
|
||||||
|
}}
|
||||||
dataSource={field?.value?.slice?.()}
|
dataSource={field?.value?.slice?.()}
|
||||||
/>
|
/>
|
||||||
</SortableWrapper>
|
</SortableWrapper>
|
||||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTableBlockContext } from '../../../block-provider';
|
import { useTableBlockContext } from '../../../block-provider';
|
||||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||||
import { useCollection } from '../../../collection-manager';
|
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
import { useSchemaTemplate } from '../../../schema-templates';
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
@ -13,6 +13,7 @@ import { useFixedBlockDesignerSetting } from '../page';
|
|||||||
|
|
||||||
export const TableBlockDesigner = () => {
|
export const TableBlockDesigner = () => {
|
||||||
const { name, title, sortable } = useCollection();
|
const { name, title, sortable } = useCollection();
|
||||||
|
const { getCollectionField } = useCollectionManager();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const dataSource = useCollectionFilterOptions(name);
|
const dataSource = useCollectionFilterOptions(name);
|
||||||
@ -36,12 +37,33 @@ export const TableBlockDesigner = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const template = useSchemaTemplate();
|
const template = useSchemaTemplate();
|
||||||
const { dragSort } = field.decoratorProps;
|
const collection = useCollection();
|
||||||
|
const { dragSort, resource } = field.decoratorProps;
|
||||||
|
const treeChildren = resource.includes('.') ? getCollectionField(resource)?.treeChildren : !!collection?.tree;
|
||||||
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
|
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
<SchemaSettings.BlockTitleItem />
|
<SchemaSettings.BlockTitleItem />
|
||||||
|
{collection?.tree && (
|
||||||
|
<SchemaSettings.SwitchItem
|
||||||
|
title={t('Tree table')}
|
||||||
|
defaultChecked={true}
|
||||||
|
checked={treeChildren ? field.decoratorProps.treeTable !== false : false}
|
||||||
|
onChange={(flag) => {
|
||||||
|
field.decoratorProps.treeTable = flag;
|
||||||
|
fieldSchema['x-decorator-props'].treeTable = flag;
|
||||||
|
const params = {
|
||||||
|
...service.params?.[0],
|
||||||
|
tree: flag ? true : null,
|
||||||
|
};
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: fieldSchema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
service.run(params);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{sortable && (
|
{sortable && (
|
||||||
<SchemaSettings.SwitchItem
|
<SchemaSettings.SwitchItem
|
||||||
title={t('Enable drag and drop sorting')}
|
title={t('Enable drag and drop sorting')}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ArrayItems } from '@formily/antd';
|
import { ArrayItems } from '@formily/antd';
|
||||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTableSelectorContext } from '../../../block-provider';
|
import { useTableSelectorContext } from '../../../block-provider';
|
||||||
@ -8,7 +9,6 @@ import { useCollectionFilterOptions, useSortFields } from '../../../collection-m
|
|||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
import { useSchemaTemplate } from '../../../schema-templates';
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useDesignable } from '../../hooks';
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
|
|
||||||
export const TableSelectorDesigner = () => {
|
export const TableSelectorDesigner = () => {
|
||||||
const { name, title } = useCollection();
|
const { name, title } = useCollection();
|
||||||
@ -33,6 +33,7 @@ export const TableSelectorDesigner = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const template = useSchemaTemplate();
|
const template = useSchemaTemplate();
|
||||||
|
const collection = useCollection();
|
||||||
const { dragSort } = field.decoratorProps;
|
const { dragSort } = field.decoratorProps;
|
||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner template={template} title={title || name} disableInitializer>
|
<GeneralSchemaDesigner template={template} title={title || name} disableInitializer>
|
||||||
@ -77,6 +78,27 @@ export const TableSelectorDesigner = () => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{collection?.tree && (
|
||||||
|
<SchemaSettings.SwitchItem
|
||||||
|
title={t('Tree table')}
|
||||||
|
defaultChecked={true}
|
||||||
|
checked={field.decoratorProps.treeTable !== false}
|
||||||
|
onChange={(flag) => {
|
||||||
|
field.form.clearFormGraph(`${field.address}.*`);
|
||||||
|
field.decoratorProps.treeTable = flag;
|
||||||
|
fieldSchema['x-decorator-props'].treeTable = flag;
|
||||||
|
const params = {
|
||||||
|
...service.params?.[0],
|
||||||
|
tree: flag ? true : null,
|
||||||
|
};
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: fieldSchema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
service.run(params);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!dragSort && (
|
{!dragSort && (
|
||||||
<SchemaSettings.ModalItem
|
<SchemaSettings.ModalItem
|
||||||
title={t('Set default sorting rules')}
|
title={t('Set default sorting rules')}
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { connect, ISchema, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import { Table, TableColumnType } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ArrayTable = observer((props: any) => {
|
||||||
|
const { rowKey } = props;
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const columnSchemas = schema.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === 'ArrayTable.Column') {
|
||||||
|
buf.push(s);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const columns = columnSchemas.map((s) => {
|
||||||
|
return {
|
||||||
|
render: (value, record) => {
|
||||||
|
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
|
||||||
|
},
|
||||||
|
} as TableColumnType<any>;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Table {...props} rowKey={rowKey} columns={columns} dataSource={field.value} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const Value = connect((props) => {
|
||||||
|
return <li>value: {props.value}</li>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
objArr: {
|
||||||
|
type: 'array',
|
||||||
|
default: [
|
||||||
|
{ __path: '0', id: 1, value: 't1' },
|
||||||
|
{
|
||||||
|
__path: '1',
|
||||||
|
id: 2,
|
||||||
|
value: 't2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
__path: '1.children.0',
|
||||||
|
id: 5,
|
||||||
|
value: 't5',
|
||||||
|
parentId: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__path: '2',
|
||||||
|
id: 3,
|
||||||
|
value: 't3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
__path: '2.children.0',
|
||||||
|
id: 4,
|
||||||
|
value: 't4',
|
||||||
|
parentId: 3,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
__path: '2.children.0.children.0',
|
||||||
|
id: 6,
|
||||||
|
value: 't6',
|
||||||
|
parentId: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__path: '2.children.0.children.1',
|
||||||
|
id: 7,
|
||||||
|
value: 't7',
|
||||||
|
parentId: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'x-component': 'ArrayTable',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
c1: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ArrayTable.Column',
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ ArrayTable, Value }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -9,8 +9,4 @@ group:
|
|||||||
|
|
||||||
## TableBlock
|
## TableBlock
|
||||||
|
|
||||||
<code src="./demos/demo1.tsx" />
|
<code src="./demos/demo3.tsx" />
|
||||||
|
|
||||||
## TableField
|
|
||||||
|
|
||||||
<code src="./demos/demo2.tsx" />
|
|
||||||
|
@ -7,3 +7,26 @@ export const isCollectionFieldComponent = (schema: ISchema) => {
|
|||||||
export const isColumnComponent = (schema: Schema) => {
|
export const isColumnComponent = (schema: Schema) => {
|
||||||
return schema['x-component']?.endsWith('.Column') > -1;
|
return schema['x-component']?.endsWith('.Column') > -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function extractIndex(str) {
|
||||||
|
const numbers = [];
|
||||||
|
str?.split('.').forEach(function (element) {
|
||||||
|
if (!isNaN(element)) {
|
||||||
|
numbers.push(String(Number(element) + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return numbers.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIdsWithChildren(nodes) {
|
||||||
|
const ids = [];
|
||||||
|
if (nodes) {
|
||||||
|
for (let node of nodes) {
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
ids.push(node.id);
|
||||||
|
ids.push(...getIdsWithChildren(node.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
SchemaInitializerButtonProps,
|
SchemaInitializerButtonProps,
|
||||||
SchemaInitializerItemComponent,
|
SchemaInitializerItemComponent,
|
||||||
SchemaInitializerItemOptions,
|
SchemaInitializerItemOptions,
|
||||||
SchemaInitializerItemProps,
|
SchemaInitializerItemProps
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const defaultWrap = (s: ISchema) => s;
|
const defaultWrap = (s: ISchema) => s;
|
||||||
@ -47,59 +47,63 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const renderItems = (items: any) => {
|
const renderItems = (items: any) => {
|
||||||
return items?.map((item, indexA) => {
|
return items
|
||||||
if (item.type === 'divider') {
|
.filter((v) => {
|
||||||
return <Menu.Divider key={item.key || `item-${indexA}`} />;
|
return v && (v?.visible ? v.visible() : true);
|
||||||
}
|
})
|
||||||
if (item.type === 'item' && item.component) {
|
?.map((item, indexA) => {
|
||||||
const Component = findComponent(item.component);
|
if (item.type === 'divider') {
|
||||||
item.key = `${item.key || item.title}-${indexA}`;
|
return <Menu.Divider key={item.key || `item-${indexA}`} />;
|
||||||
return (
|
}
|
||||||
Component && (
|
if (item.type === 'item' && item.component) {
|
||||||
<SchemaInitializerItemContext.Provider
|
const Component = findComponent(item.component);
|
||||||
key={item.key}
|
item.key = `${item.key || item.title}-${indexA}`;
|
||||||
value={{
|
return (
|
||||||
index: indexA,
|
Component && (
|
||||||
item,
|
<SchemaInitializerItemContext.Provider
|
||||||
info: item,
|
key={item.key}
|
||||||
insert: insertSchema,
|
value={{
|
||||||
}}
|
index: indexA,
|
||||||
>
|
item,
|
||||||
<Component
|
info: item,
|
||||||
{...item}
|
insert: insertSchema,
|
||||||
item={{
|
|
||||||
...item,
|
|
||||||
title: compile(item.title),
|
|
||||||
}}
|
}}
|
||||||
insert={insertSchema}
|
>
|
||||||
/>
|
<Component
|
||||||
</SchemaInitializerItemContext.Provider>
|
{...item}
|
||||||
)
|
item={{
|
||||||
);
|
...item,
|
||||||
}
|
title: compile(item.title),
|
||||||
if (item.type === 'itemGroup') {
|
}}
|
||||||
return (
|
insert={insertSchema}
|
||||||
!!item.children?.length && (
|
/>
|
||||||
<Menu.ItemGroup key={item.key || `item-group-${indexA}`} title={compile(item.title)}>
|
</SchemaInitializerItemContext.Provider>
|
||||||
{renderItems(item.children)}
|
)
|
||||||
</Menu.ItemGroup>
|
);
|
||||||
)
|
}
|
||||||
);
|
if (item.type === 'itemGroup') {
|
||||||
}
|
return (
|
||||||
if (item.type === 'subMenu') {
|
!!item.children?.length && (
|
||||||
return (
|
<Menu.ItemGroup key={item.key || `item-group-${indexA}`} title={compile(item.title)}>
|
||||||
!!item.children?.length && (
|
{renderItems(item.children)}
|
||||||
<Menu.SubMenu
|
</Menu.ItemGroup>
|
||||||
key={item.key || `item-group-${indexA}`}
|
)
|
||||||
title={compile(item.title)}
|
);
|
||||||
popupClassName={menuItemGroupCss}
|
}
|
||||||
>
|
if (item.type === 'subMenu') {
|
||||||
{renderItems(item.children)}
|
return (
|
||||||
</Menu.SubMenu>
|
!!item.children?.length && (
|
||||||
)
|
<Menu.SubMenu
|
||||||
);
|
key={item.key || `item-group-${indexA}`}
|
||||||
}
|
title={compile(item.title)}
|
||||||
});
|
popupClassName={menuItemGroupCss}
|
||||||
|
>
|
||||||
|
{renderItems(item.children)}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
|
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
|
||||||
if (!designable && props.designable !== true) {
|
if (!designable && props.designable !== true) {
|
||||||
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializer, SchemaSettings } from '../..';
|
import { SchemaInitializer, SchemaSettings } from '../..';
|
||||||
import { useAPIClient } from '../../api-client';
|
import { useAPIClient } from '../../api-client';
|
||||||
|
import { useCollection } from '../../collection-manager';
|
||||||
import { createDesignable, useDesignable } from '../../schema-component';
|
import { createDesignable, useDesignable } from '../../schema-component';
|
||||||
|
|
||||||
export const Resizable = (props) => {
|
export const Resizable = (props) => {
|
||||||
@ -49,6 +50,8 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { refresh } = useDesignable();
|
const { refresh } = useDesignable();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const collection = useCollection();
|
||||||
|
const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props']||{};
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Button
|
<SchemaInitializer.Button
|
||||||
insertPosition={'beforeEnd'}
|
insertPosition={'beforeEnd'}
|
||||||
@ -106,6 +109,17 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
collection.tree &&
|
||||||
|
treeTable !== false && {
|
||||||
|
type: 'item',
|
||||||
|
title: t('Add child'),
|
||||||
|
component: 'CreateChildInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-action': 'create',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Schema } from '@formily/react';
|
import { Schema, useFieldSchema } from '@formily/react';
|
||||||
|
import { useCollection } from '../../';
|
||||||
|
|
||||||
// 表格操作配置
|
// 表格操作配置
|
||||||
export const TableActionInitializers = {
|
export const TableActionInitializers = {
|
||||||
@ -49,6 +50,20 @@ export const TableActionInitializers = {
|
|||||||
'x-align': 'right',
|
'x-align': 'right',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: "{{t('Expand/Collapse')}}",
|
||||||
|
component: 'ExpandActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
visible: () => {
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const collection = useCollection();
|
||||||
|
const { treeTable } = schema?.parent?.['x-decorator-props']||{};
|
||||||
|
return collection.tree && treeTable !== false;
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ActionInitializer } from './ActionInitializer';
|
||||||
|
export const CreateChildInitializer = (props) => {
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Add child") }}',
|
||||||
|
'x-action': 'create',
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-visible': '{{treeTable}}',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'PlusOutlined',
|
||||||
|
openMode: 'drawer',
|
||||||
|
type: 'primary',
|
||||||
|
addChild: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Add record") }}',
|
||||||
|
'x-component': 'Action.Container',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-initializer': 'TabPaneInitializersForCreateFormBlock',
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Add new")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
'x-designer': 'Tabs.Designer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'CreateFormBlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return <ActionInitializer {...props} schema={schema} />;
|
||||||
|
};
|
@ -1,49 +1,49 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { FormOutlined } from '@ant-design/icons';
|
import { FormOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import { useBlockAssociationContext } from "../../block-provider";
|
import { useBlockAssociationContext } from '../../block-provider';
|
||||||
import { useCollection } from "../../collection-manager";
|
import { useCollection } from '../../collection-manager';
|
||||||
import { useSchemaTemplateManager } from "../../schema-templates";
|
import { useSchemaTemplateManager } from '../../schema-templates';
|
||||||
import { SchemaInitializer } from "../SchemaInitializer";
|
import { SchemaInitializer } from '../SchemaInitializer';
|
||||||
import { createFormBlockSchema, useRecordCollectionDataSourceItems } from "../utils";
|
import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
|
||||||
|
|
||||||
export const CreateFormBlockInitializer = (props) => {
|
export const CreateFormBlockInitializer = (props) => {
|
||||||
const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
|
const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
|
||||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
const association = useBlockAssociationContext();
|
const association = useBlockAssociationContext();
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Item
|
<SchemaInitializer.Item
|
||||||
icon={<FormOutlined />}
|
icon={<FormOutlined />}
|
||||||
{...others}
|
{...others}
|
||||||
onClick={async ({ item }) => {
|
onClick={async ({ item }) => {
|
||||||
if (item.template) {
|
if (item.template) {
|
||||||
const s = await getTemplateSchemaByMode(item);
|
const s = await getTemplateSchemaByMode(item);
|
||||||
if (item.template.componentName === 'FormItem') {
|
if (item.template.componentName === 'FormItem') {
|
||||||
const blockSchema = createFormBlockSchema({
|
const blockSchema = createFormBlockSchema({
|
||||||
actionInitializers: 'CreateFormActionInitializers',
|
actionInitializers: 'CreateFormActionInitializers',
|
||||||
association,
|
association,
|
||||||
collection: collection.name,
|
collection: collection.name,
|
||||||
template: s,
|
template: s,
|
||||||
});
|
});
|
||||||
if (item.mode === 'reference') {
|
if (item.mode === 'reference') {
|
||||||
blockSchema['x-template-key'] = item.template.key;
|
blockSchema['x-template-key'] = item.template.key;
|
||||||
}
|
|
||||||
insert(blockSchema);
|
|
||||||
} else {
|
|
||||||
insert(s);
|
|
||||||
}
|
}
|
||||||
|
insert(blockSchema);
|
||||||
} else {
|
} else {
|
||||||
insert(
|
insert(s);
|
||||||
createFormBlockSchema({
|
|
||||||
actionInitializers: 'CreateFormActionInitializers',
|
|
||||||
association,
|
|
||||||
collection: collection.name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
} else {
|
||||||
items={useRecordCollectionDataSourceItems('FormItem')}
|
insert(
|
||||||
/>
|
createFormBlockSchema({
|
||||||
);
|
actionInitializers: 'CreateFormActionInitializers',
|
||||||
};
|
association,
|
||||||
|
collection: collection.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
items={useRecordCollectionDataSourceItems('FormItem')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { ActionInitializer } from "./ActionInitializer";
|
import { ActionInitializer } from './ActionInitializer';
|
||||||
|
|
||||||
export const CreateSubmitActionInitializer = (props) => {
|
export const CreateSubmitActionInitializer = (props) => {
|
||||||
const schema = {
|
const schema = {
|
||||||
title: '{{ t("Submit") }}',
|
title: '{{ t("Submit") }}',
|
||||||
'x-action': 'submit',
|
'x-action': 'submit',
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-designer': 'Action.Designer',
|
'x-designer': 'Action.Designer',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
htmlType: 'submit',
|
htmlType: 'submit',
|
||||||
useProps: '{{ useCreateActionProps }}',
|
useProps: '{{ useCreateActionProps }}',
|
||||||
},
|
},
|
||||||
};
|
|
||||||
return <ActionInitializer {...props} schema={schema} />;
|
|
||||||
};
|
};
|
||||||
|
return <ActionInitializer {...props} schema={schema} />;
|
||||||
|
};
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import { ActionInitializer } from './ActionInitializer';
|
||||||
|
import { useTableBlockContext, useTableSelectorContext } from '../../';
|
||||||
|
import { NodeCollapseOutlined, NodeExpandOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const ExpandActionInitializer = (props) => {
|
||||||
|
const schema = {
|
||||||
|
'x-action': 'expandAll',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: '{{ expandIcon}}',
|
||||||
|
useProps: '{{ useExpandAllActionProps }}',
|
||||||
|
component: 'ExpandActionComponent',
|
||||||
|
useAction: () => {
|
||||||
|
return {
|
||||||
|
run() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return <ActionInitializer {...props} schema={schema} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionDesignerCss = css`
|
||||||
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
.general-schema-designer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.general-schema-designer {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
background: rgba(241, 139, 98, 0.06);
|
||||||
|
border: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
> .general-schema-designer-icons {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 2px;
|
||||||
|
line-height: 16px;
|
||||||
|
pointer-events: all;
|
||||||
|
.ant-space-item {
|
||||||
|
background-color: #f18b62;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
padding-left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ExpandActionComponent = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const isTableSelector = schema.parent?.parent?.['x-decorator'] === 'TableSelectorProvider';
|
||||||
|
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
|
||||||
|
return (
|
||||||
|
<div className={actionDesignerCss}>
|
||||||
|
{ctx.params['tree'] && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
ctx?.setExpandFlag();
|
||||||
|
}}
|
||||||
|
icon={ctx?.expandFlag ? <NodeCollapseOutlined /> : <NodeExpandOutlined />}
|
||||||
|
type={props.type}
|
||||||
|
>
|
||||||
|
{props.children[1]}
|
||||||
|
<span style={{ marginLeft: 10 }}>{ctx?.expandFlag ? t('Collapse all') : t('Expand all')}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -42,3 +42,10 @@ export * from './TableSelectorInitializer';
|
|||||||
export * from './UpdateActionInitializer';
|
export * from './UpdateActionInitializer';
|
||||||
export * from './UpdateSubmitActionInitializer';
|
export * from './UpdateSubmitActionInitializer';
|
||||||
export * from './ViewActionInitializer';
|
export * from './ViewActionInitializer';
|
||||||
|
export * from './CreateChildInitializer';
|
||||||
|
export * from './ExpandActionInitializer';
|
||||||
|
// association filter
|
||||||
|
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||||
|
export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction';
|
||||||
|
export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField';
|
||||||
|
export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDelete';
|
||||||
|
@ -76,7 +76,9 @@ export const useTableColumnInitializerFields = () => {
|
|||||||
const { name, currentFields = [] } = useCollection();
|
const { name, currentFields = [] } = useCollection();
|
||||||
const { getInterface } = useCollectionManager();
|
const { getInterface } = useCollectionManager();
|
||||||
return currentFields
|
return currentFields
|
||||||
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
|
.filter(
|
||||||
|
(field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren,
|
||||||
|
)
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const interfaceConfig = getInterface(field.interface);
|
const interfaceConfig = getInterface(field.interface);
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -113,7 +115,9 @@ export const useAssociatedTableColumnInitializerFields = () => {
|
|||||||
const subFields = getCollectionFields(field.target);
|
const subFields = getCollectionFields(field.target);
|
||||||
const items = subFields
|
const items = subFields
|
||||||
// ?.filter((subField) => subField?.interface && !['o2o', 'oho', 'obo', 'o2m', 'm2o', 'subTable', 'linkTo'].includes(subField?.interface))
|
// ?.filter((subField) => subField?.interface && !['o2o', 'oho', 'obo', 'o2m', 'm2o', 'subTable', 'linkTo'].includes(subField?.interface))
|
||||||
?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface))
|
?.filter(
|
||||||
|
(subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField?.treeChildren,
|
||||||
|
)
|
||||||
?.map((subField) => {
|
?.map((subField) => {
|
||||||
const interfaceConfig = getInterface(subField.interface);
|
const interfaceConfig = getInterface(subField.interface);
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -198,7 +202,7 @@ export const useFormItemInitializerFields = (options?: any) => {
|
|||||||
const { snapshot } = useActionContext();
|
const { snapshot } = useActionContext();
|
||||||
|
|
||||||
return currentFields
|
return currentFields
|
||||||
?.filter((field) => field?.interface && !field?.isForeignKey)
|
?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren)
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
const interfaceConfig = getInterface(field.interface);
|
const interfaceConfig = getInterface(field.interface);
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -246,7 +250,9 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
|
|||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
const subFields = getCollectionFields(field.target);
|
const subFields = getCollectionFields(field.target);
|
||||||
const items = subFields
|
const items = subFields
|
||||||
?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface))
|
?.filter(
|
||||||
|
(subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField.treeChildren,
|
||||||
|
)
|
||||||
?.map((subField) => {
|
?.map((subField) => {
|
||||||
const interfaceConfig = getInterface(subField.interface);
|
const interfaceConfig = getInterface(subField.interface);
|
||||||
const schema = {
|
const schema = {
|
||||||
|
217
packages/core/database/src/__tests__/tree.test.ts
Normal file
217
packages/core/database/src/__tests__/tree.test.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { Database } from '../database';
|
||||||
|
import { mockDatabase } from './';
|
||||||
|
|
||||||
|
describe('sort', function () {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be auto completed', () => {
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'categories',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
treeParent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
treeChildren: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(collection.treeChildrenField?.name).toBe('children');
|
||||||
|
expect(collection.treeParentField?.name).toBe('parent');
|
||||||
|
expect(collection.getField('parent').options.target).toBe('categories');
|
||||||
|
expect(collection.getField('parent').options.foreignKey).toBe('parentId');
|
||||||
|
expect(collection.getField('children').options.target).toBe('categories');
|
||||||
|
expect(collection.getField('children').options.foreignKey).toBe('parentId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be auto completed', () => {
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'categories',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
foreignKey: 'cid',
|
||||||
|
treeParent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
foreignKey: 'cid',
|
||||||
|
treeChildren: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(collection.treeChildrenField?.name).toBe('children');
|
||||||
|
expect(collection.treeParentField?.name).toBe('parent');
|
||||||
|
expect(collection.getField('parent').options.target).toBe('categories');
|
||||||
|
expect(collection.getField('parent').options.foreignKey).toBe('cid');
|
||||||
|
expect(collection.getField('children').options.target).toBe('categories');
|
||||||
|
expect(collection.getField('children').options.foreignKey).toBe('cid');
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
name: '1',
|
||||||
|
__index: '0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '1-1',
|
||||||
|
__index: '0.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '1-1-1',
|
||||||
|
__index: '0.children.0.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '1-1-1-1',
|
||||||
|
__index: '0.children.0.children.0.children.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '2',
|
||||||
|
__index: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '2-1',
|
||||||
|
__index: '1.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '2-1-1',
|
||||||
|
__index: '1.children.0.children.0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: '2-1-1-1',
|
||||||
|
__index: '1.children.0.children.0.children.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should be tree', async () => {
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'categories',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
treeParent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
treeChildren: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await db.getRepository('categories').create({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
|
||||||
|
const instances = await db.getRepository('categories').find({
|
||||||
|
filter: {
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
tree: true,
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
sort: 'id',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instances.map((i) => i.toJSON())).toMatchObject(values);
|
||||||
|
|
||||||
|
const instance = await db.getRepository('categories').findOne({
|
||||||
|
filterByTk: 1,
|
||||||
|
tree: true,
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instance.toJSON()).toMatchObject(values[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be tree', async () => {
|
||||||
|
const collection = db.collection({
|
||||||
|
name: 'categories',
|
||||||
|
tree: 'adjacency-list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
foreignKey: 'cid',
|
||||||
|
treeParent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
foreignKey: 'cid',
|
||||||
|
treeChildren: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await db.getRepository('categories').create({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
|
||||||
|
const instances = await db.getRepository('categories').find({
|
||||||
|
filter: {
|
||||||
|
cid: null,
|
||||||
|
},
|
||||||
|
tree: true,
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
sort: 'id',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instances.map((i) => i.toJSON())).toMatchObject(values);
|
||||||
|
|
||||||
|
const instance = await db.getRepository('categories').findOne({
|
||||||
|
filterByTk: 1,
|
||||||
|
tree: true,
|
||||||
|
fields: ['id', 'name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instance.toJSON()).toMatchObject(values[0]);
|
||||||
|
});
|
||||||
|
});
|
@ -7,10 +7,10 @@ import {
|
|||||||
QueryInterfaceDropTableOptions,
|
QueryInterfaceDropTableOptions,
|
||||||
SyncOptions,
|
SyncOptions,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
Utils,
|
Utils
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { Field, FieldOptions } from './fields';
|
import { BelongsToField, Field, FieldOptions, HasManyField } from './fields';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import { Repository } from './repository';
|
import { Repository } from './repository';
|
||||||
import { checkIdentifier, md5, snakeCase } from './utils';
|
import { checkIdentifier, md5, snakeCase } from './utils';
|
||||||
@ -48,6 +48,7 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
|
|||||||
*/
|
*/
|
||||||
magicAttribute?: string;
|
magicAttribute?: string;
|
||||||
|
|
||||||
|
tree?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +83,22 @@ export class Collection<
|
|||||||
return this.context.database;
|
return this.context.database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get treeParentField(): BelongsToField | null {
|
||||||
|
for (const [_, field] of this.fields) {
|
||||||
|
if (field.options.treeParent) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get treeChildrenField(): HasManyField | null {
|
||||||
|
for (const [_, field] of this.fields) {
|
||||||
|
if (field.options.treeChildren) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(options: CollectionOptions, context: CollectionContext) {
|
constructor(options: CollectionOptions, context: CollectionContext) {
|
||||||
super();
|
super();
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@ -243,6 +260,7 @@ export class Collection<
|
|||||||
this.checkFieldType(name, options);
|
this.checkFieldType(name, options);
|
||||||
|
|
||||||
const { database } = this.context;
|
const { database } = this.context;
|
||||||
|
this.emit('field.beforeAdd', name, options, { collection: this });
|
||||||
|
|
||||||
const field = database.buildField(
|
const field = database.buildField(
|
||||||
{ name, ...options },
|
{ name, ...options },
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
Sequelize,
|
Sequelize,
|
||||||
SyncOptions,
|
SyncOptions,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
Utils,
|
Utils
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { SequelizeStorage, Umzug } from 'umzug';
|
import { SequelizeStorage, Umzug } from 'umzug';
|
||||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||||
@ -58,11 +58,12 @@ import {
|
|||||||
SyncListener,
|
SyncListener,
|
||||||
UpdateListener,
|
UpdateListener,
|
||||||
UpdateWithAssociationsListener,
|
UpdateWithAssociationsListener,
|
||||||
ValidateListener,
|
ValidateListener
|
||||||
} from './types';
|
} from './types';
|
||||||
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
||||||
|
|
||||||
import DatabaseUtils from './database-utils';
|
import DatabaseUtils from './database-utils';
|
||||||
|
import { registerBuiltInListeners } from './listeners';
|
||||||
import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
|
import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
|
||||||
import buildQueryInterface from './query-interface/query-interface-builder';
|
import buildQueryInterface from './query-interface/query-interface-builder';
|
||||||
import QueryInterface from './query-interface/query-interface';
|
import QueryInterface from './query-interface/query-interface';
|
||||||
@ -363,6 +364,8 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
options.schema = this.options.schema;
|
options.schema = this.options.schema;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerBuiltInListeners(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
addMigration(item: MigrationItem) {
|
addMigration(item: MigrationItem) {
|
||||||
|
60
packages/core/database/src/listeners/adjacency-list.ts
Normal file
60
packages/core/database/src/listeners/adjacency-list.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import lodash from 'lodash';
|
||||||
|
import { Collection, CollectionOptions } from '../collection';
|
||||||
|
import { Model } from '../model';
|
||||||
|
|
||||||
|
export const beforeDefineAdjacencyListCollection = (options: CollectionOptions) => {
|
||||||
|
if (!options.tree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(options.fields || []).forEach((field) => {
|
||||||
|
if (field.treeParent || field.treeChildren) {
|
||||||
|
if (!field.target) {
|
||||||
|
field.target = options.name;
|
||||||
|
}
|
||||||
|
if (!field.foreignKey) {
|
||||||
|
field.foreignKey = 'parentId';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const afterDefineAdjacencyListCollection = (collection: Collection) => {
|
||||||
|
if (!collection.options.tree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
collection.model.afterFind(async (instances, options: any) => {
|
||||||
|
if (!options.tree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const foreignKey = collection.treeParentField?.foreignKey ?? 'parentId';
|
||||||
|
const childrenKey = collection.treeChildrenField?.name ?? 'children';
|
||||||
|
const arr: Model[] = Array.isArray(instances) ? instances : [instances];
|
||||||
|
let index = 0;
|
||||||
|
for (const instance of arr) {
|
||||||
|
const opts = {
|
||||||
|
...lodash.pick(options, ['tree', 'fields', 'appends', 'except', 'sort']),
|
||||||
|
};
|
||||||
|
let __index = `${index++}`;
|
||||||
|
if (options.parentIndex) {
|
||||||
|
__index = `${options.parentIndex}.${__index}`;
|
||||||
|
}
|
||||||
|
instance.setDataValue('__index', __index);
|
||||||
|
const children = await collection.repository.find({
|
||||||
|
filter: {
|
||||||
|
[foreignKey]: instance.id,
|
||||||
|
},
|
||||||
|
transaction: options.transaction,
|
||||||
|
...opts,
|
||||||
|
// @ts-ignore
|
||||||
|
parentIndex: `${__index}.${childrenKey}`,
|
||||||
|
context: options.context,
|
||||||
|
});
|
||||||
|
if (children?.length > 0) {
|
||||||
|
instance.setDataValue(
|
||||||
|
childrenKey,
|
||||||
|
children.map((r) => r.toJSON()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
7
packages/core/database/src/listeners/index.ts
Normal file
7
packages/core/database/src/listeners/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Database } from '../database';
|
||||||
|
import { afterDefineAdjacencyListCollection, beforeDefineAdjacencyListCollection } from './adjacency-list';
|
||||||
|
|
||||||
|
export const registerBuiltInListeners = (db: Database) => {
|
||||||
|
db.on('beforeDefineCollection', beforeDefineAdjacencyListCollection);
|
||||||
|
db.on('afterDefineCollection', afterDefineAdjacencyListCollection);
|
||||||
|
};
|
@ -11,7 +11,7 @@ import {
|
|||||||
Op,
|
Op,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
UpdateOptions as SequelizeUpdateOptions,
|
UpdateOptions as SequelizeUpdateOptions,
|
||||||
WhereOperators,
|
WhereOperators
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from './collection';
|
import { Collection } from './collection';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable {
|
|||||||
except?: Except;
|
except?: Except;
|
||||||
sort?: Sort;
|
sort?: Sort;
|
||||||
context?: any;
|
context?: any;
|
||||||
|
tree?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FindOneOptions = Omit<FindOptions, 'limit'>;
|
export type FindOneOptions = Omit<FindOptions, 'limit'>;
|
||||||
|
@ -283,4 +283,128 @@ describe('list association action with acl', () => {
|
|||||||
expect(data['meta']['allowedActions'].view).toContain(1);
|
expect(data['meta']['allowedActions'].view).toContain(1);
|
||||||
expect(data['meta']['allowedActions'].view).toContain(2);
|
expect(data['meta']['allowedActions'].view).toContain(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tree list action allowActions', async () => {
|
||||||
|
await db.getRepository('roles').create({
|
||||||
|
values: {
|
||||||
|
name: 'newRole',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await db.getRepository('users').create({
|
||||||
|
values: {
|
||||||
|
roles: ['newRole'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userPlugin = app.getPlugin('users');
|
||||||
|
const agent = app
|
||||||
|
.agent()
|
||||||
|
.set('X-With-ACL-Meta', true)
|
||||||
|
.auth(
|
||||||
|
userPlugin.jwtService.sign({
|
||||||
|
userId: user.get('id'),
|
||||||
|
}),
|
||||||
|
{ type: 'bearer' },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.acl.allow('table_a', ['*']);
|
||||||
|
app.acl.allow('collections', ['*']);
|
||||||
|
|
||||||
|
await agent.resource('collections').create({
|
||||||
|
values: {
|
||||||
|
autoGenId: true,
|
||||||
|
createdBy: false,
|
||||||
|
updatedBy: false,
|
||||||
|
createdAt: false,
|
||||||
|
updatedAt: false,
|
||||||
|
sortable: false,
|
||||||
|
name: 'table_a',
|
||||||
|
template: 'tree',
|
||||||
|
tree: 'adjacencyList',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
interface: 'integer',
|
||||||
|
name: 'parentId',
|
||||||
|
type: 'bigInt',
|
||||||
|
isForeignKey: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Parent ID")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
target: 'table_a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'm2o',
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
uiSchema: {
|
||||||
|
title: '{{t("Parent")}}',
|
||||||
|
'x-component': 'RecordPicker',
|
||||||
|
'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } },
|
||||||
|
},
|
||||||
|
target: 'table_a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'o2m',
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
uiSchema: {
|
||||||
|
title: '{{t("Children")}}',
|
||||||
|
'x-component': 'RecordPicker',
|
||||||
|
'x-component-props': { multiple: true, fieldNames: { label: 'id', value: 'id' } },
|
||||||
|
},
|
||||||
|
target: 'table_a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true },
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: 'table_a',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.resource('table_a').create({
|
||||||
|
values: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.resource('table_a').create({
|
||||||
|
values: {
|
||||||
|
parent: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.resource('table_a').create({
|
||||||
|
values: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.resource('table_a').create({
|
||||||
|
values: {
|
||||||
|
parent: {
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await agent.resource('table_a').list({
|
||||||
|
filter: JSON.stringify({
|
||||||
|
parentId: null,
|
||||||
|
}),
|
||||||
|
tree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.meta.allowedActions.view.sort()).toMatchObject([1, 2, 3, 4]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -673,7 +673,15 @@ export class PluginACL extends Plugin {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = listData.map((item) => item[primaryKeyField]);
|
const ids = (() => {
|
||||||
|
if (collection.options.tree) {
|
||||||
|
if (listData.length == 0) return [];
|
||||||
|
const getAllNodeIds = (data) => [data[primaryKeyField], ...(data.children || []).flatMap(getAllNodeIds)];
|
||||||
|
return listData.map((tree) => getAllNodeIds(tree.toJSON())).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
return listData.map((item) => item[primaryKeyField]);
|
||||||
|
})();
|
||||||
|
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
|
|
||||||
@ -823,4 +831,4 @@ export class PluginACL extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PluginACL;
|
export default PluginACL;
|
@ -99,7 +99,7 @@ export function afterCreateForForeignKeyField(db: Database) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return async (model, { transaction, context }) => {
|
const hook = async (model, { transaction, context }) => {
|
||||||
// skip if no app context
|
// skip if no app context
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
@ -174,4 +174,12 @@ export function afterCreateForForeignKeyField(db: Database) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return async (model, options) => {
|
||||||
|
try {
|
||||||
|
await hook(model, options);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user