mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-04 05:08:42 +08:00
refactor: main data source view collection support filterTargetKey (#3818)
This commit is contained in:
parent
aa96a16d1d
commit
a4cbec293d
@ -179,7 +179,6 @@ export const AddCollectionAction = (props) => {
|
||||
items,
|
||||
};
|
||||
}, [category, items]);
|
||||
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
|
@ -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();
|
||||
|
@ -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)}}'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
@ -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": "科学计数法",
|
||||
|
@ -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 {};
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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({
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user