mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
chore: support composite record unique key (#5007)
* refactor: filterTargetkey support mutiple * refactor: getRowKey * fix: bug * chore: test * chore: test * chore: test * fix: bug * fix: build * fix: useBulkDestroyActionProps support join primary key * fix: build * fix: bug * fix: bug * fix: bug * fix: bug * fix: fieldNames * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * refactor: sourceIdValue * fix: remoteselect * chore: test * chore: multi target key in has many relation repository * chore: test * chore: multiple relation repository * fix: test * refactor: target collection not support join collection * Update update-associations.ts --------- Co-authored-by: Chareice <chareice@live.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
5112b8e77e
commit
0d32ba4ff5
@ -75,8 +75,8 @@ export class SortAbleCollection {
|
|||||||
|
|
||||||
// insert source position to target position
|
// insert source position to target position
|
||||||
async move(sourceInstanceId: TargetKey, targetInstanceId: TargetKey, options: MoveOptions = {}) {
|
async move(sourceInstanceId: TargetKey, targetInstanceId: TargetKey, options: MoveOptions = {}) {
|
||||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
|
||||||
const targetInstance = await this.collection.repository.findById(targetInstanceId);
|
const targetInstance = await this.collection.repository.findByTargetKey(targetInstanceId);
|
||||||
|
|
||||||
if (this.scopeKey && sourceInstance.get(this.scopeKey) !== targetInstance.get(this.scopeKey)) {
|
if (this.scopeKey && sourceInstance.get(this.scopeKey) !== targetInstance.get(this.scopeKey)) {
|
||||||
await sourceInstance.update({
|
await sourceInstance.update({
|
||||||
@ -88,7 +88,7 @@ export class SortAbleCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async changeScope(sourceInstanceId: TargetKey, targetScope: any, method?: string) {
|
async changeScope(sourceInstanceId: TargetKey, targetScope: any, method?: string) {
|
||||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
|
||||||
const targetScopeValue = targetScope[this.scopeKey];
|
const targetScopeValue = targetScope[this.scopeKey];
|
||||||
|
|
||||||
if (targetScopeValue && sourceInstance.get(this.scopeKey) !== targetScopeValue) {
|
if (targetScopeValue && sourceInstance.get(this.scopeKey) !== targetScopeValue) {
|
||||||
@ -108,7 +108,7 @@ export class SortAbleCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sticky(sourceInstanceId: TargetKey) {
|
async sticky(sourceInstanceId: TargetKey) {
|
||||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
|
||||||
await sourceInstance.update(
|
await sourceInstance.update(
|
||||||
{
|
{
|
||||||
[this.field.get('name')]: 0,
|
[this.field.get('name')]: 0,
|
||||||
|
@ -11,6 +11,7 @@ import { Field, GeneralField } from '@formily/core';
|
|||||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
import merge from 'deepmerge';
|
import merge from 'deepmerge';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
import template from 'lodash/template';
|
import template from 'lodash/template';
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -307,7 +308,15 @@ export const useFilterByTk = () => {
|
|||||||
const association = getCollectionField(assoc);
|
const association = getCollectionField(assoc);
|
||||||
return recordData?.[association.targetKey || 'id'];
|
return recordData?.[association.targetKey || 'id'];
|
||||||
}
|
}
|
||||||
|
if (isArray(collection.filterTargetKey)) {
|
||||||
|
const filterByTk = {};
|
||||||
|
for (const key of collection.filterTargetKey) {
|
||||||
|
filterByTk[key] = recordData?.[key];
|
||||||
|
}
|
||||||
|
return filterByTk;
|
||||||
|
} else {
|
||||||
return recordData?.[collection.filterTargetKey || 'id'];
|
return recordData?.[collection.filterTargetKey || 'id'];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,9 +183,10 @@ export const useTableFieldProps = () => {
|
|||||||
rowKey: (record: any) => {
|
rowKey: (record: any) => {
|
||||||
return field.value?.indexOf?.(record);
|
return field.value?.indexOf?.(record);
|
||||||
},
|
},
|
||||||
onRowSelectionChange(selectedRowKeys) {
|
onRowSelectionChange(selectedRowKeys, selectedRowData) {
|
||||||
ctx.field.data = ctx?.field?.data || {};
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
ctx.field.data.selectedRowData = selectedRowData;
|
||||||
},
|
},
|
||||||
onChange({ current, pageSize }) {
|
onChange({ current, pageSize }) {
|
||||||
ctx.service.run({ page: current, pageSize });
|
ctx.service.run({ page: current, pageSize });
|
||||||
|
@ -319,9 +319,10 @@ export const useTableSelectorProps = () => {
|
|||||||
dragSort: false,
|
dragSort: false,
|
||||||
rowKey: ctx.rowKey || 'id',
|
rowKey: ctx.rowKey || 'id',
|
||||||
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
|
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
|
||||||
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
onRowSelectionChange(selectedRowKeys, selectedRowData) {
|
||||||
ctx.field.data = ctx?.field?.data || {};
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
ctx.field.data.selectedRowData = selectedRowData;
|
||||||
},
|
},
|
||||||
async onRowDragEnd({ from, to }) {
|
async onRowDragEnd({ from, to }) {
|
||||||
await ctx.resource.move({
|
await ctx.resource.move({
|
||||||
|
@ -1080,13 +1080,25 @@ export const useBulkDestroyActionProps = () => {
|
|||||||
const { field } = useBlockRequestContext();
|
const { field } = useBlockRequestContext();
|
||||||
const { resource, service } = useBlockRequestContext();
|
const { resource, service } = useBlockRequestContext();
|
||||||
const { setSubmitted } = useActionContext();
|
const { setSubmitted } = useActionContext();
|
||||||
|
const collection = useCollection_deprecated();
|
||||||
|
const { filterTargetKey } = collection;
|
||||||
return {
|
return {
|
||||||
async onClick(e?, callBack?) {
|
async onClick(e?, callBack?) {
|
||||||
|
let filterByTk = field.data?.selectedRowKeys;
|
||||||
|
if (Array.isArray(filterTargetKey)) {
|
||||||
|
filterByTk = field.data.selectedRowData.map((v) => {
|
||||||
|
const obj = {};
|
||||||
|
filterTargetKey.map((j) => {
|
||||||
|
obj[j] = v[j];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!field?.data?.selectedRowKeys?.length) {
|
if (!field?.data?.selectedRowKeys?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await resource.destroy({
|
await resource.destroy({
|
||||||
filterByTk: field.data?.selectedRowKeys,
|
filterByTk,
|
||||||
});
|
});
|
||||||
field.data.selectedRowKeys = [];
|
field.data.selectedRowKeys = [];
|
||||||
const currentPage = service.params[0]?.page;
|
const currentPage = service.params[0]?.page;
|
||||||
@ -1098,7 +1110,7 @@ export const useBulkDestroyActionProps = () => {
|
|||||||
callBack?.();
|
callBack?.();
|
||||||
}
|
}
|
||||||
setSubmitted?.(true);
|
setSubmitted?.(true);
|
||||||
// service?.refresh?.();
|
service?.refresh?.();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -64,6 +64,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
|
|||||||
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
|
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||||
import { getUniqueKeyFromCollection } from './o2m';
|
import { getUniqueKeyFromCollection } from './utils';
|
||||||
import { defaultProps, relationshipType, reverseFieldProperties } from './properties';
|
import { defaultProps, relationshipType, reverseFieldProperties } from './properties';
|
||||||
|
|
||||||
export class M2MFieldInterface extends CollectionFieldInterface {
|
export class M2MFieldInterface extends CollectionFieldInterface {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||||
import { getUniqueKeyFromCollection } from './o2m';
|
import { getUniqueKeyFromCollection } from './utils';
|
||||||
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
||||||
|
|
||||||
export class M2OFieldInterface extends CollectionFieldInterface {
|
export class M2OFieldInterface extends CollectionFieldInterface {
|
||||||
|
@ -8,10 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { Collection } from '../../data-source';
|
|
||||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||||
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
||||||
|
import { getUniqueKeyFromCollection } from './utils';
|
||||||
export class O2MFieldInterface extends CollectionFieldInterface {
|
export class O2MFieldInterface extends CollectionFieldInterface {
|
||||||
name = 'o2m';
|
name = 'o2m';
|
||||||
type = 'object';
|
type = 'object';
|
||||||
@ -215,7 +214,3 @@ export class O2MFieldInterface extends CollectionFieldInterface {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUniqueKeyFromCollection(collection: Collection) {
|
|
||||||
return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
|
|
||||||
}
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||||
import { getUniqueKeyFromCollection } from './o2m';
|
import { getUniqueKeyFromCollection } from './utils';
|
||||||
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
||||||
|
|
||||||
export class O2OFieldInterface extends CollectionFieldInterface {
|
export class O2OFieldInterface extends CollectionFieldInterface {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Collection } from '../../../data-source';
|
||||||
|
|
||||||
|
export function getUniqueKeyFromCollection(collection: Collection) {
|
||||||
|
if (collection?.filterTargetKey) {
|
||||||
|
if (Array.isArray(collection.filterTargetKey)) {
|
||||||
|
return collection?.filterTargetKey?.[0];
|
||||||
|
}
|
||||||
|
return collection?.filterTargetKey;
|
||||||
|
}
|
||||||
|
return collection?.getPrimaryKey() || 'id';
|
||||||
|
}
|
@ -90,6 +90,9 @@ export class SqlCollectionTemplate extends CollectionTemplate {
|
|||||||
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
|
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -153,6 +153,9 @@ export class ViewCollectionTemplate extends CollectionTemplate {
|
|||||||
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
|
description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
||||||
},
|
},
|
||||||
...getConfigurableProperties('category', 'description'),
|
...getConfigurableProperties('category', 'description'),
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SchemaKey } from '@formily/json-schema';
|
import type { SchemaKey } from '@formily/json-schema';
|
||||||
|
import qs from 'qs';
|
||||||
import type { DataSource } from '../data-source';
|
import type { DataSource } from '../data-source';
|
||||||
import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection';
|
import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection';
|
||||||
|
|
||||||
@ -159,10 +160,23 @@ export class CollectionManager {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const getTargetKey = (collection: Collection) => collection.filterTargetKey || collection.getPrimaryKey() || 'id';
|
||||||
|
|
||||||
|
const buildFilterByTk = (targetKey: string | string[], record: Record<string, any>) => {
|
||||||
|
if (Array.isArray(targetKey)) {
|
||||||
|
const filterByTk = {};
|
||||||
|
targetKey.forEach((key) => {
|
||||||
|
filterByTk[key] = record[key];
|
||||||
|
});
|
||||||
|
return qs.stringify(filterByTk);
|
||||||
|
} else {
|
||||||
|
return record[targetKey];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (collectionOrAssociation instanceof Collection) {
|
if (collectionOrAssociation instanceof Collection) {
|
||||||
const key = collectionOrAssociation.filterTargetKey || collectionOrAssociation.getPrimaryKey() || 'id';
|
const targetKey = getTargetKey(collectionOrAssociation);
|
||||||
return collectionRecordOrAssociationRecord[key];
|
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionOrAssociation.includes('.')) {
|
if (collectionOrAssociation.includes('.')) {
|
||||||
@ -186,9 +200,8 @@ export class CollectionManager {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const targetKey = getTargetKey(targetCollection);
|
||||||
const key = targetCollection?.filterTargetKey || targetCollection?.getPrimaryKey() || 'id';
|
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
|
||||||
return collectionRecordOrAssociationRecord[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSourceKeyByAssociation(associationName: string) {
|
getSourceKeyByAssociation(associationName: string) {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { IResource } from '@nocobase/sdk';
|
import { IResource } from '@nocobase/sdk';
|
||||||
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
|
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
import { useAPIClient } from '../../api-client';
|
import { useAPIClient } from '../../api-client';
|
||||||
import { useCollectionManager } from '../collection';
|
import { useCollectionManager } from '../collection';
|
||||||
import { CollectionRecord } from '../collection-record';
|
import { CollectionRecord } from '../collection-record';
|
||||||
@ -34,8 +34,17 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
|
|||||||
if (association && parentRecord) {
|
if (association && parentRecord) {
|
||||||
const sourceKey = cm.getSourceKeyByAssociation(association);
|
const sourceKey = cm.getSourceKeyByAssociation(association);
|
||||||
const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord;
|
const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord;
|
||||||
|
if (isArray(sourceKey)) {
|
||||||
|
const filterByTk = {};
|
||||||
|
for (const key of sourceKey) {
|
||||||
|
filterByTk[key] = parentRecordData?.[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeURIComponent(JSON.stringify(filterByTk));
|
||||||
|
} else {
|
||||||
return parentRecordData[sourceKey];
|
return parentRecordData[sourceKey];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [association, sourceId, parentRecord]);
|
}, [association, sourceId, parentRecord]);
|
||||||
|
|
||||||
const resource = useMemo(() => {
|
const resource = useMemo(() => {
|
||||||
|
@ -16,7 +16,6 @@ import { findFilterTargets } from '../../../../../block-provider/hooks';
|
|||||||
import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider';
|
import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider';
|
||||||
import { mergeFilter } from '../../../../../filter-provider/utils';
|
import { mergeFilter } from '../../../../../filter-provider/utils';
|
||||||
import { removeNullCondition } from '../../../../../schema-component';
|
import { removeNullCondition } from '../../../../../schema-component';
|
||||||
import { useCollection } from '../../../../../data-source';
|
|
||||||
|
|
||||||
export const useTableBlockProps = () => {
|
export const useTableBlockProps = () => {
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
@ -58,9 +57,10 @@ export const useTableBlockProps = () => {
|
|||||||
dragSort: ctx.dragSort && ctx.dragSortBy,
|
dragSort: ctx.dragSort && ctx.dragSortBy,
|
||||||
rowKey: ctx.rowKey || 'id',
|
rowKey: ctx.rowKey || 'id',
|
||||||
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
|
pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination,
|
||||||
onRowSelectionChange: useCallback((selectedRowKeys) => {
|
onRowSelectionChange: useCallback((selectedRowKeys, selectedRowData) => {
|
||||||
ctx.field.data = ctx?.field?.data || {};
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
ctx.field.data.selectedRowData = selectedRowData;
|
||||||
ctx?.field?.onRowSelect?.(selectedRowKeys);
|
ctx?.field?.onRowSelect?.(selectedRowKeys);
|
||||||
}, []),
|
}, []),
|
||||||
onRowDragEnd: useCallback(
|
onRowDragEnd: useCallback(
|
||||||
|
@ -53,7 +53,7 @@ export const useTableSelectorProps = () => {
|
|||||||
type: multiple ? 'checkbox' : 'radio',
|
type: multiple ? 'checkbox' : 'radio',
|
||||||
selectedRowKeys: rcSelectRows
|
selectedRowKeys: rcSelectRows
|
||||||
?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey]))
|
?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey]))
|
||||||
.map((item) => item[rowKey]),
|
.map((item) => item[rowKey]?.toString()),
|
||||||
},
|
},
|
||||||
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
|
@ -169,7 +169,7 @@ const PagePopupsItemProvider: FC<{
|
|||||||
collection={params.collection || context.collection}
|
collection={params.collection || context.collection}
|
||||||
association={context.association}
|
association={context.association}
|
||||||
sourceId={params.sourceid}
|
sourceId={params.sourceid}
|
||||||
filterByTk={params.filterbytk}
|
filterByTk={parseQueryString(params.filterbytk)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
record={storedContext.record}
|
record={storedContext.record}
|
||||||
parentRecord={storedContext.parentRecord}
|
parentRecord={storedContext.parentRecord}
|
||||||
@ -465,3 +465,25 @@ function findSchemaByUid(uid: string, rootSchema: Schema, resultRef: { value: Sc
|
|||||||
});
|
});
|
||||||
return resultRef.value;
|
return resultRef.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseQueryString(queryString) {
|
||||||
|
// 如果没有 '&',直接返回原始字符串
|
||||||
|
if (!queryString?.includes?.('=')) {
|
||||||
|
return queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码查询字符串
|
||||||
|
const decodedString = decodeURIComponent(queryString);
|
||||||
|
|
||||||
|
// 将解码后的字符串按 '&' 分隔成键值对
|
||||||
|
const pairs = decodedString.split('&');
|
||||||
|
|
||||||
|
// 将键值对转换为对象
|
||||||
|
const params = pairs.reduce((acc, pair) => {
|
||||||
|
const [key, value] = pair.split('=');
|
||||||
|
acc[key] = value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
@ -92,7 +92,9 @@ const InternalRemoteSelect = connect(
|
|||||||
(options) => {
|
(options) => {
|
||||||
try {
|
try {
|
||||||
return options
|
return options
|
||||||
.filter((v) => ['number', 'string'].includes(typeof v[fieldNames.value]))
|
.filter((v) => {
|
||||||
|
return ['number', 'string'].includes(typeof v[fieldNames.value]) || !v[fieldNames.value];
|
||||||
|
})
|
||||||
.map((option) => {
|
.map((option) => {
|
||||||
let label = compile(option[fieldNames.label]);
|
let label = compile(option[fieldNames.label]);
|
||||||
|
|
||||||
|
@ -521,6 +521,9 @@ export const Table: any = withDynamicSchemaProps(
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const defaultRowKey = useCallback((record: any) => {
|
const defaultRowKey = useCallback((record: any) => {
|
||||||
|
if (rowKey) {
|
||||||
|
return getRowKey(record);
|
||||||
|
}
|
||||||
if (record.key) {
|
if (record.key) {
|
||||||
return record.key;
|
return record.key;
|
||||||
}
|
}
|
||||||
@ -536,13 +539,21 @@ export const Table: any = withDynamicSchemaProps(
|
|||||||
|
|
||||||
const getRowKey = useCallback(
|
const getRowKey = useCallback(
|
||||||
(record: any) => {
|
(record: any) => {
|
||||||
if (typeof rowKey === 'string') {
|
if (Array.isArray(rowKey)) {
|
||||||
|
// 使用多个字段值组合生成唯一键
|
||||||
|
return rowKey
|
||||||
|
.map((keyField) => {
|
||||||
|
return record[keyField]?.toString() || '';
|
||||||
|
})
|
||||||
|
.join('-');
|
||||||
|
} else if (typeof rowKey === 'string') {
|
||||||
return record[rowKey]?.toString();
|
return record[rowKey]?.toString();
|
||||||
} else {
|
} else {
|
||||||
|
// 如果 rowKey 是函数或未提供,使用 defaultRowKey
|
||||||
return (rowKey ?? defaultRowKey)(record)?.toString();
|
return (rowKey ?? defaultRowKey)(record)?.toString();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rowKey, defaultRowKey],
|
[JSON.stringify(rowKey), defaultRowKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSourceKeys = field?.value?.map?.(getRowKey);
|
const dataSourceKeys = field?.value?.map?.(getRowKey);
|
||||||
@ -623,6 +634,7 @@ export const Table: any = withDynamicSchemaProps(
|
|||||||
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.selectedRowKeys = selectedRowKeys;
|
field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
field.data.selectedRowData = selectedRows;
|
||||||
setSelectedRowKeys(selectedRowKeys);
|
setSelectedRowKeys(selectedRowKeys);
|
||||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||||
},
|
},
|
||||||
@ -720,7 +732,7 @@ export const Table: any = withDynamicSchemaProps(
|
|||||||
|
|
||||||
const rowClassName = useCallback(
|
const rowClassName = useCallback(
|
||||||
(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : ''),
|
(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : ''),
|
||||||
[selectedRow, highlightRow, rowKey],
|
[selectedRow, highlightRow, JSON.stringify(rowKey)],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onExpandValue = useCallback(
|
const onExpandValue = useCallback(
|
||||||
@ -771,7 +783,7 @@ export const Table: any = withDynamicSchemaProps(
|
|||||||
<SortableWrapper>
|
<SortableWrapper>
|
||||||
<MemoizedAntdTable
|
<MemoizedAntdTable
|
||||||
ref={tableSizeRefCallback}
|
ref={tableSizeRefCallback}
|
||||||
rowKey={rowKey ?? defaultRowKey}
|
rowKey={defaultRowKey}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
tableLayout="auto"
|
tableLayout="auto"
|
||||||
{...others}
|
{...others}
|
||||||
|
@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Database, { mockDatabase } from '../../index';
|
||||||
|
|
||||||
|
describe('multi filter target key', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set multi filter target keys', async () => {
|
||||||
|
const Student = db.collection({
|
||||||
|
name: 'students',
|
||||||
|
autoGenId: false,
|
||||||
|
filterTargetKey: ['name', 'classId'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'classId',
|
||||||
|
type: 'bigInt',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const s1 = await Student.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 's1',
|
||||||
|
classId: 1,
|
||||||
|
age: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// it should find by name and classId
|
||||||
|
const findRes = await Student.repository.find({
|
||||||
|
filterByTk: {
|
||||||
|
name: 's1',
|
||||||
|
classId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findRes.length).toBe(1);
|
||||||
|
|
||||||
|
// update
|
||||||
|
await Student.repository.update({
|
||||||
|
filterByTk: {
|
||||||
|
name: 's1',
|
||||||
|
classId: 1,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
age: '20',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const s1Updated = await Student.repository.findOne({
|
||||||
|
filterByTk: {
|
||||||
|
name: 's1',
|
||||||
|
classId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(s1Updated.age).toBe(20);
|
||||||
|
|
||||||
|
// destroy
|
||||||
|
await Student.repository.destroy({
|
||||||
|
filterByTk: {
|
||||||
|
name: 's1',
|
||||||
|
classId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await Student.repository.count()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save multi filter target keys association', async () => {
|
||||||
|
const Class = db.collection({
|
||||||
|
name: 'classes',
|
||||||
|
autoGenId: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'students',
|
||||||
|
type: 'hasMany',
|
||||||
|
target: 'students',
|
||||||
|
foreignKey: 'classId',
|
||||||
|
targetKey: 'name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Student = db.collection({
|
||||||
|
name: 'students',
|
||||||
|
autoGenId: false,
|
||||||
|
filterTargetKey: ['key1', 'key2'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'key1',
|
||||||
|
type: 'bigInt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'key2',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const s1 = await Student.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 's1',
|
||||||
|
key1: 1,
|
||||||
|
key2: 'k1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const s2 = await Student.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 's2',
|
||||||
|
key1: 2,
|
||||||
|
key2: 'k2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const c1 = await Class.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'c1',
|
||||||
|
students: [
|
||||||
|
{
|
||||||
|
name: 's1',
|
||||||
|
key1: 1,
|
||||||
|
key2: 'k1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 's2',
|
||||||
|
key1: 2,
|
||||||
|
key2: 'k2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const c1Students = await Class.repository.find({
|
||||||
|
filterByTk: {
|
||||||
|
name: 'c1',
|
||||||
|
},
|
||||||
|
appends: ['students'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(c1Students[0].students.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Database, { HasManyRepository, mockDatabase } from '../../index';
|
||||||
|
describe('multi target key in association repository', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('use multi target key as source key', async () => {
|
||||||
|
test('has many repository', async () => {
|
||||||
|
const Student = db.collection({
|
||||||
|
name: 'students',
|
||||||
|
autoGenId: false,
|
||||||
|
filterTargetKey: ['name', 'classId'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'classId',
|
||||||
|
type: 'bigInt',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'uid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'books',
|
||||||
|
type: 'hasMany',
|
||||||
|
target: 'books',
|
||||||
|
foreignKey: 'studentId',
|
||||||
|
targetKey: 'id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Book = db.collection({
|
||||||
|
name: 'books',
|
||||||
|
autoGenId: false,
|
||||||
|
filterTargetKey: ['name', 'author'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'author',
|
||||||
|
type: 'string',
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'student',
|
||||||
|
type: 'belongsTo',
|
||||||
|
foreignKey: 'studentId',
|
||||||
|
targetKey: 'id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await Student.repository.create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
name: 's1',
|
||||||
|
classId: 1,
|
||||||
|
age: 10,
|
||||||
|
books: [
|
||||||
|
{
|
||||||
|
name: 'b1',
|
||||||
|
author: 'a1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'b2',
|
||||||
|
author: 'a1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 's2',
|
||||||
|
classId: 2,
|
||||||
|
age: 11,
|
||||||
|
books: [
|
||||||
|
{
|
||||||
|
name: 'b3',
|
||||||
|
author: 'a1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasManyRepo = db.getRepository<HasManyRepository>(
|
||||||
|
'students.books',
|
||||||
|
encodeURIComponent(JSON.stringify({ name: 's1', classId: 1 })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await hasManyRepo.find();
|
||||||
|
|
||||||
|
expect(res.length).toBe(2);
|
||||||
|
|
||||||
|
const b2a1 = await hasManyRepo.findOne({
|
||||||
|
filterByTk: {
|
||||||
|
name: 'b2',
|
||||||
|
author: 'a1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(b2a1).toBeDefined();
|
||||||
|
expect(b2a1.get('name')).toBe('b2');
|
||||||
|
expect(b2a1.get('author')).toBe('a1');
|
||||||
|
|
||||||
|
await db
|
||||||
|
.getRepository<HasManyRepository>('students.books', {
|
||||||
|
name: 's2',
|
||||||
|
classId: 2,
|
||||||
|
})
|
||||||
|
.destroy({});
|
||||||
|
|
||||||
|
expect(await db.getRepository('books').count()).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -98,7 +98,7 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
|
|||||||
viewName?: string;
|
viewName?: string;
|
||||||
writableView?: boolean;
|
writableView?: boolean;
|
||||||
|
|
||||||
filterTargetKey?: string;
|
filterTargetKey?: string | string[];
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
model?: string | ModelStatic<Model>;
|
model?: string | ModelStatic<Model>;
|
||||||
repository?: string | RepositoryType;
|
repository?: string | RepositoryType;
|
||||||
@ -169,14 +169,21 @@ export class Collection<
|
|||||||
this.setSortable(options.sortable);
|
this.setSortable(options.sortable);
|
||||||
}
|
}
|
||||||
|
|
||||||
get filterTargetKey() {
|
get filterTargetKey(): string | string[] {
|
||||||
const targetKey = this.options?.filterTargetKey;
|
const targetKey = this.options?.filterTargetKey;
|
||||||
|
|
||||||
|
if (Array.isArray(targetKey)) {
|
||||||
|
return targetKey;
|
||||||
|
}
|
||||||
|
|
||||||
if (targetKey && this.model.getAttributes()[targetKey]) {
|
if (targetKey && this.model.getAttributes()[targetKey]) {
|
||||||
return targetKey;
|
return targetKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.model.primaryKeyAttributes.length > 1) {
|
if (this.model.primaryKeyAttributes.length > 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.model.primaryKeyAttribute;
|
return this.model.primaryKeyAttribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ import extendOperators from './operators';
|
|||||||
import QueryInterface from './query-interface/query-interface';
|
import QueryInterface from './query-interface/query-interface';
|
||||||
import buildQueryInterface from './query-interface/query-interface-builder';
|
import buildQueryInterface from './query-interface/query-interface-builder';
|
||||||
import { RelationRepository } from './relation-repository/relation-repository';
|
import { RelationRepository } from './relation-repository/relation-repository';
|
||||||
import { Repository } from './repository';
|
import { Repository, TargetKey } from './repository';
|
||||||
import {
|
import {
|
||||||
AfterDefineCollectionListener,
|
AfterDefineCollectionListener,
|
||||||
BeforeDefineCollectionListener,
|
BeforeDefineCollectionListener,
|
||||||
@ -634,11 +634,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
|
|
||||||
getRepository<R extends Repository>(name: string): R;
|
getRepository<R extends Repository>(name: string): R;
|
||||||
|
|
||||||
getRepository<R extends RelationRepository>(name: string, relationId: string | number): R;
|
getRepository<R extends RelationRepository>(name: string, relationId: TargetKey): R;
|
||||||
|
|
||||||
getRepository<R extends ArrayFieldRepository>(name: string, relationId: string | number): R;
|
getRepository<R extends ArrayFieldRepository>(name: string, relationId: TargetKey): R;
|
||||||
|
|
||||||
getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
|
getRepository<R extends RelationRepository>(name: string, relationId?: TargetKey): Repository | R {
|
||||||
const [collection, relation] = name.split('.');
|
const [collection, relation] = name.split('.');
|
||||||
if (relation) {
|
if (relation) {
|
||||||
return this.getRepository(collection)?.relation(relation)?.of(relationId) as R;
|
return this.getRepository(collection)?.relation(relation)?.of(relationId) as R;
|
||||||
|
@ -76,17 +76,42 @@ export class OptionsParser {
|
|||||||
return this.isAssociation(path.split('.')[0]);
|
return this.isAssociation(path.split('.')[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterByTkToWhereOption() {
|
||||||
|
const filterByTkOption = this.options?.filterByTk;
|
||||||
|
|
||||||
|
if (!filterByTkOption) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// multi filter target key
|
||||||
|
if (lodash.isPlainObject(this.options.filterByTk)) {
|
||||||
|
const where = {};
|
||||||
|
for (const [key, value] of Object.entries(filterByTkOption)) {
|
||||||
|
where[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
// single filter target key
|
||||||
|
const filterTargetKey = this.context.targetKey || this.collection.filterTargetKey;
|
||||||
|
|
||||||
|
if (Array.isArray(filterTargetKey)) {
|
||||||
|
throw new Error('multi filter target key value must be object');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[filterTargetKey]: filterByTkOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
toSequelizeParams() {
|
toSequelizeParams() {
|
||||||
const queryParams = this.filterParser.toSequelizeParams();
|
const queryParams = this.filterParser.toSequelizeParams();
|
||||||
|
|
||||||
if (this.options?.filterByTk) {
|
if (this.options?.filterByTk) {
|
||||||
|
const filterByTkWhere = this.filterByTkToWhereOption();
|
||||||
queryParams.where = {
|
queryParams.where = {
|
||||||
[Op.and]: [
|
[Op.and]: [queryParams.where, filterByTkWhere],
|
||||||
queryParams.where,
|
|
||||||
{
|
|
||||||
[this.context.targetKey || this.collection.filterTargetKey]: this.options.filterByTk,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +140,7 @@ export class OptionsParser {
|
|||||||
|
|
||||||
let defaultSortField = this.model.primaryKeyAttribute;
|
let defaultSortField = this.model.primaryKeyAttribute;
|
||||||
|
|
||||||
if (!defaultSortField && this.collection.filterTargetKey) {
|
if (!defaultSortField && this.collection.filterTargetKey && !Array.isArray(this.collection.filterTargetKey)) {
|
||||||
defaultSortField = this.collection.filterTargetKey;
|
defaultSortField = this.collection.filterTargetKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,18 +17,20 @@ export class HasManyRepository extends MultipleRelationRepository {
|
|||||||
async find(options?: FindOptions): Promise<any> {
|
async find(options?: FindOptions): Promise<any> {
|
||||||
const targetRepository = this.targetCollection.repository;
|
const targetRepository = this.targetCollection.repository;
|
||||||
|
|
||||||
const addFilter = {
|
const targetFilterOptions = await this.targetRepositoryFilterOptionsBySourceValue();
|
||||||
[this.association.foreignKey]: this.sourceKeyValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options?.filterByTk) {
|
const findOptionsOmit = ['where', 'values', 'attributes'];
|
||||||
addFilter[this.associationField.targetKey] = options.filterByTk;
|
|
||||||
|
if (options?.filterByTk && !this.isMultiTargetKey(options.filterByTk)) {
|
||||||
|
// @ts-ignore
|
||||||
|
targetFilterOptions[this.associationField.targetKey] = options.filterByTk;
|
||||||
|
findOptionsOmit.push('filterByTk');
|
||||||
}
|
}
|
||||||
|
|
||||||
const findOptions = {
|
const findOptions = {
|
||||||
...omit(options, ['filterByTk', 'where', 'values', 'attributes']),
|
...omit(options, findOptionsOmit),
|
||||||
filter: {
|
filter: {
|
||||||
$and: [options.filter || {}, addFilter],
|
$and: [options?.filter || {}, targetFilterOptions],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,14 +39,11 @@ export class HasManyRepository extends MultipleRelationRepository {
|
|||||||
|
|
||||||
async aggregate(options: AggregateOptions) {
|
async aggregate(options: AggregateOptions) {
|
||||||
const targetRepository = this.targetCollection.repository;
|
const targetRepository = this.targetCollection.repository;
|
||||||
const addFilter = {
|
|
||||||
[this.association.foreignKey]: this.sourceKeyValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
const aggOptions = {
|
const aggOptions = {
|
||||||
...options,
|
...options,
|
||||||
filter: {
|
filter: {
|
||||||
$and: [options.filter || {}, addFilter],
|
$and: [options.filter || {}, await this.targetRepositoryFilterOptionsBySourceValue()],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,6 +32,21 @@ export interface AssociatedOptions extends Transactionable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class MultipleRelationRepository extends RelationRepository {
|
export abstract class MultipleRelationRepository extends RelationRepository {
|
||||||
|
async targetRepositoryFilterOptionsBySourceValue(): Promise<any> {
|
||||||
|
let filterForeignKeyValue = this.sourceKeyValue;
|
||||||
|
|
||||||
|
if (this.isMultiTargetKey()) {
|
||||||
|
const sourceModel = await this.getSourceModel();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
filterForeignKeyValue = sourceModel.get(this.association.sourceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[this.association.foreignKey]: filterForeignKeyValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async find(options?: FindOptions): Promise<any> {
|
async find(options?: FindOptions): Promise<any> {
|
||||||
const targetRepository = this.targetCollection.repository;
|
const targetRepository = this.targetCollection.repository;
|
||||||
|
|
||||||
@ -49,9 +64,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
|||||||
const appendFilter = {
|
const appendFilter = {
|
||||||
isPivotFilter: true,
|
isPivotFilter: true,
|
||||||
association: pivotAssoc,
|
association: pivotAssoc,
|
||||||
where: {
|
where: await this.targetRepositoryFilterOptionsBySourceValue(),
|
||||||
[association.foreignKey]: this.sourceKeyValue,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return targetRepository.find({
|
return targetRepository.find({
|
||||||
|
@ -16,7 +16,7 @@ import { RelationField } from '../fields/relation-field';
|
|||||||
import FilterParser from '../filter-parser';
|
import FilterParser from '../filter-parser';
|
||||||
import { Model } from '../model';
|
import { Model } from '../model';
|
||||||
import { OptionsParser } from '../options-parser';
|
import { OptionsParser } from '../options-parser';
|
||||||
import { CreateOptions, Filter, FindOptions } from '../repository';
|
import { CreateOptions, Filter, FindOptions, TargetKey } from '../repository';
|
||||||
import { updateAssociations } from '../update-associations';
|
import { updateAssociations } from '../update-associations';
|
||||||
import { UpdateGuard } from '../update-guard';
|
import { UpdateGuard } from '../update-guard';
|
||||||
|
|
||||||
@ -31,17 +31,19 @@ export abstract class RelationRepository {
|
|||||||
targetCollection: Collection;
|
targetCollection: Collection;
|
||||||
associationName: string;
|
associationName: string;
|
||||||
associationField: RelationField;
|
associationField: RelationField;
|
||||||
sourceKeyValue: string | number;
|
sourceKeyValue: TargetKey;
|
||||||
sourceInstance: Model;
|
sourceInstance: Model;
|
||||||
db: Database;
|
db: Database;
|
||||||
database: Database;
|
database: Database;
|
||||||
|
|
||||||
constructor(sourceCollection: Collection, association: string, sourceKeyValue: string | number) {
|
constructor(sourceCollection: Collection, association: string, sourceKeyValue: TargetKey) {
|
||||||
this.db = sourceCollection.context.database;
|
this.db = sourceCollection.context.database;
|
||||||
this.database = this.db;
|
this.database = this.db;
|
||||||
|
|
||||||
this.sourceCollection = sourceCollection;
|
this.sourceCollection = sourceCollection;
|
||||||
this.sourceKeyValue = sourceKeyValue;
|
|
||||||
|
this.setSourceKeyValue(sourceKeyValue);
|
||||||
|
|
||||||
this.associationName = association;
|
this.associationName = association;
|
||||||
this.association = this.sourceCollection.model.associations[association];
|
this.association = this.sourceCollection.model.associations[association];
|
||||||
|
|
||||||
@ -51,6 +53,24 @@ export abstract class RelationRepository {
|
|||||||
this.targetCollection = this.sourceCollection.context.database.modelCollection.get(this.targetModel);
|
this.targetCollection = this.sourceCollection.context.database.modelCollection.get(this.targetModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeMultiTargetKey(str: string) {
|
||||||
|
try {
|
||||||
|
const decoded = decodeURIComponent(str);
|
||||||
|
return JSON.parse(decoded);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSourceKeyValue(sourceKeyValue: TargetKey) {
|
||||||
|
this.sourceKeyValue =
|
||||||
|
typeof sourceKeyValue === 'string' ? this.decodeMultiTargetKey(sourceKeyValue) || sourceKeyValue : sourceKeyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMultiTargetKey(value?: any) {
|
||||||
|
return lodash.isPlainObject(value || this.sourceKeyValue);
|
||||||
|
}
|
||||||
|
|
||||||
get collection() {
|
get collection() {
|
||||||
return this.db.getCollection(this.targetModel.name);
|
return this.db.getCollection(this.targetModel.name);
|
||||||
}
|
}
|
||||||
@ -145,7 +165,15 @@ export abstract class RelationRepository {
|
|||||||
|
|
||||||
async getSourceModel(transaction?: Transaction) {
|
async getSourceModel(transaction?: Transaction) {
|
||||||
if (!this.sourceInstance) {
|
if (!this.sourceInstance) {
|
||||||
this.sourceInstance = await this.sourceCollection.model.findOne({
|
this.sourceInstance = this.isMultiTargetKey()
|
||||||
|
? await this.sourceCollection.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
// @ts-ignore
|
||||||
|
...this.sourceKeyValue,
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
: await this.sourceCollection.model.findOne({
|
||||||
where: {
|
where: {
|
||||||
[this.associationField.sourceKey]: this.sourceKeyValue,
|
[this.associationField.sourceKey]: this.sourceKeyValue,
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,10 @@ export interface FilterAble {
|
|||||||
filter: Filter;
|
filter: Filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TargetKey = string | number;
|
export type BaseTargetKey = string | number;
|
||||||
|
export type MultiTargetKey = Record<string, BaseTargetKey>;
|
||||||
|
export type TargetKey = BaseTargetKey | MultiTargetKey;
|
||||||
|
|
||||||
export type TK = TargetKey | TargetKey[];
|
export type TK = TargetKey | TargetKey[];
|
||||||
|
|
||||||
type FieldValue = string | number | bigint | boolean | Date | Buffer | null | FieldValue[] | FilterWithOperator;
|
type FieldValue = string | number | bigint | boolean | Date | Buffer | null | FieldValue[] | FilterWithOperator;
|
||||||
@ -211,7 +214,7 @@ class RelationRepositoryBuilder<R extends RelationRepository> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
of(id: string | number): R {
|
of(id: TargetKey): R {
|
||||||
if (!this.association) {
|
if (!this.association) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -312,13 +315,12 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (countOptions?.filterByTk) {
|
if (countOptions?.filterByTk) {
|
||||||
|
const optionParser = new OptionsParser(options, {
|
||||||
|
collection: this.collection,
|
||||||
|
});
|
||||||
|
|
||||||
options['where'] = {
|
options['where'] = {
|
||||||
[Op.and]: [
|
[Op.and]: [options['where'] || {}, optionParser.filterByTkToWhereOption()],
|
||||||
options['where'] || {},
|
|
||||||
{
|
|
||||||
[this.collection.filterTargetKey]: options.filterByTk,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,13 +333,11 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
delete queryOptions.include;
|
delete queryOptions.include;
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = await this.collection.model.count({
|
// @ts-ignore
|
||||||
|
return await this.collection.model.count({
|
||||||
...queryOptions,
|
...queryOptions,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async aggregate(options: AggregateOptions & { optionsTransformer?: (options: any) => any }): Promise<any> {
|
async aggregate(options: AggregateOptions & { optionsTransformer?: (options: any) => any }): Promise<any> {
|
||||||
@ -504,6 +504,10 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
return this.collection.model.findByPk(id);
|
return this.collection.model.findByPk(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findByTargetKey(targetKey: TargetKey) {
|
||||||
|
return this.findOne({ filterByTk: targetKey });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find one record from database
|
* Find one record from database
|
||||||
*
|
*
|
||||||
@ -767,15 +771,30 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filterByTk && !options.filter) {
|
if (filterByTk && !options.filter) {
|
||||||
return await this.model.destroy({
|
const where = [];
|
||||||
|
|
||||||
|
for (const tk of filterByTk) {
|
||||||
|
const optionParser = new OptionsParser(
|
||||||
|
{
|
||||||
|
filterByTk: tk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
collection: this.collection,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
where.push(optionParser.filterByTkToWhereOption());
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroyOptions = {
|
||||||
...options,
|
...options,
|
||||||
where: {
|
where: {
|
||||||
[modelFilterKey]: {
|
[Op.or]: where,
|
||||||
[Op.in]: filterByTk,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
return await this.model.destroy(destroyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.filter && isValidFilter(options.filter)) {
|
if (options.filter && isValidFilter(options.filter)) {
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
import Database from './database';
|
import Database from './database';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import { UpdateGuard } from './update-guard';
|
import { UpdateGuard } from './update-guard';
|
||||||
|
import { TargetKey } from './repository';
|
||||||
|
|
||||||
function isUndefinedOrNull(value: any) {
|
function isUndefinedOrNull(value: any) {
|
||||||
return typeof value === 'undefined' || value === null;
|
return typeof value === 'undefined' || value === null;
|
||||||
@ -58,7 +59,7 @@ type UpdateValue = { [key: string]: any };
|
|||||||
|
|
||||||
interface UpdateOptions extends Transactionable {
|
interface UpdateOptions extends Transactionable {
|
||||||
filter?: any;
|
filter?: any;
|
||||||
filterByTk?: number | string;
|
filterByTk?: TargetKey;
|
||||||
// 字段白名单
|
// 字段白名单
|
||||||
whitelist?: string[];
|
whitelist?: string[];
|
||||||
// 字段黑名单
|
// 字段黑名单
|
||||||
@ -454,6 +455,7 @@ export async function updateMultipleAssociation(
|
|||||||
const attributes = {
|
const attributes = {
|
||||||
[targetKey]: item[targetKey],
|
[targetKey]: item[targetKey],
|
||||||
};
|
};
|
||||||
|
|
||||||
const instance = association.target.build(attributes, { isNewRecord: false });
|
const instance = association.target.build(attributes, { isNewRecord: false });
|
||||||
setItems.push(instance);
|
setItems.push(instance);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,23 @@ export class SQLCollection extends Collection {
|
|||||||
super(options, context);
|
super(options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve */
|
||||||
|
get filterTargetKey() {
|
||||||
|
const targetKey = this.options?.filterTargetKey || 'id';
|
||||||
|
if (Array.isArray(targetKey)) {
|
||||||
|
return targetKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetKey && this.model.getAttributes()[targetKey]) {
|
||||||
|
return targetKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.model.primaryKeyAttributes.length > 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.model.primaryKeyAttribute;
|
||||||
|
}
|
||||||
|
|
||||||
isSql() {
|
isSql() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -32,18 +49,6 @@ export class SQLCollection extends Collection {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* istanbul ignore next -- @preserve */
|
|
||||||
get filterTargetKey() {
|
|
||||||
const targetKey = this.options?.filterTargetKey || 'id';
|
|
||||||
if (targetKey && this.model.getAttributes()[targetKey]) {
|
|
||||||
return targetKey;
|
|
||||||
}
|
|
||||||
if (this.model.primaryKeyAttributes.length > 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.model.primaryKeyAttribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelInit() {
|
modelInit() {
|
||||||
const { autoGenId, sql } = this.options;
|
const { autoGenId, sql } = this.options;
|
||||||
const model = class extends SQLModel {};
|
const model = class extends SQLModel {};
|
||||||
|
@ -77,6 +77,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
|
|||||||
),
|
),
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
enum: '{{filterTargetKeyOptions}}',
|
enum: '{{filterTargetKeyOptions}}',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
|
@ -62,13 +62,16 @@ export const SetFilterTargetKey = (props) => {
|
|||||||
{size === 'small' ? <br /> : ' '}
|
{size === 'small' ? <br /> : ' '}
|
||||||
<Space.Compact style={{ marginTop: 5 }}>
|
<Space.Compact style={{ marginTop: 5 }}>
|
||||||
<Select
|
<Select
|
||||||
onChange={(value, option) => {
|
onChange={(value, option: any) => {
|
||||||
|
console.log(value, option);
|
||||||
setFilterTargetKey(value);
|
setFilterTargetKey(value);
|
||||||
setTitle(option['label']);
|
setTitle(option.map((v) => v['label']).join(','));
|
||||||
}}
|
}}
|
||||||
placeholder={t('Select field')}
|
placeholder={t('Select field')}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
options={options}
|
options={options}
|
||||||
|
mode="multiple"
|
||||||
|
style={{ width: '200px' }}
|
||||||
/>
|
/>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@ -105,7 +108,7 @@ export const SetFilterTargetKey = (props) => {
|
|||||||
}
|
}
|
||||||
ctx?.refresh?.();
|
ctx?.refresh?.();
|
||||||
refresh();
|
refresh();
|
||||||
// await app.dataSourceManager.getDataSource(dataSourceKey).reload();
|
await app.dataSourceManager.getDataSource(dataSourceKey).reload();
|
||||||
collection.setOption('filterTargetKey', filterTargetKey);
|
collection.setOption('filterTargetKey', filterTargetKey);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -136,6 +136,10 @@ export const ConfigurationTable = () => {
|
|||||||
if (isFieldInherits && item.template === 'view') {
|
if (isFieldInherits && item.template === 'view') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
//目标表不支持联合主键表
|
||||||
|
if (field.props.name === 'target' && Array.isArray(item.filterTargetKey) && item.filterTargetKey.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const templateIncluded = !targetScope?.template || targetScope.template.includes(item.template);
|
const templateIncluded = !targetScope?.template || targetScope.template.includes(item.template);
|
||||||
const nameIncluded = !targetScope?.[field.props?.name] || targetScope[field.props.name].includes(item.name);
|
const nameIncluded = !targetScope?.[field.props?.name] || targetScope[field.props.name].includes(item.name);
|
||||||
return templateIncluded && nameIncluded;
|
return templateIncluded && nameIncluded;
|
||||||
@ -166,10 +170,10 @@ export const ConfigurationTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadFilterTargetKeys = async (field) => {
|
const loadFilterTargetKeys = async (field) => {
|
||||||
const { name } = field.form.values;
|
const { name, fields: targetFields } = field.form.values;
|
||||||
const { fields } = getCollection(name);
|
const { fields } = getCollection(name) || {};
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: fields,
|
data: fields || targetFields,
|
||||||
}).then(({ data }) => {
|
}).then(({ data }) => {
|
||||||
return data
|
return data
|
||||||
.filter((field) => {
|
.filter((field) => {
|
||||||
|
@ -13,7 +13,7 @@ import { tval } from '@nocobase/utils/client';
|
|||||||
import { NAMESPACE } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
function getUniqueKeyFromCollection(collection: Collection) {
|
function getUniqueKeyFromCollection(collection: Collection) {
|
||||||
return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
|
return collection?.filterTargetKey?.[0] || collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MBMFieldInterface extends CollectionFieldInterface {
|
export class MBMFieldInterface extends CollectionFieldInterface {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user