feat(plugin-workflow): allow manual form button to be configured with preset values (#2225)
* refactor(client): split to small components * fix(client): fix component warning * feat(plugin-workflow): allow form button to be configured more than one for each type * test(plugin-workflow): add test cases * chore(plugin-workflow): add modal tips * fix(plugin-workflow): fix test bugs * fix(plugin-workflow): fix manual button configuration and params * test(plugin-workflow): fix test cases * fix(plugin-workflow): fix manual form values * refactor(plugin-workflow): adjust component * fix(plugin-workflow): fix typo * refactor(plugin-workflow): avoid one more load when manual node resume * fix(plugin-workflow): fix currentUser to be plain object * chore(plugin-workflow): clean code * fix(plugin-workflow): fix typo
This commit is contained in:
parent
9f8460ca22
commit
a17c1ad4e4
@ -127,10 +127,10 @@ export const useSelfAndChildrenCollections = (collectionName: string) => {
|
|||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCollectionFilterOptions = (collectionName: string) => {
|
export const useCollectionFilterOptions = (collection: any) => {
|
||||||
const { getCollectionFields, getInterface } = useCollectionManager();
|
const { getCollectionFields, getInterface } = useCollectionManager();
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const fields = getCollectionFields(collectionName);
|
const fields = getCollectionFields(collection);
|
||||||
const field2option = (field, depth) => {
|
const field2option = (field, depth) => {
|
||||||
if (!field.interface) {
|
if (!field.interface) {
|
||||||
return;
|
return;
|
||||||
@ -179,7 +179,7 @@ export const useCollectionFilterOptions = (collectionName: string) => {
|
|||||||
};
|
};
|
||||||
const options = getOptions(fields, 1);
|
const options = getOptions(fields, 1);
|
||||||
return options;
|
return options;
|
||||||
}, [collectionName]);
|
}, [collection]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLinkageCollectionFilterOptions = (collectionName: string) => {
|
export const useLinkageCollectionFilterOptions = (collectionName: string) => {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -103,6 +103,7 @@ export const ActionBar = observer(
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
flexWrap: 'wrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DndContext>
|
<DndContext>
|
||||||
|
@ -78,13 +78,13 @@ const filterOption = (input, option) => (option?.label ?? '').toLowerCase().incl
|
|||||||
|
|
||||||
const InternalSelect = connect(
|
const InternalSelect = connect(
|
||||||
(props: Props) => {
|
(props: Props) => {
|
||||||
const { objectValue, loading, value, ...others } = props;
|
const { objectValue, loading, value, rawOptions, ...others } = props;
|
||||||
let mode: any = props.multiple ? 'multiple' : props.mode;
|
let mode: any = props.multiple ? 'multiple' : props.mode;
|
||||||
if (mode && !['multiple', 'tags'].includes(mode)) {
|
if (mode && !['multiple', 'tags'].includes(mode)) {
|
||||||
mode = undefined;
|
mode = undefined;
|
||||||
}
|
}
|
||||||
if (objectValue) {
|
if (objectValue) {
|
||||||
return <ObjectSelect {...others} value={value} mode={mode} loading={loading} />;
|
return <ObjectSelect rawOptions={rawOptions} {...others} value={value} mode={mode} loading={loading} />;
|
||||||
}
|
}
|
||||||
const toValue = (v) => {
|
const toValue = (v) => {
|
||||||
if (['tags', 'multiple'].includes(props.mode) || props.multiple) {
|
if (['tags', 'multiple'].includes(props.mode) || props.multiple) {
|
||||||
|
@ -26,7 +26,7 @@ export const Tabs: any = observer(
|
|||||||
key,
|
key,
|
||||||
label: <RecursionField name={key} schema={schema} onlyRenderSelf />,
|
label: <RecursionField name={key} schema={schema} onlyRenderSelf />,
|
||||||
children: (
|
children: (
|
||||||
<PaneRoot active={key === contextProps.activeKey}>
|
<PaneRoot {...(PaneRoot !== React.Fragment ? { active: key === contextProps.activeKey } : {})}>
|
||||||
<RecursionField name={key} schema={schema} onlyRenderProperties />
|
<RecursionField name={key} schema={schema} onlyRenderProperties />
|
||||||
</PaneRoot>
|
</PaneRoot>
|
||||||
),
|
),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
import { connect, mapReadPretty } from '@formily/react';
|
import { connect, mapReadPretty } from '@formily/react';
|
||||||
|
|
||||||
import { IField } from '../../../collection-manager';
|
import { IField } from '../../../collection-manager';
|
||||||
@ -6,6 +7,20 @@ import { JSONInput } from './JSONInput';
|
|||||||
import { RawTextArea } from './RawTextArea';
|
import { RawTextArea } from './RawTextArea';
|
||||||
import { TextArea } from './TextArea';
|
import { TextArea } from './TextArea';
|
||||||
|
|
||||||
|
const VariableScopeContext = createContext([]);
|
||||||
|
|
||||||
|
export function VariableScopeProvider({ scope = [], children }) {
|
||||||
|
return (
|
||||||
|
<VariableScopeContext.Provider value={scope}>
|
||||||
|
{children}
|
||||||
|
</VariableScopeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVariableScope() {
|
||||||
|
return useContext(VariableScopeContext);
|
||||||
|
}
|
||||||
|
|
||||||
export function Variable() {
|
export function Variable() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
useCollectionField,
|
useCollectionField,
|
||||||
useCollectionFilterOptions,
|
useCollectionFilterOptions,
|
||||||
} from '../../../collection-manager';
|
} from '../../../collection-manager';
|
||||||
import { Variable, useCompile, useComponent } from '../../../schema-component';
|
import { Variable, useCompile, useComponent, useVariableScope } from '../../../schema-component';
|
||||||
import { DeletedField } from '../DeletedField';
|
import { DeletedField } from '../DeletedField';
|
||||||
|
|
||||||
const InternalField: React.FC = (props) => {
|
const InternalField: React.FC = (props) => {
|
||||||
@ -91,8 +91,9 @@ export const AssignedField = (props: any) => {
|
|||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name);
|
||||||
const [options, setOptions] = useState<any[]>([]);
|
const [options, setOptions] = useState<any[]>([]);
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const fields = compile(useCollectionFilterOptions(collection?.name));
|
const fields = compile(useCollectionFilterOptions(collection));
|
||||||
const userFields = compile(useCollectionFilterOptions('users'));
|
const userFields = compile(useCollectionFilterOptions('users'));
|
||||||
|
const scope = useVariableScope();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const opt = [
|
const opt = [
|
||||||
{
|
{
|
||||||
@ -113,8 +114,9 @@ export const AssignedField = (props: any) => {
|
|||||||
children: null,
|
children: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setOptions(opt);
|
const next = opt.concat(scope);
|
||||||
}, [fields, userFields]);
|
setOptions(next);
|
||||||
|
}, [fields, userFields, scope]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Variable.Input
|
<Variable.Input
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { cx, useAPIClient, useCompile } from '@nocobase/client';
|
import { cx, css, useAPIClient, useCompile } from '@nocobase/client';
|
||||||
import { Button, Dropdown, MenuProps } from 'antd';
|
import { Button, Dropdown, MenuProps } from 'antd';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useFlowContext } from './FlowContext';
|
import { useFlowContext } from './FlowContext';
|
||||||
@ -82,7 +82,7 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
|
|||||||
|
|
||||||
const menu = useMemo<MenuProps>(() => {
|
const menu = useMemo<MenuProps>(() => {
|
||||||
return {
|
return {
|
||||||
onClick: (ev) => onCreate(ev),
|
onClick: onCreate,
|
||||||
items: compile(groups),
|
items: compile(groups),
|
||||||
};
|
};
|
||||||
}, [groups, onCreate]);
|
}, [groups, onCreate]);
|
||||||
@ -92,8 +92,18 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.addButtonClass)}>
|
<div className={styles.addButtonClass}>
|
||||||
<Dropdown trigger={['click']} menu={menu} disabled={workflow.executed}>
|
<Dropdown
|
||||||
|
trigger={['click']}
|
||||||
|
menu={menu}
|
||||||
|
disabled={workflow.executed}
|
||||||
|
overlayClassName={css`
|
||||||
|
.ant-dropdown-menu-root{
|
||||||
|
max-height: 30em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
<Button shape="circle" icon={<PlusOutlined />} />
|
<Button shape="circle" icon={<PlusOutlined />} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,14 +14,14 @@ function InnerCollectionBlockInitializer({ insert, collection, dataSource, ...pr
|
|||||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
const { getCollection } = useCollectionManager();
|
const { getCollection } = useCollectionManager();
|
||||||
const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemOptions[];
|
const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemOptions[];
|
||||||
const resovledCollection = getCollection(collection);
|
const resolvedCollection = getCollection(collection);
|
||||||
|
|
||||||
async function onConfirm({ item }) {
|
async function onConfirm({ item }) {
|
||||||
const template = item.template ? await getTemplateSchemaByMode(item) : null;
|
const template = item.template ? await getTemplateSchemaByMode(item) : null;
|
||||||
const result = {
|
const result = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
name: resovledCollection.name,
|
name: resolvedCollection.name,
|
||||||
title: resovledCollection.title,
|
title: resolvedCollection.title,
|
||||||
'x-decorator': 'DetailsBlockProvider',
|
'x-decorator': 'DetailsBlockProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection,
|
collection,
|
||||||
|
@ -90,6 +90,8 @@ export default {
|
|||||||
Manual: '人工处理',
|
Manual: '人工处理',
|
||||||
'Could be used for manually submitting data, and determine whether to continue or exit. Workflow will generate a todo item for assigned user when it reaches a manual node, and continue processing after user submits the form.':
|
'Could be used for manually submitting data, and determine whether to continue or exit. Workflow will generate a todo item for assigned user when it reaches a manual node, and continue processing after user submits the form.':
|
||||||
'可用于人工提交数据,并决定是否继续或退出流程。工作流在执行到人工节点时会为被指派的用户生成待办事项,直到用户提交对应表单后继续处理该流程。',
|
'可用于人工提交数据,并决定是否继续或退出流程。工作流在执行到人工节点时会为被指派的用户生成待办事项,直到用户提交对应表单后继续处理该流程。',
|
||||||
|
'Values preset in this form will override user submitted ones when continue or reject.':
|
||||||
|
'表单中预设的字段值会在用户提交继续或拒绝时覆盖相应字段的值。',
|
||||||
'Extended types': '扩展类型',
|
'Extended types': '扩展类型',
|
||||||
'Node type': '节点类型',
|
'Node type': '节点类型',
|
||||||
Calculation: '运算',
|
Calculation: '运算',
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
import { collection, filter } from '../schemas/collection';
|
import { collection, filter } from '../schemas/collection';
|
||||||
import { NAMESPACE, lang } from '../locale';
|
import { NAMESPACE, lang } from '../locale';
|
||||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||||
import { BaseTypeSets, nodesOptions, triggerOptions, useWorkflowVariableOptions } from '../variable';
|
import { BaseTypeSets, defaultFieldNames, nodesOptions, triggerOptions, useWorkflowVariableOptions } from '../variable';
|
||||||
import { FieldsSelect } from '../components/FieldsSelect';
|
import { FieldsSelect } from '../components/FieldsSelect';
|
||||||
import { ValueBlock } from '../components/ValueBlock';
|
import { ValueBlock } from '../components/ValueBlock';
|
||||||
import { useNodeContext } from '.';
|
import { useNodeContext } from '.';
|
||||||
@ -299,7 +299,7 @@ export default {
|
|||||||
ValueBlock,
|
ValueBlock,
|
||||||
AssociatedConfig,
|
AssociatedConfig,
|
||||||
},
|
},
|
||||||
useVariables({ id, title }, { types }) {
|
useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) {
|
||||||
if (
|
if (
|
||||||
types &&
|
types &&
|
||||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||||
@ -307,8 +307,8 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
value: `${id}`,
|
[fieldNames.value]: `${id}`,
|
||||||
label: title,
|
[fieldNames.label]: title,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FormItem, FormLayout } from '@formily/antd-v5';
|
import { FormItem, FormLayout } from '@formily/antd-v5';
|
||||||
import { SchemaInitializerItemOptions, Variable, css, useCollectionManager } from '@nocobase/client';
|
import { SchemaInitializerItemOptions, Variable, css, defaultFieldNames, useCollectionManager } from '@nocobase/client';
|
||||||
import { Evaluator, evaluators, getOptions } from '@nocobase/evaluators/client';
|
import { Evaluator, evaluators, getOptions } from '@nocobase/evaluators/client';
|
||||||
import { parse } from '@nocobase/utils/client';
|
import { parse } from '@nocobase/utils/client';
|
||||||
import { Radio } from 'antd';
|
import { Radio } from 'antd';
|
||||||
@ -192,8 +192,7 @@ export default {
|
|||||||
RadioWithTooltip,
|
RadioWithTooltip,
|
||||||
DynamicConfig,
|
DynamicConfig,
|
||||||
},
|
},
|
||||||
useVariables({ id, title }, options) {
|
useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) {
|
||||||
const { types } = options ?? {};
|
|
||||||
if (
|
if (
|
||||||
types &&
|
types &&
|
||||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||||
@ -201,8 +200,8 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
value: id,
|
[fieldNames.value]: `${id}`,
|
||||||
label: title,
|
[fieldNames.label]: title,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
useInitializers(node): SchemaInitializerItemOptions {
|
useInitializers(node): SchemaInitializerItemOptions {
|
||||||
|
@ -6,7 +6,7 @@ import { Branch } from '../Branch';
|
|||||||
import { useFlowContext } from '../FlowContext';
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { NAMESPACE, lang } from '../locale';
|
import { NAMESPACE, lang } from '../locale';
|
||||||
import useStyles from '../style';
|
import useStyles from '../style';
|
||||||
import { VariableOption, nodesOptions, triggerOptions, useWorkflowVariableOptions } from '../variable';
|
import { VariableOption, defaultFieldNames, nodesOptions, triggerOptions, useWorkflowVariableOptions } from '../variable';
|
||||||
|
|
||||||
function findOption(options: VariableOption[], paths: string[]) {
|
function findOption(options: VariableOption[], paths: string[]) {
|
||||||
let opts = options;
|
let opts = options;
|
||||||
@ -64,7 +64,7 @@ export default {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeDefaultView data={data}>
|
<NodeDefaultView data={data}>
|
||||||
<div className={cx(styles.nodeSubtreeClass)}>
|
<div className={styles.nodeSubtreeClass}>
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
styles.branchBlockClass,
|
styles.branchBlockClass,
|
||||||
@ -75,7 +75,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Branch from={data} entry={entry} branchIndex={entry?.branchIndex ?? 0} />
|
<Branch from={data} entry={entry} branchIndex={entry?.branchIndex ?? 0} />
|
||||||
|
|
||||||
<div className={cx(styles.branchClass)}>
|
<div className={styles.branchClass}>
|
||||||
<div className="workflow-branch-lines" />
|
<div className="workflow-branch-lines" />
|
||||||
<div className={cx(styles.addButtonClass, styles.loopLineClass)}>
|
<div className={cx(styles.addButtonClass, styles.loopLineClass)}>
|
||||||
<ArrowUpOutlined />
|
<ArrowUpOutlined />
|
||||||
@ -103,13 +103,15 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { fieldNames = defaultFieldNames } = options;
|
||||||
|
|
||||||
// const { workflow } = useFlowContext();
|
// const { workflow } = useFlowContext();
|
||||||
// const current = useNodeContext();
|
// const current = useNodeContext();
|
||||||
// const upstreams = useAvailableUpstreams(current);
|
// const upstreams = useAvailableUpstreams(current);
|
||||||
// find target data model by path described in `config.target`
|
// find target data model by path described in `config.target`
|
||||||
// 1. get options from $context/$jobsMapByNodeId
|
// 1. get options from $context/$jobsMapByNodeId
|
||||||
// 2. route to sub-options and use as loop target options
|
// 2. route to sub-options and use as loop target options
|
||||||
let targetOption: VariableOption = { key: 'item', value: 'item', label: lang('Loop target') };
|
let targetOption: VariableOption = { key: 'item', [fieldNames.value]: 'item', [fieldNames.label]: lang('Loop target') };
|
||||||
|
|
||||||
if (typeof target === 'string' && target.startsWith('{{') && target.endsWith('}}')) {
|
if (typeof target === 'string' && target.startsWith('{{') && target.endsWith('}}')) {
|
||||||
const paths = target
|
const paths = target
|
||||||
@ -120,10 +122,10 @@ export default {
|
|||||||
const targetOptions = [nodesOptions, triggerOptions].map((item: any) => {
|
const targetOptions = [nodesOptions, triggerOptions].map((item: any) => {
|
||||||
const opts = item.useOptions(options).filter(Boolean);
|
const opts = item.useOptions(options).filter(Boolean);
|
||||||
return {
|
return {
|
||||||
label: compile(item.title),
|
[fieldNames.label]: compile(item.title),
|
||||||
value: item.value,
|
[fieldNames.value]: item.value,
|
||||||
key: item.value,
|
key: item.value,
|
||||||
children: opts,
|
[fieldNames.children]: opts,
|
||||||
disabled: opts && !opts.length,
|
disabled: opts && !opts.length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -135,8 +137,8 @@ export default {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
targetOption,
|
targetOption,
|
||||||
{ key: 'index', value: 'index', label: lang('Loop index') },
|
{ key: 'index', [fieldNames.value]: 'index', [fieldNames.label]: lang('Loop index') },
|
||||||
{ key: 'length', value: 'length', label: lang('Loop length') },
|
{ key: 'length', [fieldNames.value]: 'length', [fieldNames.label]: lang('Loop length') },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -34,11 +34,8 @@ function InternalFormBlockInitializer({ insert, schema, ...others }) {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
useAction: '{{ useSubmit }}',
|
useAction: '{{ useSubmit }}',
|
||||||
},
|
},
|
||||||
'x-designer': 'Action.Designer',
|
'x-designer': 'ManualActionDesigner',
|
||||||
'x-designer-props': {
|
'x-designer-props': {},
|
||||||
type: 'record',
|
|
||||||
},
|
|
||||||
'x-action': `${JOB_STATUS.RESOLVED}`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...schema,
|
...schema,
|
||||||
@ -48,6 +45,10 @@ function InternalFormBlockInitializer({ insert, schema, ...others }) {
|
|||||||
delete result['x-acl-action'];
|
delete result['x-acl-action'];
|
||||||
const [formKey] = Object.keys(result.properties);
|
const [formKey] = Object.keys(result.properties);
|
||||||
result.properties[formKey].properties.actions['x-decorator'] = 'ActionBarProvider';
|
result.properties[formKey].properties.actions['x-decorator'] = 'ActionBarProvider';
|
||||||
|
result.properties[formKey].properties.actions['x-component-props'].style = {
|
||||||
|
marginTop: '1.5em',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
};
|
||||||
traverseSchema(result, (node) => {
|
traverseSchema(result, (node) => {
|
||||||
if (node['x-uid']) {
|
if (node['x-uid']) {
|
||||||
delete node['x-uid'];
|
delete node['x-uid'];
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { default as React, useContext, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { FormLayout } from '@formily/antd-v5';
|
||||||
|
import { Alert, Button, Modal, Space } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Action,
|
||||||
ActionContextProvider,
|
ActionContextProvider,
|
||||||
GeneralSchemaDesigner,
|
GeneralSchemaDesigner,
|
||||||
InitializerWithSwitch,
|
InitializerWithSwitch,
|
||||||
@ -12,22 +16,24 @@ import {
|
|||||||
SchemaInitializerItemOptions,
|
SchemaInitializerItemOptions,
|
||||||
SchemaInitializerProvider,
|
SchemaInitializerProvider,
|
||||||
SchemaSettings,
|
SchemaSettings,
|
||||||
|
VariableScopeProvider,
|
||||||
gridRowColWrap,
|
gridRowColWrap,
|
||||||
useCompile,
|
useCompile,
|
||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
|
useSchemaOptionsContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Registry, lodash } from '@nocobase/utils/client';
|
import { Registry, lodash } from '@nocobase/utils/client';
|
||||||
import { Button } from 'antd';
|
|
||||||
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
|
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
|
||||||
import { useFlowContext } from '../../FlowContext';
|
|
||||||
import { JOB_STATUS } from '../../constants';
|
import { JOB_STATUS } from '../../constants';
|
||||||
|
import { useFlowContext } from '../../FlowContext';
|
||||||
import { NAMESPACE, lang } from '../../locale';
|
import { NAMESPACE, lang } from '../../locale';
|
||||||
import { useTrigger } from '../../triggers';
|
import { useTrigger } from '../../triggers';
|
||||||
import { DetailsBlockProvider } from './DetailsBlockProvider';
|
import { DetailsBlockProvider } from './DetailsBlockProvider';
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
import createForm from './forms/create';
|
import createRecordForm from './forms/create';
|
||||||
import customForm from './forms/custom';
|
import customRecordForm from './forms/custom';
|
||||||
import updateForm from './forms/update';
|
import updateRecordForm from './forms/update';
|
||||||
|
import { useWorkflowVariableOptions } from '../../variable';
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
|
|
||||||
@ -68,9 +74,9 @@ export type ManualFormType = {
|
|||||||
|
|
||||||
export const manualFormTypes = new Registry<ManualFormType>();
|
export const manualFormTypes = new Registry<ManualFormType>();
|
||||||
|
|
||||||
manualFormTypes.register('customForm', customForm);
|
manualFormTypes.register('customForm', customRecordForm);
|
||||||
manualFormTypes.register('createForm', createForm);
|
manualFormTypes.register('createForm', createRecordForm);
|
||||||
manualFormTypes.register('updateForm', updateForm);
|
manualFormTypes.register('updateForm', updateRecordForm);
|
||||||
|
|
||||||
function useTriggerInitializers(): SchemaInitializerItemOptions | null {
|
function useTriggerInitializers(): SchemaInitializerItemOptions | null {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
@ -79,7 +85,7 @@ function useTriggerInitializers(): SchemaInitializerItemOptions | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blockTypeNames = {
|
const blockTypeNames = {
|
||||||
customForm: customForm.title,
|
customForm: customRecordForm.title,
|
||||||
record: `{{t("Data record", { ns: "${NAMESPACE}" })}}`,
|
record: `{{t("Data record", { ns: "${NAMESPACE}" })}}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,6 +165,142 @@ function AddBlockButton(props: any) {
|
|||||||
return <SchemaInitializer.Button {...props} wrap={gridRowColWrap} items={items} title="{{t('Add block')}}" />;
|
return <SchemaInitializer.Button {...props} wrap={gridRowColWrap} items={items} title="{{t('Add block')}}" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AssignedFieldValues() {
|
||||||
|
const ctx = useContext(SchemaComponentContext);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const scope = useWorkflowVariableOptions({ fieldNames: { label: 'title', value: 'name' } });
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [initialSchema, setInitialSchema] = useState(fieldSchema?.['x-action-settings']?.assignedValues?.schema ?? {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'CustomFormItemInitializers',
|
||||||
|
properties: {},
|
||||||
|
});
|
||||||
|
const [schema, setSchema] = useState<Schema>(null);
|
||||||
|
const { components } = useSchemaOptionsContext();
|
||||||
|
useEffect(() => {
|
||||||
|
setSchema(new Schema({
|
||||||
|
properties: {
|
||||||
|
grid: initialSchema
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [initialSchema]);
|
||||||
|
const form = useMemo(
|
||||||
|
() => {
|
||||||
|
const initialValues = fieldSchema?.['x-action-settings']?.assignedValues?.values;
|
||||||
|
return createForm({
|
||||||
|
initialValues: lodash.cloneDeep(initialValues),
|
||||||
|
values: lodash.cloneDeep(initialValues),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = t('Assign field values');
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
if (!fieldSchema['x-action-settings']) {
|
||||||
|
fieldSchema['x-action-settings'] = {};
|
||||||
|
}
|
||||||
|
if (!fieldSchema['x-action-settings'].assignedValues) {
|
||||||
|
fieldSchema['x-action-settings'].assignedValues = {};
|
||||||
|
}
|
||||||
|
fieldSchema['x-action-settings'].assignedValues.schema = initialSchema;
|
||||||
|
fieldSchema['x-action-settings'].assignedValues.values = form.values;
|
||||||
|
setOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
ctx.refresh?.();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SchemaSettings.Item onClick={() => setOpen(true)}>
|
||||||
|
{title}
|
||||||
|
</SchemaSettings.Item>
|
||||||
|
<Modal
|
||||||
|
width={'50%'}
|
||||||
|
title={title}
|
||||||
|
open={open}
|
||||||
|
onCancel={onCancel}
|
||||||
|
footer={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={onCancel}>{t('Cancel')}</Button>
|
||||||
|
<Button type="primary" onClick={onSubmit}>{t('Submit')}</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<VariableScopeProvider scope={scope}>
|
||||||
|
<FormProvider form={form}>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<Alert message={lang('Values preset in this form will override user submitted ones when continue or reject.')} />
|
||||||
|
<br />
|
||||||
|
{open && schema && (
|
||||||
|
<SchemaComponentContext.Provider
|
||||||
|
value={{
|
||||||
|
...ctx,
|
||||||
|
refresh() {
|
||||||
|
setInitialSchema(lodash.get(schema.toJSON(), 'properties.grid'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SchemaComponent schema={schema} components={components} />
|
||||||
|
</SchemaComponentContext.Provider>
|
||||||
|
)}
|
||||||
|
</FormLayout>
|
||||||
|
</FormProvider>
|
||||||
|
</VariableScopeProvider>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManualActionDesigner(props) {
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner {...props} disableInitializer>
|
||||||
|
<Action.Designer.ButtonEditor />
|
||||||
|
<AssignedFieldValues />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContinueInitializer({ action, actionProps, insert, ...props }) {
|
||||||
|
return (
|
||||||
|
<SchemaInitializer.Item
|
||||||
|
{...props}
|
||||||
|
onClick={() => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
title: props.title,
|
||||||
|
'x-decorator': 'ManualActionStatusProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
value: action,
|
||||||
|
},
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
...actionProps,
|
||||||
|
useAction: '{{ useSubmit }}',
|
||||||
|
},
|
||||||
|
'x-designer': 'ManualActionDesigner',
|
||||||
|
'x-action-settings': {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ActionInitializer({ action, actionProps, ...props }) {
|
function ActionInitializer({ action, actionProps, ...props }) {
|
||||||
return (
|
return (
|
||||||
<InitializerWithSwitch
|
<InitializerWithSwitch
|
||||||
@ -192,7 +334,7 @@ function AddActionButton(props) {
|
|||||||
key: JOB_STATUS.RESOLVED,
|
key: JOB_STATUS.RESOLVED,
|
||||||
type: 'item',
|
type: 'item',
|
||||||
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
|
||||||
component: ActionInitializer,
|
component: ContinueInitializer,
|
||||||
action: JOB_STATUS.RESOLVED,
|
action: JOB_STATUS.RESOLVED,
|
||||||
actionProps: {
|
actionProps: {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
@ -205,7 +347,6 @@ function AddActionButton(props) {
|
|||||||
component: ActionInitializer,
|
component: ActionInitializer,
|
||||||
action: JOB_STATUS.REJECTED,
|
action: JOB_STATUS.REJECTED,
|
||||||
actionProps: {
|
actionProps: {
|
||||||
type: 'primary',
|
|
||||||
danger: true,
|
danger: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -342,6 +483,7 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
return props.children;
|
return props.children;
|
||||||
},
|
},
|
||||||
SimpleDesigner,
|
SimpleDesigner,
|
||||||
|
ManualActionDesigner,
|
||||||
}}
|
}}
|
||||||
scope={{
|
scope={{
|
||||||
useSubmit,
|
useSubmit,
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { dayjs } from '@nocobase/utils/client';
|
import { dayjs } from '@nocobase/utils/client';
|
||||||
import { Spin, Tag } from 'antd';
|
import { Space, Spin, Tag } from 'antd';
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CollectionManagerProvider,
|
CollectionManagerProvider,
|
||||||
FormBlockContext,
|
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentContext,
|
SchemaComponentContext,
|
||||||
TableBlockProvider,
|
TableBlockProvider,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useActionContext,
|
useActionContext,
|
||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
|
useCompile,
|
||||||
useCurrentUserContext,
|
useCurrentUserContext,
|
||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
useRecord,
|
useRecord,
|
||||||
useTableBlockContext,
|
useTableBlockContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { uid } from '@nocobase/utils/client';
|
|
||||||
import { instructions, useAvailableUpstreams } from '..';
|
import { instructions, useAvailableUpstreams } from '..';
|
||||||
import { FlowContext, useFlowContext } from '../../FlowContext';
|
import { FlowContext, useFlowContext } from '../../FlowContext';
|
||||||
import { JobStatusOptions, JobStatusOptionsMap } from '../../constants';
|
import { JobStatusOptions, JobStatusOptionsMap } from '../../constants';
|
||||||
@ -177,7 +176,6 @@ const todoCollection = {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
showTime: true,
|
showTime: true,
|
||||||
},
|
},
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -217,8 +215,6 @@ export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC }
|
|||||||
}}
|
}}
|
||||||
schema={{
|
schema={{
|
||||||
type: 'void',
|
type: 'void',
|
||||||
// name: uid(),
|
|
||||||
'x-component': 'div',
|
|
||||||
properties: {
|
properties: {
|
||||||
actions: {
|
actions: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -293,7 +289,7 @@ export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC }
|
|||||||
'x-component': 'TableV2.Column',
|
'x-component': 'TableV2.Column',
|
||||||
properties: {
|
properties: {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: 'number',
|
type: 'string',
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
@ -317,7 +313,6 @@ export const WorkflowTodo: React.FC & { Drawer: React.FC; Decorator: React.FC }
|
|||||||
'x-component': 'TableV2.Column',
|
'x-component': 'TableV2.Column',
|
||||||
properties: {
|
properties: {
|
||||||
status: {
|
status: {
|
||||||
type: 'number',
|
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
@ -382,13 +377,14 @@ const ManualActionStatusContext = createContext<number | null>(null);
|
|||||||
function ManualActionStatusProvider({ value, children }) {
|
function ManualActionStatusProvider({ value, children }) {
|
||||||
const { userJob } = useFlowContext();
|
const { userJob } = useFlowContext();
|
||||||
const button = useField();
|
const button = useField();
|
||||||
|
const buttonSchema = useFieldSchema();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userJob.status) {
|
if (userJob.status) {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.visible = userJob.status === value;
|
button.visible = userJob.status === value && userJob.result._ === buttonSchema.name;
|
||||||
}
|
}
|
||||||
}, [userJob.status, value, button]);
|
}, [userJob, value, button]);
|
||||||
|
|
||||||
return <ManualActionStatusContext.Provider value={value}>{children}</ManualActionStatusContext.Provider>;
|
return <ManualActionStatusContext.Provider value={value}>{children}</ManualActionStatusContext.Provider>;
|
||||||
}
|
}
|
||||||
@ -398,21 +394,21 @@ function useSubmit() {
|
|||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
const { values, submit } = useForm();
|
const { values, submit } = useForm();
|
||||||
const buttonSchema = useFieldSchema();
|
const buttonSchema = useFieldSchema();
|
||||||
const nextStatus = useContext(ManualActionStatusContext);
|
|
||||||
const { service } = useTableBlockContext();
|
const { service } = useTableBlockContext();
|
||||||
const { userJob } = useFlowContext();
|
const { userJob } = useFlowContext();
|
||||||
const { updateAssociationValues } = useContext(FormBlockContext);
|
const { name: actionKey } = buttonSchema;
|
||||||
|
const { name: formKey } = buttonSchema.parent.parent;
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
|
if (userJob.status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await submit();
|
await submit();
|
||||||
const { name } = buttonSchema.parent.parent.toJSON();
|
|
||||||
await api.resource('users_jobs').submit({
|
await api.resource('users_jobs').submit({
|
||||||
filterByTk: userJob.id,
|
filterByTk: userJob.id,
|
||||||
values: {
|
values: {
|
||||||
status: nextStatus,
|
result: { [formKey]: values, _: actionKey },
|
||||||
result: { [name]: values },
|
|
||||||
},
|
},
|
||||||
updateAssociationValues,
|
|
||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
service.refresh();
|
service.refresh();
|
||||||
@ -520,40 +516,33 @@ function useDetailsBlockProps() {
|
|||||||
return { form };
|
return { form };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FooterStatus() {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { status, updatedAt } = useRecord();
|
||||||
|
const statusOption = JobStatusOptionsMap[status];
|
||||||
|
return status ? (
|
||||||
|
<Space>
|
||||||
|
<time
|
||||||
|
className={css`
|
||||||
|
margin-right: 0.5em;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</time>
|
||||||
|
<Tag icon={statusOption.icon} color={statusOption.color}>{compile(statusOption.label)}</Tag>
|
||||||
|
</Space>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
function Drawer() {
|
function Drawer() {
|
||||||
const ctx = useContext(SchemaComponentContext);
|
const ctx = useContext(SchemaComponentContext);
|
||||||
const { id, node, workflow, status, updatedAt } = useRecord();
|
const { id, node, workflow, status } = useRecord();
|
||||||
|
|
||||||
const statusOption = JobStatusOptionsMap[status];
|
|
||||||
const footerSchema = status
|
|
||||||
? {
|
|
||||||
date: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'time',
|
|
||||||
'x-component-props': {
|
|
||||||
className: css`
|
|
||||||
margin-right: 0.5em;
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
'x-content': dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Tag',
|
|
||||||
'x-component-props': {
|
|
||||||
icon: statusOption.icon,
|
|
||||||
color: statusOption.color,
|
|
||||||
},
|
|
||||||
'x-content': statusOption.label,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponentContext.Provider value={{ ...ctx, reset() {}, designable: false }}>
|
<SchemaComponentContext.Provider value={{ ...ctx, reset() {}, designable: false }}>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
components={{
|
components={{
|
||||||
Tag,
|
FooterStatus,
|
||||||
FlowContextProvider,
|
FlowContextProvider,
|
||||||
}}
|
}}
|
||||||
schema={{
|
schema={{
|
||||||
@ -572,7 +561,12 @@ function Drawer() {
|
|||||||
footer: {
|
footer: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Action.Drawer.Footer',
|
'x-component': 'Action.Drawer.Footer',
|
||||||
properties: footerSchema,
|
properties: {
|
||||||
|
content: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FooterStatus',
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@ -599,10 +593,16 @@ function Decorator({ params = {}, children }) {
|
|||||||
dragSort: false,
|
dragSort: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[nodeCollection, workflowCollection, todoCollection].forEach((collection) => {
|
||||||
|
if (!collections.find((item) => item.name === collection.name)) {
|
||||||
|
collections.push(collection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionManagerProvider
|
<CollectionManagerProvider
|
||||||
{...cm}
|
{...cm}
|
||||||
collections={[...collections, nodeCollection, workflowCollection, todoCollection]}
|
collections={[...collections]}
|
||||||
>
|
>
|
||||||
<TableBlockProvider {...blockProps}>{children}</TableBlockProvider>
|
<TableBlockProvider {...blockProps}>{children}</TableBlockProvider>
|
||||||
</CollectionManagerProvider>
|
</CollectionManagerProvider>
|
||||||
|
@ -71,7 +71,11 @@ export default {
|
|||||||
type: 'create',
|
type: 'create',
|
||||||
title: formBlock['x-component-props']?.title || formKey,
|
title: formBlock['x-component-props']?.title || formKey,
|
||||||
actions: findSchema(formSchema.properties.actions, (item) => item['x-component'] === 'Action').map(
|
actions: findSchema(formSchema.properties.actions, (item) => item['x-component'] === 'Action').map(
|
||||||
(item) => item['x-decorator-props'].value,
|
(item) => ({
|
||||||
|
status: item['x-decorator-props'].value,
|
||||||
|
values: item['x-action-settings']?.assignedValues?.values,
|
||||||
|
key: item.name,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
collection: formBlock['x-decorator-props'].collection,
|
collection: formBlock['x-decorator-props'].collection,
|
||||||
};
|
};
|
||||||
|
@ -105,6 +105,7 @@ function CustomFormBlockInitializer({ insert, ...props }) {
|
|||||||
layout: 'one-column',
|
layout: 'one-column',
|
||||||
style: {
|
style: {
|
||||||
marginTop: '1.5em',
|
marginTop: '1.5em',
|
||||||
|
flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'x-initializer': 'AddActionButton',
|
'x-initializer': 'AddActionButton',
|
||||||
@ -121,8 +122,7 @@ function CustomFormBlockInitializer({ insert, ...props }) {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
useAction: '{{ useSubmit }}',
|
useAction: '{{ useSubmit }}',
|
||||||
},
|
},
|
||||||
'x-designer': 'Action.Designer',
|
'x-designer': 'ManualActionDesigner',
|
||||||
'x-action': `${JOB_STATUS.RESOLVED}`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -371,7 +371,11 @@ export default {
|
|||||||
type: 'custom',
|
type: 'custom',
|
||||||
title: formBlock['x-component-props']?.title || formKey,
|
title: formBlock['x-component-props']?.title || formKey,
|
||||||
actions: findSchema(formSchema.properties.actions, (item) => item['x-component'] === 'Action').map(
|
actions: findSchema(formSchema.properties.actions, (item) => item['x-component'] === 'Action').map(
|
||||||
(item) => item['x-decorator-props'].value,
|
(item) => ({
|
||||||
|
status: item['x-decorator-props'].value,
|
||||||
|
values: item['x-action-settings']?.assignedValues?.values,
|
||||||
|
key: item.name,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
collection: formBlock['x-decorator-props'].collection,
|
collection: formBlock['x-decorator-props'].collection,
|
||||||
};
|
};
|
||||||
|
@ -114,7 +114,11 @@ export default {
|
|||||||
type: 'update',
|
type: 'update',
|
||||||
title: formBlock['x-component-props']?.title || formKey,
|
title: formBlock['x-component-props']?.title || formKey,
|
||||||
actions: findSchema(formSchema.properties.actions, (item) => item['x-component'] === 'Action').map(
|
actions: findSchema(formSchema.properties.actions, (item) => item['x-component'] === 'Action').map(
|
||||||
(item) => item['x-decorator-props'].value,
|
(item) => ({
|
||||||
|
status: item['x-decorator-props'].value,
|
||||||
|
values: item['x-action-settings']?.assignedValues?.values,
|
||||||
|
key: item.name,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager, useCompile } from '@nocobase/client';
|
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||||
import { getCollectionFieldOptions } from '../../variable';
|
import { defaultFieldNames, getCollectionFieldOptions } from '../../variable';
|
||||||
import { NAMESPACE } from '../../locale';
|
import { NAMESPACE } from '../../locale';
|
||||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||||
import { ModeConfig } from './ModeConfig';
|
import { ModeConfig } from './ModeConfig';
|
||||||
@ -85,7 +85,7 @@ export default {
|
|||||||
ModeConfig,
|
ModeConfig,
|
||||||
AssigneesSelect,
|
AssigneesSelect,
|
||||||
},
|
},
|
||||||
useVariables({ id, title, config }, { types }) {
|
useVariables({ id, title, config }, { types, fieldNames = defaultFieldNames }) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getCollectionFields } = useCollectionManager();
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const formKeys = Object.keys(config.forms ?? {});
|
const formKeys = Object.keys(config.forms ?? {});
|
||||||
@ -119,9 +119,9 @@ export default {
|
|||||||
|
|
||||||
return options.length
|
return options.length
|
||||||
? {
|
? {
|
||||||
value: `${id}`,
|
[fieldNames.value]: `${id}`,
|
||||||
label: title,
|
[fieldNames.label]: title,
|
||||||
children: options,
|
[fieldNames.children]: options,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@ import { ScheduleConfig } from './ScheduleConfig';
|
|||||||
import { SCHEDULE_MODE } from './constants';
|
import { SCHEDULE_MODE } from './constants';
|
||||||
import { NAMESPACE, lang } from '../../locale';
|
import { NAMESPACE, lang } from '../../locale';
|
||||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||||
import { getCollectionFieldOptions } from '../../variable';
|
import { defaultFieldNames, getCollectionFieldOptions } from '../../variable';
|
||||||
import { FieldsSelect } from '../../components/FieldsSelect';
|
import { FieldsSelect } from '../../components/FieldsSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -26,6 +26,7 @@ export default {
|
|||||||
useVariables(config, opts) {
|
useVariables(config, opts) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getCollectionFields } = useCollectionManager();
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
const { fieldNames = defaultFieldNames } = opts;
|
||||||
const options: any[] = [];
|
const options: any[] = [];
|
||||||
if (!opts?.types || opts.types.includes('date')) {
|
if (!opts?.types || opts.types.includes('date')) {
|
||||||
options.push({ key: 'date', value: 'date', label: lang('Trigger time') });
|
options.push({ key: 'date', value: 'date', label: lang('Trigger time') });
|
||||||
|
@ -6,17 +6,37 @@ import { triggers } from './triggers';
|
|||||||
|
|
||||||
export type VariableOption = {
|
export type VariableOption = {
|
||||||
key?: string;
|
key?: string;
|
||||||
value: string;
|
value?: string;
|
||||||
label: string;
|
label?: string;
|
||||||
children?: VariableOptions;
|
children?: VariableOptions;
|
||||||
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VariableOptions = VariableOption[] | null;
|
export type VariableOptions = VariableOption[] | null;
|
||||||
|
|
||||||
|
export type VariableDataType =
|
||||||
|
string |
|
||||||
|
{
|
||||||
|
type: string;
|
||||||
|
options?: { entity?: boolean; collection?: string }
|
||||||
|
} |
|
||||||
|
((field: any, appends?: string[]) => boolean);
|
||||||
|
|
||||||
|
export type OptionsOfUseVariableOptions = {
|
||||||
|
types?: VariableDataType[];
|
||||||
|
fieldNames?: {
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
children?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFieldNames = { label: 'label', value: 'value', children: 'children' } as const;
|
||||||
|
|
||||||
export const nodesOptions = {
|
export const nodesOptions = {
|
||||||
label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: '$jobsMapByNodeId',
|
value: '$jobsMapByNodeId',
|
||||||
useOptions(options) {
|
useOptions(options: OptionsOfUseVariableOptions) {
|
||||||
const current = useNodeContext();
|
const current = useNodeContext();
|
||||||
const upstreams = useAvailableUpstreams(current);
|
const upstreams = useAvailableUpstreams(current);
|
||||||
const result: VariableOption[] = [];
|
const result: VariableOption[] = [];
|
||||||
@ -34,7 +54,7 @@ export const nodesOptions = {
|
|||||||
export const triggerOptions = {
|
export const triggerOptions = {
|
||||||
label: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: '$context',
|
value: '$context',
|
||||||
useOptions(options) {
|
useOptions(options: OptionsOfUseVariableOptions) {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
const trigger = triggers.get(workflow.type);
|
const trigger = triggers.get(workflow.type);
|
||||||
return trigger?.useVariables?.(workflow.config, options) ?? null;
|
return trigger?.useVariables?.(workflow.config, options) ?? null;
|
||||||
@ -44,7 +64,8 @@ export const triggerOptions = {
|
|||||||
export const scopeOptions = {
|
export const scopeOptions = {
|
||||||
label: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: '$scopes',
|
value: '$scopes',
|
||||||
useOptions(options) {
|
useOptions(options: OptionsOfUseVariableOptions) {
|
||||||
|
const { fieldNames = defaultFieldNames } = options;
|
||||||
const current = useNodeContext();
|
const current = useNodeContext();
|
||||||
const scopes = useUpstreamScopes(current);
|
const scopes = useUpstreamScopes(current);
|
||||||
const result: VariableOption[] = [];
|
const result: VariableOption[] = [];
|
||||||
@ -54,9 +75,9 @@ export const scopeOptions = {
|
|||||||
if (subOptions) {
|
if (subOptions) {
|
||||||
result.push({
|
result.push({
|
||||||
key: node.id.toString(),
|
key: node.id.toString(),
|
||||||
value: node.id.toString(),
|
[fieldNames.value]: node.id.toString(),
|
||||||
label: node.title ?? `#${node.id}`,
|
[fieldNames.label]: node.title ?? `#${node.id}`,
|
||||||
children: subOptions,
|
[fieldNames.children]: subOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -67,14 +88,14 @@ export const scopeOptions = {
|
|||||||
export const systemOptions = {
|
export const systemOptions = {
|
||||||
label: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: '$system',
|
value: '$system',
|
||||||
useOptions({ types }) {
|
useOptions({ types, fieldNames = defaultFieldNames }: OptionsOfUseVariableOptions) {
|
||||||
return [
|
return [
|
||||||
...(!types || types.includes('date')
|
...(!types || types.includes('date')
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'now',
|
key: 'now',
|
||||||
value: 'now',
|
[fieldNames.label]: lang('System time'),
|
||||||
label: lang('System time'),
|
[fieldNames.value]: 'now',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@ -169,16 +190,18 @@ function filterTypedFields({ fields, types, appends, compile, getCollectionField
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkflowVariableOptions(options = {}) {
|
export function useWorkflowVariableOptions(options: OptionsOfUseVariableOptions = {}) {
|
||||||
|
const fieldNames = Object.assign({}, defaultFieldNames, options.fieldNames ?? {});
|
||||||
|
const opts = Object.assign(options, { fieldNames });
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
|
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
|
||||||
const opts = item.useOptions(options).filter(Boolean);
|
const children = item.useOptions(opts).filter(Boolean);
|
||||||
return {
|
return {
|
||||||
label: compile(item.label),
|
[fieldNames.label]: compile(item.label),
|
||||||
value: item.value,
|
[fieldNames.value]: item.value,
|
||||||
key: item.value,
|
key: item[fieldNames.value],
|
||||||
children: opts,
|
[fieldNames.children]: children,
|
||||||
disabled: opts && !opts.length,
|
disabled: children && !children.length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -245,15 +268,13 @@ async function loadChildren(option) {
|
|||||||
collection: option.field.target,
|
collection: option.field.target,
|
||||||
types: option.types,
|
types: option.types,
|
||||||
appends: getNextAppends(option.field, option.appends),
|
appends: getNextAppends(option.field, option.appends),
|
||||||
sourceKey: option.field.key,
|
...this,
|
||||||
compile: this.compile,
|
|
||||||
getCollectionFields: this.getCollectionFields,
|
|
||||||
});
|
});
|
||||||
|
option.loadChildren = null;
|
||||||
if (result.length) {
|
if (result.length) {
|
||||||
option.children = result;
|
option.children = result;
|
||||||
} else {
|
} else {
|
||||||
option.isLeaf = true;
|
option.isLeaf = true;
|
||||||
option.loadChildren = null;
|
|
||||||
const matchingType = option.types?.some((type) => matchFieldType(option.field, type, 0));
|
const matchingType = option.types?.some((type) => matchFieldType(option.field, type, 0));
|
||||||
if (!matchingType) {
|
if (!matchingType) {
|
||||||
option.disabled = true;
|
option.disabled = true;
|
||||||
@ -262,10 +283,10 @@ async function loadChildren(option) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCollectionFieldOptions(options): VariableOption[] {
|
export function getCollectionFieldOptions(options): VariableOption[] {
|
||||||
const { fields, collection, types, appends = [], compile, getCollectionFields } = options;
|
const { fields, collection, types, appends = [], compile, getCollectionFields, fieldNames = defaultFieldNames } = options;
|
||||||
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
||||||
const computedFields = fields ?? normalizedFields;
|
const computedFields = fields ?? normalizedFields;
|
||||||
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields });
|
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields, fieldNames });
|
||||||
|
|
||||||
const result: VariableOption[] = filterTypedFields({
|
const result: VariableOption[] = filterTypedFields({
|
||||||
fields: computedFields,
|
fields: computedFields,
|
||||||
@ -276,13 +297,14 @@ export function getCollectionFieldOptions(options): VariableOption[] {
|
|||||||
getCollectionFields,
|
getCollectionFields,
|
||||||
}).map((field) => {
|
}).map((field) => {
|
||||||
const label = compile(field.uiSchema?.title || field.name);
|
const label = compile(field.uiSchema?.title || field.name);
|
||||||
|
// console.log('===', label, field);
|
||||||
const nextAppends = getNextAppends(field, appends);
|
const nextAppends = getNextAppends(field, appends);
|
||||||
// TODO: no matching fields in next appends should consider isLeaf as true
|
// TODO: no matching fields in next appends should consider isLeaf as true
|
||||||
const isLeaf = !isAssociationField(field) || (!nextAppends.length && !appends.includes(field.name));
|
const isLeaf = !isAssociationField(field) || (!nextAppends.length && !appends.includes(field.name));
|
||||||
return {
|
return {
|
||||||
label,
|
[fieldNames.label]: label,
|
||||||
key: field.name,
|
key: field.name,
|
||||||
value: field.name,
|
[fieldNames.value]: field.name,
|
||||||
isLeaf,
|
isLeaf,
|
||||||
loadChildren: isLeaf ? null : boundLoadChildren,
|
loadChildren: isLeaf ? null : boundLoadChildren,
|
||||||
field,
|
field,
|
||||||
|
@ -343,9 +343,9 @@ export default class Processor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getParsedValue(value, node?) {
|
public getParsedValue(value, node?, additionalScope?: object) {
|
||||||
const template = parse(value);
|
const template = parse(value);
|
||||||
const scope = this.getScope(node);
|
const scope = Object.assign(this.getScope(node), additionalScope);
|
||||||
template.parameters.forEach(({ key }) => {
|
template.parameters.forEach(({ key }) => {
|
||||||
appendArrayColumn(scope, key);
|
appendArrayColumn(scope, key);
|
||||||
});
|
});
|
||||||
|
@ -51,6 +51,293 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
afterEach(() => db.close());
|
afterEach(() => db.close());
|
||||||
|
|
||||||
|
describe('actions configuration', () => {
|
||||||
|
it('no action configured', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'manual',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id],
|
||||||
|
forms: {
|
||||||
|
f1: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].id,
|
||||||
|
values: {
|
||||||
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res1.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no actionKey provided', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'manual',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id],
|
||||||
|
forms: {
|
||||||
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].id,
|
||||||
|
values: {
|
||||||
|
result: { f1: { a: 1 } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res1.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('values resolved will be overrided by action assigned', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'manual',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id],
|
||||||
|
forms: {
|
||||||
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
key: 'resolve',
|
||||||
|
values: { a: 2 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].id,
|
||||||
|
values: {
|
||||||
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(job.result).toEqual({ f1: { a: 2 }, _: 'resolve' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('values rejected will not be overrided by action assigned', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'manual',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id],
|
||||||
|
forms: {
|
||||||
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
status: JOB_STATUS.REJECTED,
|
||||||
|
key: 'reject',
|
||||||
|
values: { a: 2 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].id,
|
||||||
|
values: {
|
||||||
|
result: { f1: { a: 1 }, _: 'reject' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
expect(execution.status).toBe(EXECUTION_STATUS.REJECTED);
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.status).toBe(JOB_STATUS.REJECTED);
|
||||||
|
expect(job.result).toEqual({ f1: { a: 1 }, _: 'reject' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('values saved as pending will not be overrided by action assigned', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'manual',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id],
|
||||||
|
forms: {
|
||||||
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
status: JOB_STATUS.PENDING,
|
||||||
|
key: 'save',
|
||||||
|
values: { a: 2 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].id,
|
||||||
|
values: {
|
||||||
|
result: { f1: { a: 1 }, _: 'save' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
expect(execution.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(job.result).toEqual({ f1: { a: 1 }, _: 'save' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('variable within assigned values should work when resolve', async () => {
|
||||||
|
const n1 = await workflow.createNode({
|
||||||
|
type: 'manual',
|
||||||
|
config: {
|
||||||
|
assignees: [users[0].id],
|
||||||
|
forms: {
|
||||||
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
key: 'resolve',
|
||||||
|
values: {
|
||||||
|
a: '{{currentUser.id}}',
|
||||||
|
b: '{{currentRecord.id}}',
|
||||||
|
c: '{{currentTime}}',
|
||||||
|
d: '{{$context.data.title}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [pending] = await workflow.getExecutions();
|
||||||
|
expect(pending.status).toBe(EXECUTION_STATUS.STARTED);
|
||||||
|
const [j1] = await pending.getJobs();
|
||||||
|
expect(j1.status).toBe(JOB_STATUS.PENDING);
|
||||||
|
|
||||||
|
const usersJobs = await UserJobModel.findAll();
|
||||||
|
expect(usersJobs.length).toBe(1);
|
||||||
|
expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING);
|
||||||
|
expect(usersJobs[0].userId).toBe(users[0].id);
|
||||||
|
expect(usersJobs[0].jobId).toBe(j1.id);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
|
filterByTk: usersJobs[0].id,
|
||||||
|
values: {
|
||||||
|
result: { f1: { a: 2, id: 3 }, _: 'resolve' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const [execution] = await workflow.getExecutions();
|
||||||
|
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
|
const [job] = await execution.getJobs();
|
||||||
|
expect(job.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
|
expect(job.result).toMatchObject({ f1: { a: users[0].id, id: 3, b: 3, d: post.title }, _: 'resolve' });
|
||||||
|
const time = new Date(job.result.f1.c);
|
||||||
|
expect(time.getTime() - now.getTime()).toBeLessThan(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('mode: 0 (single record)', () => {
|
describe('mode: 0 (single record)', () => {
|
||||||
it('the only user assigned could submit', async () => {
|
it('the only user assigned could submit', async () => {
|
||||||
const n1 = await workflow.createNode({
|
const n1 = await workflow.createNode({
|
||||||
@ -59,7 +346,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id],
|
assignees: [users[0].id],
|
||||||
forms: {
|
forms: {
|
||||||
f1: {
|
f1: {
|
||||||
actions: [JOB_STATUS.RESOLVED],
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -82,15 +371,14 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
const res1 = await agent.resource('users_jobs').submit({
|
const res1 = await agent.resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs[0].id,
|
filterByTk: usersJobs[0].id,
|
||||||
values: { status: JOB_STATUS.RESOLVED },
|
values: { result: { f1: {}, _: 'resolve' } },
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(401);
|
expect(res1.status).toBe(401);
|
||||||
|
|
||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs[0].id,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: {}, _: 'resolve' },
|
||||||
result: { f1: {} },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(403);
|
expect(res2.status).toBe(403);
|
||||||
@ -98,8 +386,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res3 = await userAgents[0].resource('users_jobs').submit({
|
const res3 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs[0].id,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res3.status).toBe(202);
|
expect(res3.status).toBe(202);
|
||||||
@ -108,18 +395,17 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
const [j2] = await pending.getJobs();
|
const [j2] = await pending.getJobs();
|
||||||
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(j2.result).toEqual({ f1: { a: 1 } });
|
expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||||
|
|
||||||
const usersJobsAfter = await UserJobModel.findAll();
|
const usersJobsAfter = await UserJobModel.findAll();
|
||||||
expect(usersJobsAfter.length).toBe(1);
|
expect(usersJobsAfter.length).toBe(1);
|
||||||
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 } });
|
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||||
|
|
||||||
const res4 = await userAgents[0].resource('users_jobs').submit({
|
const res4 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs[0].id,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 2 }, _: 'resolve' },
|
||||||
result: { f1: { a: 2 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res4.status).toBe(400);
|
expect(res4.status).toBe(400);
|
||||||
@ -131,7 +417,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -150,8 +440,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[1].resource('users_jobs').submit({
|
const res1 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs.find((item) => item.userId === users[1].id).id,
|
filterByTk: usersJobs.find((item) => item.userId === users[1].id).id,
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -160,13 +449,12 @@ describe('workflow > instructions > manual', () => {
|
|||||||
|
|
||||||
const [j2] = await pending.getJobs();
|
const [j2] = await pending.getJobs();
|
||||||
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
expect(j2.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(j2.result).toEqual({ f1: { a: 1 } });
|
expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||||
|
|
||||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs.find((item) => item.userId === users[0].id).id,
|
filterByTk: usersJobs.find((item) => item.userId === users[0].id).id,
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(400);
|
expect(res2.status).toBe(400);
|
||||||
@ -178,7 +466,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id],
|
assignees: [users[0].id],
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -196,8 +488,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res = await userAgents[0].resource('users_jobs').submit({
|
const res = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: usersJobs[0].get('id'),
|
filterByTk: usersJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(202);
|
expect(res.status).toBe(202);
|
||||||
@ -208,7 +499,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||||
const [job] = await execution.getJobs();
|
const [job] = await execution.getJobs();
|
||||||
expect(job.status).toBe(JOB_STATUS.RESOLVED);
|
expect(job.status).toBe(JOB_STATUS.RESOLVED);
|
||||||
expect(job.result).toEqual({ f1: { a: 1 } });
|
expect(job.result).toEqual({ f1: { a: 1 }, _: 'resolve' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -220,7 +511,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: 1,
|
mode: 1,
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -238,8 +533,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -259,8 +553,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 2 }, _: 'resolve' },
|
||||||
result: { f1: { a: 2 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -281,7 +574,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: 1,
|
mode: 1,
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.REJECTED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.REJECTED, key: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -299,8 +596,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -320,8 +616,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(400);
|
expect(res2.status).toBe(400);
|
||||||
@ -334,7 +629,12 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: 1,
|
mode: 1,
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
{ status: JOB_STATUS.REJECTED, key: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -352,8 +652,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -373,8 +672,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -397,7 +695,12 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: -1,
|
mode: -1,
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
{ status: JOB_STATUS.REJECTED, key: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -415,8 +718,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -432,8 +734,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(400);
|
expect(res2.status).toBe(400);
|
||||||
@ -446,7 +747,12 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: -1,
|
mode: -1,
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
{ status: JOB_STATUS.REJECTED, key: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -464,8 +770,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -481,8 +786,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { a: 1 }, _: 'resolve' },
|
||||||
result: { f1: { a: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -503,7 +807,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: -1,
|
mode: -1,
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.REJECTED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.REJECTED, key: 'reject' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -521,8 +829,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -538,8 +845,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[1].resource('users_jobs').submit({
|
const res2 = await userAgents[1].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
result: { f1: { a: 0 }, _: 'reject' },
|
||||||
result: { f1: { a: 0 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -565,7 +871,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED] },
|
f1: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -594,8 +904,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { number: 1 }, _: 'resolve' },
|
||||||
result: { f1: { number: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -615,8 +924,18 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
forms: {
|
forms: {
|
||||||
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.PENDING] },
|
f1: {
|
||||||
f2: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.PENDING] },
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
{ status: JOB_STATUS.PENDING, key: 'pending' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
f2: {
|
||||||
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
{ status: JOB_STATUS.PENDING, key: 'pending' },
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -634,8 +953,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.PENDING,
|
result: { f1: { number: 1 }, _: 'pending' },
|
||||||
result: { f1: { number: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -651,8 +969,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.PENDING,
|
result: { f2: { number: 2 }, _: 'pending' },
|
||||||
result: { f2: { number: 2 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -671,8 +988,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res3 = await userAgents[0].resource('users_jobs').submit({
|
const res3 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f2: { number: 3 }, _: 'resolve' },
|
||||||
result: { f2: { number: 3 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res3.status).toBe(202);
|
expect(res3.status).toBe(202);
|
||||||
@ -697,7 +1013,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
forms: {
|
forms: {
|
||||||
f1: {
|
f1: {
|
||||||
type: 'create',
|
type: 'create',
|
||||||
actions: [JOB_STATUS.RESOLVED],
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
collection: 'comments',
|
collection: 'comments',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -717,8 +1035,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { status: 1 }, _: 'resolve' },
|
||||||
result: { f1: { status: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -744,7 +1061,10 @@ describe('workflow > instructions > manual', () => {
|
|||||||
forms: {
|
forms: {
|
||||||
f1: {
|
f1: {
|
||||||
type: 'create',
|
type: 'create',
|
||||||
actions: [JOB_STATUS.RESOLVED, JOB_STATUS.PENDING],
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
{ status: JOB_STATUS.PENDING, key: 'pending' },
|
||||||
|
],
|
||||||
collection: 'comments',
|
collection: 'comments',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -764,8 +1084,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.PENDING,
|
result: { f1: { status: 1 }, _: 'pending' },
|
||||||
result: { f1: { status: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -784,8 +1103,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res2 = await userAgents[0].resource('users_jobs').submit({
|
const res2 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { status: 1 }, _: 'resolve' },
|
||||||
result: { f1: { status: 1 } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -811,7 +1129,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
forms: {
|
forms: {
|
||||||
f1: {
|
f1: {
|
||||||
type: 'update',
|
type: 'update',
|
||||||
actions: [JOB_STATUS.RESOLVED],
|
actions: [
|
||||||
|
{ status: JOB_STATUS.RESOLVED, key: 'resolve' },
|
||||||
|
],
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -831,8 +1151,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
const res1 = await userAgents[0].resource('users_jobs').submit({
|
const res1 = await userAgents[0].resource('users_jobs').submit({
|
||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
result: { f1: { title: 't2' }, _: 'resolve' },
|
||||||
result: { f1: { title: 't2' } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
|
@ -30,15 +30,17 @@ export async function submit(context: Context, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { forms = {} } = userJob.node.config;
|
const { forms = {} } = userJob.node.config;
|
||||||
const [formKey] = Object.keys(values.result ?? {});
|
const [formKey] = Object.keys(values.result ?? {}).filter(key => key !== '_');
|
||||||
|
const actionKey = values.result?._;
|
||||||
|
|
||||||
|
const actionItem = forms[formKey]?.actions?.find((item) => item.key === actionKey);
|
||||||
// NOTE: validate status
|
// NOTE: validate status
|
||||||
if (
|
if (
|
||||||
userJob.status !== JOB_STATUS.PENDING ||
|
userJob.status !== JOB_STATUS.PENDING ||
|
||||||
userJob.job.status !== JOB_STATUS.PENDING ||
|
userJob.job.status !== JOB_STATUS.PENDING ||
|
||||||
userJob.execution.status !== EXECUTION_STATUS.STARTED ||
|
userJob.execution.status !== EXECUTION_STATUS.STARTED ||
|
||||||
!userJob.workflow.enabled ||
|
!userJob.workflow.enabled ||
|
||||||
!forms[formKey]?.actions?.includes(values.status)
|
!actionKey || actionItem?.status == null
|
||||||
) {
|
) {
|
||||||
return context.throw(400);
|
return context.throw(400);
|
||||||
}
|
}
|
||||||
@ -52,10 +54,17 @@ export async function submit(context: Context, next) {
|
|||||||
if (!assignees.includes(currentUser.id) || userJob.userId !== currentUser.id) {
|
if (!assignees.includes(currentUser.id) || userJob.userId !== currentUser.id) {
|
||||||
return context.throw(403);
|
return context.throw(403);
|
||||||
}
|
}
|
||||||
|
const presetValues = processor.getParsedValue(actionItem.values ?? {}, null, {
|
||||||
|
currentUser: currentUser.toJSON(),
|
||||||
|
currentRecord: values.result[formKey],
|
||||||
|
currentTime: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
userJob.set({
|
userJob.set({
|
||||||
status: values.status,
|
status: actionItem.status,
|
||||||
result: values.status ? values.result : Object.assign(userJob.result ?? {}, values.result),
|
result: actionItem.status > JOB_STATUS.PENDING
|
||||||
|
? { [formKey]: Object.assign(values.result[formKey], presetValues), _: actionKey }
|
||||||
|
: Object.assign(userJob.result ?? {}, values.result),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handler = instruction.formTypes.get(forms[formKey].type);
|
const handler = instruction.formTypes.get(forms[formKey].type);
|
||||||
@ -72,8 +81,11 @@ export async function submit(context: Context, next) {
|
|||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
|
userJob.job.execution = userJob.execution;
|
||||||
userJob.job.latestUserJob = userJob;
|
userJob.job.latestUserJob = userJob;
|
||||||
|
|
||||||
// NOTE: resume the process and no `await` for quick returning
|
// NOTE: resume the process and no `await` for quick returning
|
||||||
|
processor.logger.info(`manual node (${userJob.nodeId}) action trigger execution (${userJob.execution.id}) to resume`);
|
||||||
|
|
||||||
plugin.resume(userJob.job);
|
plugin.resume(userJob.job);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ export default async function (this: ManualInstruction, instance, { collection }
|
|||||||
throw new Error(`collection ${collection} for create data on manual node not found`);
|
throw new Error(`collection ${collection} for create data on manual node not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [values] = Object.values(instance.result);
|
const { _, ...form } = instance.result;
|
||||||
|
const [values] = Object.values(form);
|
||||||
await repo.create({
|
await repo.create({
|
||||||
values: {
|
values: {
|
||||||
...((values as { [key: string]: any }) ?? {}),
|
...((values as { [key: string]: any }) ?? {}),
|
||||||
|
@ -7,11 +7,12 @@ export default async function (this: ManualInstruction, instance, { collection,
|
|||||||
throw new Error(`collection ${collection} for update data on manual node not found`);
|
throw new Error(`collection ${collection} for update data on manual node not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [values] = Object.values(instance.result as { [formKey: string]: { [key: string]: any } });
|
const { _, ...form } = instance.result;
|
||||||
|
const [values] = Object.values(form);
|
||||||
await repo.update({
|
await repo.update({
|
||||||
filter: processor.getParsedValue(filter),
|
filter: processor.getParsedValue(filter),
|
||||||
values: {
|
values: {
|
||||||
...(values ?? {}),
|
...((values as { [key: string]: any }) ?? {}),
|
||||||
updatedBy: instance.userId,
|
updatedBy: instance.userId,
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Migration } from '@nocobase/server';
|
||||||
|
|
||||||
|
function findSchema(root, filter, onlyLeaf = false) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter(root) && (!onlyLeaf || !root.properties)) {
|
||||||
|
result.push(root);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.properties) {
|
||||||
|
Object.keys(root.properties).forEach((key) => {
|
||||||
|
result.push(...findSchema(root.properties[key], filter));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateConfig(config): object {
|
||||||
|
const { forms = {}, schema = {} } = config;
|
||||||
|
const root = { properties: schema };
|
||||||
|
Object.keys(forms).forEach((key) => {
|
||||||
|
const form = forms[key];
|
||||||
|
const formSchema = findSchema(root, (item) => item.name === key);
|
||||||
|
const actions = findSchema(formSchema[0], (item) => item['x-component'] === 'Action');
|
||||||
|
form.actions = actions.map((action) => {
|
||||||
|
action['x-designer'] = 'ManualActionDesigner';
|
||||||
|
action['x-action-settings'] = {};
|
||||||
|
delete action['x-action'];
|
||||||
|
return {
|
||||||
|
status: action['x-decorator-props'].value,
|
||||||
|
values: {},
|
||||||
|
key: action.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class extends Migration {
|
||||||
|
async up() {
|
||||||
|
const match = await this.app.version.satisfies('<0.11.0-alpha.2');
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { db } = this.context;
|
||||||
|
const NodeRepo = db.getRepository('flow_nodes');
|
||||||
|
await db.sequelize.transaction(async (transaction) => {
|
||||||
|
const nodes = await NodeRepo.find({
|
||||||
|
filter: {
|
||||||
|
type: 'manual',
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
console.log('%d nodes need to be migrated.', nodes.length);
|
||||||
|
|
||||||
|
await nodes.reduce(
|
||||||
|
(promise, node) =>
|
||||||
|
promise.then(() => {
|
||||||
|
node.set('config', {
|
||||||
|
...migrateConfig(node.config),
|
||||||
|
});
|
||||||
|
node.changed('config', true);
|
||||||
|
return node.save({
|
||||||
|
silent: true,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
Promise.resolve(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user