diff --git a/packages/core/actions/src/actions/list.ts b/packages/core/actions/src/actions/list.ts index 2920c832b..3df711f50 100644 --- a/packages/core/actions/src/actions/list.ts +++ b/packages/core/actions/src/actions/list.ts @@ -1,22 +1,7 @@ import { assign } from '@nocobase/utils'; import { Context } from '..'; -import { getRepositoryFromParams } from '../utils'; - -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, - }; -} +import { getRepositoryFromParams, pageArgsToLimitArgs } from '../utils'; +import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constants'; function totalPage(total, pageSize): number { return Math.ceil(total / pageSize); diff --git a/packages/core/actions/src/constants.ts b/packages/core/actions/src/constants.ts new file mode 100644 index 000000000..bf7207acd --- /dev/null +++ b/packages/core/actions/src/constants.ts @@ -0,0 +1,2 @@ +export const DEFAULT_PAGE = 1; +export const DEFAULT_PER_PAGE = 20; diff --git a/packages/core/actions/src/index.ts b/packages/core/actions/src/index.ts index 75528e515..af6f8fe6e 100644 --- a/packages/core/actions/src/index.ts +++ b/packages/core/actions/src/index.ts @@ -7,6 +7,8 @@ import * as actions from './actions'; export * as utils from './utils'; +export * from './constants'; + export type Next = () => Promise; export interface Context extends Koa.Context { diff --git a/packages/core/actions/src/utils.ts b/packages/core/actions/src/utils.ts index 8ba71cce3..b0a1444da 100644 --- a/packages/core/actions/src/utils.ts +++ b/packages/core/actions/src/utils.ts @@ -1,6 +1,19 @@ import { MultipleRelationRepository, Repository } from '@nocobase/database'; 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) { const { resourceName, resourceOf } = ctx.action; diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 399dce014..8628eb58e 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -1,5 +1,7 @@ export default { "Display <1><0>10<1>20<2>50<3>100 items per page": "每页显示 <1><0>10<1>20<2>50<3>100 条", + "Page number": "页码", + "Page size": "每页条数", "Meet <1><0>All<1>Any conditions in the group": "满足组内 <1><0>全部<1>任意 条件", "Open in<1><0>Modal<1>Drawer<2>Window": "在 <1><0>对话框<1>抽屉<2>窗口 内打开", "{{count}} filter items": "{{count}} 个筛选项", diff --git a/packages/plugins/localization-management/src/server/actions/localizationTexts.ts b/packages/plugins/localization-management/src/server/actions/localizationTexts.ts index 0b174f8c9..3f4556dc9 100644 --- a/packages/plugins/localization-management/src/server/actions/localizationTexts.ts +++ b/packages/plugins/localization-management/src/server/actions/localizationTexts.ts @@ -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 { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constans'; const appendTranslations = async (db: Database, rows: Model[], locale: string): Promise => { const texts = rows || []; diff --git a/packages/plugins/localization-management/src/server/constans.ts b/packages/plugins/localization-management/src/server/constans.ts index 2b0083ca2..48bf9ca31 100644 --- a/packages/plugins/localization-management/src/server/constans.ts +++ b/packages/plugins/localization-management/src/server/constans.ts @@ -1,4 +1 @@ -export const DEFAULT_PAGE = 1; -export const DEFAULT_PER_PAGE = 20; - export const CACHE_KEY = 'localization:texts'; diff --git a/packages/plugins/workflow/src/client/ExecutionCanvas.tsx b/packages/plugins/workflow/src/client/ExecutionCanvas.tsx index 9d35cf0d5..9ae22798a 100644 --- a/packages/plugins/workflow/src/client/ExecutionCanvas.tsx +++ b/packages/plugins/workflow/src/client/ExecutionCanvas.tsx @@ -95,10 +95,7 @@ function JobModal() { 'x-decorator': 'FormItem', 'x-component': 'Input.JSON', 'x-component-props': { - className: css` - padding: 1em; - background-color: #eee; - `, + className: styles.nodeJobResultClass, }, 'x-read-pretty': true, }, diff --git a/packages/plugins/workflow/src/client/nodes/query.tsx b/packages/plugins/workflow/src/client/nodes/query.tsx index 62aabc95a..6c54060f0 100644 --- a/packages/plugins/workflow/src/client/nodes/query.tsx +++ b/packages/plugins/workflow/src/client/nodes/query.tsx @@ -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 { CollectionBlockInitializer } from '../components/CollectionBlockInitializer'; import { FilterDynamicComponent } from '../components/FilterDynamicComponent'; -import { getCollectionFieldOptions } from '../variable'; -import { FieldsSelect } from '../components/FieldsSelect'; +import { getCollectionFieldOptions, useWorkflowVariableOptions } from '../variable'; +import { useForm } from '@formily/react'; export default { title: `{{t("Query record", { ns: "${NAMESPACE}" })}}`, @@ -16,31 +24,70 @@ export default { collection, multiple: { type: 'boolean', - title: `{{t("Allow multiple records as result", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', '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}" })}}`, }, params: { type: 'object', + 'x-component': 'fieldset', properties: { filter, + sort, + pagination, appends, }, + 'x-reactions': [ + { + dependencies: ['collection'], + fulfill: { + state: { + visible: '{{$deps[0] != null}}', + }, + }, + }, + ], }, failOnEmpty: { type: 'boolean', - title: `{{t("Exit when query result is null", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'Checkbox', + 'x-content': `{{t("Exit when query result is null", { ns: "${NAMESPACE}" })}}`, }, }, view: {}, scope: { 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: { + ArrayItems, FilterDynamicComponent, + SchemaComponentContext, }, useVariables({ id, title, config }, options) { const compile = useCompile(); diff --git a/packages/plugins/workflow/src/client/schemas/collection.ts b/packages/plugins/workflow/src/client/schemas/collection.ts index 2d2810ae1..30239b434 100644 --- a/packages/plugins/workflow/src/client/schemas/collection.ts +++ b/packages/plugins/workflow/src/client/schemas/collection.ts @@ -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 = { type: 'array', title: `{{t("Preload associations", { ns: "${NAMESPACE}" })}}`, diff --git a/packages/plugins/workflow/src/client/style.tsx b/packages/plugins/workflow/src/client/style.tsx index e754ebea1..4c3d21781 100644 --- a/packages/plugins/workflow/src/client/style.tsx +++ b/packages/plugins/workflow/src/client/style.tsx @@ -320,6 +320,11 @@ const useStyles = createStyles(({ css, token }) => { align-items: center; `, + nodeJobResultClass: css` + padding: 1em; + background-color: ${token.colorBgContainer}; + `, + addButtonClass: css` flex-shrink: 0; padding: 2em 0; diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/query.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/query.test.ts index 97ad5d60c..e7ebf1a37 100644 --- a/packages/plugins/workflow/src/server/__tests__/instructions/query.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/instructions/query.test.ts @@ -226,7 +226,7 @@ describe('workflow > instructions > query', () => { config: { collection: 'posts', params: { - sort: 'id', + sort: [{ field: 'id', direction: 'asc' }], }, }, }); @@ -313,6 +313,31 @@ describe('workflow > instructions > query', () => { const [job] = await execution.getJobs(); 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', () => { diff --git a/packages/plugins/workflow/src/server/instructions/query.ts b/packages/plugins/workflow/src/server/instructions/query.ts index fa610f269..fba805170 100644 --- a/packages/plugins/workflow/src/server/instructions/query.ts +++ b/packages/plugins/workflow/src/server/instructions/query.ts @@ -1,3 +1,5 @@ +import { DEFAULT_PAGE, DEFAULT_PER_PAGE, utils } from '@nocobase/actions'; + import Processor from '../Processor'; import { JOB_STATUS } from '../constants'; import { toJSON } from '../utils'; @@ -8,7 +10,12 @@ export default { const { collection, multiple, params = {}, failOnEmpty = false } = node.config; const repo = (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 ? Array.from( options.appends.reduce((set, field) => { @@ -20,7 +27,11 @@ export default { : options.appends; const result = await (multiple ? repo.find : repo.findOne).call(repo, { ...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, });