refactor: main data source view collection support filterTargetKey (#3818)

This commit is contained in:
katherinehhh 2024-04-05 12:11:51 +08:00 committed by GitHub
parent aa96a16d1d
commit a4cbec293d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 198 additions and 39 deletions

View File

@ -179,7 +179,6 @@ export const AddCollectionAction = (props) => {
items, items,
}; };
}, [category, items]); }, [category, items]);
return ( return (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>

View File

@ -3,7 +3,7 @@ import { ArrayTable } from '@formily/antd-v5';
import { useField, useForm } from '@formily/react'; import { useField, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Button } from 'antd'; import { Button } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep, omit } from 'lodash';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
@ -138,7 +138,7 @@ const useSyncFromDatabase = () => {
try { try {
await api.resource(`collections`).setFields({ await api.resource(`collections`).setFields({
filterByTk, filterByTk,
values: form.values, values: omit(form.values, 'preview'),
}); });
ctx.setVisible(false); ctx.setVisible(false);
await form.reset(); await form.reset();

View File

@ -1,8 +1,8 @@
import { Field } from '@formily/core'; 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 { 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 { export class SqlCollectionTemplate extends CollectionTemplate {
name = 'sql'; name = 'sql';
@ -75,5 +75,13 @@ export class SqlCollectionTemplate extends CollectionTemplate {
}, },
}, },
...getConfigurableProperties('category'), ...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)}}'],
},
}; };
} }

View File

@ -112,7 +112,7 @@ export class ViewCollectionTemplate extends CollectionTemplate {
fields: { fields: {
type: 'array', type: 'array',
'x-component': PreviewFields, 'x-component': PreviewFields,
'x-visible': '{{ createOnly }}', 'x-hidden': '{{ !createOnly }}',
'x-reactions': { 'x-reactions': {
dependencies: ['name'], dependencies: ['name'],
fulfill: { fulfill: {
@ -123,7 +123,7 @@ export class ViewCollectionTemplate extends CollectionTemplate {
}, },
}, },
preview: { preview: {
type: 'object', type: 'void',
'x-visible': '{{ createOnly }}', 'x-visible': '{{ createOnly }}',
'x-component': PreviewTable, 'x-component': PreviewTable,
'x-reactions': { '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'), ...getConfigurableProperties('category', 'description'),
}; };
} }

View File

@ -916,6 +916,8 @@
"Separator": "分隔符", "Separator": "分隔符",
"Prefix": "前缀", "Prefix": "前缀",
"Suffix": "后缀", "Suffix": "后缀",
"Filter target key":"筛选目标键",
"Filter data based on the specific field, with the requirement that the field value must be unique.": "根据特定的字段筛选数据,字段值必须具备唯一性。",
"Multiply by": "乘以", "Multiply by": "乘以",
"Divide by": "除以", "Divide by": "除以",
"Scientifix notation": "科学计数法", "Scientifix notation": "科学计数法",

View File

@ -18,6 +18,17 @@ export class SqlCollection extends Collection {
return undefined; 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() { modelInit() {
const { autoGenId, sql } = this.options; const { autoGenId, sql } = this.options;
const model = class extends SQLModel {}; const model = class extends SQLModel {};

View File

@ -60,9 +60,7 @@ const defineCommonConfig = () => {
testTimeout: 300000, testTimeout: 300000,
hookTimeout: 300000, hookTimeout: 300000,
silent: !!process.env.GITHUB_ACTIONS, silent: !!process.env.GITHUB_ACTIONS,
include: [ include: ['packages/**/src/**/__tests__/**/*.test.{ts,tsx}'],
'packages/**/src/**/__tests__/**/*.test.{ts,tsx}',
],
exclude: [ exclude: [
'**/demos/**', '**/demos/**',
'**/node_modules/**', '**/node_modules/**',
@ -86,9 +84,7 @@ const defineCommonConfig = () => {
], ],
coverage: { coverage: {
provider: 'istanbul', provider: 'istanbul',
include: [ include: ['packages/**/src/**/*.{ts,tsx}'],
'packages/**/src/**/*.{ts,tsx}',
],
exclude: [ exclude: [
'**/demos/**', '**/demos/**',
'**/swagger/**', '**/swagger/**',
@ -100,31 +96,31 @@ const defineCommonConfig = () => {
'**/e2e/**', '**/e2e/**',
'**/client.js', '**/client.js',
'**/server.js', '**/server.js',
'**/*.d.ts' '**/*.d.ts',
] ],
} },
} },
}) });
} };
function getExclude(isServer) { function getExclude(isServer) {
return [ return [
`packages/core/${isServer ? '' : '!'}(${CORE_CLIENT_PACKAGES.join('|')})/**/*`, `packages/core/${isServer ? '' : '!'}(${CORE_CLIENT_PACKAGES.join('|')})/**/*`,
`packages/**/src/${isServer ? 'client' : 'server'}/**/*`, `packages/**/src/${isServer ? 'client' : 'server'}/**/*`,
] ];
} }
const defineServerConfig = () => { const defineServerConfig = () => {
return vitestConfig({ return vitestConfig({
test: { test: {
setupFiles: resolve(__dirname, './setup/server.ts'), setupFiles: resolve(__dirname, './setup/server.ts'),
exclude: getExclude(true) exclude: getExclude(true),
}, },
coverage: { coverage: {
exclude: getExclude(true) exclude: getExclude(true),
} },
}) });
} };
const defineClientConfig = () => { const defineClientConfig = () => {
return vitestConfig({ return vitestConfig({
@ -132,6 +128,7 @@ const defineClientConfig = () => {
define: { define: {
'process.env.__TEST__': true, 'process.env.__TEST__': true,
'process.env.__E2E__': false, 'process.env.__E2E__': false,
global: 'window',
}, },
test: { test: {
environment: 'jsdom', environment: 'jsdom',
@ -144,14 +141,14 @@ const defineClientConfig = () => {
}, },
exclude: getExclude(false), exclude: getExclude(false),
coverage: { coverage: {
exclude: getExclude(false) exclude: getExclude(false),
} },
} },
}) });
} };
export const getFilterInclude = (isServer, isCoverage) => { 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; if (!filterFileOrDir) return;
const absPath = path.join(process.cwd(), filterFileOrDir); const absPath = path.join(process.cwd(), filterFileOrDir);
const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory(); const isDir = fs.existsSync(absPath) && fs.statSync(absPath).isDirectory();
@ -160,7 +157,7 @@ export const getFilterInclude = (isServer, isCoverage) => {
return [filterFileOrDir]; 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')); const isPackage = fs.existsSync(path.join(absPath, 'package.json'));
@ -176,10 +173,10 @@ export const getFilterInclude = (isServer, isCoverage) => {
// 插件目录,区分 client 和 server // 插件目录,区分 client 和 server
return [`${filterFileOrDir}/src/${isServer ? 'server' : 'client'}/${suffix}`]; return [`${filterFileOrDir}/src/${isServer ? 'server' : 'client'}/${suffix}`];
} };
export const getReportsDirectory = (isServer) => { 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; if (!filterFileOrDir) return;
const isPackage = fs.existsSync(path.join(process.cwd(), filterFileOrDir, 'package.json')); const isPackage = fs.existsSync(path.join(process.cwd(), filterFileOrDir, 'package.json'));
if (isPackage) { if (isPackage) {
@ -193,11 +190,13 @@ export const getReportsDirectory = (isServer) => {
return reportsDirectory; return reportsDirectory;
} }
} };
export const defineConfig = () => { export const defineConfig = () => {
const isServer = process.env.TEST_ENV === 'server-side'; 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'); const isCoverage = process.argv.includes('--coverage');
if (!isCoverage) { if (!isCoverage) {

View File

@ -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);
});
});

