feat: now workflow can response (#1186)
Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#1186
This commit is contained in:
parent
96c9f954e7
commit
5acee2f57f
@ -336,7 +336,7 @@ export function RemoveButton(
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowSelect({ actionType, direct = false, ...props }) {
|
||||
function WorkflowSelect({ formAction, buttonAction, actionType, direct = false, ...props }) {
|
||||
const { t } = useTranslation();
|
||||
const index = ArrayTable.useIndex();
|
||||
const { setValuesIn } = useForm();
|
||||
@ -377,17 +377,28 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
||||
});
|
||||
|
||||
const optionFilter = useCallback(
|
||||
({ type, config }) => {
|
||||
({ key, type, config }) => {
|
||||
if (key === props.value) {
|
||||
return true;
|
||||
}
|
||||
const trigger = workflowPlugin.triggers.get(type);
|
||||
if (trigger.isActionTriggerable === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof trigger.isActionTriggerable === 'function') {
|
||||
return trigger.isActionTriggerable(config, { action: actionType, direct });
|
||||
return trigger.isActionTriggerable(config, {
|
||||
action: actionType,
|
||||
formAction,
|
||||
buttonAction,
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
direct: buttonAction === 'customize:triggerWorkflows',
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[workflowPlugin.triggers, actionType, direct],
|
||||
[props.value, workflowPlugin.triggers, actionType, formAction, buttonAction],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -436,6 +447,8 @@ export function WorkflowConfig() {
|
||||
// TODO(refactor): should refactor for getting certain action type, better from 'x-action'.
|
||||
const formBlock = useFormBlockContext();
|
||||
const actionType = formBlock?.type || fieldSchema['x-action'];
|
||||
const formAction = formBlock?.type;
|
||||
const buttonAction = fieldSchema['x-action'];
|
||||
|
||||
const description = {
|
||||
submit: t('Workflow will be triggered before or after submitting succeeded based on workflow type.', {
|
||||
@ -510,7 +523,7 @@ export function WorkflowConfig() {
|
||||
value: '',
|
||||
},
|
||||
allowClear: false,
|
||||
loadData: actionType === 'destroy' ? null : undefined,
|
||||
loadData: buttonAction === 'destroy' ? null : undefined,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
@ -530,6 +543,8 @@ export function WorkflowConfig() {
|
||||
'x-component-props': {
|
||||
placeholder: t('Select workflow', { ns: 'workflow' }),
|
||||
actionType,
|
||||
formAction,
|
||||
buttonAction,
|
||||
direct: fieldSchema['x-action'] === 'customize:triggerWorkflows',
|
||||
},
|
||||
required: true,
|
||||
|
@ -3,20 +3,33 @@ import { ToposortOptions } from '@tachybase/utils';
|
||||
import { DataSource } from './data-source';
|
||||
import { DataSourceFactory } from './data-source-factory';
|
||||
|
||||
type DataSourceHook = (dataSource: DataSource) => void;
|
||||
|
||||
export class DataSourceManager {
|
||||
dataSources: Map<string, DataSource>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
factory: DataSourceFactory = new DataSourceFactory();
|
||||
|
||||
protected middlewares = [];
|
||||
private onceHooks: Array<DataSourceHook> = [];
|
||||
|
||||
constructor(public options = {}) {
|
||||
this.dataSources = new Map();
|
||||
this.middlewares = [];
|
||||
}
|
||||
|
||||
get(dataSourceKey: string) {
|
||||
return this.dataSources.get(dataSourceKey);
|
||||
}
|
||||
|
||||
async add(dataSource: DataSource, options: any = {}) {
|
||||
await dataSource.load(options);
|
||||
this.dataSources.set(dataSource.name, dataSource);
|
||||
|
||||
for (const hook of this.onceHooks) {
|
||||
hook(dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
use(fn: any, options?: ToposortOptions) {
|
||||
@ -25,17 +38,39 @@ export class DataSourceManager {
|
||||
|
||||
middleware() {
|
||||
return async (ctx, next) => {
|
||||
const name = ctx.get('x-data-source');
|
||||
if (name) {
|
||||
if (this.dataSources.has(name)) {
|
||||
const ds = this.dataSources.get(name);
|
||||
ctx.dataSource = ds;
|
||||
return ds.middleware(this.middlewares)(ctx, next);
|
||||
} else {
|
||||
const name = ctx.get('x-data-source') || 'main';
|
||||
|
||||
if (!this.dataSources.has(name)) {
|
||||
ctx.throw(`data source ${name} does not exist`);
|
||||
}
|
||||
}
|
||||
await next();
|
||||
|
||||
const ds = this.dataSources.get(name);
|
||||
ctx.dataSource = ds;
|
||||
|
||||
return ds.middleware(this.middlewares)(ctx, next);
|
||||
};
|
||||
}
|
||||
|
||||
registerDataSourceType(type: string, DataSourceClass: typeof DataSource) {
|
||||
this.factory.register(type, DataSourceClass);
|
||||
}
|
||||
|
||||
getDataSourceType(type: string): typeof DataSource | undefined {
|
||||
return this.factory.getClass(type);
|
||||
}
|
||||
|
||||
buildDataSourceByType(type: string, options: any = {}): DataSource {
|
||||
return this.factory.create(type, options);
|
||||
}
|
||||
|
||||
afterAddDataSource(hook: DataSourceHook) {
|
||||
this.addHookAndRun(hook);
|
||||
}
|
||||
|
||||
private addHookAndRun(hook: DataSourceHook) {
|
||||
this.onceHooks.push(hook);
|
||||
for (const dataSource of this.dataSources.values()) {
|
||||
hook(dataSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ export type MergeOptions = {
|
||||
};
|
||||
|
||||
export interface ICollectionManager {
|
||||
db: any;
|
||||
registerFieldTypes(types: Record<string, any>): void;
|
||||
registerFieldInterfaces(interfaces: Record<string, any>): void;
|
||||
registerCollectionTemplates(templates: Record<string, any>): void;
|
||||
|
@ -13,8 +13,10 @@ import PluginWorkflowJSParseClient from './features/js-parse';
|
||||
import PluginWorkflowJsonParseClient from './features/json-parse';
|
||||
import { PluginLoop } from './features/loop';
|
||||
import { PluginManual } from './features/manual';
|
||||
import { PluginOmniTrigger } from './features/omni-trigger';
|
||||
import { PluginParallel } from './features/parallel';
|
||||
import { PluginRequest } from './features/request';
|
||||
import { PluginResponse } from './features/response';
|
||||
import { PluginSql } from './features/sql';
|
||||
import { PluginVariables } from './features/variables';
|
||||
import { NAMESPACE } from './locale';
|
||||
@ -85,6 +87,8 @@ export class PluginWorkflow extends Plugin {
|
||||
await this.pm.add(PluginAPIRegularClient);
|
||||
await this.pm.add(PluginWorkflowInterceptor);
|
||||
await this.pm.add(PluginVariables);
|
||||
await this.pm.add(PluginResponse);
|
||||
await this.pm.add(PluginOmniTrigger);
|
||||
}
|
||||
|
||||
async load() {
|
||||
@ -131,16 +135,3 @@ export class PluginWorkflow extends Plugin {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export * from './Branch';
|
||||
export * from './FlowContext';
|
||||
export * from './constants';
|
||||
export * from './nodes';
|
||||
export { Trigger, useTrigger } from './triggers';
|
||||
export * from './variable';
|
||||
export * from './components';
|
||||
export * from './utils';
|
||||
export * from './hooks';
|
||||
export { default as useStyles } from './style';
|
||||
export * from './variable';
|
||||
export * from './ExecutionContextProvider';
|
||||
|
@ -33,12 +33,9 @@ const recordTriggerWorkflowActionInitializer: SchemaInitializerItemType = {
|
||||
schema: {
|
||||
title: '{{t("Submit to workflow", { ns: "workflow" })}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useRecordTriggerWorkflowsActionProps }}',
|
||||
},
|
||||
'x-use-component-props': 'useRecordTriggerWorkflowsActionProps',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
// assignedValues: {},
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
|
@ -95,9 +95,9 @@ export class WorkflowTriggerInterceptor extends Trigger {
|
||||
RadioWithTooltip,
|
||||
CheckboxGroupWithTooltip,
|
||||
};
|
||||
isActionTriggerable = (a, u) => {
|
||||
const { global: m } = a;
|
||||
return !m && !u.direct;
|
||||
isActionTriggerable = (config, context) => {
|
||||
const { global } = config;
|
||||
return !global && !context.direct;
|
||||
};
|
||||
|
||||
useVariables(config, options) {
|
||||
|
@ -0,0 +1,297 @@
|
||||
import {
|
||||
Plugin,
|
||||
useActionContext,
|
||||
useBlockRequestContext,
|
||||
useCollectionDataSource,
|
||||
useCollectionManager_deprecated,
|
||||
useCollectValuesToSubmit,
|
||||
useCompile,
|
||||
useFilterByTk,
|
||||
} from '@tachybase/client';
|
||||
import { useField, useFieldSchema, useForm } from '@tachybase/schema';
|
||||
import { isURL } from '@tachybase/utils/client';
|
||||
|
||||
import { App, message } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { CollectionBlockInitializer } from '../../components';
|
||||
import { lang, tval } from '../../locale';
|
||||
import { PluginWorkflow } from '../../Plugin';
|
||||
import { Trigger } from '../../triggers';
|
||||
import { getCollectionFieldOptions, UseVariableOptions } from '../../variable';
|
||||
|
||||
class OmniAction extends Trigger {
|
||||
title = tval('Custom action event');
|
||||
description = tval(
|
||||
`When the "Trigger Workflow" button is clicked, the event is triggered based on the single piece of data where the button is located. For complex data processing that cannot be handled simply by NocoBase\\'s built-in operations (CRUD), you can define a series of operations through a workflow and trigger it with the "Trigger Workflow" button.`,
|
||||
);
|
||||
fieldset = {
|
||||
collection: {
|
||||
type: 'string',
|
||||
title: tval('Collection'),
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DataSourceCollectionCascader',
|
||||
'x-reactions': [{ target: 'changed', effects: ['onFieldValueChange'], fulfill: { state: { value: [] } } }],
|
||||
},
|
||||
appends: {
|
||||
type: 'array',
|
||||
title: tval('Associations to use'),
|
||||
description: tval(
|
||||
'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.',
|
||||
),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'AppendsTreeSelect',
|
||||
'x-component-props': {
|
||||
multiple: true,
|
||||
useCollection() {
|
||||
const form = useForm();
|
||||
return form.values?.collection;
|
||||
},
|
||||
},
|
||||
'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }],
|
||||
},
|
||||
};
|
||||
scope = { useCollectionDataSource };
|
||||
components = {};
|
||||
isActionTriggerable = (config, context) => {
|
||||
return context.buttonAction === 'customize:triggerWorkflows';
|
||||
};
|
||||
useVariables(config: Record<string, any>, options?: UseVariableOptions) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const compile = useCompile();
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { getCollectionFields: getCollectionFields } = useCollectionManager_deprecated();
|
||||
const m = getCollectionFieldOptions({
|
||||
appends: ['user'],
|
||||
...options,
|
||||
fields: [
|
||||
{
|
||||
collectionName: 'users',
|
||||
name: 'user',
|
||||
type: 'hasOne',
|
||||
target: 'users',
|
||||
uiSchema: { title: lang('User acted') },
|
||||
},
|
||||
],
|
||||
compile,
|
||||
getCollectionFields,
|
||||
});
|
||||
return [
|
||||
...getCollectionFieldOptions({
|
||||
appends: ['data', ...(config.appends?.map((append) => `data.${append}`) || [])],
|
||||
fields: [
|
||||
{
|
||||
collectionName: config.collection,
|
||||
name: 'data',
|
||||
type: 'hasOne',
|
||||
target: config.collection,
|
||||
uiSchema: { title: lang('Trigger data') },
|
||||
},
|
||||
],
|
||||
compile,
|
||||
getCollectionFields,
|
||||
}),
|
||||
...m,
|
||||
{ label: lang('Role of user acted'), value: 'roleName' },
|
||||
];
|
||||
}
|
||||
useInitializers(config) {
|
||||
return config.collection
|
||||
? {
|
||||
name: 'triggerData',
|
||||
type: 'item',
|
||||
key: 'triggerData',
|
||||
title: tval('Trigger data'),
|
||||
Component: CollectionBlockInitializer,
|
||||
collection: config.collection,
|
||||
dataPath: '$context.data',
|
||||
}
|
||||
: null;
|
||||
}
|
||||
}
|
||||
function useFormWorkflowCustomActionProps() {
|
||||
const form = useForm();
|
||||
const { field, __parent, resource } = useBlockRequestContext();
|
||||
const { setVisible } = useActionContext();
|
||||
const filterByTk = useFilterByTk();
|
||||
const navigate = useNavigate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const _field = useField();
|
||||
const compile = useCompile();
|
||||
const { modal } = App.useApp();
|
||||
const collectionValuesToSubmit = useCollectValuesToSubmit();
|
||||
return (
|
||||
_field.componentProps.filterKeys,
|
||||
{
|
||||
async onClick() {
|
||||
const { onSuccess, skipValidator, triggerWorkflows } = fieldSchema?.['x-action-settings'] || {};
|
||||
if (!skipValidator) {
|
||||
await form.submit();
|
||||
}
|
||||
const values = await collectionValuesToSubmit();
|
||||
(_field.data = field.data || {}), (_field.data.loading = true);
|
||||
try {
|
||||
const B = await resource.trigger({
|
||||
values,
|
||||
filterByTk,
|
||||
triggerWorkflows: triggerWorkflows?.length
|
||||
? triggerWorkflows
|
||||
.map((workflow) => [workflow.workflowKey, workflow.context].filter(Boolean).join('!'))
|
||||
.join(',')
|
||||
: void 0,
|
||||
});
|
||||
if (
|
||||
((_field.data.data = B),
|
||||
__parent?.service?.refresh?.(),
|
||||
setVisible == null || setVisible(false),
|
||||
!(onSuccess != null && onSuccess.successMessage))
|
||||
)
|
||||
return;
|
||||
onSuccess != null && onSuccess.manualClose
|
||||
? modal.success({
|
||||
title: compile(onSuccess?.successMessage),
|
||||
onOk: async () => {
|
||||
await form.reset();
|
||||
return (
|
||||
onSuccess?.redirecting &&
|
||||
onSuccess?.redirectTo &&
|
||||
(isURL(onSuccess.redirectTo)
|
||||
? (window.location.href = onSuccess.redirectTo)
|
||||
: navigate(onSuccess.redirectTo))
|
||||
);
|
||||
},
|
||||
})
|
||||
: message.success(compile(onSuccess == null ? void 0 : onSuccess.successMessage));
|
||||
} catch (B) {
|
||||
console.error(B);
|
||||
} finally {
|
||||
_field.data.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
function useRecordWorkflowCustomTriggerActionProps() {
|
||||
const compile = useCompile();
|
||||
const filterByTk = useFilterByTk();
|
||||
const _field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { field, resource } = useBlockRequestContext();
|
||||
const { setVisible, setSubmitted } = useActionContext() as any;
|
||||
const { modal } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { onSuccess, triggerWorkflows } = fieldSchema?.['x-action-settings'] || {};
|
||||
return {
|
||||
async onClick(N, w) {
|
||||
(_field.data = field.data || {}), (_field.data.loading = true);
|
||||
try {
|
||||
if (
|
||||
(await resource.trigger({
|
||||
filterByTk: filterByTk,
|
||||
triggerWorkflows: triggerWorkflows?.length
|
||||
? triggerWorkflows
|
||||
.map((workflow) => [workflow.workflowKey, workflow.context].filter(Boolean).join('!'))
|
||||
.join(',')
|
||||
: void 0,
|
||||
}),
|
||||
w && w(),
|
||||
setVisible == null || setVisible(false),
|
||||
setSubmitted == null || setSubmitted(true),
|
||||
!onSuccess?.successMessage)
|
||||
)
|
||||
return;
|
||||
if (onSuccess?.manualClose) {
|
||||
modal.success({
|
||||
title: compile(onSuccess?.successMessage),
|
||||
onOk() {
|
||||
onSuccess?.redirecting &&
|
||||
onSuccess?.redirectTo &&
|
||||
(isURL(onSuccess.redirectTo)
|
||||
? (window.location.href = onSuccess.redirectTo)
|
||||
: navigate(onSuccess.redirectTo));
|
||||
},
|
||||
});
|
||||
} else message.success(compile(onSuccess?.successMessage));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
_field.data.loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const triggerWorkflowItem = {
|
||||
name: 'triggerWorkflow',
|
||||
title: tval('Trigger workflow'),
|
||||
Component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
title: tval('Trigger workflow'),
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useFormWorkflowCustomActionProps',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
skipValidator: false,
|
||||
onSuccess: { manualClose: true, redirecting: false, successMessage: tval('Submitted successfully') },
|
||||
triggerWorkflows: [],
|
||||
},
|
||||
'x-action': 'customize:triggerWorkflows',
|
||||
},
|
||||
};
|
||||
const triggerWorkflowAction = {
|
||||
name: 'triggerWorkflow',
|
||||
title: tval('Trigger workflow'),
|
||||
Component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
title: tval('Trigger workflow'),
|
||||
'x-component': 'Action',
|
||||
'x-use-component-props': 'useRecordWorkflowCustomTriggerActionProps',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
successMessage: tval('Submitted successfully'),
|
||||
},
|
||||
triggerWorkflows: [],
|
||||
},
|
||||
'x-action': 'customize:triggerWorkflows',
|
||||
},
|
||||
};
|
||||
const triggerWorkflowLinkItem = {
|
||||
...triggerWorkflowAction,
|
||||
schema: { ...triggerWorkflowAction.schema, 'x-component': 'Action.Link' },
|
||||
};
|
||||
export class PluginOmniTrigger extends Plugin {
|
||||
async load() {
|
||||
this.app.pm.get<PluginWorkflow>('workflow').registerTrigger('custom-action', OmniAction);
|
||||
this.app.addScopes({
|
||||
useFormWorkflowCustomActionProps,
|
||||
useRecordWorkflowCustomTriggerActionProps,
|
||||
});
|
||||
this.app.schemaInitializerManager
|
||||
.get('FormActionInitializers')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowItem);
|
||||
this.app.schemaInitializerManager
|
||||
.get('createForm:configureActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowItem);
|
||||
this.app.schemaInitializerManager
|
||||
.get('editForm:configureActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowItem);
|
||||
this.app.schemaInitializerManager
|
||||
.get('detailsWithPaging:configureActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowAction);
|
||||
this.app.schemaInitializerManager
|
||||
.get('details:configureActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowAction);
|
||||
this.app.schemaInitializerManager
|
||||
.get('table:configureItemActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowLinkItem);
|
||||
this.app.schemaInitializerManager
|
||||
.get('gridCard:configureItemActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowLinkItem);
|
||||
this.app.schemaInitializerManager
|
||||
.get('list:configureItemActions')
|
||||
.add('customize.triggerWorkflow', triggerWorkflowLinkItem);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { Plugin } from '@tachybase/client';
|
||||
|
||||
import { RadioWithTooltip } from '../../components';
|
||||
import { tval } from '../../locale';
|
||||
import { Instruction } from '../../nodes';
|
||||
import { PluginWorkflow } from '../../Plugin';
|
||||
import { WorkflowVariableInput, WorkflowVariableTextArea } from '../../variable';
|
||||
|
||||
class ResponseInstruction extends Instruction {
|
||||
title = tval('Response message');
|
||||
type = 'response-message';
|
||||
group = 'extended';
|
||||
description = tval('Add response message, will be send to client when process of request ends.');
|
||||
fieldset = {
|
||||
message: {
|
||||
type: 'string',
|
||||
title: tval('Message content'),
|
||||
description: tval('Supports variables in template.', { name: '{{name}}' }),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'WorkflowVariableTextArea',
|
||||
},
|
||||
};
|
||||
scope = {};
|
||||
components = {
|
||||
RadioWithTooltip,
|
||||
WorkflowVariableTextArea,
|
||||
WorkflowVariableInput,
|
||||
};
|
||||
isAvailable({ workflow }) {
|
||||
return workflow.type === 'request-interception' || ('action' === workflow.type && workflow.sync);
|
||||
}
|
||||
}
|
||||
export class PluginResponse extends Plugin {
|
||||
async load() {
|
||||
this.app.pm.get<PluginWorkflow>('workflow').registerInstruction('response-message', ResponseInstruction);
|
||||
}
|
||||
}
|
@ -11,5 +11,5 @@ export function useWorkflowTranslation() {
|
||||
return useTranslation(NAMESPACE);
|
||||
}
|
||||
|
||||
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
|
||||
export const tval = (key: string, opts={}) => nTval(key, { ns: NAMESPACE, ...opts });
|
||||
|
||||
|
@ -18,8 +18,10 @@ import PluginWorkflowJSParseServer from './features/js-parse/plugin';
|
||||
import PluginWorkflowJSONParseServer from './features/json-parse/plugin';
|
||||
import { PluginLoop } from './features/loop/Plugin';
|
||||
import { PluginManual } from './features/manual/Plugin';
|
||||
import { PluginOmniTrigger } from './features/omni-trigger';
|
||||
import { PluginParallel } from './features/parallel/Plugin';
|
||||
import { PluginRequest } from './features/request/Plugin';
|
||||
import { PluginResponse } from './features/response';
|
||||
import { PluginSql } from './features/sql/Plugin';
|
||||
import { PluginVariables } from './features/variables';
|
||||
import initFunctions, { CustomFunction } from './functions';
|
||||
@ -72,6 +74,8 @@ export default class PluginWorkflowServer extends Plugin {
|
||||
pluginAPIRegular: PluginWorkflowAPIRegularServer;
|
||||
pluginInterception: PluginInterception;
|
||||
pluginVariables: PluginVariables;
|
||||
pluginResponse: PluginResponse;
|
||||
pluginOmni: PluginOmniTrigger;
|
||||
|
||||
constructor(app: Application, options?: PluginOptions) {
|
||||
super(app, options);
|
||||
@ -89,6 +93,8 @@ export default class PluginWorkflowServer extends Plugin {
|
||||
this.pluginAPIRegular = new PluginWorkflowAPIRegularServer(app, options);
|
||||
this.pluginInterception = new PluginInterception(app, options);
|
||||
this.pluginVariables = new PluginVariables(app, options);
|
||||
this.pluginResponse = new PluginResponse(app, options);
|
||||
this.pluginOmni = new PluginOmniTrigger(app, options);
|
||||
}
|
||||
|
||||
getLogger(workflowId: ID): Logger {
|
||||
@ -317,6 +323,8 @@ export default class PluginWorkflowServer extends Plugin {
|
||||
await this.pluginAPIRegular.load();
|
||||
await this.pluginInterception.load();
|
||||
await this.pluginVariables.load();
|
||||
await this.pluginResponse.load();
|
||||
await this.pluginOmni.load();
|
||||
}
|
||||
|
||||
toggle(workflow: WorkflowModel, enable?: boolean) {
|
||||
|
@ -0,0 +1,132 @@
|
||||
import { joinCollectionName, parseCollectionName } from '@tachybase/data-source-manager';
|
||||
import PluginErrorHandler from '@tachybase/plugin-error-handler';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { EXECUTION_STATUS } from '../../constants';
|
||||
import Trigger from '../../triggers';
|
||||
import { toJSON } from '../../utils';
|
||||
|
||||
class CustomActionInterceptionError extends Error {
|
||||
status = 400;
|
||||
messages = [];
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'CustomActionInterceptionError';
|
||||
}
|
||||
}
|
||||
export class OmniTrigger extends Trigger {
|
||||
static TYPE = 'custom-action';
|
||||
triggerAction = async (context, next) => {
|
||||
const {
|
||||
resourceName,
|
||||
actionName,
|
||||
params: { filterByTk, values, triggerWorkflows = '' },
|
||||
} = context.action;
|
||||
if (actionName !== 'trigger' || resourceName === 'workflows') {
|
||||
return next();
|
||||
}
|
||||
const { currentUser, currentRole } = context.state;
|
||||
const { model: UserModel } = this.workflow.db.getCollection('users');
|
||||
const userInfo = {
|
||||
user: UserModel.build(currentUser).desensitize(),
|
||||
roleName: currentRole,
|
||||
};
|
||||
const dataSourceHeader = context.get('x-data-source');
|
||||
const jointCollectionName = joinCollectionName(dataSourceHeader, resourceName);
|
||||
const triggerWorkflowsMap = new Map();
|
||||
const triggerWorkflowsArray = [];
|
||||
for (const trigger of triggerWorkflows.split(',')) {
|
||||
const [key, path] = trigger.split('!');
|
||||
triggerWorkflowsMap.set(key, path);
|
||||
triggerWorkflowsArray.push(key);
|
||||
}
|
||||
const workflows = Array.from(this.workflow.enabledCache.values())
|
||||
.filter((item) => item.type === OmniTrigger.TYPE && item.config.collection === jointCollectionName)
|
||||
.sort((a, b) => {
|
||||
const aIndex = triggerWorkflowsArray.indexOf(a.key);
|
||||
const bIndex = triggerWorkflowsArray.indexOf(b.key);
|
||||
if (aIndex === -1 && bIndex === -1) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
if (aIndex === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (bIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
const syncGroup = [];
|
||||
const asyncGroup = [];
|
||||
for (const workflow of workflows) {
|
||||
const { appends = [] } = workflow.config;
|
||||
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||
const dataPath = triggerWorkflowsMap.get(workflow.key);
|
||||
const event = [workflow];
|
||||
const { repository } = context.app.dataSourceManager.dataSources
|
||||
.get(dataSourceName)
|
||||
.collectionManager.getCollection(collectionName);
|
||||
const formData = dataPath ? _.get(values, dataPath) : values;
|
||||
let data = formData;
|
||||
if (filterByTk != null) {
|
||||
data = await repository.findOne({ filterByTk, appends });
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
Object.assign(data, formData);
|
||||
}
|
||||
// @ts-ignore
|
||||
event.push({ data: toJSON(data), ...userInfo });
|
||||
(workflow.sync ? syncGroup : asyncGroup).push(event);
|
||||
}
|
||||
for (const event of syncGroup) {
|
||||
const processor = await this.workflow.trigger(event[0], event[1], { httpContext: context });
|
||||
if (!processor) {
|
||||
return context.throw(500);
|
||||
}
|
||||
const { lastSavedJob, nodesMap } = processor;
|
||||
const lastNode = nodesMap.get(lastSavedJob?.nodeId);
|
||||
if (processor.execution.status === EXECUTION_STATUS.RESOLVED) {
|
||||
if (lastNode?.type === 'end') {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (processor.execution.status < EXECUTION_STATUS.STARTED) {
|
||||
if (lastNode?.type !== 'end') {
|
||||
return context.throw(500, 'Workflow on your action failed, please contact the administrator');
|
||||
}
|
||||
const err = new CustomActionInterceptionError('Request is intercepted by workflow');
|
||||
err.status = 400;
|
||||
err.messages = context.state.messages;
|
||||
return context.throw(err.status, err);
|
||||
}
|
||||
return context.throw(500, 'Workflow on your action hangs, please contact the administrator');
|
||||
}
|
||||
for (const event of asyncGroup) {
|
||||
this.workflow.trigger(event[0], event[1]);
|
||||
}
|
||||
await next();
|
||||
};
|
||||
constructor(workflow) {
|
||||
super(workflow);
|
||||
this.workflow.app.dataSourceManager.afterAddDataSource((dataSource) => {
|
||||
if (!dataSource.collectionManager?.db) {
|
||||
return;
|
||||
}
|
||||
dataSource.resourceManager.registerActionHandler('trigger', this.triggerAction);
|
||||
});
|
||||
workflow.app.pm.get(PluginErrorHandler).errorHandler.register(
|
||||
(err) => err instanceof CustomActionInterceptionError,
|
||||
async (err, ctx) => {
|
||||
ctx.body = {
|
||||
errors: err.messages,
|
||||
};
|
||||
ctx.status = err.status;
|
||||
},
|
||||
);
|
||||
}
|
||||
on() {}
|
||||
off() {}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { Plugin } from '@tachybase/server';
|
||||
|
||||
import PluginWorkflowServer from '../../Plugin';
|
||||
import { OmniTrigger } from './CustomActionTrigger';
|
||||
|
||||
export class PluginOmniTrigger extends Plugin {
|
||||
async load() {
|
||||
const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
|
||||
workflowPlugin.registerTrigger('custom-action', OmniTrigger);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './Plugin';
|
@ -0,0 +1,11 @@
|
||||
import { Plugin } from '@tachybase/server';
|
||||
|
||||
import PluginWorkflowServer from '../../Plugin';
|
||||
import { ResponseInstruction } from './ResponseMessageInstruction';
|
||||
|
||||
export class PluginResponse extends Plugin {
|
||||
async load() {
|
||||
const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer;
|
||||
workflowPlugin.registerInstruction('response-message', ResponseInstruction);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { JOB_STATUS } from '../../constants';
|
||||
import Instruction from '../../instructions';
|
||||
|
||||
export class ResponseInstruction extends Instruction {
|
||||
async run(node, prevJob, processor) {
|
||||
const { httpContext } = processor.options;
|
||||
if (!httpContext.state) {
|
||||
httpContext.state = {};
|
||||
}
|
||||
if (!httpContext.state.messages) {
|
||||
httpContext.state.messages = [];
|
||||
}
|
||||
const message = processor.getParsedValue(node.config.message, node.id);
|
||||
if (message) {
|
||||
httpContext.state.messages.push({ message });
|
||||
}
|
||||
return {
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
result: message,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './Plugin';
|
Loading…
Reference in New Issue
Block a user