mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +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
|
||||
async move(sourceInstanceId: TargetKey, targetInstanceId: TargetKey, options: MoveOptions = {}) {
|
||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
||||
const targetInstance = await this.collection.repository.findById(targetInstanceId);
|
||||
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
|
||||
const targetInstance = await this.collection.repository.findByTargetKey(targetInstanceId);
|
||||
|
||||
if (this.scopeKey && sourceInstance.get(this.scopeKey) !== targetInstance.get(this.scopeKey)) {
|
||||
await sourceInstance.update({
|
||||
@ -88,7 +88,7 @@ export class SortAbleCollection {
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
if (targetScopeValue && sourceInstance.get(this.scopeKey) !== targetScopeValue) {
|
||||
@ -108,7 +108,7 @@ export class SortAbleCollection {
|
||||
}
|
||||
|
||||
async sticky(sourceInstanceId: TargetKey) {
|
||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
||||
const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId);
|
||||
await sourceInstance.update(
|
||||
{
|
||||
[this.field.get('name')]: 0,
|
||||
|
@ -11,6 +11,7 @@ import { Field, GeneralField } from '@formily/core';
|
||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { Col, Row } from 'antd';
|
||||
import merge from 'deepmerge';
|
||||
import { isArray } from 'lodash';
|
||||
import template from 'lodash/template';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -307,7 +308,15 @@ export const useFilterByTk = () => {
|
||||
const association = getCollectionField(assoc);
|
||||
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'];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -183,9 +183,10 @@ export const useTableFieldProps = () => {
|
||||
rowKey: (record: any) => {
|
||||
return field.value?.indexOf?.(record);
|
||||
},
|
||||
onRowSelectionChange(selectedRowKeys) {
|
||||
onRowSelectionChange(selectedRowKeys, selectedRowData) {
|
||||
ctx.field.data = ctx?.field?.data || {};
|
||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||
ctx.field.data.selectedRowData = selectedRowData;
|
||||
},
|
||||
onChange({ current, pageSize }) {
|
||||
ctx.service.run({ page: current, pageSize });
|
||||
|
@ -319,9 +319,10 @@ export const useTableSelectorProps = () => {
|
||||
dragSort: false,
|
||||
rowKey: ctx.rowKey || 'id',
|
||||
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.selectedRowKeys = selectedRowKeys;
|
||||
ctx.field.data.selectedRowData = selectedRowData;
|
||||
},
|
||||
async onRowDragEnd({ from, to }) {
|
||||
await ctx.resource.move({
|
||||
|
@ -1080,13 +1080,25 @@ export const useBulkDestroyActionProps = () => {
|
||||
const { field } = useBlockRequestContext();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const { setSubmitted } = useActionContext();
|
||||
const collection = useCollection_deprecated();
|
||||
const { filterTargetKey } = collection;
|
||||
return {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
await resource.destroy({
|
||||
filterByTk: field.data?.selectedRowKeys,
|
||||
filterByTk,
|
||||
});
|
||||
field.data.selectedRowKeys = [];
|
||||
const currentPage = service.params[0]?.page;
|
||||
@ -1098,7 +1110,7 @@ export const useBulkDestroyActionProps = () => {
|
||||
callBack?.();
|
||||
}
|
||||
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.")}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
||||
},
|
||||
footer: {
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { getUniqueKeyFromCollection } from './o2m';
|
||||
import { getUniqueKeyFromCollection } from './utils';
|
||||
import { defaultProps, relationshipType, reverseFieldProperties } from './properties';
|
||||
|
||||
export class M2MFieldInterface extends CollectionFieldInterface {
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { ISchema } from '@formily/react';
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { getUniqueKeyFromCollection } from './o2m';
|
||||
import { getUniqueKeyFromCollection } from './utils';
|
||||
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
||||
|
||||
export class M2OFieldInterface extends CollectionFieldInterface {
|
||||
|
@ -8,10 +8,9 @@
|
||||
*/
|
||||
|
||||
import { ISchema } from '@formily/react';
|
||||
import { Collection } from '../../data-source';
|
||||
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
||||
|
||||
import { getUniqueKeyFromCollection } from './utils';
|
||||
export class O2MFieldInterface extends CollectionFieldInterface {
|
||||
name = 'o2m';
|
||||
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 { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
|
||||
import { getUniqueKeyFromCollection } from './o2m';
|
||||
import { getUniqueKeyFromCollection } from './utils';
|
||||
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
|
||||
|
||||
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.")}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
'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.")}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
|
||||
},
|
||||
...getConfigurableProperties('category', 'description'),
|
||||
|
@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { SchemaKey } from '@formily/json-schema';
|
||||
import qs from 'qs';
|
||||
import type { DataSource } from '../data-source';
|
||||
import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection';
|
||||
|
||||
@ -159,10 +160,23 @@ export class CollectionManager {
|
||||
);
|
||||
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) {
|
||||
const key = collectionOrAssociation.filterTargetKey || collectionOrAssociation.getPrimaryKey() || 'id';
|
||||
return collectionRecordOrAssociationRecord[key];
|
||||
const targetKey = getTargetKey(collectionOrAssociation);
|
||||
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
|
||||
}
|
||||
|
||||
if (collectionOrAssociation.includes('.')) {
|
||||
@ -186,9 +200,8 @@ export class CollectionManager {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = targetCollection?.filterTargetKey || targetCollection?.getPrimaryKey() || 'id';
|
||||
return collectionRecordOrAssociationRecord[key];
|
||||
const targetKey = getTargetKey(targetCollection);
|
||||
return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord);
|
||||
}
|
||||
|
||||
getSourceKeyByAssociation(associationName: string) {
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { IResource } from '@nocobase/sdk';
|
||||
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
|
||||
|
||||
import { isArray } from 'lodash';
|
||||
import { useAPIClient } from '../../api-client';
|
||||
import { useCollectionManager } from '../collection';
|
||||
import { CollectionRecord } from '../collection-record';
|
||||
@ -34,8 +34,17 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
|
||||
if (association && parentRecord) {
|
||||
const sourceKey = cm.getSourceKeyByAssociation(association);
|
||||
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];
|
||||
}
|
||||
}
|
||||
}, [association, sourceId, parentRecord]);
|
||||
|
||||
const resource = useMemo(() => {
|
||||
|
@ -16,7 +16,6 @@ import { findFilterTargets } from '../../../../../block-provider/hooks';
|
||||
import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider';
|
||||
import { mergeFilter } from '../../../../../filter-provider/utils';
|
||||
import { removeNullCondition } from '../../../../../schema-component';
|
||||
import { useCollection } from '../../../../../data-source';
|
||||
|
||||
export const useTableBlockProps = () => {
|
||||
const field = useField<ArrayField>();
|
||||
@ -58,9 +57,10 @@ export const useTableBlockProps = () => {
|
||||
dragSort: ctx.dragSort && ctx.dragSortBy,
|
||||
rowKey: ctx.rowKey || 'id',
|
||||
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.selectedRowKeys = selectedRowKeys;
|
||||
ctx.field.data.selectedRowData = selectedRowData;
|
||||
ctx?.field?.onRowSelect?.(selectedRowKeys);
|
||||
}, []),
|
||||
onRowDragEnd: useCallback(
|
||||
|
@ -53,7 +53,7 @@ export const useTableSelectorProps = () => {
|
||||
type: multiple ? 'checkbox' : 'radio',
|
||||
selectedRowKeys: rcSelectRows
|
||||
?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey]))
|
||||
.map((item) => item[rowKey]),
|
||||
.map((item) => item[rowKey]?.toString()),
|
||||
},
|
||||
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||
if (multiple) {
|
||||
|
@ -169,7 +169,7 @@ const PagePopupsItemProvider: FC<{
|
||||
collection={params.collection || context.collection}
|
||||
association={context.association}
|
||||
sourceId={params.sourceid}
|
||||
filterByTk={params.filterbytk}
|
||||
filterByTk={parseQueryString(params.filterbytk)}
|
||||
// @ts-ignore
|
||||
record={storedContext.record}
|
||||
parentRecord={storedContext.parentRecord}
|
||||
@ -465,3 +465,25 @@ function findSchemaByUid(uid: string, rootSchema: Schema, resultRef: { value: Sc
|
||||
});
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
let label = compile(option[fieldNames.label]);
|
||||
|
||||
|
@ -521,6 +521,9 @@ export const Table: any = withDynamicSchemaProps(
|
||||
* @returns
|
||||
*/
|
||||
const defaultRowKey = useCallback((record: any) => {
|
||||
if (rowKey) {
|
||||
return getRowKey(record);
|
||||
}
|
||||
if (record.key) {
|
||||
return record.key;
|
||||
}
|
||||
@ -536,13 +539,21 @@ export const Table: any = withDynamicSchemaProps(
|
||||
|
||||
const getRowKey = useCallback(
|
||||
(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();
|
||||
} else {
|
||||
// 如果 rowKey 是函数或未提供,使用 defaultRowKey
|
||||
return (rowKey ?? defaultRowKey)(record)?.toString();
|
||||
}
|
||||
},
|
||||
[rowKey, defaultRowKey],
|
||||
[JSON.stringify(rowKey), defaultRowKey],
|
||||
);
|
||||
|
||||
const dataSourceKeys = field?.value?.map?.(getRowKey);
|
||||
@ -623,6 +634,7 @@ export const Table: any = withDynamicSchemaProps(
|
||||
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
||||
field.data = field.data || {};
|
||||
field.data.selectedRowKeys = selectedRowKeys;
|
||||
field.data.selectedRowData = selectedRows;
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||
},
|
||||
@ -720,7 +732,7 @@ export const Table: any = withDynamicSchemaProps(
|
||||
|
||||
const rowClassName = useCallback(
|
||||
(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : ''),
|
||||
[selectedRow, highlightRow, rowKey],
|
||||
[selectedRow, highlightRow, JSON.stringify(rowKey)],
|
||||
);
|
||||
|
||||
const onExpandValue = useCallback(
|
||||
@ -771,7 +783,7 @@ export const Table: any = withDynamicSchemaProps(
|
||||
<SortableWrapper>
|
||||
<MemoizedAntdTable
|
||||
ref={tableSizeRefCallback}
|
||||
rowKey={rowKey ?? defaultRowKey}
|
||||
rowKey={defaultRowKey}
|
||||
dataSource={dataSource}
|
||||
tableLayout="auto"
|
||||
{...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;
|
||||
writableView?: boolean;
|
||||
|
||||
filterTargetKey?: string;
|
||||
filterTargetKey?: string | string[];
|
||||
fields?: FieldOptions[];
|
||||
model?: string | ModelStatic<Model>;
|
||||
repository?: string | RepositoryType;
|
||||
@ -169,14 +169,21 @@ export class Collection<
|
||||
this.setSortable(options.sortable);
|
||||
}
|
||||
|
||||
get filterTargetKey() {
|
||||
get filterTargetKey(): string | string[] {
|
||||
const targetKey = this.options?.filterTargetKey;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ import extendOperators from './operators';
|
||||
import QueryInterface from './query-interface/query-interface';
|
||||
import buildQueryInterface from './query-interface/query-interface-builder';
|
||||
import { RelationRepository } from './relation-repository/relation-repository';
|
||||
import { Repository } from './repository';
|
||||
import { Repository, TargetKey } from './repository';
|
||||
import {
|
||||
AfterDefineCollectionListener,
|
||||
BeforeDefineCollectionListener,
|
||||
@ -634,11 +634,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
|
||||
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('.');
|
||||
if (relation) {
|
||||
return this.getRepository(collection)?.relation(relation)?.of(relationId) as R;
|
||||
|
@ -76,17 +76,42 @@ export class OptionsParser {
|
||||
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() {
|
||||
const queryParams = this.filterParser.toSequelizeParams();
|
||||
|
||||
if (this.options?.filterByTk) {
|
||||
const filterByTkWhere = this.filterByTkToWhereOption();
|
||||
queryParams.where = {
|
||||
[Op.and]: [
|
||||
queryParams.where,
|
||||
{
|
||||
[this.context.targetKey || this.collection.filterTargetKey]: this.options.filterByTk,
|
||||
},
|
||||
],
|
||||
[Op.and]: [queryParams.where, filterByTkWhere],
|
||||
};
|
||||
}
|
||||
|
||||
@ -115,7 +140,7 @@ export class OptionsParser {
|
||||
|
||||
let defaultSortField = this.model.primaryKeyAttribute;
|
||||
|
||||
if (!defaultSortField && this.collection.filterTargetKey) {
|
||||
if (!defaultSortField && this.collection.filterTargetKey && !Array.isArray(this.collection.filterTargetKey)) {
|
||||
defaultSortField = this.collection.filterTargetKey;
|
||||
}
|
||||
|
||||
|
@ -17,18 +17,20 @@ export class HasManyRepository extends MultipleRelationRepository {
|
||||
async find(options?: FindOptions): Promise<any> {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
|
||||
const addFilter = {
|
||||
[this.association.foreignKey]: this.sourceKeyValue,
|
||||
};
|
||||
const targetFilterOptions = await this.targetRepositoryFilterOptionsBySourceValue();
|
||||
|
||||
if (options?.filterByTk) {
|
||||
addFilter[this.associationField.targetKey] = options.filterByTk;
|
||||
const findOptionsOmit = ['where', 'values', 'attributes'];
|
||||
|
||||
if (options?.filterByTk && !this.isMultiTargetKey(options.filterByTk)) {
|
||||
// @ts-ignore
|
||||
targetFilterOptions[this.associationField.targetKey] = options.filterByTk;
|
||||
findOptionsOmit.push('filterByTk');
|
||||
}
|
||||
|
||||
const findOptions = {
|
||||
...omit(options, ['filterByTk', 'where', 'values', 'attributes']),
|
||||
...omit(options, findOptionsOmit),
|
||||
filter: {
|
||||
$and: [options.filter || {}, addFilter],
|
||||
$and: [options?.filter || {}, targetFilterOptions],
|
||||
},
|
||||
};
|
||||
|
||||
@ -37,14 +39,11 @@ export class HasManyRepository extends MultipleRelationRepository {
|
||||
|
||||
async aggregate(options: AggregateOptions) {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
const addFilter = {
|
||||
[this.association.foreignKey]: this.sourceKeyValue,
|
||||
};
|
||||
|
||||
const aggOptions = {
|
||||
...options,
|
||||
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 {
|
||||
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> {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
|
||||
@ -49,9 +64,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
||||
const appendFilter = {
|
||||
isPivotFilter: true,
|
||||
association: pivotAssoc,
|
||||
where: {
|
||||
[association.foreignKey]: this.sourceKeyValue,
|
||||
},
|
||||
where: await this.targetRepositoryFilterOptionsBySourceValue(),
|
||||
};
|
||||
|
||||
return targetRepository.find({
|
||||
|
@ -16,7 +16,7 @@ import { RelationField } from '../fields/relation-field';
|
||||
import FilterParser from '../filter-parser';
|
||||
import { Model } from '../model';
|
||||
import { OptionsParser } from '../options-parser';
|
||||
import { CreateOptions, Filter, FindOptions } from '../repository';
|
||||
import { CreateOptions, Filter, FindOptions, TargetKey } from '../repository';
|
||||
import { updateAssociations } from '../update-associations';
|
||||
import { UpdateGuard } from '../update-guard';
|
||||
|
||||
@ -31,17 +31,19 @@ export abstract class RelationRepository {
|
||||
targetCollection: Collection;
|
||||
associationName: string;
|
||||
associationField: RelationField;
|
||||
sourceKeyValue: string | number;
|
||||
sourceKeyValue: TargetKey;
|
||||
sourceInstance: Model;
|
||||
db: Database;
|
||||
database: Database;
|
||||
|
||||
constructor(sourceCollection: Collection, association: string, sourceKeyValue: string | number) {
|
||||
constructor(sourceCollection: Collection, association: string, sourceKeyValue: TargetKey) {
|
||||
this.db = sourceCollection.context.database;
|
||||
this.database = this.db;
|
||||
|
||||
this.sourceCollection = sourceCollection;
|
||||
this.sourceKeyValue = sourceKeyValue;
|
||||
|
||||
this.setSourceKeyValue(sourceKeyValue);
|
||||
|
||||
this.associationName = 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);
|
||||
}
|
||||
|
||||
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() {
|
||||
return this.db.getCollection(this.targetModel.name);
|
||||
}
|
||||
@ -145,7 +165,15 @@ export abstract class RelationRepository {
|
||||
|
||||
async getSourceModel(transaction?: Transaction) {
|
||||
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: {
|
||||
[this.associationField.sourceKey]: this.sourceKeyValue,
|
||||
},
|
||||
|
@ -59,7 +59,10 @@ export interface FilterAble {
|
||||
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[];
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -312,13 +315,12 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
}
|
||||
|
||||
if (countOptions?.filterByTk) {
|
||||
const optionParser = new OptionsParser(options, {
|
||||
collection: this.collection,
|
||||
});
|
||||
|
||||
options['where'] = {
|
||||
[Op.and]: [
|
||||
options['where'] || {},
|
||||
{
|
||||
[this.collection.filterTargetKey]: options.filterByTk,
|
||||
},
|
||||
],
|
||||
[Op.and]: [options['where'] || {}, optionParser.filterByTkToWhereOption()],
|
||||
};
|
||||
}
|
||||
|
||||
@ -331,13 +333,11 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
delete queryOptions.include;
|
||||
}
|
||||
|
||||
const count = await this.collection.model.count({
|
||||
// @ts-ignore
|
||||
return await this.collection.model.count({
|
||||
...queryOptions,
|
||||
transaction,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return count;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
findByTargetKey(targetKey: TargetKey) {
|
||||
return this.findOne({ filterByTk: targetKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find one record from database
|
||||
*
|
||||
@ -767,15 +771,30 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
}
|
||||
|
||||
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,
|
||||
where: {
|
||||
[modelFilterKey]: {
|
||||
[Op.in]: filterByTk,
|
||||
},
|
||||
[Op.or]: where,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
};
|
||||
|
||||
return await this.model.destroy(destroyOptions);
|
||||
}
|
||||
|
||||
if (options.filter && isValidFilter(options.filter)) {
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
import Database from './database';
|
||||
import { Model } from './model';
|
||||
import { UpdateGuard } from './update-guard';
|
||||
import { TargetKey } from './repository';
|
||||
|
||||
function isUndefinedOrNull(value: any) {
|
||||
return typeof value === 'undefined' || value === null;
|
||||
@ -58,7 +59,7 @@ type UpdateValue = { [key: string]: any };
|
||||
|
||||
interface UpdateOptions extends Transactionable {
|
||||
filter?: any;
|
||||
filterByTk?: number | string;
|
||||
filterByTk?: TargetKey;
|
||||
// 字段白名单
|
||||
whitelist?: string[];
|
||||
// 字段黑名单
|
||||
@ -454,6 +455,7 @@ export async function updateMultipleAssociation(
|
||||
const attributes = {
|
||||
[targetKey]: item[targetKey],
|
||||
};
|
||||
|
||||
const instance = association.target.build(attributes, { isNewRecord: false });
|
||||
setItems.push(instance);
|
||||
}
|
||||
|
@ -20,6 +20,23 @@ export class SQLCollection extends Collection {
|
||||
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() {
|
||||
return true;
|
||||
}
|
||||
@ -32,18 +49,6 @@ export class SQLCollection extends Collection {
|
||||
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() {
|
||||
const { autoGenId, sql } = this.options;
|
||||
const model = class extends SQLModel {};
|
||||
|
@ -77,6 +77,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
|
||||
),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
},
|
||||
enum: '{{filterTargetKeyOptions}}',
|
||||
},
|
||||
footer: {
|
||||
|
@ -62,13 +62,16 @@ export const SetFilterTargetKey = (props) => {
|
||||
{size === 'small' ? <br /> : ' '}
|
||||
<Space.Compact style={{ marginTop: 5 }}>
|
||||
<Select
|
||||
onChange={(value, option) => {
|
||||
onChange={(value, option: any) => {
|
||||
console.log(value, option);
|
||||
setFilterTargetKey(value);
|
||||
setTitle(option['label']);
|
||||
setTitle(option.map((v) => v['label']).join(','));
|
||||
}}
|
||||
placeholder={t('Select field')}
|
||||
size={'small'}
|
||||
options={options}
|
||||
mode="multiple"
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
<Popconfirm
|
||||
placement="bottom"
|
||||
@ -105,7 +108,7 @@ export const SetFilterTargetKey = (props) => {
|
||||
}
|
||||
ctx?.refresh?.();
|
||||
refresh();
|
||||
// await app.dataSourceManager.getDataSource(dataSourceKey).reload();
|
||||
await app.dataSourceManager.getDataSource(dataSourceKey).reload();
|
||||
collection.setOption('filterTargetKey', filterTargetKey);
|
||||
}}
|
||||
>
|
||||
|
@ -136,6 +136,10 @@ export const ConfigurationTable = () => {
|
||||
if (isFieldInherits && item.template === 'view') {
|
||||
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 nameIncluded = !targetScope?.[field.props?.name] || targetScope[field.props.name].includes(item.name);
|
||||
return templateIncluded && nameIncluded;
|
||||
@ -166,10 +170,10 @@ export const ConfigurationTable = () => {
|
||||
};
|
||||
|
||||
const loadFilterTargetKeys = async (field) => {
|
||||
const { name } = field.form.values;
|
||||
const { fields } = getCollection(name);
|
||||
const { name, fields: targetFields } = field.form.values;
|
||||
const { fields } = getCollection(name) || {};
|
||||
return Promise.resolve({
|
||||
data: fields,
|
||||
data: fields || targetFields,
|
||||
}).then(({ data }) => {
|
||||
return data
|
||||
.filter((field) => {
|
||||
|
@ -13,7 +13,7 @@ import { tval } from '@nocobase/utils/client';
|
||||
import { NAMESPACE } from './locale';
|
||||
|
||||
function getUniqueKeyFromCollection(collection: Collection) {
|
||||
return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
|
||||
return collection?.filterTargetKey?.[0] || collection?.filterTargetKey || collection?.getPrimaryKey() || 'id';
|
||||
}
|
||||
|
||||
export class MBMFieldInterface extends CollectionFieldInterface {
|
||||
|
Loading…
x
Reference in New Issue
Block a user