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_PASSWORD: password
|
||||
DB_DATABASE: nocobase
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 180
|
||||
|
@ -61,4 +61,96 @@ describe('example', () => {
|
||||
expect(m.name).toBe('n1');
|
||||
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();
|
||||
console.log('next....');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -314,4 +314,63 @@ describe('destroy', () => {
|
||||
await User.repository.destroy(u2['id']);
|
||||
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([]);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await db.getRepository('posts').create({
|
||||
values: {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { isValidFilter } from '@nocobase/utils';
|
||||
|
||||
const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
const oldValue = descriptor.value;
|
||||
|
||||
@ -8,7 +10,7 @@ const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: Prop
|
||||
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`);
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ import {
|
||||
UpdateOptions as SequelizeUpdateOptions,
|
||||
WhereOperators,
|
||||
} from 'sequelize';
|
||||
import { isValidFilter } from '@nocobase/utils';
|
||||
|
||||
import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
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 (
|
||||
this.collection.model.primaryKeyAttributes.length !== 1 &&
|
||||
!lodash.get(this.collection.options, 'filterTargetKey')
|
||||
|
@ -200,6 +200,7 @@ export function parseQuery(input: string): any {
|
||||
// 逗号分隔转换为数组
|
||||
// comma: true,
|
||||
});
|
||||
|
||||
// filter 支持 json string
|
||||
if (typeof query.filter === 'string') {
|
||||
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 packageJson from '../package.json';
|
||||
import { MainDataSource } from './main-data-source';
|
||||
import validateFilterParams from './middlewares/validate-filter-params';
|
||||
|
||||
export type PluginType = string | typeof Plugin;
|
||||
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(validateFilterParams, { tag: 'validate-filter-params', before: ['auth'] });
|
||||
|
||||
this.resourcer.use(this._authManager.middleware(), { tag: 'auth' });
|
||||
this.resourcer.use(validateFilterParams, { tag: 'validate-filter-params', before: ['auth'] });
|
||||
|
||||
if (this.options.acl !== false) {
|
||||
this.resourcer.use(this.acl.middleware(), { tag: 'acl', after: ['auth'] });
|
||||
|
@ -85,6 +85,7 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
|
||||
tag: 'parseVariables',
|
||||
after: 'acl',
|
||||
});
|
||||
|
||||
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
if (restParams.filter) {
|
||||
restParams.filter = JSON.stringify(restParams.filter);
|
||||
}
|
||||
|
||||
const queryString = qs.stringify(restParams, { arrayFormat: 'brackets' });
|
||||
|
||||
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 './forEach';
|
||||
export * from './getValuesByPath';
|
||||
export * from './isValidFilter';
|
||||
export * from './json-templates';
|
||||
export * from './log';
|
||||
export * from './merge';
|
||||
|
@ -8,6 +8,7 @@ export * from './date';
|
||||
export * from './dayjs';
|
||||
export * from './forEach';
|
||||
export * from './fs-exists';
|
||||
export * from './isValidFilter';
|
||||
export * from './json-templates';
|
||||
export * from './koa-multer';
|
||||
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 page.getByRole('menuitem', { name: preManualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${preManualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
|
@ -104,6 +104,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -260,6 +271,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -416,6 +438,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -572,6 +605,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -728,6 +772,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -884,6 +939,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -1056,6 +1122,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -1228,6 +1305,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -1400,6 +1488,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -1573,6 +1672,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -1754,6 +1864,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -1926,6 +2047,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
@ -2104,6 +2236,17 @@ test.describe('field data update', () => {
|
||||
}
|
||||
await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||
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
|
||||
.locator(`button[aria-label^="schema-initializer-Grid-form:configureFields-${manualNodeCollectionName}"]`)
|
||||
.hover();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FormLayout } from '@formily/antd-v5';
|
||||
import { createForm } from '@formily/core';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -45,7 +45,7 @@ import WorkflowPlugin, {
|
||||
} from '@nocobase/plugin-workflow/client';
|
||||
import { Registry, lodash } from '@nocobase/utils/client';
|
||||
|
||||
import { NAMESPACE, useLang } from '../../locale';
|
||||
import { NAMESPACE, usePluginTranslation } from '../../locale';
|
||||
import { FormBlockProvider } from './FormBlockProvider';
|
||||
import createRecordForm from './forms/create';
|
||||
import customRecordForm from './forms/custom';
|
||||
@ -86,13 +86,14 @@ export type ManualFormType = {
|
||||
[key: string]: React.FC;
|
||||
};
|
||||
};
|
||||
validate?: (config: any) => string | null;
|
||||
};
|
||||
|
||||
export const manualFormTypes = new Registry<ManualFormType>();
|
||||
|
||||
manualFormTypes.register('customForm', customRecordForm);
|
||||
manualFormTypes.register('createForm', createRecordForm);
|
||||
manualFormTypes.register('updateForm', updateRecordForm);
|
||||
manualFormTypes.register('custom', customRecordForm);
|
||||
manualFormTypes.register('create', createRecordForm);
|
||||
manualFormTypes.register('update', updateRecordForm);
|
||||
|
||||
function useTriggerInitializers(): SchemaInitializerItemType | null {
|
||||
const { workflow } = useFlowContext();
|
||||
@ -243,7 +244,8 @@ export const addBlockButton = new CompatibleSchemaInitializer(
|
||||
|
||||
function AssignedFieldValues() {
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const { t } = useTranslation();
|
||||
const { t: coreT } = useTranslation();
|
||||
const { t } = usePluginTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const scope = useWorkflowVariableOptions();
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -275,7 +277,7 @@ function AssignedFieldValues() {
|
||||
}, [fieldSchema]);
|
||||
const upLevelActiveFields = useFormActiveFields();
|
||||
|
||||
const title = t('Assign field values');
|
||||
const title = coreT('Assign field values');
|
||||
|
||||
function onCancel() {
|
||||
setOpen(false);
|
||||
@ -321,9 +323,7 @@ function AssignedFieldValues() {
|
||||
<FormProvider form={form}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<Alert
|
||||
message={useLang(
|
||||
'Values preset in this form will override user submitted ones when continue or reject.',
|
||||
)}
|
||||
message={t('Values preset in this form will override user submitted ones when continue or reject.')}
|
||||
/>
|
||||
<br />
|
||||
{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) {
|
||||
const { workflow } = useFlowContext();
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<ActionContextProvider value={{ visible, setVisible, formValueChanged: false }}>
|
||||
<ActionContextProvider value={{ visible, setVisible: onSetVisible, formValueChanged: false }}>
|
||||
{props.children}
|
||||
</ActionContextProvider>
|
||||
</>
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
useDesignable,
|
||||
useMenuSearch,
|
||||
} from '@nocobase/client';
|
||||
import { isValidFilter } from '@nocobase/utils/client';
|
||||
import { FilterDynamicComponent } from '@nocobase/plugin-workflow/client';
|
||||
|
||||
import { NAMESPACE } from '../../../locale';
|
||||
@ -156,4 +157,11 @@ export default {
|
||||
},
|
||||
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;
|
||||
|
@ -7,6 +7,8 @@ export function useLang(key: string, options = {}) {
|
||||
return t(key);
|
||||
}
|
||||
|
||||
export function usePluginTranslation(options) {
|
||||
export const lang = useLang;
|
||||
|
||||
export function usePluginTranslation(options?) {
|
||||
return useTranslation(NAMESPACE, options);
|
||||
}
|
||||
|
@ -27,5 +27,6 @@
|
||||
"Filter settings": "筛选设置",
|
||||
"Workflow todos": "工作流待办",
|
||||
"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',
|
||||
actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }],
|
||||
collection: 'posts',
|
||||
filter: { title: 't1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useCollectionDataSource } from '@nocobase/client';
|
||||
import { isValidFilter } from '@nocobase/utils/client';
|
||||
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import { collection, filter } from '../schemas/collection';
|
||||
import { isValidFilter } from '../utils';
|
||||
import { Instruction } from '.';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { useField, useForm } from '@formily/react';
|
||||
import React from 'react';
|
||||
|
||||
import { useCollectionDataSource, useCollectionManager_deprecated } from '@nocobase/client';
|
||||
import { isValidFilter } from '@nocobase/utils/client';
|
||||
|
||||
import CollectionFieldset from '../components/CollectionFieldset';
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
@ -9,7 +10,6 @@ import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import { collection, filter, values } from '../schemas/collection';
|
||||
import { isValidFilter } from '../utils';
|
||||
import { Instruction } from '.';
|
||||
|
||||
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) {
|
||||
fn(schema);
|
||||
if (schema.properties) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user