diff --git a/packages/core/client/src/workflow/ExecutionResourceProvider.tsx b/packages/core/client/src/workflow/ExecutionResourceProvider.tsx index f2f08323a..97a5f8e2f 100644 --- a/packages/core/client/src/workflow/ExecutionResourceProvider.tsx +++ b/packages/core/client/src/workflow/ExecutionResourceProvider.tsx @@ -11,7 +11,7 @@ export const ExecutionResourceProvider = ({ request, ...others }) => { ...request?.params, filter: { ...(request?.params?.filter), - workflowId: workflow.id + key: workflow.key } } }, diff --git a/packages/core/client/src/workflow/WorkflowCanvas.tsx b/packages/core/client/src/workflow/WorkflowCanvas.tsx index 71caa158a..e2bfb8f65 100644 --- a/packages/core/client/src/workflow/WorkflowCanvas.tsx +++ b/packages/core/client/src/workflow/WorkflowCanvas.tsx @@ -8,7 +8,6 @@ import { useAPIClient, useCompile, useDocumentTitle, - useRecord, useResourceActionContext, useResourceContext } from '..'; @@ -79,13 +78,13 @@ export function WorkflowCanvas() { refresh(); } - async function onDuplicate() { - const { data: { data: duplicated } } = await resource.duplicate({ + async function onRevision() { + const { data: { data: revision } } = await resource.revision({ filterByTk: workflow[targetKey] }); message.success(t('Operation succeeded')); - history.push(`${duplicated.id}`); + history.push(`${revision.id}`); } return ( @@ -134,7 +133,7 @@ export function WorkflowCanvas() { /> {workflow.executed && !revisions.find(item => !item.executed && new Date(item.createdAt) > new Date(workflow.createdAt)) ? ( - + ) : null } diff --git a/packages/core/client/src/workflow/schemas/executions.ts b/packages/core/client/src/workflow/schemas/executions.ts index ea8579cb4..acad4a1c3 100644 --- a/packages/core/client/src/workflow/schemas/executions.ts +++ b/packages/core/client/src/workflow/schemas/executions.ts @@ -16,6 +16,17 @@ const collection = { 'x-read-pretty': true, } as ISchema, }, + { + interface: 'number', + type: 'number', + name: 'workflowId', + uiSchema: { + type: 'number', + title: '{{t("Version")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + } as ISchema, + }, { type: 'number', name: 'status', @@ -95,6 +106,18 @@ export const executionSchema = { }, } }, + workflowId: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + workflowId: { + type: 'number', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + } + }, status: { type: 'void', 'x-decorator': 'Table.Column.Decorator', diff --git a/packages/plugins/workflow/src/__tests__/workflow.test.ts b/packages/plugins/workflow/src/__tests__/workflow.test.ts index 90f3c3360..bf4883b1a 100644 --- a/packages/plugins/workflow/src/__tests__/workflow.test.ts +++ b/packages/plugins/workflow/src/__tests__/workflow.test.ts @@ -1,12 +1,12 @@ -import { Application } from '@nocobase/server'; +import { MockServer } from '@nocobase/test'; import Database from '@nocobase/database'; import { getApp } from '.'; -import { EXECUTION_STATUS } from '../constants'; describe('workflow > workflow', () => { - let app: Application; + let app: MockServer; + let agent; let db: Database; let PostModel; let PostRepo; @@ -14,7 +14,7 @@ describe('workflow > workflow', () => { beforeEach(async () => { app = await getApp(); - + agent = app.agent(); db = app.db; WorkflowModel = db.getCollection('workflows').model; PostModel = db.getCollection('posts').model; @@ -41,4 +41,55 @@ describe('workflow > workflow', () => { expect(executions.length).toBe(0); }); }); + + describe('revisions', () => { + it('executed count', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts' + } + }); + expect(w1.current).toBe(true); + + const { body, status } = await agent.resource(`workflows`).revision({ + filterByTk: w1.id + }); + + expect(status).toBe(200); + const { data: w2 } = body; + expect(w2.config).toMatchObject(w1.config); + expect(w2.key).toBe(w1.key); + + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await WorkflowModel.update({ + enabled: true + }, { + where: { + id: w2.id + }, + individualHooks: true + }); + + const [w1next, w2next] = await WorkflowModel.findAll({ + order: [['id', 'ASC']] + }); + + expect(w1next.enabled).toBe(false); + expect(w1next.current).toBe(false); + expect(w2next.enabled).toBe(true); + expect(w2next.current).toBe(true); + + const p2 = await PostRepo.create({ values: { title: 't2' } }); + + const [e1] = await w1next.getExecutions(); + const [e2] = await w2next.getExecutions(); + expect(e1.key).toBe(e2.key); + expect(e1.workflowId).toBe(w1.id); + expect(e2.workflowId).toBe(w2.id); + }); + }); }); diff --git a/packages/plugins/workflow/src/actions/workflows.ts b/packages/plugins/workflow/src/actions/workflows.ts index 4e1dc32ad..996143f6d 100644 --- a/packages/plugins/workflow/src/actions/workflows.ts +++ b/packages/plugins/workflow/src/actions/workflows.ts @@ -1,53 +1,7 @@ import { Context, utils } from '@nocobase/actions'; -import { Op } from '@nocobase/database'; -export async function update(context: Context, next) { - const { db } = context; - const repository = utils.getRepositoryFromParams(context); - const { filterByTk, values, whitelist, blacklist, filter, updateAssociationValues } = context.action.params; - - context.body = await db.sequelize.transaction(async transaction => { - const others: { enabled?: boolean, current?: boolean } = {}; - - if (values.enabled) { - values.current = true; - others.enabled = false; - } - - if (values.current) { - others.current = false; - await repository.update({ - filter: { - key: values.key, - id: { - [Op.ne]: filterByTk - } - }, - values: others, - context, - transaction - }); - } - - const instance = await repository.update({ - filterByTk, - values, - whitelist, - blacklist, - filter, - updateAssociationValues, - context, - transaction - }); - - return instance; - }); - - await next(); -} - function typeOf(value) { if (Array.isArray(value)) { return 'array'; @@ -81,7 +35,7 @@ function migrateConfig(config, oldToNew) { return migrate(config); } -export async function duplicate(context: Context, next) { +export async function revision(context: Context, next) { const { db } = context; const repository = utils.getRepositoryFromParams(context); const { filterByTk } = context.action.params; diff --git a/packages/plugins/workflow/src/collections/executions.ts b/packages/plugins/workflow/src/collections/executions.ts index 6df4a317f..dbf17dbea 100644 --- a/packages/plugins/workflow/src/collections/executions.ts +++ b/packages/plugins/workflow/src/collections/executions.ts @@ -3,18 +3,18 @@ import { CollectionOptions } from '@nocobase/database'; export default { name: 'executions', model: 'ExecutionModel', - title: '执行流程', fields: [ { - interface: 'linkTo', type: 'belongsTo', - name: 'workflow', - title: '所属工作流' + name: 'workflow' + }, + { + type: 'uid', + name: 'key' }, { type: 'boolean', name: 'useTransaction', - title: '使用事务', defaultValue: false }, { @@ -23,22 +23,16 @@ export default { defaultValue: null }, { - interface: 'linkTo', type: 'hasMany', name: 'jobs', - title: '流程记录' }, { - interface: 'json', type: 'jsonb', name: 'context', - title: '上下文数据' }, { - interface: 'select', type: 'integer', name: 'status', - title: '状态' } ] } as CollectionOptions; diff --git a/packages/plugins/workflow/src/collections/workflows.ts b/packages/plugins/workflow/src/collections/workflows.ts index 22a6b09c8..8b1f9f70e 100644 --- a/packages/plugins/workflow/src/collections/workflows.ts +++ b/packages/plugins/workflow/src/collections/workflows.ts @@ -3,7 +3,6 @@ import { CollectionOptions } from '@nocobase/database'; export default { name: 'workflows', model: 'WorkflowModel', - title: '自动化', fields: [ { name: 'key', @@ -12,56 +11,52 @@ export default { { type: 'string', name: 'title', - title: '工作流名称', required: true }, { type: 'boolean', name: 'enabled', - title: '启用', defaultValue: false }, { type: 'text', - name: 'description', - title: '描述' + name: 'description' }, { type: 'string', - title: '触发方式', name: 'type', required: true }, { type: 'jsonb', - title: '触发配置', name: 'config', required: true, defaultValue: {} }, { type: 'boolean', - title: '使用事务', name: 'useTransaction', defaultValue: true }, { type: 'hasMany', name: 'nodes', - target: 'flow_nodes', - title: '流程节点' + target: 'flow_nodes' }, { type: 'hasMany', - name: 'executions', - target: 'executions', - title: '触发执行' + name: 'executions' }, { type: 'integer', name: 'executed', defaultValue: 0 }, + { + type: 'integer', + name: 'allExecuted', + defaultValue: 0 + }, { type: 'boolean', name: 'current', diff --git a/packages/plugins/workflow/src/models/Execution.ts b/packages/plugins/workflow/src/models/Execution.ts index f28cbae35..017cba957 100644 --- a/packages/plugins/workflow/src/models/Execution.ts +++ b/packages/plugins/workflow/src/models/Execution.ts @@ -26,6 +26,8 @@ export default class ExecutionModel extends Model { declare createdAt: Date; declare updatedAt: Date; + declare key: string; + declare workflow?: WorkflowModel; declare getWorkflow: BelongsToGetAssociationMixin; diff --git a/packages/plugins/workflow/src/models/Workflow.ts b/packages/plugins/workflow/src/models/Workflow.ts index 38e81e00f..845c77b8b 100644 --- a/packages/plugins/workflow/src/models/Workflow.ts +++ b/packages/plugins/workflow/src/models/Workflow.ts @@ -9,8 +9,10 @@ export default class WorkflowModel extends Model { declare static database: Database; declare id: number; + declare key: string; declare title: string; declare enabled: boolean; + declare current: boolean; declare description?: string; declare type: string; declare config: any; @@ -63,6 +65,7 @@ export default class WorkflowModel extends Model { const execution = await this.createExecution({ context, + key: this.key, status: EXECUTION_STATUS.STARTED, useTransaction: this.useTransaction, transaction: transaction.id @@ -73,6 +76,21 @@ export default class WorkflowModel extends Model { // NOTE: not to trigger afterUpdate hook here await this.update({ executed }, { transaction, hooks: false }); + const allExecuted = await (execution.constructor).count({ + where: { + key: this.key + }, + transaction + }); + await (this.constructor).update({ + allExecuted + }, { + where: { + key: this.key + }, + transaction + }); + execution.workflow = this; await execution.start({ transaction }); diff --git a/packages/plugins/workflow/src/server.ts b/packages/plugins/workflow/src/server.ts index a8dd52fbc..ac6481af9 100644 --- a/packages/plugins/workflow/src/server.ts +++ b/packages/plugins/workflow/src/server.ts @@ -1,6 +1,7 @@ import path from 'path'; import { Plugin } from '@nocobase/server'; +import { Op } from '@nocobase/database'; import WorkflowModel from './models/Workflow'; import ExecutionModel from './models/Execution'; @@ -33,6 +34,10 @@ export default class extends Plugin { initTriggers(this); + db.on('workflows.beforeSave', this.setCurrent); + db.on('workflows.afterSave', (model: WorkflowModel) => this.toggle(model)); + db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false)); + // [Life Cycle]: // * load all workflows in db // * add all hooks for enabled workflows @@ -46,15 +51,34 @@ export default class extends Plugin { workflows.forEach((workflow: WorkflowModel) => { this.toggle(workflow); }); - - db.on('workflows.afterSave', (model: WorkflowModel) => this.toggle(model)); - db.on('workflows.afterDestroy', (model: WorkflowModel) => this.toggle(model, false)); }); - // [Life Cycle]: initialize all necessary seed data // this.app.on('db.init', async () => {}); } + setCurrent = async (workflow: WorkflowModel, options) => { + const others: { enabled?: boolean, current?: boolean } = {}; + + if (workflow.enabled) { + workflow.set('current', true); + others.enabled = false; + } + + if (workflow.current) { + others.current = false; + await (workflow.constructor).update(others, { + where: { + key: workflow.key, + id: { + [Op.ne]: workflow.id + } + }, + individualHooks: true, + transaction: options.transaction + }); + } + } + toggle(workflow: WorkflowModel, enable?: boolean) { const type = workflow.get('type'); const trigger = this.triggers.get(type); diff --git a/packages/plugins/workflow/src/triggers/schedule.ts b/packages/plugins/workflow/src/triggers/schedule.ts index e2ae26f33..2b1e712aa 100644 --- a/packages/plugins/workflow/src/triggers/schedule.ts +++ b/packages/plugins/workflow/src/triggers/schedule.ts @@ -320,7 +320,7 @@ function nextInCycle(this: ScheduleTrigger, workflow, now: Date): boolean { export default class ScheduleTrigger implements Trigger { static CacheRules = [ // ({ enabled }) => enabled, - ({ config, executed }) => config.limit ? executed < config.limit : true, + ({ config, allExecuted }) => config.limit ? allExecuted < config.limit : true, ({ config }) => ['repeat', 'startsOn'].some(key => config[key]), nextInCycle, function(workflow, now) { @@ -331,7 +331,7 @@ export default class ScheduleTrigger implements Trigger { ]; static TriggerRules = [ - ({ config, executed }) => config.limit ? executed < config.limit : true, + ({ config, allExecuted }) => config.limit ? allExecuted < config.limit : true, ({ config }) => ['repeat', 'startsOn'].some(key => config[key]), function (workflow, now) { const { repeat } = workflow.config;