feat(plugin-workflow): add sort and pagination to query node params (#2453)
* refactor(core): expose some utils and constants * feat(plugin-workfow): add sort and pagination to query node params * fix(plugin-workflow): fix job result json style * fix(plugin-workflow): fix sort param * fix(plugin-workflow): fix test cases
This commit is contained in:
parent
3baebf2a71
commit
dea4c6f9c7
@ -1,22 +1,7 @@
|
|||||||
import { assign } from '@nocobase/utils';
|
import { assign } from '@nocobase/utils';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import { getRepositoryFromParams } from '../utils';
|
import { getRepositoryFromParams, pageArgsToLimitArgs } from '../utils';
|
||||||
|
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constants';
|
||||||
export const DEFAULT_PAGE = 1;
|
|
||||||
export const DEFAULT_PER_PAGE = 20;
|
|
||||||
|
|
||||||
function pageArgsToLimitArgs(
|
|
||||||
page: number,
|
|
||||||
pageSize: number,
|
|
||||||
): {
|
|
||||||
offset: number;
|
|
||||||
limit: number;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
offset: (page - 1) * pageSize,
|
|
||||||
limit: pageSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function totalPage(total, pageSize): number {
|
function totalPage(total, pageSize): number {
|
||||||
return Math.ceil(total / pageSize);
|
return Math.ceil(total / pageSize);
|
||||||
|
2
packages/core/actions/src/constants.ts
Normal file
2
packages/core/actions/src/constants.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const DEFAULT_PAGE = 1;
|
||||||
|
export const DEFAULT_PER_PAGE = 20;
|
@ -7,6 +7,8 @@ import * as actions from './actions';
|
|||||||
|
|
||||||
export * as utils from './utils';
|
export * as utils from './utils';
|
||||||
|
|
||||||
|
export * from './constants';
|
||||||
|
|
||||||
export type Next = () => Promise<any>;
|
export type Next = () => Promise<any>;
|
||||||
|
|
||||||
export interface Context extends Koa.Context {
|
export interface Context extends Koa.Context {
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
import { MultipleRelationRepository, Repository } from '@nocobase/database';
|
import { MultipleRelationRepository, Repository } from '@nocobase/database';
|
||||||
import { Context } from '.';
|
import { Context } from '.';
|
||||||
|
|
||||||
|
export function pageArgsToLimitArgs(
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
): {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
offset: (page - 1) * pageSize,
|
||||||
|
limit: pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getRepositoryFromParams(ctx: Context) {
|
export function getRepositoryFromParams(ctx: Context) {
|
||||||
const { resourceName, resourceOf } = ctx.action;
|
const { resourceName, resourceOf } = ctx.action;
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "每页显示 <1><0>10</0><1>20</1><2>50</2><3>100</3></1> 条",
|
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "每页显示 <1><0>10</0><1>20</1><2>50</2><3>100</3></1> 条",
|
||||||
|
"Page number": "页码",
|
||||||
|
"Page size": "每页条数",
|
||||||
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "满足组内 <1><0>全部</0><1>任意</1></1> 条件",
|
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "满足组内 <1><0>全部</0><1>任意</1></1> 条件",
|
||||||
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "在 <1><0>对话框</0><1>抽屉</1><2>窗口</2></1> 内打开",
|
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "在 <1><0>对话框</0><1>抽屉</1><2>窗口</2></1> 内打开",
|
||||||
"{{count}} filter items": "{{count}} 个筛选项",
|
"{{count}} filter items": "{{count}} 个筛选项",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Context, Next } from '@nocobase/actions';
|
import { Context, Next, DEFAULT_PAGE, DEFAULT_PER_PAGE } from '@nocobase/actions';
|
||||||
import { Database, Model, Op } from '@nocobase/database';
|
import { Database, Model, Op } from '@nocobase/database';
|
||||||
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constans';
|
|
||||||
|
|
||||||
const appendTranslations = async (db: Database, rows: Model[], locale: string): Promise<any[]> => {
|
const appendTranslations = async (db: Database, rows: Model[], locale: string): Promise<any[]> => {
|
||||||
const texts = rows || [];
|
const texts = rows || [];
|
||||||
|
@ -1,4 +1 @@
|
|||||||
export const DEFAULT_PAGE = 1;
|
|
||||||
export const DEFAULT_PER_PAGE = 20;
|
|
||||||
|
|
||||||
export const CACHE_KEY = 'localization:texts';
|
export const CACHE_KEY = 'localization:texts';
|
||||||
|
@ -95,10 +95,7 @@ function JobModal() {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input.JSON',
|
'x-component': 'Input.JSON',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
className: css`
|
className: styles.nodeJobResultClass,
|
||||||
padding: 1em;
|
|
||||||
background-color: #eee;
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
import { ArrayItems } from '@formily/antd-v5';
|
||||||
|
|
||||||
import { appends, collection, filter } from '../schemas/collection';
|
import {
|
||||||
|
SchemaComponentContext,
|
||||||
|
SchemaInitializerItemOptions,
|
||||||
|
useCollectionDataSource,
|
||||||
|
useCollectionManager,
|
||||||
|
useCompile,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
|
||||||
|
import { appends, collection, filter, pagination, sort } from '../schemas/collection';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||||
import { getCollectionFieldOptions } from '../variable';
|
import { getCollectionFieldOptions, useWorkflowVariableOptions } from '../variable';
|
||||||
import { FieldsSelect } from '../components/FieldsSelect';
|
import { useForm } from '@formily/react';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: `{{t("Query record", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Query record", { ns: "${NAMESPACE}" })}}`,
|
||||||
@ -16,31 +24,70 @@ export default {
|
|||||||
collection,
|
collection,
|
||||||
multiple: {
|
multiple: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: `{{t("Allow multiple records as result", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
|
'x-content': `{{t("Allow multiple records as result", { ns: "${NAMESPACE}" })}}`,
|
||||||
description: `{{t("If checked, when there are multiple records in the query result, an array will be returned as the result, which can be operated on one by one using a loop node. Otherwise, only one record will be returned.", { ns: "${NAMESPACE}" })}}`,
|
description: `{{t("If checked, when there are multiple records in the query result, an array will be returned as the result, which can be operated on one by one using a loop node. Otherwise, only one record will be returned.", { ns: "${NAMESPACE}" })}}`,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
'x-component': 'fieldset',
|
||||||
properties: {
|
properties: {
|
||||||
filter,
|
filter,
|
||||||
|
sort,
|
||||||
|
pagination,
|
||||||
appends,
|
appends,
|
||||||
},
|
},
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
dependencies: ['collection'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{$deps[0] != null}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
failOnEmpty: {
|
failOnEmpty: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: `{{t("Exit when query result is null", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
|
'x-content': `{{t("Exit when query result is null", { ns: "${NAMESPACE}" })}}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
view: {},
|
view: {},
|
||||||
scope: {
|
scope: {
|
||||||
useCollectionDataSource,
|
useCollectionDataSource,
|
||||||
|
useWorkflowVariableOptions,
|
||||||
|
useSortableFields() {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { getCollectionFields, getInterface } = useCollectionManager();
|
||||||
|
const { values } = useForm();
|
||||||
|
const fields = getCollectionFields(values.collection);
|
||||||
|
return fields
|
||||||
|
.filter((field: any) => {
|
||||||
|
if (!field.interface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fieldInterface = getInterface(field.interface);
|
||||||
|
if (fieldInterface?.sortable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((field: any) => {
|
||||||
|
return {
|
||||||
|
value: field.name,
|
||||||
|
label: field?.uiSchema?.title ? compile(field?.uiSchema?.title) : field.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
ArrayItems,
|
||||||
FilterDynamicComponent,
|
FilterDynamicComponent,
|
||||||
|
SchemaComponentContext,
|
||||||
},
|
},
|
||||||
useVariables({ id, title, config }, options) {
|
useVariables({ id, title, config }, options) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
@ -49,6 +49,123 @@ export const filter = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sort = {
|
||||||
|
type: 'array',
|
||||||
|
title: '{{t("Sort")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
space: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
properties: {
|
||||||
|
sort: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems.SortHandle',
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: 'string',
|
||||||
|
enum: '{{useSortableFields()}}',
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
width: 260,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
direction: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-component-props': {
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
label: '{{t("ASC")}}',
|
||||||
|
value: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("DESC")}}',
|
||||||
|
value: 'desc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'ArrayItems.Remove',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
add: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Add sort field")}}',
|
||||||
|
'x-component': 'ArrayItems.Addition',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pagination = {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Pagination")}}',
|
||||||
|
'x-decorator': 'SchemaComponentContext.Provider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
value: { designable: false },
|
||||||
|
},
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Page number")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Variable.Input',
|
||||||
|
'x-component-props': {
|
||||||
|
scope: '{{useWorkflowVariableOptions()}}',
|
||||||
|
useTypedConstant: ['number', 'null'],
|
||||||
|
},
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageSize: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
pageSize: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("Page size")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-component-props': {
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const appends = {
|
export const appends = {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
title: `{{t("Preload associations", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Preload associations", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
@ -320,6 +320,11 @@ const useStyles = createStyles(({ css, token }) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
nodeJobResultClass: css`
|
||||||
|
padding: 1em;
|
||||||
|
background-color: ${token.colorBgContainer};
|
||||||
|
`,
|
||||||
|
|
||||||
addButtonClass: css`
|
addButtonClass: css`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 2em 0;
|
padding: 2em 0;
|
||||||
|
@ -226,7 +226,7 @@ describe('workflow > instructions > query', () => {
|
|||||||
config: {
|
config: {
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
params: {
|
params: {
|
||||||
sort: 'id',
|
sort: [{ field: 'id', direction: 'asc' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -313,6 +313,31 @@ describe('workflow > instructions > query', () => {
|
|||||||
const [job] = await execution.getJobs();
|
const [job] = await execution.getJobs();
|
||||||
expect(job.result.length).toBe(0);
|
expect(job.result.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('params.sort & params.page & params.pageSize', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'query',
|
||||||
|
config: {
|
||||||
|
collection: 'posts',
|
||||||
|
multiple: true,
|
||||||
|
params: {
|
||||||
|
sort: [{ field: 'id', direction: 'asc' }],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
const p2 = await PostRepo.create({ values: { title: 't2' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.result.length).toBe(1);
|
||||||
|
expect(job.result[0].title).toBe(p1.title);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('failOnEmpty', () => {
|
describe('failOnEmpty', () => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, utils } from '@nocobase/actions';
|
||||||
|
|
||||||
import Processor from '../Processor';
|
import Processor from '../Processor';
|
||||||
import { JOB_STATUS } from '../constants';
|
import { JOB_STATUS } from '../constants';
|
||||||
import { toJSON } from '../utils';
|
import { toJSON } from '../utils';
|
||||||
@ -8,7 +10,12 @@ export default {
|
|||||||
const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
|
const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
|
||||||
|
|
||||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||||
const options = processor.getParsedValue(params, node);
|
const {
|
||||||
|
page = DEFAULT_PAGE,
|
||||||
|
pageSize = DEFAULT_PER_PAGE,
|
||||||
|
sort = [],
|
||||||
|
...options
|
||||||
|
} = processor.getParsedValue(params, node);
|
||||||
const appends = options.appends
|
const appends = options.appends
|
||||||
? Array.from(
|
? Array.from(
|
||||||
options.appends.reduce((set, field) => {
|
options.appends.reduce((set, field) => {
|
||||||
@ -20,7 +27,11 @@ export default {
|
|||||||
: options.appends;
|
: options.appends;
|
||||||
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
|
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
|
||||||
...options,
|
...options,
|
||||||
appends: appends,
|
...utils.pageArgsToLimitArgs(page, pageSize),
|
||||||
|
sort: sort
|
||||||
|
.filter((item) => item.field)
|
||||||
|
.map((item) => `${item.direction?.toLowerCase() === 'desc' ? '-' : ''}${item.field}`),
|
||||||
|
appends,
|
||||||
transaction: processor.transaction,
|
transaction: processor.transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user