refactor(plugin-workflow): add revision column to execution (#491)
This commit is contained in:
parent
e57e60e6cb
commit
7839e78164
@ -11,7 +11,7 @@ export const ExecutionResourceProvider = ({ request, ...others }) => {
|
||||
...request?.params,
|
||||
filter: {
|
||||
...(request?.params?.filter),
|
||||
workflowId: workflow.id
|
||||
key: workflow.key
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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))
|
||||
? (
|
||||
<Button onClick={onDuplicate}>{t('Copy to new version')}</Button>
|
||||
<Button onClick={onRevision}>{t('Copy to new version')}</Button>
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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<WorkflowModel>;
|
||||
|
||||
|
@ -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 (<typeof ExecutionModel>execution.constructor).count({
|
||||
where: {
|
||||
key: this.key
|
||||
},
|
||||
transaction
|
||||
});
|
||||
await (<typeof WorkflowModel>this.constructor).update({
|
||||
allExecuted
|
||||
}, {
|
||||
where: {
|
||||
key: this.key
|
||||
},
|
||||
transaction
|
||||
});
|
||||
|
||||
execution.workflow = this;
|
||||
|
||||
await execution.start({ transaction });
|
||||
|
@ -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 (<typeof WorkflowModel>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);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user