refactor: view collection options (#1643)
* feat: add schema prefix to view only if view name exists * chore: view-collection name without schema prefix * chore: skip list views already connected * chore: comment * fix: update field error * refactor: collection edit can not config fields * fix: viewName set * fix: transaction * refactor: viewname * chore: collection view name * refactor: viewname * chore: remove rename view collection name * refactor: loading wthen action submjit --------- Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
parent
914260ed7e
commit
fff11df2ba
@ -1,6 +1,6 @@
|
||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { ArrayTable } from '@formily/antd';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { ISchema, useField, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
@ -28,7 +28,7 @@ const getSchema = (schema, category, compile): ISchema => {
|
||||
properties['defaultValue']['x-decorator'] = 'FormItem';
|
||||
}
|
||||
const initialValue: any = {
|
||||
name: schema.name !== 'view' ? `t_${uid()}` : null,
|
||||
name: `t_${uid()}`,
|
||||
template: schema.name,
|
||||
view: schema.name === 'view',
|
||||
category,
|
||||
@ -194,9 +194,12 @@ const useCreateCollection = (schema?: any) => {
|
||||
const { refreshCM } = useCollectionManager();
|
||||
const ctx = useActionContext();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource, collection } = useResourceContext();
|
||||
const { resource } = useResourceContext();
|
||||
const field = useField();
|
||||
return {
|
||||
async run() {
|
||||
field.data = field.data || {};
|
||||
field.data.loading = true;
|
||||
await form.submit();
|
||||
const values = cloneDeep(form.values);
|
||||
if (schema?.events?.beforeSubmit) {
|
||||
@ -218,6 +221,7 @@ const useCreateCollection = (schema?: any) => {
|
||||
});
|
||||
ctx.setVisible(false);
|
||||
await form.reset();
|
||||
field.data.loading = false;
|
||||
refresh();
|
||||
await refreshCM();
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { ArrayTable } from '@formily/antd';
|
||||
import { useForm } from '@formily/react';
|
||||
import { useForm, useField } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
@ -136,9 +136,12 @@ const useCreateCollectionField = () => {
|
||||
const ctx = useActionContext();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource } = useResourceContext();
|
||||
const field = useField();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
field.data = field.data || {};
|
||||
field.data.loading = true;
|
||||
const values = cloneDeep(form.values);
|
||||
if (values.autoCreateReverseField) {
|
||||
} else {
|
||||
@ -148,6 +151,7 @@ const useCreateCollectionField = () => {
|
||||
await resource.create({ values });
|
||||
ctx.setVisible(false);
|
||||
await form.reset();
|
||||
field.data.loading = false;
|
||||
refresh();
|
||||
await refreshCM();
|
||||
},
|
||||
|
@ -21,7 +21,7 @@ const getInterfaceOptions = (data, type) => {
|
||||
return interfaceOptions.filter((v) => v.children.length > 0);
|
||||
};
|
||||
const PreviewCom = (props) => {
|
||||
const { name, sources, viewName, schema } = props;
|
||||
const { databaseView, viewName,sources, schema } = props;
|
||||
const { data: fields } = useContext(ResourceActionContext);
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
@ -47,10 +47,10 @@ const PreviewCom = (props) => {
|
||||
});
|
||||
});
|
||||
setSourceFields(data);
|
||||
}, [sources, name]);
|
||||
}, [sources, databaseView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (name) {
|
||||
if (databaseView) {
|
||||
setLoading(true);
|
||||
api
|
||||
.resource(`dbViews`)
|
||||
@ -72,7 +72,7 @@ const PreviewCom = (props) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [name]);
|
||||
}, [databaseView]);
|
||||
|
||||
const handleFieldChange = (record, index) => {
|
||||
dataSource.splice(index, 1, record);
|
||||
@ -175,7 +175,7 @@ const PreviewCom = (props) => {
|
||||
const item = dataSource[index];
|
||||
return (
|
||||
<Input
|
||||
defaultValue={record?.uiSchema?.title}
|
||||
defaultValue={record?.uiSchema?.title||text}
|
||||
onChange={(e) =>
|
||||
handleFieldChange({ ...item, uiSchema: { ...item?.uiSchema, title: e.target.value } }, index)
|
||||
}
|
||||
@ -204,7 +204,7 @@ const PreviewCom = (props) => {
|
||||
scroll={{ y: 300 }}
|
||||
pagination={false}
|
||||
rowClassName="editable-row"
|
||||
key={name}
|
||||
key={viewName}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -7,7 +7,7 @@ import { useAPIClient } from '../../../api-client';
|
||||
import { useCollectionManager } from '../../hooks/useCollectionManager';
|
||||
|
||||
export const PreviewTable = (props) => {
|
||||
const { name, viewName, schema, fields } = props;
|
||||
const { databaseView, schema, viewName, fields } = props;
|
||||
const [previewColumns, setPreviewColumns] = useState([]);
|
||||
const [previewData, setPreviewData] = useState([]);
|
||||
const compile = useCompile();
|
||||
@ -17,10 +17,10 @@ export const PreviewTable = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm();
|
||||
useEffect(() => {
|
||||
if (name) {
|
||||
if (databaseView) {
|
||||
getPreviewData();
|
||||
}
|
||||
}, [name]);
|
||||
}, [databaseView]);
|
||||
|
||||
useEffect(() => {
|
||||
const pColumns = formatPreviewColumns(fields);
|
||||
@ -96,7 +96,7 @@ export const PreviewTable = (props) => {
|
||||
columns={previewColumns}
|
||||
dataSource={previewData}
|
||||
scroll={{ x: 1000, y: 300 }}
|
||||
key={name}
|
||||
key={viewName}
|
||||
/>,
|
||||
]}
|
||||
</div>
|
||||
|
@ -3,7 +3,6 @@ import { ICollectionTemplate } from './types';
|
||||
import { PreviewFields } from './components/PreviewFields';
|
||||
import { PreviewTable } from './components/PreviewTable';
|
||||
|
||||
|
||||
export const view: ICollectionTemplate = {
|
||||
name: 'view',
|
||||
title: '{{t("Connect to database view")}}',
|
||||
@ -21,7 +20,8 @@ export const view: ICollectionTemplate = {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
name: {
|
||||
|
||||
databaseView: {
|
||||
title: '{{t("Connect to database view")}}',
|
||||
type: 'single',
|
||||
required: true,
|
||||
@ -30,12 +30,37 @@ export const view: ICollectionTemplate = {
|
||||
'x-reactions': ['{{useAsyncDataSource(loadDBViews)}}'],
|
||||
'x-disabled': '{{ !createOnly }}',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
title: '{{t("Collection name")}}',
|
||||
required: true,
|
||||
'x-disabled': '{{ !createOnly }}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-validator': 'uid',
|
||||
description:
|
||||
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
|
||||
'x-reactions': {
|
||||
dependencies: ['databaseView'],
|
||||
when: '{{isPG}}',
|
||||
fulfill: {
|
||||
state: {
|
||||
initialValue: '{{$deps[0]&&$deps[0].match(/^([^_]+)_(.*)$/)?.[2]}}',
|
||||
},
|
||||
},
|
||||
otherwise: {
|
||||
state: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
'x-hidden': true,
|
||||
'x-reactions': {
|
||||
dependencies: ['name'],
|
||||
when: "{{isPG}}",
|
||||
dependencies: ['databaseView'],
|
||||
when: '{{isPG}}',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: "{{$deps[0].split('_')?.[0]}}",
|
||||
@ -52,8 +77,8 @@ export const view: ICollectionTemplate = {
|
||||
type: 'string',
|
||||
'x-hidden': true,
|
||||
'x-reactions': {
|
||||
dependencies: ['name'],
|
||||
when: "{{isPG}}",
|
||||
dependencies: ['databaseView'],
|
||||
when: '{{isPG}}',
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{$deps[0].match(/^([^_]+)_(.*)$/)?.[2]}}',
|
||||
@ -80,6 +105,7 @@ export const view: ICollectionTemplate = {
|
||||
fields: {
|
||||
type: 'array',
|
||||
'x-component': PreviewFields,
|
||||
'x-visible': '{{ createOnly }}',
|
||||
'x-reactions': {
|
||||
dependencies: ['name'],
|
||||
fulfill: {
|
||||
@ -91,9 +117,10 @@ export const view: ICollectionTemplate = {
|
||||
},
|
||||
preview: {
|
||||
type: 'object',
|
||||
'x-visible': '{{ createOnly }}',
|
||||
'x-component': PreviewTable,
|
||||
'x-reactions': {
|
||||
dependencies: ['name','fields'],
|
||||
dependencies: ['name', 'fields'],
|
||||
fulfill: {
|
||||
schema: {
|
||||
'x-component-props': '{{$form.values}}', //任意层次属性都支持表达式
|
||||
|
@ -47,6 +47,25 @@ SELECT * FROM numbers;
|
||||
const response = await agent.resource('dbViews').list();
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.find((item) => item.name === testViewName)).toBeTruthy();
|
||||
|
||||
await app.db.getCollection('collections').repository.create({
|
||||
values: {
|
||||
name: testViewName,
|
||||
view: true,
|
||||
schema: app.db.inDialect('postgres') ? 'public' : undefined,
|
||||
fields: [
|
||||
{
|
||||
name: 'numbers',
|
||||
type: 'integer',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const response2 = await agent.resource('dbViews').list();
|
||||
expect(response2.status).toBe(200);
|
||||
expect(response2.body.data.find((item) => item.name === testViewName)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should query views data', async () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Database, { Repository, ViewCollection } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '../index';
|
||||
import { uid } from '@nocobase/utils';
|
||||
|
||||
describe('view collection', function () {
|
||||
let db: Database;
|
||||
@ -27,8 +28,67 @@ describe('view collection', function () {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should save view collection in difference schema', async () => {
|
||||
if (!db.inDialect('postgres')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewName = 'test_view';
|
||||
const dbSchema = db.options.schema || 'public';
|
||||
const randomSchema = `s_${uid(6)}`;
|
||||
await db.sequelize.query(`CREATE SCHEMA IF NOT EXISTS ${randomSchema};`);
|
||||
await db.sequelize.query(`CREATE OR REPLACE VIEW ${dbSchema}.${viewName} AS select 1+1 as "view_1"`);
|
||||
await db.sequelize.query(`CREATE OR REPLACE VIEW ${randomSchema}.${viewName} AS select 1+1 as "view_2"`);
|
||||
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: viewName,
|
||||
view: true,
|
||||
fields: [{ type: 'string', name: 'view_1' }],
|
||||
schema: dbSchema,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const viewCollection = db.getCollection(viewName);
|
||||
expect(viewCollection).toBeInstanceOf(ViewCollection);
|
||||
|
||||
let err;
|
||||
try {
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: viewName,
|
||||
view: true,
|
||||
fields: [{ type: 'string', name: 'view_2' }],
|
||||
schema: randomSchema,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toBeTruthy();
|
||||
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: `${randomSchema}_${viewName}`,
|
||||
view: true,
|
||||
viewName: 'test_view',
|
||||
fields: [{ type: 'string', name: 'view_2' }],
|
||||
schema: randomSchema,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const otherSchemaView = db.getCollection(`${randomSchema}_${viewName}`);
|
||||
expect(otherSchemaView.options.viewName).toBe(viewName);
|
||||
expect(otherSchemaView.options.schema).toBe(randomSchema);
|
||||
});
|
||||
|
||||
it('should support view with dot field', async () => {
|
||||
const dropViewSQL = `DROP VIEW IF EXISTS test_view`;
|
||||
|
||||
await db.sequelize.query(dropViewSQL);
|
||||
const viewSQL = `CREATE VIEW test_view AS select 1+1 as "dot.results"`;
|
||||
await db.sequelize.query(viewSQL);
|
||||
|
@ -1,16 +1,5 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
|
||||
export function beforeCreateForViewCollection(db: Database) {
|
||||
return async (model, { transaction, context }) => {
|
||||
if (model.get('viewSQL')) {
|
||||
const name = model.get('name');
|
||||
const sql = model.get('viewSQL');
|
||||
|
||||
await db.sequelize.query(`CREATE OR REPLACE VIEW "${name}" AS ${sql}`, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
model.set('viewName', name);
|
||||
}
|
||||
};
|
||||
return async (model, { transaction, context }) => {};
|
||||
}
|
||||
|
@ -27,13 +27,29 @@ export default {
|
||||
|
||||
await next();
|
||||
},
|
||||
async list(ctx, next) {
|
||||
|
||||
list: async function (ctx, next) {
|
||||
const db = ctx.app.db as Database;
|
||||
const dbViews = await db.queryInterface.listViews();
|
||||
ctx.body = dbViews.map((dbView) => {
|
||||
|
||||
const viewCollections = Array.from(db.collections.values()).filter((collection) => collection.isView());
|
||||
|
||||
ctx.body = dbViews
|
||||
.map((dbView) => {
|
||||
return {
|
||||
...dbView,
|
||||
};
|
||||
})
|
||||
.filter((dbView) => {
|
||||
// if view is connected, skip
|
||||
return !viewCollections.find((collection) => {
|
||||
const viewName = dbView.name;
|
||||
const schema = dbView.schema;
|
||||
|
||||
const collectionViewName = collection.options.viewName || collection.options.name;
|
||||
|
||||
return collectionViewName === viewName && collection.options.schema === schema;
|
||||
});
|
||||
});
|
||||
|
||||
await next();
|
||||
@ -46,7 +62,9 @@ export default {
|
||||
const limit = 1 * pageSize;
|
||||
|
||||
const sql = `SELECT *
|
||||
FROM ${ctx.app.db.utils.quoteTable(ctx.app.db.utils.addSchema(filterByTk, schema))} LIMIT ${limit} OFFSET ${offset}`;
|
||||
FROM ${ctx.app.db.utils.quoteTable(
|
||||
ctx.app.db.utils.addSchema(filterByTk, schema),
|
||||
)} LIMIT ${limit} OFFSET ${offset}`;
|
||||
|
||||
ctx.body = await ctx.app.db.sequelize.query(sql, { type: 'SELECT' });
|
||||
await next();
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
afterCreateForReverseField,
|
||||
beforeCreateForReverseField,
|
||||
beforeDestroyForeignKey,
|
||||
beforeInitOptions
|
||||
beforeInitOptions,
|
||||
} from './hooks';
|
||||
|
||||
import { InheritedCollection } from '@nocobase/database';
|
||||
@ -277,16 +277,22 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
for (const field of castArray(fields)) {
|
||||
if (field.get('source')) {
|
||||
const [collectionSource, fieldSource] = field.get('source').split('.');
|
||||
// find original field
|
||||
const collectionField = this.app.db.getCollection(collectionSource).getField(fieldSource);
|
||||
|
||||
const newOptions = {};
|
||||
|
||||
// write original field options
|
||||
lodash.merge(newOptions, collectionField.options);
|
||||
|
||||
// merge with current field options
|
||||
lodash.mergeWith(newOptions, field.get(), (objValue, srcValue) => {
|
||||
if (srcValue === null) {
|
||||
return objValue;
|
||||
}
|
||||
});
|
||||
|
||||
// set final options
|
||||
field.set('options', newOptions);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user