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,
|
...request?.params,
|
||||||
filter: {
|
filter: {
|
||||||
...(request?.params?.filter),
|
...(request?.params?.filter),
|
||||||
workflowId: workflow.id
|
key: workflow.key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
useAPIClient,
|
useAPIClient,
|
||||||
useCompile,
|
useCompile,
|
||||||
useDocumentTitle,
|
useDocumentTitle,
|
||||||
useRecord,
|
|
||||||
useResourceActionContext,
|
useResourceActionContext,
|
||||||
useResourceContext
|
useResourceContext
|
||||||
} from '..';
|
} from '..';
|
||||||
@ -79,13 +78,13 @@ export function WorkflowCanvas() {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDuplicate() {
|
async function onRevision() {
|
||||||
const { data: { data: duplicated } } = await resource.duplicate({
|
const { data: { data: revision } } = await resource.revision({
|
||||||
filterByTk: workflow[targetKey]
|
filterByTk: workflow[targetKey]
|
||||||
});
|
});
|
||||||
message.success(t('Operation succeeded'));
|
message.success(t('Operation succeeded'));
|
||||||
|
|
||||||
history.push(`${duplicated.id}`);
|
history.push(`${revision.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -134,7 +133,7 @@ export function WorkflowCanvas() {
|
|||||||
/>
|
/>
|
||||||
{workflow.executed && !revisions.find(item => !item.executed && new Date(item.createdAt) > new Date(workflow.createdAt))
|
{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
|
: null
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,17 @@ const collection = {
|
|||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
} as ISchema,
|
} 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',
|
type: 'number',
|
||||||
name: 'status',
|
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: {
|
status: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'Table.Column.Decorator',
|
'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 Database from '@nocobase/database';
|
||||||
import { getApp } from '.';
|
import { getApp } from '.';
|
||||||
import { EXECUTION_STATUS } from '../constants';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe('workflow > workflow', () => {
|
describe('workflow > workflow', () => {
|
||||||
let app: Application;
|
let app: MockServer;
|
||||||
|
let agent;
|
||||||
let db: Database;
|
let db: Database;
|
||||||
let PostModel;
|
let PostModel;
|
||||||
let PostRepo;
|
let PostRepo;
|
||||||
@ -14,7 +14,7 @@ describe('workflow > workflow', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp();
|
app = await getApp();
|
||||||
|
agent = app.agent();
|
||||||
db = app.db;
|
db = app.db;
|
||||||
WorkflowModel = db.getCollection('workflows').model;
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
PostModel = db.getCollection('posts').model;
|
PostModel = db.getCollection('posts').model;
|
||||||
@ -41,4 +41,55 @@ describe('workflow > workflow', () => {
|
|||||||
expect(executions.length).toBe(0);
|
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 { 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) {
|
function typeOf(value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return 'array';
|
return 'array';
|
||||||
@ -81,7 +35,7 @@ function migrateConfig(config, oldToNew) {
|
|||||||
return migrate(config);
|
return migrate(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function duplicate(context: Context, next) {
|
export async function revision(context: Context, next) {
|
||||||
const { db } = context;
|
const { db } = context;
|
||||||
const repository = utils.getRepositoryFromParams(context);
|
const repository = utils.getRepositoryFromParams(context);
|
||||||
const { filterByTk } = context.action.params;
|
const { filterByTk } = context.action.params;
|
||||||
|
@ -3,18 +3,18 @@ import { CollectionOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'executions',
|
name: 'executions',
|
||||||
model: 'ExecutionModel',
|
model: 'ExecutionModel',
|
||||||
title: '执行流程',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
interface: 'linkTo',
|
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'workflow',
|
name: 'workflow'
|
||||||
title: '所属工作流'
|
},
|
||||||
|
{
|
||||||
|
type: 'uid',
|
||||||
|
name: 'key'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'useTransaction',
|
name: 'useTransaction',
|
||||||
title: '使用事务',
|
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -23,22 +23,16 @@ export default {
|
|||||||
defaultValue: null
|
defaultValue: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
interface: 'linkTo',
|
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'jobs',
|
name: 'jobs',
|
||||||
title: '流程记录'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
interface: 'json',
|
|
||||||
type: 'jsonb',
|
type: 'jsonb',
|
||||||
name: 'context',
|
name: 'context',
|
||||||
title: '上下文数据'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
interface: 'select',
|
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'status',
|
name: 'status',
|
||||||
title: '状态'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} as CollectionOptions;
|
} as CollectionOptions;
|
||||||
|
@ -3,7 +3,6 @@ import { CollectionOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'workflows',
|
name: 'workflows',
|
||||||
model: 'WorkflowModel',
|
model: 'WorkflowModel',
|
||||||
title: '自动化',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'key',
|
name: 'key',
|
||||||
@ -12,56 +11,52 @@ export default {
|
|||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
title: '工作流名称',
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'enabled',
|
name: 'enabled',
|
||||||
title: '启用',
|
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'description',
|
name: 'description'
|
||||||
title: '描述'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: '触发方式',
|
|
||||||
name: 'type',
|
name: 'type',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'jsonb',
|
type: 'jsonb',
|
||||||
title: '触发配置',
|
|
||||||
name: 'config',
|
name: 'config',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: '使用事务',
|
|
||||||
name: 'useTransaction',
|
name: 'useTransaction',
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'nodes',
|
name: 'nodes',
|
||||||
target: 'flow_nodes',
|
target: 'flow_nodes'
|
||||||
title: '流程节点'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'executions',
|
name: 'executions'
|
||||||
target: 'executions',
|
|
||||||
title: '触发执行'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'executed',
|
name: 'executed',
|
||||||
defaultValue: 0
|
defaultValue: 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
name: 'allExecuted',
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'current',
|
name: 'current',
|
||||||
|
@ -26,6 +26,8 @@ export default class ExecutionModel extends Model {
|
|||||||
declare createdAt: Date;
|
declare createdAt: Date;
|
||||||
declare updatedAt: Date;
|
declare updatedAt: Date;
|
||||||
|
|
||||||
|
declare key: string;
|
||||||
|
|
||||||
declare workflow?: WorkflowModel;
|
declare workflow?: WorkflowModel;
|
||||||
declare getWorkflow: BelongsToGetAssociationMixin<WorkflowModel>;
|
declare getWorkflow: BelongsToGetAssociationMixin<WorkflowModel>;
|
||||||
|
|
||||||
|
@ -9,8 +9,10 @@ export default class WorkflowModel extends Model {
|
|||||||
declare static database: Database;
|
declare static database: Database;
|
||||||
|
|
||||||
declare id: number;
|
declare id: number;
|
||||||
|
declare key: string;
|
||||||
declare title: string;
|
declare title: string;
|
||||||
declare enabled: boolean;
|
declare enabled: boolean;
|
||||||
|
declare current: boolean;
|
||||||
declare description?: string;
|
declare description?: string;
|
||||||
declare type: string;
|
declare type: string;
|
||||||
declare config: any;
|
declare config: any;
|
||||||
@ -63,6 +65,7 @@ export default class WorkflowModel extends Model {
|
|||||||
|
|
||||||
const execution = await this.createExecution({
|
const execution = await this.createExecution({
|
||||||
context,
|
context,
|
||||||
|
key: this.key,
|
||||||
status: EXECUTION_STATUS.STARTED,
|
status: EXECUTION_STATUS.STARTED,
|
||||||
useTransaction: this.useTransaction,
|
useTransaction: this.useTransaction,
|
||||||
transaction: transaction.id
|
transaction: transaction.id
|
||||||
@ -73,6 +76,21 @@ export default class WorkflowModel extends Model {
|
|||||||
// NOTE: not to trigger afterUpdate hook here
|
// NOTE: not to trigger afterUpdate hook here
|
||||||
await this.update({ executed }, { transaction, hooks: false });
|
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;
|
execution.workflow = this;
|
||||||
|
|
||||||
await execution.start({ transaction });
|
await execution.start({ transaction });
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
|
import { Op } from '@nocobase/database';
|
||||||
|
|
||||||
import WorkflowModel from './models/Workflow';
|
import WorkflowModel from './models/Workflow';
|
||||||
import ExecutionModel from './models/Execution';
|
import ExecutionModel from './models/Execution';
|
||||||
@ -33,6 +34,10 @@ export default class extends Plugin {
|
|||||||
|
|
||||||
initTriggers(this);
|
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]:
|
// [Life Cycle]:
|
||||||
// * load all workflows in db
|
// * load all workflows in db
|
||||||
// * add all hooks for enabled workflows
|
// * add all hooks for enabled workflows
|
||||||
@ -46,15 +51,34 @@ export default class extends Plugin {
|
|||||||
workflows.forEach((workflow: WorkflowModel) => {
|
workflows.forEach((workflow: WorkflowModel) => {
|
||||||
this.toggle(workflow);
|
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
|
// [Life Cycle]: initialize all necessary seed data
|
||||||
// this.app.on('db.init', async () => {});
|
// 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) {
|
toggle(workflow: WorkflowModel, enable?: boolean) {
|
||||||
const type = workflow.get('type');
|
const type = workflow.get('type');
|
||||||
const trigger = this.triggers.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 {
|
export default class ScheduleTrigger implements Trigger {
|
||||||
static CacheRules = [
|
static CacheRules = [
|
||||||
// ({ enabled }) => enabled,
|
// ({ 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]),
|
({ config }) => ['repeat', 'startsOn'].some(key => config[key]),
|
||||||
nextInCycle,
|
nextInCycle,
|
||||||
function(workflow, now) {
|
function(workflow, now) {
|
||||||
@ -331,7 +331,7 @@ export default class ScheduleTrigger implements Trigger {
|
|||||||
];
|
];
|
||||||
|
|
||||||
static TriggerRules = [
|
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]),
|
({ config }) => ['repeat', 'startsOn'].some(key => config[key]),
|
||||||
function (workflow, now) {
|
function (workflow, now) {
|
||||||
const { repeat } = workflow.config;
|
const { repeat } = workflow.config;
|
||||||
|
Loading…
Reference in New Issue
Block a user