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,
};
}, [category, items]);
return (
<RecordProvider record={record}>
<ActionContextProvider value={{ visible, setVisible }}>

View File

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

View File

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

View File

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

View File

@ -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": "科学计数法",

View File

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

View File

@ -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) {

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.afterCreateWithAssociations',
'collections.afterSaveWithAssociations',
async (model: CollectionModel, { context, transaction }) => {
if (context) {
await model.migrate({

View File

@ -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,