mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
fix: reject update when filter is empty object (#3777)
* fix: reject update when filter is empty object * chore: valid filter when destroy data * fix: test * refactor(utils): move isValidFilter to utils * chore: test * chore: test * chore: test * fix(plugin-workflow-manual): fix test case * fix(plugin-workflow-manual): add filter check for update form in manual node * chore: validate filter params as middleware * chore: action filter validate in data-source-manager * chore: acl filter params validate test * chore: move validate filter params middleware into core * Update nocobase-test-e2e.yml * chore: only run workflow's tests * chore: only run workflow's tests * fix: updateRecordForm * Revert "chore: only run workflow's tests" This reverts commit 64ce1241718ef516ff9bf7245296dee963ab2e43. * Revert "chore: only run workflow's tests" This reverts commit b9057b35ec4f87ba13c08650ffc7919e32eb3fc3. --------- Co-authored-by: mytharcher <mytharcher@gmail.com> Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: Zeke Zhang <958414905@qq.com> Co-authored-by: hongboji <j414562100@qq.com>
This commit is contained in:
parent
b1aa6cff5e
commit
89733247bd
2
.github/workflows/nocobase-test-e2e.yml
vendored
2
.github/workflows/nocobase-test-e2e.yml
vendored
@ -78,4 +78,4 @@ jobs:
|
|||||||
DB_USER: nocobase
|
DB_USER: nocobase
|
||||||
DB_PASSWORD: password
|
DB_PASSWORD: password
|
||||||
DB_DATABASE: nocobase
|
DB_DATABASE: nocobase
|
||||||
timeout-minutes: 120
|
timeout-minutes: 180
|
||||||
|
@ -61,4 +61,96 @@ describe('example', () => {
|
|||||||
expect(m.name).toBe('n1');
|
expect(m.name).toBe('n1');
|
||||||
await app.destroy();
|
await app.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should validate filter params in update actions', async () => {
|
||||||
|
const app = await createMockServer({
|
||||||
|
acl: false,
|
||||||
|
resourcer: {
|
||||||
|
prefix: '/api/',
|
||||||
|
},
|
||||||
|
name: 'update-filter',
|
||||||
|
});
|
||||||
|
|
||||||
|
const database = mockDatabase({
|
||||||
|
tablePrefix: 'ds1_',
|
||||||
|
});
|
||||||
|
|
||||||
|
await database.clean({ drop: true });
|
||||||
|
|
||||||
|
const ds1 = new SequelizeDataSource({
|
||||||
|
name: 'ds1',
|
||||||
|
resourceManager: {},
|
||||||
|
collectionManager: {
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ds1.collectionManager.defineCollection({
|
||||||
|
name: 'test1',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await ds1.collectionManager.sync();
|
||||||
|
|
||||||
|
ds1.acl.allow('test1', 'update', 'public');
|
||||||
|
|
||||||
|
await app.dataSourceManager.add(ds1);
|
||||||
|
|
||||||
|
const res = await app
|
||||||
|
.agent()
|
||||||
|
.post(`/api/test1:update?filter=${JSON.stringify({})}`)
|
||||||
|
.set('x-data-source', 'ds1')
|
||||||
|
.auth('abc', { type: 'bearer' })
|
||||||
|
.set('X-Authenticator', 'basic');
|
||||||
|
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate filter params in destroy actions', async () => {
|
||||||
|
const app = await createMockServer({
|
||||||
|
acl: false,
|
||||||
|
resourcer: {
|
||||||
|
prefix: '/api/',
|
||||||
|
},
|
||||||
|
name: 'update-filter',
|
||||||
|
});
|
||||||
|
|
||||||
|
const database = mockDatabase({
|
||||||
|
tablePrefix: 'ds1_',
|
||||||
|
});
|
||||||
|
|
||||||
|
await database.clean({ drop: true });
|
||||||
|
|
||||||
|
const ds1 = new SequelizeDataSource({
|
||||||
|
name: 'ds1',
|
||||||
|
resourceManager: {},
|
||||||
|
collectionManager: {
|
||||||
|
database,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ds1.collectionManager.defineCollection({
|
||||||
|
name: 'test1',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await ds1.collectionManager.sync();
|
||||||
|
|
||||||
|
ds1.acl.allow('test1', 'destroy', 'public');
|
||||||
|
|
||||||
|
await app.dataSourceManager.add(ds1);
|
||||||
|
|
||||||
|
const res = await app
|
||||||
|
.agent()
|
||||||
|
.post(`/api/test1:destroy?filter=${JSON.stringify({})}`)
|
||||||
|
.set('x-data-source', 'ds1')
|
||||||
|
.auth('abc', { type: 'bearer' })
|
||||||
|
.set('X-Authenticator', 'basic');
|
||||||
|
|
||||||
|
expect(res.status).toBe(500);
|
||||||
|
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -35,7 +35,6 @@ export class DataSourceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
console.log('next....');
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,4 +314,63 @@ describe('destroy', () => {
|
|||||||
await User.repository.destroy(u2['id']);
|
await User.repository.destroy(u2['id']);
|
||||||
expect(await User.repository.count()).toEqual(2);
|
expect(await User.repository.count()).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not destroy data when filter is empty', async () => {
|
||||||
|
await User.repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await User.repository.destroy({
|
||||||
|
filter: {},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await User.repository.count()).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not destroy data when filter is not valid', async () => {
|
||||||
|
await User.repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await User.repository.destroy({
|
||||||
|
filter: {
|
||||||
|
$and: [],
|
||||||
|
$or: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await User.repository.count()).toBe(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -202,6 +202,67 @@ describe('update', () => {
|
|||||||
expect(p1.toJSON()['tags']).toEqual([]);
|
expect(p1.toJSON()['tags']).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not update items when filter is empty', async () => {
|
||||||
|
await db.getRepository('posts').create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'p2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.getRepository('posts').update({
|
||||||
|
values: {
|
||||||
|
title: 'p3',
|
||||||
|
},
|
||||||
|
filter: {},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toBeDefined();
|
||||||
|
expect(err.message).toContain('must provide filter or filterByTk for update call');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update items when filter is not a valid filter object', async () => {
|
||||||
|
await db.getRepository('posts').create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'p2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.getRepository('posts').update({
|
||||||
|
values: {
|
||||||
|
title: 'p3',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
$and: [],
|
||||||
|
$or: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toBeDefined();
|
||||||
|
expect(err.message).toContain('must provide filter or filterByTk for update call');
|
||||||
|
});
|
||||||
|
|
||||||
it('should not update items without filter or filterByPk', async () => {
|
it('should not update items without filter or filterByPk', async () => {
|
||||||
await db.getRepository('posts').create({
|
await db.getRepository('posts').create({
|
||||||
values: {
|
values: {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { isValidFilter } from '@nocobase/utils';
|
||||||
|
|
||||||
const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||||
const oldValue = descriptor.value;
|
const oldValue = descriptor.value;
|
||||||
|
|
||||||
@ -8,7 +10,7 @@ const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: Prop
|
|||||||
return oldValue.apply(this, args);
|
return oldValue.apply(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options?.filter && !options?.filterByTk && !options?.forceUpdate) {
|
if (!isValidFilter(options?.filter) && !options?.filterByTk && !options?.forceUpdate) {
|
||||||
throw new Error(`must provide filter or filterByTk for ${propertyKey} call, or set forceUpdate to true`);
|
throw new Error(`must provide filter or filterByTk for ${propertyKey} call, or set forceUpdate to true`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import {
|
|||||||
UpdateOptions as SequelizeUpdateOptions,
|
UpdateOptions as SequelizeUpdateOptions,
|
||||||
WhereOperators,
|
WhereOperators,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
|
import { isValidFilter } from '@nocobase/utils';
|
||||||
|
|
||||||
import { Collection } from './collection';
|
import { Collection } from './collection';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import mustHaveFilter from './decorators/must-have-filter-decorator';
|
import mustHaveFilter from './decorators/must-have-filter-decorator';
|
||||||
@ -718,7 +720,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.filter) {
|
if (options.filter && isValidFilter(options.filter)) {
|
||||||
if (
|
if (
|
||||||
this.collection.model.primaryKeyAttributes.length !== 1 &&
|
this.collection.model.primaryKeyAttributes.length !== 1 &&
|
||||||
!lodash.get(this.collection.options, 'filterTargetKey')
|
!lodash.get(this.collection.options, 'filterTargetKey')
|
||||||
|
@ -200,6 +200,7 @@ export function parseQuery(input: string): any {
|
|||||||
// 逗号分隔转换为数组
|
// 逗号分隔转换为数组
|
||||||
// comma: true,
|
// comma: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// filter 支持 json string
|
// filter 支持 json string
|
||||||
if (typeof query.filter === 'string') {
|
if (typeof query.filter === 'string') {
|
||||||
query.filter = JSON.parse(query.filter);
|
query.filter = JSON.parse(query.filter);
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import supertest from 'supertest';
|
||||||
|
import { Application } from '../application';
|
||||||
|
|
||||||
|
describe('i18next', () => {
|
||||||
|
let app: Application;
|
||||||
|
let agent: supertest.SuperAgentTest;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new Application({
|
||||||
|
database: {
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: ':memory:',
|
||||||
|
},
|
||||||
|
resourcer: {
|
||||||
|
prefix: '/api',
|
||||||
|
},
|
||||||
|
acl: false,
|
||||||
|
dataWrapping: false,
|
||||||
|
registerActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
agent = supertest.agent(app.callback());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
return app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate filter params when request update', async () => {
|
||||||
|
app.resource({
|
||||||
|
name: 'tests',
|
||||||
|
actions: {
|
||||||
|
update: async (ctx, next) => {
|
||||||
|
ctx.body = 'ok';
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRes = await agent.post(`/api/tests:update?filter=${JSON.stringify({})}`);
|
||||||
|
expect(updateRes.status).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
@ -48,6 +48,7 @@ import { InstallOptions, PluginManager } from './plugin-manager';
|
|||||||
import { DataSourceManager, SequelizeDataSource } from '@nocobase/data-source-manager';
|
import { DataSourceManager, SequelizeDataSource } from '@nocobase/data-source-manager';
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
import { MainDataSource } from './main-data-source';
|
import { MainDataSource } from './main-data-source';
|
||||||
|
import validateFilterParams from './middlewares/validate-filter-params';
|
||||||
|
|
||||||
export type PluginType = string | typeof Plugin;
|
export type PluginType = string | typeof Plugin;
|
||||||
export type PluginConfiguration = PluginType | [PluginType, any];
|
export type PluginConfiguration = PluginType | [PluginType, any];
|
||||||
@ -1070,7 +1071,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._dataSourceManager.use(this._authManager.middleware(), { tag: 'auth' });
|
this._dataSourceManager.use(this._authManager.middleware(), { tag: 'auth' });
|
||||||
|
this._dataSourceManager.use(validateFilterParams, { tag: 'validate-filter-params', before: ['auth'] });
|
||||||
|
|
||||||
this.resourcer.use(this._authManager.middleware(), { tag: 'auth' });
|
this.resourcer.use(this._authManager.middleware(), { tag: 'auth' });
|
||||||
|
this.resourcer.use(validateFilterParams, { tag: 'validate-filter-params', before: ['auth'] });
|
||||||
|
|
||||||
if (this.options.acl !== false) {
|
if (this.options.acl !== false) {
|
||||||
this.resourcer.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] });
|
this.resourcer.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] });
|
||||||
|
@ -85,6 +85,7 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
|
|||||||
tag: 'parseVariables',
|
tag: 'parseVariables',
|
||||||
after: 'acl',
|
after: 'acl',
|
||||||
});
|
});
|
||||||
|
|
||||||
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
||||||
|
|
||||||
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { isValidFilter } from '@nocobase/utils';
|
||||||
|
|
||||||
|
export default async function validateFilterParams(ctx, next) {
|
||||||
|
const { params } = ctx.action;
|
||||||
|
const guardedActions = ['update', 'destroy'];
|
||||||
|
|
||||||
|
if (params.filter && !isValidFilter(params.filter) && guardedActions.includes(params.actionName)) {
|
||||||
|
throw new Error(`Invalid filter: ${JSON.stringify(params.filter)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
@ -152,6 +152,10 @@ export class MockServer extends Application {
|
|||||||
url += `/${filterByTk}`;
|
url += `/${filterByTk}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (restParams.filter) {
|
||||||
|
restParams.filter = JSON.stringify(restParams.filter);
|
||||||
|
}
|
||||||
|
|
||||||
const queryString = qs.stringify(restParams, { arrayFormat: 'brackets' });
|
const queryString = qs.stringify(restParams, { arrayFormat: 'brackets' });
|
||||||
|
|
||||||
let request;
|
let request;
|
||||||
|
23
packages/core/utils/src/__tests__/isValidFilter.test.ts
Normal file
23
packages/core/utils/src/__tests__/isValidFilter.test.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { isValidFilter } from '..';
|
||||||
|
|
||||||
|
describe('isValidFilter', () => {
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(isValidFilter(undefined)).toBe(false);
|
||||||
|
expect(isValidFilter({})).toBe(false);
|
||||||
|
expect(isValidFilter({ $and: [] })).toBe(false);
|
||||||
|
expect(isValidFilter({ $or: [] })).toBe(false);
|
||||||
|
expect(isValidFilter({ $and: [{}] })).toBe(false);
|
||||||
|
expect(isValidFilter({ $or: [{}] })).toBe(false);
|
||||||
|
expect(isValidFilter({ $and: [{ $or: [] }] })).toBe(false);
|
||||||
|
expect(isValidFilter({ $or: [{ $and: [] }] })).toBe(false);
|
||||||
|
expect(isValidFilter({ $and: [{}], $or: [{ $and: [], $or: [] }] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(isValidFilter({ $and: [{ name: { $eq: 'test' } }] })).toBe(true);
|
||||||
|
expect(isValidFilter({ $or: [{ name: { $eq: 'test' } }] })).toBe(true);
|
||||||
|
expect(isValidFilter({ $and: [{ $or: [{ name: { $eq: 'test' } }] }] })).toBe(true);
|
||||||
|
expect(isValidFilter({ $or: [{ $and: [{ name: { $eq: 'test' } }] }] })).toBe(true);
|
||||||
|
expect(isValidFilter({ $and: [], $or: [{ name: 'test' }] })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
@ -6,6 +6,7 @@ export * from './common';
|
|||||||
export * from './date';
|
export * from './date';
|
||||||
export * from './forEach';
|
export * from './forEach';
|
||||||
export * from './getValuesByPath';
|
export * from './getValuesByPath';
|
||||||
|
export * from './isValidFilter';
|
||||||
export * from './json-templates';
|
export * from './json-templates';
|
||||||
export * from './log';
|
export * from './log';
|
||||||
export * from './merge';
|
export * from './merge';
|
||||||
|
@ -8,6 +8,7 @@ export * from './date';
|
|||||||
export * from './dayjs';
|
export * from './dayjs';
|
||||||
export * from './forEach';
|
export * from './forEach';
|
||||||
export * from './fs-exists';
|
export * from './fs-exists';
|
||||||
|
export * from './isValidFilter';
|
||||||
export * from './json-templates';
|
export * from './json-templates';
|
||||||
export * from './koa-multer';
|
export * from './koa-multer';
|
||||||
export * from './measure-execution-time';
|
export * from './measure-execution-time';
|
||||||
|
33
packages/core/utils/src/isValidFilter.ts
Normal file
33
packages/core/utils/src/isValidFilter.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export function isValidFilter(condition: any) {
|
||||||
|
if (!condition) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = [condition.$and, condition.$or].filter(Boolean);
|
||||||
|
|
||||||
|
if (groups.length == 0) {
|
||||||
|
return Object.keys(condition).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.some((item) => {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
return item.some(isValidFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.$and || item.$or) {
|
||||||
|
return isValidFilter(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name] = Object.keys(item);
|
||||||
|
if (!name || !item[name]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [op] = Object.keys(item[name]);
|
||||||
|
|
||||||
|
if (!op || typeof item[name][op] === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
import { MockServer } from '@nocobase/test';
|
||||||
|
import { Database } from '@nocobase/database';
|
||||||
|
import { ACL } from '@nocobase/acl';
|
||||||
|
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||||
|
import { prepareApp } from './prepare';
|
||||||
|
|
||||||
|
describe('acl', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let acl: ACL;
|
||||||
|
let admin;
|
||||||
|
let adminAgent;
|
||||||
|
|
||||||
|
let userPlugin;
|
||||||
|
|
||||||
|
let uiSchemaRepository: UiSchemaRepository;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await prepareApp();
|
||||||
|
db = app.db;
|
||||||
|
acl = app.acl;
|
||||||
|
|
||||||
|
const UserRepo = db.getCollection('users').repository;
|
||||||
|
|
||||||
|
admin = await UserRepo.create({
|
||||||
|
values: {
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
adminAgent = app.agent().login(admin);
|
||||||
|
uiSchemaRepository = db.getRepository('uiSchemas');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when filter is empty during update resource', async () => {
|
||||||
|
await db.getRepository('collections').create({
|
||||||
|
context: {},
|
||||||
|
values: {
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'published',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.getRepository('posts').create({
|
||||||
|
values: {
|
||||||
|
title: 'old title',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await adminAgent.resource('dataSourcesRolesResourcesScopes').create({
|
||||||
|
values: {
|
||||||
|
resourceName: 'posts',
|
||||||
|
name: 'published posts',
|
||||||
|
scope: {
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toEqual(200);
|
||||||
|
|
||||||
|
const scope = await db.getRepository('dataSourcesRolesResourcesScopes').findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'published posts',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scope.get('scope')).toMatchObject({
|
||||||
|
published: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// assign scope to admin role
|
||||||
|
const createResp = await adminAgent.resource('roles.resources', 'admin').create({
|
||||||
|
values: {
|
||||||
|
name: 'posts',
|
||||||
|
usingActionsConfig: true,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'update',
|
||||||
|
scope: scope.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResp.statusCode).toEqual(200);
|
||||||
|
|
||||||
|
const updateResp = await adminAgent.resource('posts').update({
|
||||||
|
filter: {},
|
||||||
|
values: {
|
||||||
|
title: 'new title',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateResp.statusCode).not.toBe(200);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await db.getRepository('posts').count({
|
||||||
|
filter: {
|
||||||
|
title: 'new title',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,97 @@
|
|||||||
|
import { Database } from '@nocobase/database';
|
||||||
|
import { MockServer } from '@nocobase/test';
|
||||||
|
import { createApp } from '../index';
|
||||||
|
|
||||||
|
describe('action test', () => {
|
||||||
|
let db: Database;
|
||||||
|
let app: MockServer;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createApp();
|
||||||
|
db = app.db;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate update action filter params', async () => {
|
||||||
|
await db.getRepository('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.getRepository('posts').create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'p2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const resp = await app
|
||||||
|
.agent()
|
||||||
|
.resource('posts')
|
||||||
|
.update({
|
||||||
|
filter: {},
|
||||||
|
values: {
|
||||||
|
title: 'p3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.status).toBe(500);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await db.getRepository('posts').count({
|
||||||
|
filter: {
|
||||||
|
title: 'p3',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate destroy action filter params', async () => {
|
||||||
|
await db.getRepository('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.getRepository('posts').create({
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'p2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const resp = await app.agent().resource('posts').destroy({
|
||||||
|
filter: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resp.status).toBe(500);
|
||||||
|
|
||||||
|
expect(await db.getRepository('posts').count({})).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
@ -1101,6 +1101,19 @@ test.describe('field data', () => {
|
|||||||
await preManualNodePom.updateRecordFormMenu.hover();
|
await preManualNodePom.updateRecordFormMenu.hover();
|
||||||
await page.getByRole('menuitem', { name: preManualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: preManualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${preManualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page
|
||||||
|
.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${preManualNodeCollectionName}`)
|
||||||
|
.click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${preManualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${preManualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
|
@ -104,6 +104,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -260,6 +271,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -416,6 +438,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -572,6 +605,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -728,6 +772,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -884,6 +939,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -1056,6 +1122,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -1228,6 +1305,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -1400,6 +1488,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -1573,6 +1672,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -1754,6 +1864,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -1926,6 +2047,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
@ -2104,6 +2236,17 @@ test.describe('field data update', () => {
|
|||||||
}
|
}
|
||||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
|
await page
|
||||||
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
|
.hover();
|
||||||
|
await page.getByLabel(`designer-schema-settings-CardItem-UpdateFormDesigner-${manualNodeCollectionName}`).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Filter settings' }).click();
|
||||||
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-field').click();
|
||||||
|
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
|
||||||
|
await page.getByTestId('select-filter-operator').click();
|
||||||
|
await page.getByRole('option', { name: 'exists', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FormLayout } from '@formily/antd-v5';
|
import { FormLayout } from '@formily/antd-v5';
|
||||||
import { createForm } from '@formily/core';
|
import { createForm } from '@formily/core';
|
||||||
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { Alert, Button, Modal, Space } from 'antd';
|
import { Alert, Button, Modal, Space, message } from 'antd';
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ import WorkflowPlugin, {
|
|||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
import { Registry, lodash } from '@nocobase/utils/client';
|
import { Registry, lodash } from '@nocobase/utils/client';
|
||||||
|
|
||||||
import { NAMESPACE, useLang } from '../../locale';
|
import { NAMESPACE, usePluginTranslation } from '../../locale';
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
import createRecordForm from './forms/create';
|
import createRecordForm from './forms/create';
|
||||||
import customRecordForm from './forms/custom';
|
import customRecordForm from './forms/custom';
|
||||||
@ -86,13 +86,14 @@ export type ManualFormType = {
|
|||||||
[key: string]: React.FC;
|
[key: string]: React.FC;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
validate?: (config: any) => string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const manualFormTypes = new Registry<ManualFormType>();
|
export const manualFormTypes = new Registry<ManualFormType>();
|
||||||
|
|
||||||
manualFormTypes.register('customForm', customRecordForm);
|
manualFormTypes.register('custom', customRecordForm);
|
||||||
manualFormTypes.register('createForm', createRecordForm);
|
manualFormTypes.register('create', createRecordForm);
|
||||||
manualFormTypes.register('updateForm', updateRecordForm);
|
manualFormTypes.register('update', updateRecordForm);
|
||||||
|
|
||||||
function useTriggerInitializers(): SchemaInitializerItemType | null {
|
function useTriggerInitializers(): SchemaInitializerItemType | null {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
@ -243,7 +244,8 @@ export const addBlockButton = new CompatibleSchemaInitializer(
|
|||||||
|
|
||||||
function AssignedFieldValues() {
|
function AssignedFieldValues() {
|
||||||
const ctx = useContext(SchemaComponentContext);
|
const ctx = useContext(SchemaComponentContext);
|
||||||
const { t } = useTranslation();
|
const { t: coreT } = useTranslation();
|
||||||
|
const { t } = usePluginTranslation();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const scope = useWorkflowVariableOptions();
|
const scope = useWorkflowVariableOptions();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -275,7 +277,7 @@ function AssignedFieldValues() {
|
|||||||
}, [fieldSchema]);
|
}, [fieldSchema]);
|
||||||
const upLevelActiveFields = useFormActiveFields();
|
const upLevelActiveFields = useFormActiveFields();
|
||||||
|
|
||||||
const title = t('Assign field values');
|
const title = coreT('Assign field values');
|
||||||
|
|
||||||
function onCancel() {
|
function onCancel() {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@ -321,9 +323,7 @@ function AssignedFieldValues() {
|
|||||||
<FormProvider form={form}>
|
<FormProvider form={form}>
|
||||||
<FormLayout layout={'vertical'}>
|
<FormLayout layout={'vertical'}>
|
||||||
<Alert
|
<Alert
|
||||||
message={useLang(
|
message={t('Values preset in this form will override user submitted ones when continue or reject.')}
|
||||||
'Values preset in this form will override user submitted ones when continue or reject.',
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
{open && schema && (
|
{open && schema && (
|
||||||
@ -612,15 +612,46 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateForms(forms: Record<string, any> = {}) {
|
||||||
|
for (const form of Object.values(forms)) {
|
||||||
|
const formType = manualFormTypes.get(form.type);
|
||||||
|
if (typeof formType.validate === 'function') {
|
||||||
|
const msg = formType.validate(form);
|
||||||
|
if (msg) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function SchemaConfigButton(props) {
|
export function SchemaConfigButton(props) {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { values } = useForm();
|
||||||
|
const { t } = usePluginTranslation();
|
||||||
|
const onSetVisible = useCallback(
|
||||||
|
(v) => {
|
||||||
|
if (!v) {
|
||||||
|
const msg = validateForms(values.forms);
|
||||||
|
if (msg) {
|
||||||
|
message.error({
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
title: t('Validation failed'),
|
||||||
|
content: t(msg),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setVisible(v);
|
||||||
|
},
|
||||||
|
[values.forms],
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button type="primary" onClick={() => setVisible(true)} disabled={false}>
|
<Button type="primary" onClick={() => setVisible(true)} disabled={false}>
|
||||||
{useLang(workflow.executed ? 'View user interface' : 'Configure user interface')}
|
{t(workflow.executed ? 'View user interface' : 'Configure user interface')}
|
||||||
</Button>
|
</Button>
|
||||||
<ActionContextProvider value={{ visible, setVisible, formValueChanged: false }}>
|
<ActionContextProvider value={{ visible, setVisible: onSetVisible, formValueChanged: false }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ActionContextProvider>
|
</ActionContextProvider>
|
||||||
</>
|
</>
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
useDesignable,
|
useDesignable,
|
||||||
useMenuSearch,
|
useMenuSearch,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
|
import { isValidFilter } from '@nocobase/utils/client';
|
||||||
import { FilterDynamicComponent } from '@nocobase/plugin-workflow/client';
|
import { FilterDynamicComponent } from '@nocobase/plugin-workflow/client';
|
||||||
|
|
||||||
import { NAMESPACE } from '../../../locale';
|
import { NAMESPACE } from '../../../locale';
|
||||||
@ -156,4 +157,11 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
},
|
},
|
||||||
|
validate({ filter }) {
|
||||||
|
if (!filter || !isValidFilter(filter)) {
|
||||||
|
return 'Please check one of your update record form, and add at least one filter condition in form settings.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
} as ManualFormType;
|
} as ManualFormType;
|
||||||
|
@ -7,6 +7,8 @@ export function useLang(key: string, options = {}) {
|
|||||||
return t(key);
|
return t(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePluginTranslation(options) {
|
export const lang = useLang;
|
||||||
|
|
||||||
|
export function usePluginTranslation(options?) {
|
||||||
return useTranslation(NAMESPACE, options);
|
return useTranslation(NAMESPACE, options);
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,6 @@
|
|||||||
"Filter settings": "筛选设置",
|
"Filter settings": "筛选设置",
|
||||||
"Workflow todos": "工作流待办",
|
"Workflow todos": "工作流待办",
|
||||||
"Task node": "任务节点",
|
"Task node": "任务节点",
|
||||||
"Unprocessed": "未处理"
|
"Unprocessed": "未处理",
|
||||||
|
"Please check one of your update record form, and add at least one filter condition in form settings.": "请检查您的其中的更新数据表单,并在表单设置中至少添加一个筛选条件。"
|
||||||
}
|
}
|
||||||
|
@ -596,6 +596,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
type: 'update',
|
type: 'update',
|
||||||
actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
|
actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
|
filter: { title: 't1' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useCollectionDataSource } from '@nocobase/client';
|
import { useCollectionDataSource } from '@nocobase/client';
|
||||||
|
import { isValidFilter } from '@nocobase/utils/client';
|
||||||
|
|
||||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||||
import { collection, filter } from '../schemas/collection';
|
import { collection, filter } from '../schemas/collection';
|
||||||
import { isValidFilter } from '../utils';
|
|
||||||
import { Instruction } from '.';
|
import { Instruction } from '.';
|
||||||
import { NAMESPACE, lang } from '../locale';
|
import { NAMESPACE, lang } from '../locale';
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { useField, useForm } from '@formily/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useCollectionDataSource, useCollectionManager_deprecated } from '@nocobase/client';
|
import { useCollectionDataSource, useCollectionManager_deprecated } from '@nocobase/client';
|
||||||
|
import { isValidFilter } from '@nocobase/utils/client';
|
||||||
|
|
||||||
import CollectionFieldset from '../components/CollectionFieldset';
|
import CollectionFieldset from '../components/CollectionFieldset';
|
||||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||||
@ -9,7 +10,6 @@ import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
|||||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||||
import { NAMESPACE, lang } from '../locale';
|
import { NAMESPACE, lang } from '../locale';
|
||||||
import { collection, filter, values } from '../schemas/collection';
|
import { collection, filter, values } from '../schemas/collection';
|
||||||
import { isValidFilter } from '../utils';
|
|
||||||
import { Instruction } from '.';
|
import { Instruction } from '.';
|
||||||
|
|
||||||
function IndividualHooksRadioWithTooltip({ onChange, ...props }) {
|
function IndividualHooksRadioWithTooltip({ onChange, ...props }) {
|
||||||
|
@ -12,29 +12,6 @@ export function linkNodes(nodes): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidFilter(condition) {
|
|
||||||
const group = condition.$and || condition.$or;
|
|
||||||
if (!group) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return group.some((item) => {
|
|
||||||
if (item.$and || item.$or) {
|
|
||||||
return isValidFilter(item);
|
|
||||||
}
|
|
||||||
const [name] = Object.keys(item);
|
|
||||||
if (!name || !item[name]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const [op] = Object.keys(item[name]);
|
|
||||||
if (!op || typeof item[name][op] === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function traverseSchema(schema, fn) {
|
export function traverseSchema(schema, fn) {
|
||||||
fn(schema);
|
fn(schema);
|
||||||
if (schema.properties) {
|
if (schema.properties) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user