diff --git a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx index 82ea4e993..036215bf0 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx @@ -179,7 +179,6 @@ export const AddCollectionAction = (props) => { items, }; }, [category, items]); - return ( diff --git a/packages/core/client/src/collection-manager/Configuration/SyncFieldsAction.tsx b/packages/core/client/src/collection-manager/Configuration/SyncFieldsAction.tsx index b4019f8ab..3d2c64b6f 100644 --- a/packages/core/client/src/collection-manager/Configuration/SyncFieldsAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/SyncFieldsAction.tsx @@ -3,7 +3,7 @@ import { ArrayTable } from '@formily/antd-v5'; import { useField, useForm } from '@formily/react'; import { uid } from '@formily/shared'; import { Button } from 'antd'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, omit } from 'lodash'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../../api-client'; @@ -138,7 +138,7 @@ const useSyncFromDatabase = () => { try { await api.resource(`collections`).setFields({ filterByTk, - values: form.values, + values: omit(form.values, 'preview'), }); ctx.setVisible(false); await form.reset(); diff --git a/packages/core/client/src/collection-manager/templates/sql.tsx b/packages/core/client/src/collection-manager/templates/sql.tsx index 9fd7b11f4..a55a2a9de 100644 --- a/packages/core/client/src/collection-manager/templates/sql.tsx +++ b/packages/core/client/src/collection-manager/templates/sql.tsx @@ -1,8 +1,8 @@ import { Field } from '@formily/core'; -import { SQLInput, PreviewTable, FieldsConfigure, SQLRequestProvider } from './components/sql-collection'; -import { getConfigurableProperties } from './properties'; -import { i18n } from '../../i18n'; import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate'; +import { i18n } from '../../i18n'; +import { FieldsConfigure, PreviewTable, SQLInput, SQLRequestProvider } from './components/sql-collection'; +import { getConfigurableProperties } from './properties'; export class SqlCollectionTemplate extends CollectionTemplate { name = 'sql'; @@ -75,5 +75,13 @@ export class SqlCollectionTemplate extends CollectionTemplate { }, }, ...getConfigurableProperties('category'), + filterTargetKey: { + title: `{{ t("Filter target key")}}`, + type: 'single', + description: `{{t( "Filter data based on the specific field, with the requirement that the field value must be unique.")}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], + }, }; } diff --git a/packages/core/client/src/collection-manager/templates/view.tsx b/packages/core/client/src/collection-manager/templates/view.tsx index 3555910d7..c44883a3e 100644 --- a/packages/core/client/src/collection-manager/templates/view.tsx +++ b/packages/core/client/src/collection-manager/templates/view.tsx @@ -112,7 +112,7 @@ export class ViewCollectionTemplate extends CollectionTemplate { fields: { type: 'array', 'x-component': PreviewFields, - 'x-visible': '{{ createOnly }}', + 'x-hidden': '{{ !createOnly }}', 'x-reactions': { dependencies: ['name'], fulfill: { @@ -123,7 +123,7 @@ export class ViewCollectionTemplate extends CollectionTemplate { }, }, preview: { - type: 'object', + type: 'void', 'x-visible': '{{ createOnly }}', 'x-component': PreviewTable, 'x-reactions': { @@ -135,7 +135,14 @@ export class ViewCollectionTemplate extends CollectionTemplate { }, }, }, - + filterTargetKey: { + title: `{{ t("Filter target key")}}`, + type: 'single', + description: `{{t( "Filter data based on the specific field, with the requirement that the field value must be unique.")}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], + }, ...getConfigurableProperties('category', 'description'), }; } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 5f064576b..62404d2a8 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -916,6 +916,8 @@ "Separator": "分隔符", "Prefix": "前缀", "Suffix": "后缀", + "Filter target key":"筛选目标键", + "Filter data based on the specific field, with the requirement that the field value must be unique.": "根据特定的字段筛选数据,字段值必须具备唯一性。", "Multiply by": "乘以", "Divide by": "除以", "Scientifix notation": "科学计数法", diff --git a/packages/core/database/src/sql-collection/sql-collection.ts b/packages/core/database/src/sql-collection/sql-collection.ts index 198e68fc6..80a395d3c 100644 --- a/packages/core/database/src/sql-collection/sql-collection.ts +++ b/packages/core/database/src/sql-collection/sql-collection.ts @@ -18,6 +18,17 @@ export class SqlCollection extends Collection { return undefined; } + 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 {}; diff --git a/packages/core/test/vitest.mjs b/packages/core/test/vitest.mjs index bef560a64..a9a5e09ce 100644 --- a/packages/core/test/vitest.mjs +++ b/packages/core/test/vitest.mjs @@ -60,9 +60,7 @@ const defineCommonConfig = () => { testTimeout: 300000, hookTimeout: 300000, silent: !!process.env.GITHUB_ACTIONS, - include: [ - 'packages/**/src/**/__tests__/**/*.test.{ts,tsx}', - ], + include: ['packages/**/src/**/__tests__/**/*.test.{ts,tsx}'], exclude: [ '**/demos/**', '**/node_modules/**', @@ -86,9 +84,7 @@ const defineCommonConfig = () => { ], coverage: { provider: 'istanbul', - include: [ - 'packages/**/src/**/*.{ts,tsx}', - ], + include: ['packages/**/src/**/*.{ts,tsx}'], exclude: [ '**/demos/**', '**/swagger/**', @@ -100,31 +96,31 @@ const defineCommonConfig = () => { '**/e2e/**', '**/client.js', '**/server.js', - '**/*.d.ts' - ] - } - } - }) -} + '**/*.d.ts', + ], + }, + }, + }); +}; function getExclude(isServer) { return [ `packages/core/${isServer ? '' : '!'}(${CORE_CLIENT_PACKAGES.join('|')})/**/*`, `packages/**/src/${isServer ? 'client' : 'server'}/**/*`, - ] + ]; } const defineServerConfig = () => { return vitestConfig({ test: { setupFiles: resolve(__dirname, './setup/server.ts'), - exclude: getExclude(true) + exclude: getExclude(true), }, coverage: { - exclude: getExclude(true) - } - }) -} + exclude: getExclude(true), + }, + }); +}; const defineClientConfig = () => { return vitestConfig({ @@ -132,6 +128,7 @@ const defineClientConfig = () => { define: { 'process.env.__TEST__': true, 'process.env.__E2E__': false, + global: 'window', }, test: { environment: 'jsdom', @@ -144,14 +141,14 @@ const defineClientConfig = () => { }, exclude: getExclude(false), coverage: { - exclude: getExclude(false) - } - } - }) -} + exclude: getExclude(false), + }, + }, + }); +}; export const getFilterInclude = (isServer, isCoverage) => { - let filterFileOrDir = process.argv.slice(2).find(arg => !arg.startsWith('-')); + let filterFileOrDir = process.argv.slice(2).find((arg) => !arg.startsWith('-')); if (!filterFileOrDir) return; const absPath = path.join(process.cwd(), filterFileOrDir); const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory(); @@ -160,7 +157,7 @@ export const getFilterInclude = (isServer, isCoverage) => { return [filterFileOrDir]; } - const suffix = isCoverage ? `**/*.{ts,tsx}` : `**/__tests__/**/*.{test,spec}.{ts,tsx}` + const suffix = isCoverage ? `**/*.{ts,tsx}` : `**/__tests__/**/*.{test,spec}.{ts,tsx}`; // 判断是否为包目录,如果不是包目录,则只测试当前目录 const isPackage = fs.existsSync(path.join(absPath, 'package.json')); @@ -176,10 +173,10 @@ export const getFilterInclude = (isServer, isCoverage) => { // 插件目录,区分 client 和 server return [`${filterFileOrDir}/src/${isServer ? 'server' : 'client'}/${suffix}`]; -} +}; export const getReportsDirectory = (isServer) => { - let filterFileOrDir = process.argv.slice(2).find(arg => !arg.startsWith('-')); + let filterFileOrDir = process.argv.slice(2).find((arg) => !arg.startsWith('-')); if (!filterFileOrDir) return; const isPackage = fs.existsSync(path.join(process.cwd(), filterFileOrDir, 'package.json')); if (isPackage) { @@ -193,11 +190,13 @@ export const getReportsDirectory = (isServer) => { return reportsDirectory; } -} +}; export const defineConfig = () => { const isServer = process.env.TEST_ENV === 'server-side'; - const config = vitestConfig(mergeConfig(defineCommonConfig(), isServer ? defineServerConfig() : defineClientConfig())); + const config = vitestConfig( + mergeConfig(defineCommonConfig(), isServer ? defineServerConfig() : defineClientConfig()), + ); const isCoverage = process.argv.includes('--coverage'); if (!isCoverage) { diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-with-filter-target-key.test.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-with-filter-target-key.test.ts new file mode 100644 index 000000000..267fdba88 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-with-filter-target-key.test.ts @@ -0,0 +1,115 @@ +import Database, { Repository } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '../index'; +import { uid } from '@nocobase/utils'; + +describe('view collection', function () { + let db: Database; + let app: Application; + + let collectionRepository: Repository; + + let fieldsRepository: Repository; + + beforeEach(async () => { + app = await createApp({ + database: { + tablePrefix: '', + }, + }); + + db = app.db; + + collectionRepository = db.getCollection('collections').repository; + fieldsRepository = db.getCollection('fields').repository; + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should set view collection filterTargetKey', async () => { + await collectionRepository.create({ + values: { + name: 'tests', + autoGenId: false, + timestamps: false, + fields: [ + { + name: 'test', + type: 'string', + }, + { + name: 'uuid', + type: 'uuid', + }, + ], + }, + context: {}, + }); + + // insert some data + await db.getCollection('tests').repository.create({ + values: [ + { + test: 'test1', + }, + { + test: 'test2', + }, + { + test: 'test3', + }, + ], + }); + + const viewName = `test_view_${uid(6)}`; + await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); + + const createSQL = `CREATE VIEW ${viewName} AS SELECT * FROM ${db.getCollection('tests').quotedTableName()}`; + + await db.sequelize.query(createSQL); + + // create view collection + await collectionRepository.create({ + values: { + name: 'view_tests', + autoGenId: false, + timestamps: false, + view: true, + viewName, + fields: [ + { name: 'test', type: 'string' }, + { name: 'uuid', type: 'uuid' }, + ], + schema: db.inDialect('postgres') ? 'public' : undefined, + }, + context: {}, + }); + + // update filterTargetKey Options + await collectionRepository.update({ + values: { + filterTargetKey: 'uuid', + }, + filter: { + name: 'view_tests', + }, + context: {}, + }); + + expect(db.getCollection('view_tests').options['filterTargetKey']).toBe('uuid'); + + // get view collection items + const items = await db.getCollection('view_tests').repository.find(); + const uuidVal = items[0].get('uuid'); + console.log('uuidVal:', uuidVal); + + // filter item by uuid + const item = await db.getCollection('view_tests').repository.findOne({ + filterByTk: uuidVal, + }); + + expect(item.get('uuid')).toBe(uuidVal); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts index 9cbabd7db..0136f7f61 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts @@ -60,7 +60,7 @@ export class CollectionManagerPlugin extends Plugin { this.app.db.on('collections.beforeCreate', beforeCreateForViewCollection(this.db)); this.app.db.on( - 'collections.afterCreateWithAssociations', + 'collections.afterSaveWithAssociations', async (model: CollectionModel, { context, transaction }) => { if (context) { await model.migrate({ diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx index a77223ac3..cebbddf9d 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx @@ -19,6 +19,7 @@ import { CollectionCategroriesContext, FieldSummary, TemplateSummary, + useRequest, } from '@nocobase/client'; import { CollectionFields } from './CollectionFields'; import { collectionSchema } from './schemas/collections'; @@ -100,6 +101,7 @@ export const ConfigurationTable = () => { const api = useAPIClient(); const resource = api.resource('dbViews'); const compile = useCompile(); + const form = useForm(); /** * @@ -131,6 +133,7 @@ export const ConfigurationTable = () => { value: item.name, })); }; + const loadCategories = async () => { return data.data.map((item: any) => ({ label: compile(item.name), @@ -150,6 +153,20 @@ export const ConfigurationTable = () => { }); }; + const loadFilterTargetKeys = async (field) => { + const { fields } = field.form.values; + return Promise.resolve({ + data: fields, + }).then(({ data }) => { + return data?.map((item: any) => { + return { + label: compile(item.uiSchema?.title) || item.name, + value: item.name, + }; + }); + }); + }; + const loadStorages = async () => { return api .resource('storages') @@ -178,6 +195,7 @@ export const ConfigurationTable = () => { CollectionFields, }} scope={{ + loadFilterTargetKeys, useDestroySubField, useBulkDestroySubField, useSelectedRowKeys,