View File

@ -60,7 +60,7 @@ export class CollectionManagerPlugin extends Plugin {
this.app.db.on('collections.beforeCreate', beforeCreateForViewCollection(this.db)); this.app.db.on('collections.beforeCreate', beforeCreateForViewCollection(this.db));
this.app.db.on( this.app.db.on(
'collections.afterCreateWithAssociations', 'collections.afterSaveWithAssociations',
async (model: CollectionModel, { context, transaction }) => { async (model: CollectionModel, { context, transaction }) => {
if (context) { if (context) {
await model.migrate({ await model.migrate({

View File

@ -19,6 +19,7 @@ import {
CollectionCategroriesContext, CollectionCategroriesContext,
FieldSummary, FieldSummary,
TemplateSummary, TemplateSummary,
useRequest,
} from '@nocobase/client'; } from '@nocobase/client';
import { CollectionFields } from './CollectionFields'; import { CollectionFields } from './CollectionFields';
import { collectionSchema } from './schemas/collections'; import { collectionSchema } from './schemas/collections';
@ -100,6 +101,7 @@ export const ConfigurationTable = () => {
const api = useAPIClient(); const api = useAPIClient();
const resource = api.resource('dbViews'); const resource = api.resource('dbViews');
const compile = useCompile(); const compile = useCompile();
const form = useForm();
/** /**
* *
@ -131,6 +133,7 @@ export const ConfigurationTable = () => {
value: item.name, value: item.name,
})); }));
}; };
const loadCategories = async () => { const loadCategories = async () => {
return data.data.map((item: any) => ({ return data.data.map((item: any) => ({
label: compile(item.name), 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 () => { const loadStorages = async () => {
return api return api
.resource('storages') .resource('storages')
@ -178,6 +195,7 @@ export const ConfigurationTable = () => {
CollectionFields, CollectionFields,
}} }}
scope={{ scope={{
loadFilterTargetKeys,
useDestroySubField, useDestroySubField,
useBulkDestroySubField, useBulkDestroySubField,
useSelectedRowKeys, useSelectedRowKeys,