feat(plugin-workflow-form-trigger): add trigger button to all single record action bar (#3563)

* feat(plugin-workflow-form-trigger): add trigger button to all single record actionbar

* fix(plugin-workflow-form-trigger): fix button style and triggering

* fix(plugin-workflow): fix unused hook ref in workflow

* fix(plugin-workflow-form-trigger): fix button style

* refactor(plugin-workflow-action-trigger): change plugin name

* fix(plugin-workflow-action-trigger): fix unmigrated stuff

* fix(plugin-workflow-action-trigger): fix test case

* fix(plugin-workflow-action-trigger): fix test case

* fix(presets): fix package name

* fix(plugin-workflow-action-trigger): fix e2e test and migration

* fix(plugin-workflow-action-trigger): fix migration

* fix(plugin-workflow-action-trigger): fix migration

* fix(plugin-workflow-action-trigger): fix migration

* feat(plugin-workflow-action-trigger): add destroy to trigger

* fix(plugin-workflow-action-trigger): fix appends select

* fix(plugin-workflow-action-trigger): remove support for destroy action

* fix(plugin-workflow-action-trigger): fix collection check

* fix(plugin-workflow-action-trigger): fix test case

* fix(plugin-workflow-action-trigger): fix test case

* fix(plugin-workflow-action-trigger): fix test case
This commit is contained in:
Junyi 2024-03-06 14:42:20 +08:00 committed by GitHub
parent d3627c5ba9
commit c2b121cda6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 383 additions and 184 deletions

View File

@ -507,6 +507,7 @@ export const useCustomizeUpdateActionProps = () => {
assignedValues: originalAssignedValues = {},
onSuccess,
skipValidator,
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
const assignedValues = {};
@ -537,6 +538,10 @@ export const useCustomizeUpdateActionProps = () => {
await resource.update({
filterByTk,
values: { ...assignedValues },
// TODO(refactor): should change to inject by plugin
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
});
service?.refresh?.();
if (!(resource instanceof TableFieldResource)) {
@ -902,10 +907,16 @@ export const useDestroyActionProps = () => {
const { resource, service, block, __parent } = useBlockRequestContext();
const { setVisible } = useActionContext();
const data = useParamsFromRecord();
const actionSchema = useFieldSchema();
return {
async onClick() {
const { triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {};
await resource.destroy({
filterByTk,
// TODO(refactor): should change to inject by plugin
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
...data,
});

View File

@ -19,6 +19,7 @@ export const UpdateRecordActionInitializer = (props) => {
redirecting: false,
successMessage: '{{t("Updated successfully")}}',
},
triggerWorkflows: [],
},
'x-component-props': {
useProps: '{{ useCustomizeUpdateActionProps }}',

View File

@ -1,3 +1,5 @@
import { isValid } from '@formily/shared';
import { useFieldSchema } from '@formily/react';
import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../collection-manager';
@ -7,6 +9,7 @@ import {
ButtonEditor,
RemoveButton,
SecondConFirm,
WorkflowConfig,
} from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
@ -45,6 +48,14 @@ export const customizeUpdateRecordActionSettings = new SchemaSettings({
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
},
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{
name: 'delete',
sort: 100,

View File

@ -37,5 +37,15 @@ export const detailsActionInitializers = new SchemaInitializer({
},
],
},
{
name: 'divider',
type: 'divider',
},
{
type: 'subMenu',
name: 'customize',
title: '{{t("Customize")}}',
children: [],
},
],
});

View File

@ -1,5 +1,4 @@
import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer';
import { formTriggerWorkflowActionInitializerV2 } from './formActionInitializers';
export const createFormActionInitializers = new SchemaInitializer({
name: 'CreateFormActionInitializers',
@ -35,7 +34,6 @@ export const createFormActionInitializers = new SchemaInitializer({
title: '{{t("Save record")}}',
Component: 'SaveRecordActionInitializer',
},
formTriggerWorkflowActionInitializerV2,
{
name: 'customRequest',
title: '{{t("Custom request")}}',

View File

@ -1,12 +1,6 @@
import { SchemaInitializerItemType } from '../../../../application';
import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer';
export const formTriggerWorkflowActionInitializerV2: SchemaInitializerItemType = {
name: 'submitToWorkflow',
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
Component: 'FormTriggerWorkflowActionInitializerV2',
};
// 表单的操作配置
export const formActionInitializers = new SchemaInitializer({
name: 'FormActionInitializers',
@ -42,7 +36,6 @@ export const formActionInitializers = new SchemaInitializer({
title: '{{t("Save record")}}',
Component: 'SaveRecordActionInitializer',
},
formTriggerWorkflowActionInitializerV2,
{
name: 'customRequest',
title: '{{t("Custom request")}}',

View File

@ -1,5 +1,4 @@
import { SchemaInitializer } from '../../../../application/schema-initializer/SchemaInitializer';
import { formTriggerWorkflowActionInitializerV2 } from './formActionInitializers';
export const updateFormActionInitializers = new SchemaInitializer({
name: 'UpdateFormActionInitializers',
@ -45,7 +44,6 @@ export const updateFormActionInitializers = new SchemaInitializer({
title: '{{t("Save record")}}',
Component: 'SaveRecordActionInitializer',
},
formTriggerWorkflowActionInitializerV2,
{
type: 'item',
name: 'customRequest',

View File

@ -139,6 +139,7 @@ export const CalendarFormActionInitializers: SchemaInitializer = new SchemaIniti
redirecting: false,
successMessage: generateNTemplate('Updated successfully'),
},
triggerWorkflows: [],
},
'x-component-props': {
useProps: '{{ useCustomizeUpdateActionProps }}',

View File

@ -0,0 +1,28 @@
{
"name": "@nocobase/plugin-workflow-action-trigger",
"displayName": "Workflow: Action trigger",
"displayName.zh-CN": "工作流:数据操作触发器",
"description": "Bind action buttons to trigger workflow events when clicked.",
"description.zh-CN": "可对数据操作按钮绑定,在点击后触发对应的工作流事件。",
"version": "0.20.0-alpha.5",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/plugins/workflow-action-trigger",
"homepage.zh-CN": "https://docs-cn.nocobase.com/plugins/workflow-action-trigger",
"devDependencies": {
"antd": "5.x",
"react": "18.x",
"react-i18next": "^11.15.1"
},
"peerDependencies": {
"@nocobase/client": "0.x",
"@nocobase/database": "0.x",
"@nocobase/plugin-workflow": ">=0.17.0-alpha.3",
"@nocobase/server": "0.x",
"@nocobase/test": "0.x"
},
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1",
"keywords": [
"Workflow"
]
}

View File

@ -10,8 +10,8 @@ import { Trigger, CollectionBlockInitializer, getCollectionFieldOptions } from '
import { NAMESPACE, useLang } from '../locale';
export default class extends Trigger {
title = `{{t("Form event", { ns: "${NAMESPACE}" })}}`;
description = `{{t("Event triggers when submitted a workflow bound form action.", { ns: "${NAMESPACE}" })}}`;
title = `{{t("Action event", { ns: "${NAMESPACE}" })}}`;
description = `{{t("Triggers after specific operations on data are submitted, such as create, update, delete, etc., or directly submitting a record to the workflow.", { ns: "${NAMESPACE}" })}}`;
fieldset = {
collection: {
type: 'string',
@ -21,8 +21,8 @@ export default class extends Trigger {
'x-component-props': {
className: 'auto-width',
},
title: `{{t("Form data model", { ns: "${NAMESPACE}" })}}`,
description: `{{t("Use a collection to match form data.", { ns: "${NAMESPACE}" })}}`,
title: `{{t("Collection", { ns: "${NAMESPACE}" })}}`,
description: `{{t("Which collection record belongs to.", { ns: "${NAMESPACE}" })}}`,
'x-reactions': [
{
target: 'appends',
@ -38,7 +38,7 @@ export default class extends Trigger {
appends: {
type: 'array',
title: `{{t("Associations to use", { ns: "${NAMESPACE}" })}}`,
description: `{{t("Please select the associated fields that need to be accessed in subsequent nodes. With more than two levels of to-many associations may cause performance issue, please use with caution.", { ns: "${NAMESPACE}" })}}`,
description: `{{t("Please select the associated fields that need to be accessed in subsequent nodes. With more than two levels of to-many associations may cause performance issue, please use with caution.", { ns: "workflow" })}}`,
'x-decorator': 'FormItem',
'x-component': 'AppendsTreeSelect',
'x-component-props': {
@ -65,7 +65,7 @@ export default class extends Trigger {
useCollectionDataSource,
};
isActionTriggerable = (config, context) => {
return ['create', 'update'].includes(context.action);
return ['create', 'update', 'customize:update', 'customize:triggerWorkflows'].includes(context.action);
};
useVariables(config, options) {
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -75,9 +75,9 @@ export default class extends Trigger {
// eslint-disable-next-line react-hooks/rules-of-hooks
const langTriggerData = useLang('Trigger data');
// eslint-disable-next-line react-hooks/rules-of-hooks
const langUserSubmittedForm = useLang('User submitted form');
const langUserSubmittedForm = useLang('User submitted action');
// eslint-disable-next-line react-hooks/rules-of-hooks
const langRoleSubmittedForm = useLang('Role of user submitted form');
const langRoleSubmittedForm = useLang('Role of user submitted action');
const rootFields = [
{
collectionName: config.collection,

View File

@ -38,7 +38,7 @@ test.describe('Configuration page to configure the Trigger node', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -130,7 +130,7 @@ test.describe('Configuration page to configure the Trigger node', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -182,7 +182,7 @@ test.describe('Configuration page to configure the Trigger node', () => {
await page.getByRole('button', { name: 'Submit to workflow' }).hover();
await page
.getByRole('button', {
name: `designer-schema-settings-Action-actionSettings:submitToWorkflow-${triggerNodeCollectionName}`,
name: `designer-schema-settings-Action-Action.Designer-${triggerNodeCollectionName}`,
})
.hover();
await page.getByRole('menuitem', { name: 'Bind workflows' }).click();
@ -211,7 +211,7 @@ test.describe('Configuration page to configure the Trigger node', () => {
});
test.describe('Configuration Page Path Jump Workflow Management Page', () => {
test('Form event Workflow Configuration Page Path Jump Workflow Management Page', async ({
test('Action event Workflow Configuration Page Path Jump Workflow Management Page', async ({
page,
mockPage,
mockCollections,
@ -232,7 +232,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -254,7 +254,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
await apiDeleteWorkflow(workflowId);
});
test('Form event Workflow History Version Configuration Page Path Jump Workflow Management Page', async ({
test('Action event Workflow History Version Configuration Page Path Jump Workflow Management Page', async ({
page,
mockPage,
mockCollections,
@ -278,7 +278,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -320,7 +320,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
await apiDeleteWorkflow(workflowId);
});
test('Form event Workflow Execution Log Page Path Jump Workflow Management Page', async ({
test('Action event Workflow Execution Log Page Path Jump Workflow Management Page', async ({
page,
mockPage,
mockCollections,
@ -344,7 +344,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -384,7 +384,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
await apiDeleteWorkflow(workflowId);
});
test.skip('Form event Workflow Execution Log Page Path Jump Execution Log Screen', async ({
test.skip('Action event Workflow Execution Log Page Path Jump Execution Log Screen', async ({
page,
mockCollections,
mockRecords,
@ -407,7 +407,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -451,7 +451,7 @@ test.describe('Configuration Page Path Jump Workflow Management Page', () => {
test.describe('Configuration page version switching', () => {});
test.describe('Configuration page disable enable', () => {
test('Form event Workflow Add Data Trigger Disable Do Not Trigger', async ({
test('Action event Workflow Add Data Trigger Disable Do Not Trigger', async ({
page,
mockCollections,
mockRecords,
@ -474,7 +474,7 @@ test.describe('Configuration page disable enable', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: false,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -522,7 +522,7 @@ test.describe('Configuration page disable enable', () => {
await apiDeleteWorkflow(workflowId);
});
test('Form event Workflow Add Data Trigger Disable Enable Post Trigger', async ({
test('Action event Workflow Add Data Trigger Disable Enable Post Trigger', async ({
page,
mockCollections,
mockRecords,
@ -545,7 +545,7 @@ test.describe('Configuration page disable enable', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -597,7 +597,7 @@ test.describe('Configuration page disable enable', () => {
test.describe('Configuration page execution history', () => {});
test.describe('Configuration page copy to new version', () => {
test('Copy the Form event of the Configuration Trigger node', async ({ page, mockCollections, mockRecords }) => {
test('Copy the action event of the Configuration Trigger node', async ({ page, mockCollections, mockRecords }) => {
//数据表后缀标识
const triggerNodeAppendText = faker.string.alphanumeric(5);
@ -616,7 +616,7 @@ test.describe('Configuration page copy to new version', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);

View File

@ -24,7 +24,7 @@ test.describe('Filter', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -47,7 +47,7 @@ test.describe('Filter', () => {
});
test.describe('Add new', () => {
test('add new Form event', async ({ page }) => {
test('add new Action event', async ({ page }) => {
// 添加工作流
await page.goto('/admin/settings/workflow');
await page.waitForLoadState('networkidle');
@ -56,7 +56,7 @@ test.describe('Add new', () => {
const workFlowName = faker.string.alphanumeric(5);
await createWorkFlow.name.fill(workFlowName);
await createWorkFlow.triggerType.click();
await page.getByRole('option', { name: 'Form event' }).click();
await page.getByRole('option', { name: 'Action event' }).click();
await page.getByLabel('action-Action-Submit-workflows').click();
// 3、预期结果列表中出现新建的工作流
@ -75,7 +75,7 @@ test.describe('Add new', () => {
test.describe('Sync', () => {});
test.describe('Delete', () => {
test('delete Form event', async ({ page }) => {
test('delete Action event', async ({ page }) => {
//添加工作流
const triggerNodeAppendText = faker.string.alphanumeric(5);
const workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
@ -83,7 +83,7 @@ test.describe('Delete', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -107,7 +107,7 @@ test.describe('Delete', () => {
});
test.describe('Edit', () => {
test('edit Form event name', async ({ page }) => {
test('edit Action event name', async ({ page }) => {
//添加工作流
const triggerNodeAppendText = faker.string.alphanumeric(5);
let workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
@ -115,7 +115,7 @@ test.describe('Edit', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);
@ -140,7 +140,7 @@ test.describe('Edit', () => {
});
test.describe('Duplicate', () => {
test('Duplicate Form event triggers with only unconfigured trigger nodes', async ({ page }) => {
test('Duplicate Action event triggers with only unconfigured trigger nodes', async ({ page }) => {
//添加工作流
const triggerNodeAppendText = faker.string.alphanumeric(5);
const workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
@ -148,7 +148,7 @@ test.describe('Duplicate', () => {
current: true,
options: { deleteExecutionOnStatus: [] },
title: workFlowName,
type: 'form',
type: 'action',
enabled: true,
};
const workflow = await apiCreateWorkflow(workflowData);

View File

@ -0,0 +1,100 @@
import { Plugin, SchemaInitializerItemType } from '@nocobase/client';
import WorkflowPlugin, {
useTriggerWorkflowsActionProps,
useRecordTriggerWorkflowsActionProps,
} from '@nocobase/plugin-workflow/client';
import ActionTrigger from './ActionTrigger';
const submitToWorkflowActionInitializer: SchemaInitializerItemType = {
name: 'submitToWorkflow',
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
Component: 'CustomizeActionInitializer',
schema: {
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
'x-component': 'Action',
'x-component-props': {
useProps: '{{ useTriggerWorkflowsActionProps }}',
},
'x-designer': 'Action.Designer',
'x-action-settings': {
// assignedValues: {},
skipValidator: false,
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Submitted successfully")}}',
},
triggerWorkflows: [],
},
'x-action': 'customize:triggerWorkflows',
},
};
const recordTriggerWorkflowActionInitializer: SchemaInitializerItemType = {
name: 'submitToWorkflow',
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
Component: 'CustomizeActionInitializer',
schema: {
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
'x-component': 'Action',
'x-component-props': {
useProps: '{{ useRecordTriggerWorkflowsActionProps }}',
},
'x-designer': 'Action.Designer',
'x-action-settings': {
// assignedValues: {},
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Submitted successfully")}}',
},
triggerWorkflows: [],
},
'x-action': 'customize:triggerWorkflows',
},
};
const recordTriggerWorkflowActionLinkInitializer = {
...recordTriggerWorkflowActionInitializer,
schema: {
...recordTriggerWorkflowActionInitializer.schema,
'x-component': 'Action.Link',
},
};
export default class extends Plugin {
async load() {
const workflow = this.app.pm.get('workflow') as WorkflowPlugin;
workflow.registerTrigger('action', ActionTrigger);
this.app.addScopes({
useTriggerWorkflowsActionProps,
useRecordTriggerWorkflowsActionProps,
});
const FormActionInitializers = this.app.schemaInitializerManager.get('FormActionInitializers');
FormActionInitializers.add('customize.submitToWorkflow', submitToWorkflowActionInitializer);
const CreateFormActionInitializers = this.app.schemaInitializerManager.get('CreateFormActionInitializers');
CreateFormActionInitializers.add('customize.submitToWorkflow', submitToWorkflowActionInitializer);
const UpdateFormActionInitializers = this.app.schemaInitializerManager.get('UpdateFormActionInitializers');
UpdateFormActionInitializers.add('customize.submitToWorkflow', submitToWorkflowActionInitializer);
const DetailsActionInitializers = this.app.schemaInitializerManager.get('DetailsActionInitializers');
DetailsActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionInitializer);
const ReadPrettyFormActionInitializers = this.app.schemaInitializerManager.get('ReadPrettyFormActionInitializers');
ReadPrettyFormActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionInitializer);
const TableActionColumnInitializers = this.app.schemaInitializerManager.get('TableActionColumnInitializers');
TableActionColumnInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionLinkInitializer);
const GridCardItemActionInitializers = this.app.schemaInitializerManager.get('GridCardItemActionInitializers');
GridCardItemActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionLinkInitializer);
const ListItemActionInitializers = this.app.schemaInitializerManager.get('ListItemActionInitializers');
ListItemActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionLinkInitializer);
}
}

View File

@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
export const NAMESPACE = 'workflow-form-trigger';
export const NAMESPACE = 'workflow-action-trigger';
export function useLang(key: string, options = {}) {
const { t } = usePluginTranslation(options);

View File

@ -0,0 +1,10 @@
{
"Action event": "操作事件",
"Triggers after specific operations on data are submitted, such as create, update, delete, etc., or directly submitting a record to the workflow.": "在对数据的特定操作提交后触发,如创建、更新、删除等,或直接提交一条数据至工作流。",
"Collection": "数据表",
"Which collection record belongs to.": "数据所属的数据表。",
"Associations to use": "待使用的关系数据",
"Trigger data": "触发器数据",
"User submitted action": "提交操作的用户",
"Role of user submitted action": "提交操作用户的角色"
}

View File

@ -63,7 +63,7 @@ export default class extends Trigger {
filter: {
key: triggers.map((trigger) => trigger[0]),
current: true,
type: 'form',
type: 'action',
enabled: true,
},
});
@ -91,15 +91,17 @@ export default class extends Trigger {
}
}
const { collection, appends = [] } = workflow.config;
const model = <typeof Model>payload.constructor;
if (collection !== model.collection.name) {
continue;
}
if (appends.length) {
payload = await model.collection.repository.findOne({
filterByTk: payload.get(model.primaryKeyAttribute),
appends,
});
const model = payload.constructor;
if (payload instanceof Model) {
if (collection !== model.collection.name) {
continue;
}
if (appends.length) {
payload = await model.collection.repository.findOne({
filterByTk: payload.get(model.primaryKeyAttribute),
appends,
});
}
}
// this.workflow.trigger(workflow, { data: toJSON(payload), ...userInfo });
event.push({ data: toJSON(payload), ...userInfo });

View File

@ -0,0 +1,11 @@
import { Plugin } from '@nocobase/server';
import WorkflowPlugin from '@nocobase/plugin-workflow';
import ActionTrigger from './ActionTrigger';
export default class extends Plugin {
async load() {
const workflowPlugin = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
workflowPlugin.triggers.register('action', new ActionTrigger(workflowPlugin));
}
}

View File

@ -5,7 +5,7 @@ import { MockServer } from '@nocobase/test';
import Plugin from '..';
describe('workflow > form-trigge[r', () => {
describe('workflow > action-trigger', () => {
let app: MockServer;
let db: Database;
let agent;
@ -20,7 +20,7 @@ describe('workflow > form-trigge[r', () => {
app = await getApp({
plugins: ['users', 'auth', Plugin],
});
await app.getPlugin('auth').install();
await app.pm.get('auth').install();
agent = app.agent();
db = app.db;
WorkflowModel = db.getCollection('workflows').model;
@ -42,7 +42,7 @@ describe('workflow > form-trigge[r', () => {
it('enabled / disabled', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
},
@ -97,7 +97,7 @@ describe('workflow > form-trigge[r', () => {
it('only trigger if params provided matching collection config', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
},
@ -139,7 +139,7 @@ describe('workflow > form-trigge[r', () => {
it('system fields could be accessed', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
},
@ -162,7 +162,7 @@ describe('workflow > form-trigge[r', () => {
it('appends', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
appends: ['createdBy'],
@ -186,7 +186,7 @@ describe('workflow > form-trigge[r', () => {
it('user submitted form', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
},
@ -211,7 +211,7 @@ describe('workflow > form-trigge[r', () => {
it('trigger after updated', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
},
@ -241,11 +241,50 @@ describe('workflow > form-trigge[r', () => {
});
});
describe.skip('destroy', () => {
it('trigger after destroyed', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
},
});
const p1 = await PostRepo.create({
values: { title: 't1' },
});
const p2 = await PostRepo.create({
values: { title: 't2' },
});
const res1 = await userAgents[0].resource('posts').destroy({
filterByTk: p1.id,
});
expect(res1.status).toBe(200);
await sleep(500);
const e1 = await workflow.getExecutions();
expect(e1.length).toBe(0);
const res2 = await userAgents[0].resource('posts').destroy({
filterByTk: p2.id,
triggerWorkflows: `${workflow.key}`,
});
await sleep(500);
const [e2] = await workflow.getExecutions();
expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
expect(e2.context.data).toBe(1);
});
});
describe('directly trigger', () => {
it('trigger data', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
});
const res1 = await userAgents[0].resource('workflows').trigger({
@ -264,12 +303,12 @@ describe('workflow > form-trigge[r', () => {
it('multi trigger', async () => {
const w1 = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
});
const w2 = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
});
const res1 = await userAgents[0].resource('workflows').trigger({
@ -292,7 +331,7 @@ describe('workflow > form-trigge[r', () => {
it('user submitted form', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
appends: ['createdBy'],
@ -318,7 +357,7 @@ describe('workflow > form-trigge[r', () => {
it('level: 1', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
});
const res1 = await userAgents[0].resource('workflows').trigger({
@ -337,7 +376,7 @@ describe('workflow > form-trigge[r', () => {
it('level: 2', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
});
const res1 = await userAgents[0].resource('workflows').trigger({
@ -358,7 +397,7 @@ describe('workflow > form-trigge[r', () => {
it('revision', async () => {
const w1 = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
});
const res1 = await userAgents[0].resource('workflows').trigger({
@ -405,7 +444,7 @@ describe('workflow > form-trigge[r', () => {
it('sync form trigger', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
sync: true,
config: {
collection: 'posts',
@ -426,7 +465,7 @@ describe('workflow > form-trigge[r', () => {
it('sync and async will all be triggered in one action', async () => {
const w1 = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
config: {
collection: 'posts',
},
@ -434,7 +473,7 @@ describe('workflow > form-trigge[r', () => {
const w2 = await WorkflowModel.create({
enabled: true,
type: 'form',
type: 'action',
sync: true,
config: {
collection: 'posts',

View File

@ -0,0 +1,22 @@
import { Migration } from '@nocobase/server';
export default class extends Migration {
appVersion = '<0.19.0-alpha.10';
on = 'afterSync';
async up() {
const { db } = this.context;
const WorkflowRepo = db.getRepository('workflows');
await db.sequelize.transaction(async (transaction) => {
await WorkflowRepo.update({
filter: {
type: 'form',
},
values: {
type: 'action',
},
transaction,
});
});
}
}

View File

@ -1,28 +0,0 @@
{
"name": "@nocobase/plugin-workflow-form-trigger",
"displayName": "Workflow: Form trigger",
"displayName.zh-CN": "工作流:表单触发器",
"description": "Bind form buttons to trigger workflow events when submitting.",
"description.zh-CN": "可对表单按钮绑定,在提交时触发对应的工作流事件。",
"version": "0.20.0-alpha.5",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/workflow-form-trigger",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow-form-trigger",
"devDependencies": {
"antd": "5.x",
"react": "18.x",
"react-i18next": "^11.15.1"
},
"peerDependencies": {
"@nocobase/client": "0.x",
"@nocobase/database": "0.x",
"@nocobase/plugin-workflow": ">=0.17.0-alpha.3",
"@nocobase/server": "0.x",
"@nocobase/test": "0.x"
},
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1",
"keywords": [
"Workflow"
]
}

View File

@ -1,18 +0,0 @@
import { Plugin } from '@nocobase/client';
import WorkflowPlugin from '@nocobase/plugin-workflow/client';
import FormTrigger from './FormTrigger';
export default class extends Plugin {
async afterAdd() {
// await this.app.pm.add()
}
async beforeLoad() {}
// You can get and modify the app instance here
async load() {
const workflow = this.app.pm.get('workflow') as WorkflowPlugin;
workflow.registerTrigger('form', FormTrigger);
}
}

View File

@ -1,10 +0,0 @@
{
"Form event": "表单事件",
"Event triggers when submitted a workflow bound form action.": "在提交绑定工作流的表单操作按钮后触发。",
"Form data model": "表单数据模型",
"Use a collection to match form data.": "使用一个数据表来匹配表单数据。",
"Associations to use": "待使用的关系数据",
"Trigger data": "触发器数据",
"User submitted form": "提交表单的用户",
"Role of user submitted form": "提交表单用户的角色"
}

View File

@ -1,11 +0,0 @@
import { Plugin } from '@nocobase/server';
import WorkflowPlugin from '@nocobase/plugin-workflow';
import FormTrigger from './FormTrigger';
export default class extends Plugin {
async load() {
const workflowPlugin = this.app.getPlugin<WorkflowPlugin>(WorkflowPlugin);
workflowPlugin.triggers.register('form', new FormTrigger(workflowPlugin));
}
}

View File

@ -1,28 +0,0 @@
import React from 'react';
import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client';
export const FormTriggerWorkflowActionInitializerV2 = () => {
const schema = {
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
'x-component': 'Action',
'x-component-props': {
useProps: '{{ useTriggerWorkflowsActionProps }}',
},
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:submitToWorkflow',
'x-action-settings': {
assignedValues: {},
skipValidator: false,
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Submitted successfully")}}',
},
triggerWorkflows: [],
},
'x-action': 'customize:triggerWorkflows',
};
const itemConfig = useSchemaInitializerItem();
return <BlockInitializer {...itemConfig} schema={schema} item={itemConfig} />;
};

View File

@ -4,4 +4,3 @@ export * from './FilterDynamicComponent';
export * from './RadioWithTooltip';
export * from './CheckboxGroupWithTooltip';
export * from './ValueBlock';
export * from './FormTriggerWorkflowActionInitializerV2';

View File

@ -8,6 +8,7 @@ import {
useBlockRequestContext,
useCollectValuesToSubmit,
useCompile,
useRecord,
} from '@nocobase/client';
import { isURL } from '@nocobase/utils/client';
@ -73,3 +74,58 @@ export function useTriggerWorkflowsActionProps() {
},
};
}
export function useRecordTriggerWorkflowsActionProps() {
const compile = useCompile();
const api = useAPIClient();
const record = useRecord();
const actionField = useField();
const actionSchema = useFieldSchema();
const { field, __parent } = useBlockRequestContext();
const { setVisible } = useActionContext();
const { modal } = App.useApp();
const navigate = useNavigate();
const { onSuccess, triggerWorkflows } = actionSchema?.['x-action-settings'] ?? {};
return {
async onClick() {
actionField.data = field.data || {};
actionField.data.loading = true;
try {
await api.resource('workflows').trigger({
values: record,
// TODO(refactor): should change to inject by plugin
triggerWorkflows: triggerWorkflows?.length
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
: undefined,
});
__parent?.service?.refresh?.();
setVisible?.(false);
if (!onSuccess?.successMessage) {
return;
}
if (onSuccess?.manualClose) {
modal.success({
title: compile(onSuccess?.successMessage),
async onOk() {
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
if (isURL(onSuccess.redirectTo)) {
window.location.href = onSuccess.redirectTo;
} else {
navigate(onSuccess.redirectTo);
}
}
},
});
} else {
message.success(compile(onSuccess?.successMessage));
}
} catch (error) {
console.error(error);
} finally {
actionField.data.loading = false;
}
},
};
}

View File

@ -9,7 +9,7 @@ export * from './utils';
export * from './hooks/useGetAriaLabelOfAddButton';
export { default as useStyles } from './style';
export * from './variable';
export { getCollectionFieldOptions, useWorkflowVariableOptions } from './variable';
export * from './hooks/useTriggerWorkflowActionProps';
import React from 'react';
@ -30,8 +30,6 @@ import QueryInstruction from './nodes/query';
import CreateInstruction from './nodes/create';
import UpdateInstruction from './nodes/update';
import DestroyInstruction from './nodes/destroy';
import { useTriggerWorkflowsActionProps } from './hooks/useTriggerWorkflowActionProps';
import { FormTriggerWorkflowActionInitializerV2 } from './components/FormTriggerWorkflowActionInitializerV2';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
import { NAMESPACE } from './locale';
import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings';
@ -74,7 +72,6 @@ export default class PluginWorkflowClient extends Plugin {
async load() {
this.addRoutes();
this.addScopes();
this.addComponents();
this.app.pluginSettingsManager.add(NAMESPACE, {
@ -99,17 +96,10 @@ export default class PluginWorkflowClient extends Plugin {
this.registerInstruction('destroy', DestroyInstruction);
}
addScopes() {
this.app.addScopes({
useTriggerWorkflowsActionProps,
});
}
addComponents() {
this.app.addComponents({
WorkflowPage,
ExecutionPage,
FormTriggerWorkflowActionInitializerV2,
});
}

View File

@ -1,6 +1,6 @@
import { Model } from '@nocobase/database';
export function toJSON(data: Model | Model[]): object {
export function toJSON(data: any): any {
if (Array.isArray(data)) {
return data.map(toJSON);
}

View File

@ -51,10 +51,10 @@
"@nocobase/plugin-users": "0.20.0-alpha.5",
"@nocobase/plugin-verification": "0.20.0-alpha.5",
"@nocobase/plugin-workflow": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-action-trigger": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-aggregate": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-delay": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-dynamic-calculation": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-form-trigger": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-loop": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-manual": "0.20.0-alpha.5",
"@nocobase/plugin-workflow-parallel": "0.20.0-alpha.5",

View File

@ -16,10 +16,10 @@ export class PresetNocoBase extends Plugin {
'acl',
'china-region',
'workflow',
'workflow-action-trigger',
'workflow-aggregate',
'workflow-delay',
'workflow-dynamic-calculation',
'workflow-form-trigger',
'workflow-loop',
'workflow-manual',
'workflow-parallel',

View File

@ -0,0 +1,14 @@
import { Migration } from '@nocobase/server';
export default class extends Migration {
on = 'beforeLoad'; // 'beforeLoad' or 'afterLoad'
appVersion = '<0.19.0-alpha.10';
async up() {
await this.pm.repository.destroy({
filter: {
packageName: '@nocobase/plugin-workflow-form-trigger',
},
});
}
}