refactor(plugin-workflow): change single form to custom form block (#1707)
* refactor(plugin-workflow): change single form to custom form block * fix(plugin-workflow): fix migration * fix(plugin-workflow): fix manual node initializer * test(plugin-workflow): migrate test cases * refactor(plugin-workflow): split custom form code * fix(plugin-workflow): fix types * refactor(plugin-workflow): change manual form config data structure * fix(plugin-workflow): fix no options manual node variable * fix(plugin-workflow): fix migration script
This commit is contained in:
parent
05fd419729
commit
a5b4684a48
@ -115,6 +115,7 @@ export default {
|
|||||||
"Submit": "提交",
|
"Submit": "提交",
|
||||||
"Close": "关闭",
|
"Close": "关闭",
|
||||||
"Set the data scope": "设置数据范围",
|
"Set the data scope": "设置数据范围",
|
||||||
|
"Block": "区块",
|
||||||
"Data blocks": "数据区块",
|
"Data blocks": "数据区块",
|
||||||
"Filter blocks": "筛选区块",
|
"Filter blocks": "筛选区块",
|
||||||
"Table": "表格",
|
"Table": "表格",
|
||||||
|
@ -24,6 +24,9 @@ export function CollectionBlockInitializer({ insert, collection, dataSource, ...
|
|||||||
// title: props.title
|
// title: props.title
|
||||||
},
|
},
|
||||||
'x-designer': 'SimpleDesigner',
|
'x-designer': 'SimpleDesigner',
|
||||||
|
'x-designer-props': {
|
||||||
|
type: 'record'
|
||||||
|
},
|
||||||
properties: {
|
properties: {
|
||||||
grid: {
|
grid: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
@ -135,6 +135,8 @@ export default {
|
|||||||
'Any pass': '任意通过',
|
'Any pass': '任意通过',
|
||||||
'Anyone pass': '任何一人通过即通过',
|
'Anyone pass': '任何一人通过即通过',
|
||||||
'Field name existed in form': '表单中已有对应标识的字段',
|
'Field name existed in form': '表单中已有对应标识的字段',
|
||||||
|
'Custom form': '自定义表单',
|
||||||
|
'Data record': '数据记录',
|
||||||
|
|
||||||
'Create record': '新增数据',
|
'Create record': '新增数据',
|
||||||
'Update record': '更新数据',
|
'Update record': '更新数据',
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import React, { useState, useContext } from 'react';
|
import React, { useState, useContext } from 'react';
|
||||||
import { Field } from '@formily/core';
|
|
||||||
import { useForm, Schema } from '@formily/react';
|
import { useForm, ISchema, Schema, useFieldSchema } from '@formily/react';
|
||||||
import { ArrayTable } from '@formily/antd';
|
import { get } from 'lodash';
|
||||||
import { cloneDeep, get, set } from 'lodash';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CollectionProvider,
|
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentContext,
|
SchemaComponentContext,
|
||||||
SchemaInitializer,
|
SchemaInitializer,
|
||||||
@ -15,18 +12,17 @@ import {
|
|||||||
SchemaInitializerProvider,
|
SchemaInitializerProvider,
|
||||||
useSchemaComponentContext,
|
useSchemaComponentContext,
|
||||||
gridRowColWrap,
|
gridRowColWrap,
|
||||||
useCollectionManager,
|
|
||||||
ActionContext,
|
ActionContext,
|
||||||
CollectionContext,
|
|
||||||
GeneralSchemaDesigner,
|
GeneralSchemaDesigner,
|
||||||
SchemaSettings
|
SchemaSettings,
|
||||||
|
useCompile
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { merge, uid } from '@nocobase/utils/client';
|
|
||||||
import { useTrigger } from '../../triggers';
|
import { useTrigger } from '../../triggers';
|
||||||
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
|
import { instructions, useAvailableUpstreams, useNodeContext } from '..';
|
||||||
import { useFlowContext } from '../../FlowContext';
|
import { useFlowContext } from '../../FlowContext';
|
||||||
import { lang, NAMESPACE } from '../../locale';
|
import { lang, NAMESPACE } from '../../locale';
|
||||||
import { JOB_STATUS } from '../../constants';
|
import { JOB_STATUS } from '../../constants';
|
||||||
|
import customForm from './forms/customForm';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -36,10 +32,17 @@ function useTriggerInitializers(): SchemaInitializerItemOptions | null {
|
|||||||
return trigger.useInitializers? trigger.useInitializers(workflow.config) : null;
|
return trigger.useInitializers? trigger.useInitializers(workflow.config) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const blockTypeNames = {
|
||||||
|
'customForm': customForm.title,
|
||||||
|
'record': `{{t("Data record", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
};
|
||||||
|
|
||||||
function SimpleDesigner() {
|
function SimpleDesigner() {
|
||||||
const { t } = useTranslation();
|
const schema = useFieldSchema();
|
||||||
|
const title = blockTypeNames[schema['x-designer-props']?.type] ?? '{{t("Block")}}';
|
||||||
|
const compile = useCompile();
|
||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner title={t('Form')}>
|
<GeneralSchemaDesigner title={compile(title)}>
|
||||||
<SchemaSettings.BlockTitleItem />
|
<SchemaSettings.BlockTitleItem />
|
||||||
<SchemaSettings.Divider />
|
<SchemaSettings.Divider />
|
||||||
<SchemaSettings.Remove
|
<SchemaSettings.Remove
|
||||||
@ -52,28 +55,6 @@ function SimpleDesigner() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormBlockInitializer({ insert, ...props }) {
|
|
||||||
return (
|
|
||||||
<SchemaInitializer.Item
|
|
||||||
{...props}
|
|
||||||
onClick={() => {
|
|
||||||
insert({
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'CardItem',
|
|
||||||
'x-designer': 'SimpleDesigner',
|
|
||||||
properties: {
|
|
||||||
grid: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Grid',
|
|
||||||
'x-initializer': 'AddFormField',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AddBlockButton(props: any) {
|
function AddBlockButton(props: any) {
|
||||||
const current = useNodeContext();
|
const current = useNodeContext();
|
||||||
const nodes = useAvailableUpstreams(current);
|
const nodes = useAvailableUpstreams(current);
|
||||||
@ -102,12 +83,19 @@ function AddBlockButton(props: any) {
|
|||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: '{{t("Form")}}',
|
title: '{{t("Form")}}',
|
||||||
children: [
|
children: [
|
||||||
{
|
customForm.config.initializer,
|
||||||
key: 'form',
|
// {
|
||||||
type: 'item',
|
// key: 'createForm',
|
||||||
title: '{{t("Form")}}',
|
// type: 'item',
|
||||||
component: FormBlockInitializer
|
// title: '{{t("Create record form")}}',
|
||||||
},
|
// component: CustomFormBlockInitializer,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'updateForm',
|
||||||
|
// type: 'item',
|
||||||
|
// title: '{{t("Update record form")}}',
|
||||||
|
// component: CustomFormBlockInitializer,
|
||||||
|
// }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -133,214 +121,24 @@ function AddBlockButton(props: any) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupLabels = {
|
function findSchema(schema, filter, onlyLeaf = false) {
|
||||||
basic: '{{t("Basic")}}',
|
const result = [];
|
||||||
choices: '{{t("Choices")}}',
|
|
||||||
media: '{{t("Media")}}',
|
|
||||||
datetime: '{{t("Date & Time")}}',
|
|
||||||
relation: '{{t("Relation")}}',
|
|
||||||
advanced: '{{t("Advanced type")}}',
|
|
||||||
systemInfo: '{{t("System info")}}',
|
|
||||||
others: '{{t("Others")}}',
|
|
||||||
};
|
|
||||||
|
|
||||||
function getOptions(interfaces) {
|
if (!schema) {
|
||||||
const fields = {};
|
return result;
|
||||||
|
|
||||||
Object.keys(interfaces).forEach((type) => {
|
|
||||||
const schema = interfaces[type];
|
|
||||||
const { group = 'others' } = schema;
|
|
||||||
fields[group] = fields[group] || {};
|
|
||||||
set(fields, [group, type], schema);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.keys(GroupLabels)
|
|
||||||
.filter(groupName => ['basic', 'choices', 'datetime', 'media'].includes(groupName))
|
|
||||||
.map((groupName) => ({
|
|
||||||
title: GroupLabels[groupName],
|
|
||||||
children: Object.keys(fields[groupName] || {})
|
|
||||||
.map((type) => {
|
|
||||||
const field = fields[groupName][type];
|
|
||||||
return {
|
|
||||||
value: type,
|
|
||||||
title: field.title,
|
|
||||||
name: type,
|
|
||||||
...fields[groupName][type],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.order - b.order),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function useCommonInterfaceInitializers(): SchemaInitializerItemOptions[] {
|
|
||||||
const { interfaces } = useCollectionManager();
|
|
||||||
const options = getOptions(interfaces);
|
|
||||||
|
|
||||||
return options.map(group => ({
|
|
||||||
key: group.title,
|
|
||||||
type: 'itemGroup',
|
|
||||||
title: group.title,
|
|
||||||
children: group.children.map(item => ({
|
|
||||||
key: item.name,
|
|
||||||
type: 'item',
|
|
||||||
title: item.title,
|
|
||||||
component: FormFieldInitializer,
|
|
||||||
fieldInterface: item.name,
|
|
||||||
}))
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddFormFieldButtonContext = React.createContext<any>({});
|
|
||||||
|
|
||||||
function AddFormField(props) {
|
|
||||||
const { insertPosition = 'beforeEnd', component } = props;
|
|
||||||
const items = useCommonInterfaceInitializers();
|
|
||||||
const collection = useContext(CollectionContext);
|
|
||||||
const [interfaceOptions, setInterface] = useState<any>(null);
|
|
||||||
const [insert, setCallback] = useState<any>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AddFormFieldButtonContext.Provider value={{
|
|
||||||
onAddField(item) {
|
|
||||||
const { properties: { unique, type, ...properties }, ...options } = cloneDeep(item);
|
|
||||||
delete properties.name['x-disabled'];
|
|
||||||
setInterface({
|
|
||||||
...options,
|
|
||||||
properties
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setCallback
|
|
||||||
}}>
|
|
||||||
<SchemaInitializer.Button
|
|
||||||
wrap={gridRowColWrap}
|
|
||||||
insertPosition={insertPosition}
|
|
||||||
items={items}
|
|
||||||
component={component}
|
|
||||||
title="{{t('Configure fields')}}"
|
|
||||||
/>
|
|
||||||
<ActionContext.Provider value={{ visible: Boolean(interfaceOptions) }}>
|
|
||||||
{interfaceOptions
|
|
||||||
? (
|
|
||||||
<SchemaComponent
|
|
||||||
schema={{
|
|
||||||
type: 'void',
|
|
||||||
name: 'drawer',
|
|
||||||
title: '{{t("Configure field")}}',
|
|
||||||
'x-decorator': 'Form',
|
|
||||||
'x-component': 'Action.Drawer',
|
|
||||||
properties: {
|
|
||||||
...interfaceOptions.properties,
|
|
||||||
footer: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Action.Drawer.Footer',
|
|
||||||
properties: {
|
|
||||||
cancel: {
|
|
||||||
type: 'void',
|
|
||||||
title: '{{t("Cancel")}}',
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-component-props': {
|
|
||||||
useAction() {
|
|
||||||
const form = useForm();
|
|
||||||
return {
|
|
||||||
async run() {
|
|
||||||
setCallback(null);
|
|
||||||
setInterface(null);
|
|
||||||
form.reset();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submit: {
|
|
||||||
type: 'void',
|
|
||||||
title: '{{t("Submit")}}',
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-component-props': {
|
|
||||||
type: 'primary',
|
|
||||||
useAction() {
|
|
||||||
const { values, query } = useForm();
|
|
||||||
return {
|
|
||||||
async run() {
|
|
||||||
const { default: options } = interfaceOptions;
|
|
||||||
const defaultName = uid();
|
|
||||||
options.name = values.name ?? defaultName;
|
|
||||||
options.uiSchema.title = values.uiSchema?.title ?? defaultName;
|
|
||||||
options.interface = interfaceOptions.name;
|
|
||||||
const existed = collection.fields?.find(item => item.name === options.name);
|
|
||||||
if (existed) {
|
|
||||||
const field = query('name').take() as Field;
|
|
||||||
field.setFeedback({
|
|
||||||
type: 'error',
|
|
||||||
// code: 'FormulaError',
|
|
||||||
messages: [lang('Field name existed in form')],
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
collection.fields?.push(merge(options, values) as any);
|
|
||||||
insert({
|
|
||||||
name: options.name,
|
|
||||||
type: options.uiSchema.type,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'CollectionField',
|
|
||||||
'x-component-props': {},
|
|
||||||
'x-collection-field': `${collection.name}.${options.name}`,
|
|
||||||
'x-designer': 'FormItem.Designer',
|
|
||||||
});
|
|
||||||
setCallback(null);
|
|
||||||
setInterface(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
ArrayTable
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</ActionContext.Provider>
|
|
||||||
</AddFormFieldButtonContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormFieldInitializer(props) {
|
|
||||||
const { item, insert } = props;
|
|
||||||
const { onAddField, setCallback } = useContext(AddFormFieldButtonContext);
|
|
||||||
const { getInterface } = useCollectionManager();
|
|
||||||
|
|
||||||
const interfaceOptions = getInterface(item.fieldInterface);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SchemaInitializer.Item
|
|
||||||
key={item.fieldInterface}
|
|
||||||
onClick={() => {
|
|
||||||
setCallback(() => insert);
|
|
||||||
onAddField(interfaceOptions);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function findFormFields(formSchema, fields) {
|
|
||||||
if (!formSchema) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formSchema.properties) {
|
if (filter(schema) && (!onlyLeaf || !schema.properties)) {
|
||||||
if (formSchema['x-component'] === 'CollectionField') {
|
result.push(schema);
|
||||||
fields.push(formSchema);
|
return result;
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(formSchema.properties).forEach(key => {
|
if (schema.properties) {
|
||||||
findFormFields(formSchema.properties[key], fields);
|
Object.keys(schema.properties).forEach(key => {
|
||||||
});
|
result.push(...findSchema(schema.properties[key], filter));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SchemaComponentRefreshProvider(props) {
|
function SchemaComponentRefreshProvider(props) {
|
||||||
@ -424,7 +222,12 @@ function AddActionButton(props) {
|
|||||||
|
|
||||||
// NOTE: fake useAction for ui configuration
|
// NOTE: fake useAction for ui configuration
|
||||||
function useSubmit() {
|
function useSubmit() {
|
||||||
return { run() {} }
|
// const { values, submit, id: formId } = useForm();
|
||||||
|
// const formSchema = useFieldSchema();
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFlowRecordFromBlock() {
|
function useFlowRecordFromBlock() {
|
||||||
@ -439,11 +242,6 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
|
|
||||||
const { collection = {
|
|
||||||
name: uid(),
|
|
||||||
fields: []
|
|
||||||
}, blocks, actions } = value ?? {};
|
|
||||||
|
|
||||||
const nodeInitializers = {};
|
const nodeInitializers = {};
|
||||||
const nodeComponents = {};
|
const nodeComponents = {};
|
||||||
nodes.forEach(item => {
|
nodes.forEach(item => {
|
||||||
@ -471,7 +269,7 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
'x-initializer-props': {
|
'x-initializer-props': {
|
||||||
gridInitializer: 'AddBlockButton'
|
gridInitializer: 'AddBlockButton'
|
||||||
},
|
},
|
||||||
properties: blocks ?? {
|
properties: value ?? {
|
||||||
tab1: {
|
tab1: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: `{{t("Manual", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Manual", { ns: "${NAMESPACE}" })}}`,
|
||||||
@ -487,39 +285,6 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Action.Drawer.Footer',
|
|
||||||
'x-component-props': {
|
|
||||||
style: {
|
|
||||||
background: '#fff'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
actions: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ActionBar',
|
|
||||||
'x-initializer': 'AddActionButton',
|
|
||||||
properties: actions ?? {
|
|
||||||
resolve: {
|
|
||||||
type: 'void',
|
|
||||||
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'ManualActionStatusProvider',
|
|
||||||
'x-decorator-props': {
|
|
||||||
value: JOB_STATUS.RESOLVED
|
|
||||||
},
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-component-props': {
|
|
||||||
type: 'primary',
|
|
||||||
useAction: '{{ useSubmit }}',
|
|
||||||
},
|
|
||||||
'x-designer': 'Action.Designer',
|
|
||||||
'x-action': `${JOB_STATUS.RESOLVED}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -528,47 +293,58 @@ export function SchemaConfig({ value, onChange }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponentContext.Provider value={{ ...ctx, designable: !workflow.executed }}>
|
<SchemaComponentContext.Provider value={{ ...ctx, designable: !workflow.executed }}>
|
||||||
<SchemaInitializerProvider initializers={{ AddBlockButton, AddFormField, AddActionButton, ...trigger.initializers, ...nodeInitializers }}>
|
<SchemaInitializerProvider
|
||||||
|
initializers={{
|
||||||
|
AddBlockButton,
|
||||||
|
AddActionButton,
|
||||||
|
...trigger.initializers,
|
||||||
|
...nodeInitializers,
|
||||||
|
...customForm.config.initializers,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SchemaComponentRefreshProvider
|
<SchemaComponentRefreshProvider
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
const { tabs, footer } = get(schema.toJSON(), 'properties.drawer.properties') as any;
|
const forms = {};
|
||||||
const fields: any[] = [];
|
const { tabs } = get(schema.toJSON(), 'properties.drawer.properties') as { tabs: ISchema };
|
||||||
findFormFields(tabs, fields);
|
const formBlocks: any[] = findSchema(tabs, item => item['x-decorator'] === 'FormCollectionProvider');
|
||||||
|
formBlocks.forEach(formBlock => {
|
||||||
for(let i = collection.fields.length - 1; i >= 0; i--) {
|
const [formKey] = Object.keys(formBlock.properties);
|
||||||
if (!fields.find(field => field.name === collection.fields[i].name)) {
|
const formSchema = formBlock.properties[formKey];
|
||||||
collection.fields.splice(i, 1);
|
const fields = findSchema(formSchema.properties.grid, item => item['x-component'] === 'CollectionField', true);
|
||||||
}
|
formBlock['x-decorator-props'].collection.fields = fields.map(field => field['x-interface-options']);
|
||||||
}
|
forms[formKey] = {
|
||||||
|
type: 'custom',
|
||||||
const actionKeys = (Object.values(footer.properties.actions.properties ?? {}) as any[])
|
title: formBlock['x-component-props']?.title || formKey,
|
||||||
.reduce((actions: number[], { ['x-action']: status }) => actions.concat(Number.parseInt(status, 10)), []);
|
actions: findSchema(formSchema.properties.actions, item => item['x-component'] === 'Action')
|
||||||
form.setValuesIn('actions', actionKeys);
|
.map(item => item['x-decorator-props'].value),
|
||||||
|
collection: formBlock['x-decorator-props'].collection
|
||||||
onChange({
|
};
|
||||||
collection,
|
|
||||||
blocks: tabs.properties,
|
|
||||||
actions: footer.properties.actions.properties
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
form.setValuesIn('forms', forms);
|
||||||
|
|
||||||
|
onChange(tabs.properties);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CollectionProvider collection={collection}>
|
<SchemaComponent
|
||||||
<SchemaComponent
|
schema={schema}
|
||||||
schema={schema}
|
components={{
|
||||||
components={{
|
...nodeComponents,
|
||||||
...nodeComponents,
|
...customForm.config.components,
|
||||||
// NOTE: fake provider component
|
// NOTE: fake provider component
|
||||||
ManualActionStatusProvider(props) {
|
ManualActionStatusProvider(props) {
|
||||||
return props.children;
|
return props.children;
|
||||||
},
|
},
|
||||||
SimpleDesigner
|
ActionBarProvider(props) {
|
||||||
}}
|
return props.children;
|
||||||
scope={{
|
},
|
||||||
useSubmit,
|
SimpleDesigner,
|
||||||
useFlowRecordFromBlock
|
}}
|
||||||
}}
|
scope={{
|
||||||
/>
|
useSubmit,
|
||||||
</CollectionProvider>
|
useFlowRecordFromBlock
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</SchemaComponentRefreshProvider>
|
</SchemaComponentRefreshProvider>
|
||||||
</SchemaInitializerProvider>
|
</SchemaInitializerProvider>
|
||||||
</SchemaComponentContext.Provider>
|
</SchemaComponentContext.Provider>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React, { useContext, createContext, useMemo, useEffect, useState } from "react";
|
import React, { useContext, createContext, useEffect, useState } from "react";
|
||||||
import { createForm } from '@formily/core';
|
|
||||||
import { observer, useForm, useField, useFieldSchema } from '@formily/react';
|
import { observer, useForm, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
import parse from 'json-templates';
|
import parse from 'json-templates';
|
||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import { CollectionManagerProvider, CollectionProvider, SchemaComponent, SchemaComponentContext, SchemaComponentOptions, TableBlockProvider, useActionContext, useAPIClient, useCollectionManager, useCurrentUserContext, useRecord, useRequest, useTableBlockContext } from "@nocobase/client";
|
import { CollectionManagerProvider, SchemaComponent, SchemaComponentContext, SchemaComponentOptions, TableBlockProvider, useActionContext, useAPIClient, useCollectionManager, useCurrentUserContext, useRecord, useRequest, useTableBlockContext } from "@nocobase/client";
|
||||||
import { uid } from "@nocobase/utils/client";
|
import { uid } from "@nocobase/utils/client";
|
||||||
|
|
||||||
import { JobStatusOptions, JobStatusOptionsMap, JOB_STATUS } from "../../constants";
|
import { JobStatusOptions, JobStatusOptionsMap, JOB_STATUS } from "../../constants";
|
||||||
@ -14,6 +13,7 @@ import { NAMESPACE } from "../../locale";
|
|||||||
import { FlowContext, useFlowContext } from "../../FlowContext";
|
import { FlowContext, useFlowContext } from "../../FlowContext";
|
||||||
import { instructions, useAvailableUpstreams } from '..';
|
import { instructions, useAvailableUpstreams } from '..';
|
||||||
import { linkNodes } from "../../utils";
|
import { linkNodes } from "../../utils";
|
||||||
|
import customForm from "./forms/customForm";
|
||||||
|
|
||||||
const nodeCollection = {
|
const nodeCollection = {
|
||||||
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
|
||||||
@ -327,35 +327,70 @@ export function WorkflowTodo() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ManualActionStatusContext = createContext<number | null>(null);
|
function ActionBarProvider(props) {
|
||||||
|
// * status is done:
|
||||||
|
// 1. form is this form: show action button, and emphasis used status button
|
||||||
|
// 2. form is not this form: hide action bar
|
||||||
|
// * status is not done:
|
||||||
|
// 1. current user: show action bar
|
||||||
|
// 2. not current user: disabled action bar
|
||||||
|
|
||||||
function useManualActionStatusContext() {
|
const { data: user } = useCurrentUserContext();
|
||||||
return useContext(ManualActionStatusContext);
|
const { status, result, userId } = useRecord();
|
||||||
|
const buttonSchema = useFieldSchema();
|
||||||
|
const { name } = buttonSchema.parent.toJSON();
|
||||||
|
|
||||||
|
let { children: content } = props;
|
||||||
|
if (status) {
|
||||||
|
if (!result[name]) {
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (user?.data?.id !== userId) {
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ManualActionStatusContext = createContext<number | null>(null);
|
||||||
|
|
||||||
function ManualActionStatusProvider({ value, children }) {
|
function ManualActionStatusProvider({ value, children }) {
|
||||||
|
const { status } = useRecord();
|
||||||
|
const button = useField();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.visible = status === value;
|
||||||
|
}
|
||||||
|
}, [status, value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManualActionStatusContext.Provider value={value}>
|
<ManualActionStatusContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</ManualActionStatusContext.Provider>
|
</ManualActionStatusContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSubmit() {
|
function useSubmit() {
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
const { values, submit } = useForm();
|
const { values, submit } = useForm();
|
||||||
const nextStatus = useManualActionStatusContext();
|
const buttonSchema = useFieldSchema();
|
||||||
|
const nextStatus = useContext(ManualActionStatusContext);
|
||||||
const { service } = useTableBlockContext();
|
const { service } = useTableBlockContext();
|
||||||
const { id } = useRecord();
|
const { id } = useRecord();
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
await submit();
|
await submit();
|
||||||
|
const { name } = buttonSchema.parent.parent.toJSON();
|
||||||
await api.resource('users_jobs').submit({
|
await api.resource('users_jobs').submit({
|
||||||
filterByTk: id,
|
filterByTk: id,
|
||||||
values: {
|
values: {
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
result: values
|
result: { [name]: values }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
@ -422,24 +457,12 @@ function FlowContextProvider(props) {
|
|||||||
|
|
||||||
WorkflowTodo.Drawer = function () {
|
WorkflowTodo.Drawer = function () {
|
||||||
const ctx = useContext(SchemaComponentContext);
|
const ctx = useContext(SchemaComponentContext);
|
||||||
const { id, node, workflow, status, result, updatedAt, userId } = useRecord();
|
const { id, node, workflow, status, updatedAt } = useRecord();
|
||||||
const { data: user } = useCurrentUserContext();
|
|
||||||
|
|
||||||
const disabled = Boolean(status) || user?.data?.id !== userId;
|
const { schema } = node.config ?? {};
|
||||||
const form = useMemo(() => createForm({
|
|
||||||
readPretty: disabled,
|
|
||||||
disabled,
|
|
||||||
initialValues: result
|
|
||||||
}), [result]);
|
|
||||||
|
|
||||||
const { blocks, collection, actions } = node.config.schema ?? {};
|
|
||||||
const availableActions = Object.keys(actions).reduce((buttons, key) => ({ ...buttons, [key]: {
|
|
||||||
...actions[key],
|
|
||||||
'x-disabled': disabled
|
|
||||||
} }), {});
|
|
||||||
|
|
||||||
const statusOption = JobStatusOptionsMap[status];
|
const statusOption = JobStatusOptionsMap[status];
|
||||||
const actionSchema = status
|
const footerSchema = status
|
||||||
? {
|
? {
|
||||||
date: {
|
date: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -461,49 +484,46 @@ WorkflowTodo.Drawer = function () {
|
|||||||
'x-content': statusOption.label
|
'x-content': statusOption.label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: availableActions;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
|
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
|
||||||
<CollectionProvider collection={collection}>
|
<SchemaComponent
|
||||||
<SchemaComponent
|
components={{
|
||||||
components={{
|
Tag,
|
||||||
Tag,
|
ActionBarProvider,
|
||||||
ManualActionStatusProvider,
|
ManualActionStatusProvider,
|
||||||
FlowContextProvider
|
FlowContextProvider,
|
||||||
}}
|
...customForm.block.components
|
||||||
schema={{
|
}}
|
||||||
type: 'void',
|
schema={{
|
||||||
name: `drawer-${id}-${status}`,
|
type: 'void',
|
||||||
'x-decorator': 'Form',
|
name: `drawer-${id}-${status}`,
|
||||||
'x-decorator-props': {
|
'x-component': 'Action.Drawer',
|
||||||
form,
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
title: `${workflow.title} - ${node.title ?? `#${node.id}`}`,
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FlowContextProvider',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
properties: schema,
|
||||||
},
|
},
|
||||||
'x-component': 'Action.Drawer',
|
footer: {
|
||||||
'x-component-props': {
|
type: 'void',
|
||||||
className: 'nb-action-popup',
|
'x-component': 'Action.Drawer.Footer',
|
||||||
},
|
properties: footerSchema
|
||||||
title: `${workflow.title} - ${node.title ?? `#${node.id}`}`,
|
|
||||||
properties: {
|
|
||||||
tabs: {
|
|
||||||
type: 'void',
|
|
||||||
'x-decorator': 'FlowContextProvider',
|
|
||||||
'x-component': 'Tabs',
|
|
||||||
properties: blocks,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Action.Drawer.Footer',
|
|
||||||
properties: actionSchema
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
scope={{
|
}}
|
||||||
useSubmit,
|
scope={{
|
||||||
useFlowRecordFromBlock
|
useSubmit,
|
||||||
}}
|
useFlowRecordFromBlock,
|
||||||
/>
|
...customForm.block.scope,
|
||||||
</CollectionProvider>
|
}}
|
||||||
|
/>
|
||||||
</SchemaComponentContext.Provider>
|
</SchemaComponentContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,343 @@
|
|||||||
|
import React, { useState, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { cloneDeep, set } from 'lodash';
|
||||||
|
import { Field, createForm } from '@formily/core';
|
||||||
|
import { useForm, useFieldSchema } from '@formily/react';
|
||||||
|
import { ArrayTable } from '@formily/antd';
|
||||||
|
|
||||||
|
import { ActionContext, CollectionContext, CollectionProvider, gridRowColWrap, SchemaComponent, SchemaInitializer, SchemaInitializerItemOptions, useCollectionManager, useCurrentUserContext, useRecord } from '@nocobase/client';
|
||||||
|
import { merge, uid } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
import { JOB_STATUS } from '../../../constants';
|
||||||
|
import { lang, NAMESPACE } from '../../../locale';
|
||||||
|
|
||||||
|
const FormCollectionContext = React.createContext<any>(null);
|
||||||
|
|
||||||
|
function FormCollectionProvider(props) {
|
||||||
|
const [fields, setCollectionFields] = useState(props.collection?.fields ?? []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormCollectionContext.Provider value={{ setCollectionFields }}>
|
||||||
|
<CollectionProvider
|
||||||
|
collection={{
|
||||||
|
...props.collection,
|
||||||
|
fields
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</CollectionProvider>
|
||||||
|
</FormCollectionContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomFormBlockInitializer({ insert, ...props }) {
|
||||||
|
return (
|
||||||
|
<SchemaInitializer.Item
|
||||||
|
{...props}
|
||||||
|
onClick={() => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormCollectionProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: {
|
||||||
|
name: uid(),
|
||||||
|
fields: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-component-props': {
|
||||||
|
title: '{{t("Form")}}',
|
||||||
|
},
|
||||||
|
'x-designer': 'SimpleDesigner',
|
||||||
|
'x-designer-props': {
|
||||||
|
type: 'customForm'
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {
|
||||||
|
// disabled / read-pretty / initialValues
|
||||||
|
useProps: '{{ useFormBlockProps }}'
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'AddCustomFormField',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'ActionBarProvider',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
layout: 'one-column',
|
||||||
|
style: {
|
||||||
|
marginTop: '1.5em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-initializer': 'AddActionButton',
|
||||||
|
properties: {
|
||||||
|
resolve: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'ManualActionStatusProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
value: JOB_STATUS.RESOLVED
|
||||||
|
},
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useSubmit }}',
|
||||||
|
},
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-action': `${JOB_STATUS.RESOLVED}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupLabels = {
|
||||||
|
basic: '{{t("Basic")}}',
|
||||||
|
choices: '{{t("Choices")}}',
|
||||||
|
media: '{{t("Media")}}',
|
||||||
|
datetime: '{{t("Date & Time")}}',
|
||||||
|
relation: '{{t("Relation")}}',
|
||||||
|
advanced: '{{t("Advanced type")}}',
|
||||||
|
systemInfo: '{{t("System info")}}',
|
||||||
|
others: '{{t("Others")}}',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getOptions(interfaces) {
|
||||||
|
const fields = {};
|
||||||
|
|
||||||
|
Object.keys(interfaces).forEach((type) => {
|
||||||
|
const schema = interfaces[type];
|
||||||
|
const { group = 'others' } = schema;
|
||||||
|
fields[group] = fields[group] || {};
|
||||||
|
set(fields, [group, type], schema);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(GroupLabels)
|
||||||
|
.filter(groupName => ['basic', 'choices', 'datetime', 'media'].includes(groupName))
|
||||||
|
.map((groupName) => ({
|
||||||
|
title: GroupLabels[groupName],
|
||||||
|
children: Object.keys(fields[groupName] || {})
|
||||||
|
.map((type) => {
|
||||||
|
const field = fields[groupName][type];
|
||||||
|
return {
|
||||||
|
value: type,
|
||||||
|
title: field.title,
|
||||||
|
name: type,
|
||||||
|
...fields[groupName][type],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.order - b.order),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCommonInterfaceInitializers(): SchemaInitializerItemOptions[] {
|
||||||
|
const { interfaces } = useCollectionManager();
|
||||||
|
const options = getOptions(interfaces);
|
||||||
|
|
||||||
|
return options.map(group => ({
|
||||||
|
key: group.title,
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: group.title,
|
||||||
|
children: group.children.map(item => ({
|
||||||
|
key: item.name,
|
||||||
|
type: 'item',
|
||||||
|
title: item.title,
|
||||||
|
component: CustomFormFieldInitializer,
|
||||||
|
fieldInterface: item.name,
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddCustomFormFieldButtonContext = React.createContext<any>({});
|
||||||
|
|
||||||
|
function AddCustomFormField(props) {
|
||||||
|
const { insertPosition = 'beforeEnd', component } = props;
|
||||||
|
const items = useCommonInterfaceInitializers();
|
||||||
|
const collection = useContext(CollectionContext);
|
||||||
|
const [interfaceOptions, setInterface] = useState<any>(null);
|
||||||
|
const [insert, setCallback] = useState<any>();
|
||||||
|
const { setCollectionFields } = useContext(FormCollectionContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddCustomFormFieldButtonContext.Provider value={{
|
||||||
|
onAddField(item) {
|
||||||
|
const { properties: { unique, type, ...properties }, ...options } = cloneDeep(item);
|
||||||
|
delete properties.name['x-disabled'];
|
||||||
|
setInterface({
|
||||||
|
...options,
|
||||||
|
properties
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCallback
|
||||||
|
}}>
|
||||||
|
<SchemaInitializer.Button
|
||||||
|
wrap={gridRowColWrap}
|
||||||
|
insertPosition={insertPosition}
|
||||||
|
items={items}
|
||||||
|
component={component}
|
||||||
|
title="{{t('Configure fields')}}"
|
||||||
|
/>
|
||||||
|
<ActionContext.Provider value={{ visible: Boolean(interfaceOptions) }}>
|
||||||
|
{interfaceOptions
|
||||||
|
? (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
name: 'drawer',
|
||||||
|
title: '{{t("Configure field")}}',
|
||||||
|
'x-decorator': 'Form',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
properties: {
|
||||||
|
...interfaceOptions.properties,
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction() {
|
||||||
|
const form = useForm();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
setCallback(null);
|
||||||
|
setInterface(null);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction() {
|
||||||
|
const { values, query } = useForm();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
const { default: options } = interfaceOptions;
|
||||||
|
const defaultName = uid();
|
||||||
|
options.name = values.name ?? defaultName;
|
||||||
|
options.uiSchema.title = values.uiSchema?.title ?? defaultName;
|
||||||
|
options.interface = interfaceOptions.name;
|
||||||
|
const existed = collection.fields?.find(item => item.name === options.name);
|
||||||
|
if (existed) {
|
||||||
|
const field = query('name').take() as Field;
|
||||||
|
field.setFeedback({
|
||||||
|
type: 'error',
|
||||||
|
// code: 'FormulaError',
|
||||||
|
messages: [lang('Field name existed in form')],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newField = merge(options, values) as any;
|
||||||
|
setCollectionFields([...collection.fields, newField]);
|
||||||
|
|
||||||
|
insert({
|
||||||
|
name: options.name,
|
||||||
|
type: options.uiSchema.type,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-interface-options': newField,
|
||||||
|
'x-collection-field': `${collection.name}.${options.name}`,
|
||||||
|
'x-designer': 'FormItem.Designer',
|
||||||
|
});
|
||||||
|
setCallback(null);
|
||||||
|
setInterface(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
ArrayTable
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</ActionContext.Provider>
|
||||||
|
</AddCustomFormFieldButtonContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomFormFieldInitializer(props) {
|
||||||
|
const { item, insert } = props;
|
||||||
|
const { onAddField, setCallback } = useContext(AddCustomFormFieldButtonContext);
|
||||||
|
const { getInterface } = useCollectionManager();
|
||||||
|
|
||||||
|
const interfaceOptions = getInterface(item.fieldInterface);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaInitializer.Item
|
||||||
|
key={item.fieldInterface}
|
||||||
|
onClick={() => {
|
||||||
|
setCallback(() => insert);
|
||||||
|
onAddField(interfaceOptions);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function useFormBlockProps() {
|
||||||
|
const { status, result, userId } = useRecord();
|
||||||
|
const { data: user } = useCurrentUserContext();
|
||||||
|
const { name } = useFieldSchema();
|
||||||
|
|
||||||
|
const pattern = Boolean(status)
|
||||||
|
? (result?.[name] ? 'readPretty' : 'disabled')
|
||||||
|
: (user?.data?.id !== userId ? 'disabled' : 'editable');
|
||||||
|
const form = useMemo(() => createForm({
|
||||||
|
pattern,
|
||||||
|
initialValues: result?.[name] ?? {}
|
||||||
|
}), [result, name]);
|
||||||
|
|
||||||
|
return { form };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: `{{t("Custom form", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
config: {
|
||||||
|
initializer: {
|
||||||
|
key: 'customForm',
|
||||||
|
type: 'item',
|
||||||
|
title: `{{t("Custom form", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
component: CustomFormBlockInitializer
|
||||||
|
},
|
||||||
|
initializers: {
|
||||||
|
AddCustomFormField
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FormCollectionProvider
|
||||||
|
}
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
scope: {
|
||||||
|
useFormBlockProps
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FormCollectionProvider: CollectionProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -70,12 +70,13 @@ export default {
|
|||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
'x-component': 'SchemaConfig',
|
'x-component': 'SchemaConfig',
|
||||||
|
default: null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
forms: {
|
||||||
type: 'array',
|
type: 'object',
|
||||||
default: [JOB_STATUS.RESOLVED]
|
default: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
@ -90,29 +91,60 @@ export default {
|
|||||||
AssigneesSelect
|
AssigneesSelect
|
||||||
},
|
},
|
||||||
getOptions(config, types) {
|
getOptions(config, types) {
|
||||||
const fields = (config.schema?.collection?.fields ?? []).map(field => ({
|
const formKeys = Object.keys(config.forms ?? {});
|
||||||
key: field.name,
|
if (!formKeys.length) {
|
||||||
value: field.name,
|
|
||||||
label: field.uiSchema.title,
|
|
||||||
title: field.uiSchema.title
|
|
||||||
}));
|
|
||||||
const filteredFields = filterTypedFields(fields, types);
|
|
||||||
return filteredFields.length ? filteredFields : null;
|
|
||||||
},
|
|
||||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
|
||||||
if (!node.config.schema?.collection?.fields?.length
|
|
||||||
|| node.config.mode
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const options = formKeys.map(formKey => {
|
||||||
type: 'item',
|
const form = config.forms[formKey];
|
||||||
title: node.title ?? `#${node.id}`,
|
|
||||||
component: CollectionBlockInitializer,
|
const fields = (form.collection?.fields ?? []).map(field => ({
|
||||||
collection: node.config.schema.collection,
|
key: field.name,
|
||||||
dataSource: `{{$jobsMapByNodeId.${node.id}}}`
|
value: field.name,
|
||||||
|
label: field.uiSchema.title,
|
||||||
|
title: field.uiSchema.title
|
||||||
|
}));
|
||||||
|
const filteredFields = filterTypedFields(fields, types);
|
||||||
|
return filteredFields.length
|
||||||
|
? {
|
||||||
|
key: formKey,
|
||||||
|
value: formKey,
|
||||||
|
label: form.title || formKey,
|
||||||
|
title: form.title || formKey,
|
||||||
|
children: filteredFields
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
return options.length ? options : null;
|
||||||
|
},
|
||||||
|
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||||
|
const formKeys = Object.keys(node.config.forms ?? {});
|
||||||
|
if (!formKeys.length || node.config.mode) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const forms = formKeys.map(formKey => {
|
||||||
|
const form = node.config.forms[formKey];
|
||||||
|
|
||||||
|
return form.collection?.fields?.length
|
||||||
|
? {
|
||||||
|
type: 'item',
|
||||||
|
title: form.title ?? formKey,
|
||||||
|
component: CollectionBlockInitializer,
|
||||||
|
collection: form.collection,
|
||||||
|
dataSource: `{{$jobsMapByNodeId.${node.id}.${formKey}}}`
|
||||||
|
} as SchemaInitializerItemOptions
|
||||||
|
: null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
return forms.length ? {
|
||||||
|
key: 'forms',
|
||||||
|
type: 'subMenu',
|
||||||
|
title: node.title,
|
||||||
|
children: forms
|
||||||
|
} : null;
|
||||||
},
|
},
|
||||||
initializers: {
|
initializers: {
|
||||||
CollectionFieldInitializers
|
CollectionFieldInitializers
|
||||||
|
@ -160,6 +160,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'item',
|
type: 'item',
|
||||||
|
key: 'triggerData',
|
||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||||
component: CollectionBlockInitializer,
|
component: CollectionBlockInitializer,
|
||||||
collection: config.collection,
|
collection: config.collection,
|
||||||
|
@ -60,7 +60,11 @@ describe('workflow > instructions > manual', () => {
|
|||||||
type: 'manual',
|
type: 'manual',
|
||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id],
|
assignees: [users[0].id],
|
||||||
actions: [JOB_STATUS.RESOLVED]
|
forms: {
|
||||||
|
f1: {
|
||||||
|
actions: [JOB_STATUS.RESOLVED]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,7 +92,8 @@ describe('workflow > instructions > manual', () => {
|
|||||||
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
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { f1: {} }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(403);
|
expect(res2.status).toBe(403);
|
||||||
@ -97,7 +102,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: usersJobs[0].id,
|
filterByTk: usersJobs[0].id,
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 1 }
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res3.status).toBe(202);
|
expect(res3.status).toBe(202);
|
||||||
@ -106,18 +111,18 @@ 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({ a: 1 });
|
expect(j2.result).toEqual({ f1: { a: 1 } });
|
||||||
|
|
||||||
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({ a: 1 });
|
expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 } });
|
||||||
|
|
||||||
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,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 2 }
|
result: { f1: { a: 2 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res4.status).toBe(400);
|
expect(res4.status).toBe(400);
|
||||||
@ -128,7 +133,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
type: 'manual',
|
type: 'manual',
|
||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
actions: [JOB_STATUS.RESOLVED],
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.RESOLVED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -147,7 +154,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
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,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 1 }
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -156,13 +163,13 @@ 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({ a: 1 });
|
expect(j2.result).toEqual({ f1: { a: 1 } });
|
||||||
|
|
||||||
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,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 2 }
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(400);
|
expect(res2.status).toBe(400);
|
||||||
@ -173,7 +180,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
type: 'manual',
|
type: 'manual',
|
||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id],
|
assignees: [users[0].id],
|
||||||
actions: [JOB_STATUS.RESOLVED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.RESOLVED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -191,7 +200,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: usersJobs[0].get('id'),
|
filterByTk: usersJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 1 }
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(202);
|
expect(res.status).toBe(202);
|
||||||
@ -202,7 +211,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({ a: 1 });
|
expect(job.result).toEqual({ f1: { a: 1 } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -213,7 +222,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: 1,
|
mode: 1,
|
||||||
actions: [JOB_STATUS.RESOLVED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.RESOLVED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -231,7 +242,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 1 }
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -252,7 +263,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { a: 2 }
|
result: { f1: { a: 2 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -272,7 +283,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: 1,
|
mode: 1,
|
||||||
actions: [JOB_STATUS.REJECTED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.REJECTED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -289,7 +302,8 @@ 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
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -310,7 +324,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
status: JOB_STATUS.REJECTED,
|
||||||
result: { a: 2 }
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(400);
|
expect(res2.status).toBe(400);
|
||||||
@ -322,7 +336,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: 1,
|
mode: 1,
|
||||||
actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -339,7 +355,8 @@ 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
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -360,7 +377,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: pendingJobs[1].get('id'),
|
filterByTk: pendingJobs[1].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.REJECTED,
|
status: JOB_STATUS.REJECTED,
|
||||||
result: { a: 2 }
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -382,7 +399,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: -1,
|
mode: -1,
|
||||||
actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -399,7 +418,8 @@ 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
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -415,7 +435,8 @@ 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
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(400);
|
expect(res2.status).toBe(400);
|
||||||
@ -427,7 +448,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: -1,
|
mode: -1,
|
||||||
actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.RESOLVED, JOB_STATUS.REJECTED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -444,7 +467,8 @@ 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
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -460,7 +484,8 @@ 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
|
status: JOB_STATUS.RESOLVED,
|
||||||
|
result: { f1: { a: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -480,7 +505,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
mode: -1,
|
mode: -1,
|
||||||
actions: [JOB_STATUS.REJECTED]
|
forms: {
|
||||||
|
f1: { actions: [JOB_STATUS.REJECTED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -497,7 +524,8 @@ 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
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
@ -513,7 +541,8 @@ 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
|
status: JOB_STATUS.REJECTED,
|
||||||
|
result: { f1: { a: 0 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res2.status).toBe(202);
|
expect(res2.status).toBe(202);
|
||||||
@ -542,7 +571,9 @@ describe('workflow > instructions > manual', () => {
|
|||||||
type: 'manual',
|
type: 'manual',
|
||||||
config: {
|
config: {
|
||||||
assignees: [users[0].id, users[1].id],
|
assignees: [users[0].id, users[1].id],
|
||||||
actions: [JOB_STATUS.RESOLVED],
|
forms: {
|
||||||
|
f1: {actions: [JOB_STATUS.RESOLVED] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -550,7 +581,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
type: 'calculation',
|
type: 'calculation',
|
||||||
config: {
|
config: {
|
||||||
engine: 'math.js',
|
engine: 'math.js',
|
||||||
expression: `{{$jobsMapByNodeId.${n1.id}.number}} + 1`
|
expression: `{{$jobsMapByNodeId.${n1.id}.f1.number}} + 1`
|
||||||
},
|
},
|
||||||
upstreamId: n1.id
|
upstreamId: n1.id
|
||||||
});
|
});
|
||||||
@ -571,7 +602,7 @@ describe('workflow > instructions > manual', () => {
|
|||||||
filterByTk: pendingJobs[0].get('id'),
|
filterByTk: pendingJobs[0].get('id'),
|
||||||
values: {
|
values: {
|
||||||
status: JOB_STATUS.RESOLVED,
|
status: JOB_STATUS.RESOLVED,
|
||||||
result: { number: 1 }
|
result: { f1: { number: 1 } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(202);
|
||||||
|
@ -30,14 +30,15 @@ export async function submit(context: Context, next) {
|
|||||||
return context.throw(404);
|
return context.throw(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { actions = [] } = instance.node.config;
|
const { forms = {} } = instance.node.config;
|
||||||
|
const [form] = Object.keys(values.result ?? {});
|
||||||
|
|
||||||
// NOTE: validate status
|
// NOTE: validate status
|
||||||
if (instance.status !== JOB_STATUS.PENDING
|
if (instance.status !== JOB_STATUS.PENDING
|
||||||
|| instance.job.status !== JOB_STATUS.PENDING
|
|| instance.job.status !== JOB_STATUS.PENDING
|
||||||
|| instance.execution.status !== EXECUTION_STATUS.STARTED
|
|| instance.execution.status !== EXECUTION_STATUS.STARTED
|
||||||
|| !instance.workflow.enabled
|
|| !instance.workflow.enabled
|
||||||
|| !actions.includes(values.status)
|
|| !forms[form]?.actions?.includes(values.status)
|
||||||
) {
|
) {
|
||||||
return context.throw(400);
|
return context.throw(400);
|
||||||
}
|
}
|
||||||
|
@ -10,18 +10,18 @@ import usersJobsCollection from './collecions/users_jobs';
|
|||||||
import { submit } from './actions';
|
import { submit } from './actions';
|
||||||
|
|
||||||
|
|
||||||
|
type FormType = {
|
||||||
|
type: 'custom' | 'create' | 'update';
|
||||||
|
actions: number[];
|
||||||
|
options: {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export interface ManualConfig {
|
export interface ManualConfig {
|
||||||
schema: {
|
schema: { [key: string]: any };
|
||||||
collection: {
|
forms: { [key: string]: FormType };
|
||||||
name: string;
|
assignees?: (number | string)[];
|
||||||
fields: any[];
|
|
||||||
};
|
|
||||||
blocks: { [key: string]: any },
|
|
||||||
actions: { [key: string]: any },
|
|
||||||
};
|
|
||||||
actions: number[];
|
|
||||||
assignees?: number[];
|
|
||||||
mode?: number;
|
mode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,257 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
|
import { Migration } from '@nocobase/server';
|
||||||
|
import { uid } from '@nocobase/utils';
|
||||||
|
|
||||||
|
|
||||||
|
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 findParent(root, node) {
|
||||||
|
return findSchema(root, item => item.properties && Object.values(item.properties).includes(node))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. all form blocks combine into one, at first form block position
|
||||||
|
// 2. add fields move to the form
|
||||||
|
// 3. remove all other form blocks except first (should remove ancestors till grid)
|
||||||
|
// 4. add 'x-interface-options' property to all fields as collection field
|
||||||
|
// 5. make collection fields decorator as `FormCollectionProvider` for form block
|
||||||
|
// 6. add form component wrapper for form grid
|
||||||
|
// 7. move action footer to form block
|
||||||
|
function migrateConfig({ schema = {}, actions = [] }: { schema: any; actions: number[] }): Object {
|
||||||
|
const { blocks, collection } = schema;
|
||||||
|
if (!blocks) {
|
||||||
|
return {
|
||||||
|
forms: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const root = { properties: blocks };
|
||||||
|
const formBlocks = findSchema(root, (item) => {
|
||||||
|
return item['x-component'] === 'CardItem'
|
||||||
|
&& item['x-designer'] === 'SimpleDesigner'
|
||||||
|
&& item.properties.grid['x-initializer'] === 'AddFormField';
|
||||||
|
});
|
||||||
|
if (!formBlocks.length) {
|
||||||
|
return {
|
||||||
|
schema: blocks,
|
||||||
|
forms: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.
|
||||||
|
formBlocks.forEach((formBlock, i) => {
|
||||||
|
const formItems = findSchema(formBlock, (item) => {
|
||||||
|
return item['x-component'] === 'CollectionField'
|
||||||
|
&& item['x-decorator'] === 'FormItem';
|
||||||
|
}, true);
|
||||||
|
// 4.
|
||||||
|
formItems.forEach(item => {
|
||||||
|
Object.assign(item, {
|
||||||
|
'x-interface-options': collection.fields.find(field => field.name === item.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// skip first form block
|
||||||
|
if (!i) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2.
|
||||||
|
Object.assign(formBlocks[0].properties.grid.properties, formBlock.properties.grid.properties);
|
||||||
|
|
||||||
|
// 3.
|
||||||
|
const col = findParent(root, formBlock);
|
||||||
|
const row = findParent(root, col);
|
||||||
|
delete row.properties[col.name];
|
||||||
|
if (!Object.keys(row.properties).length) {
|
||||||
|
const grid = findParent(root, row);
|
||||||
|
delete grid.properties[row.name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5.
|
||||||
|
const [formBlock] = formBlocks;
|
||||||
|
Object.assign(formBlock, {
|
||||||
|
'x-decorator': 'FormCollectionProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection
|
||||||
|
},
|
||||||
|
'x-component-props': {
|
||||||
|
title: '{{t("Form")}}',
|
||||||
|
},
|
||||||
|
'x-designer-props': {
|
||||||
|
type: 'customForm',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formId = uid();
|
||||||
|
// 6.
|
||||||
|
const newFormBlock = {
|
||||||
|
[formId]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useFormBlockProps }}'
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
grid: Object.assign(formBlock.properties.grid, {
|
||||||
|
'x-initializer': 'AddCustomFormField'
|
||||||
|
}),
|
||||||
|
// 7.
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'ActionBarProvider',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
layout: 'one-column',
|
||||||
|
style: {
|
||||||
|
marginTop: '1.5em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-initializer': 'AddActionButton',
|
||||||
|
properties: schema.actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
delete formBlock.properties.grid;
|
||||||
|
Object.assign(formBlock.properties, newFormBlock);
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema: blocks,
|
||||||
|
forms: {
|
||||||
|
[formId]: {
|
||||||
|
type: 'custom',
|
||||||
|
title: '{{t("Form")}}',
|
||||||
|
actions,
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateUsedConfig(config, manualForms) {
|
||||||
|
Object.keys(config).forEach(key => {
|
||||||
|
const valueType = typeof config[key];
|
||||||
|
if (valueType === 'string') {
|
||||||
|
config[key] = config[key].replace(/{{\s*\$jobsMapByNodeId\.(\d+)(\.[^}]+)?\s*}}/g, (matched, id, path) => {
|
||||||
|
if (!manualForms[id]) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
return `{{$jobsMapByNodeId.${id}.${manualForms[id]}${path || ''}}}`;
|
||||||
|
});
|
||||||
|
} else if (valueType === 'object') {
|
||||||
|
migrateUsedConfig(config[key], manualForms);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default class extends Migration {
|
||||||
|
async up() {
|
||||||
|
const match = await this.app.version.satisfies('<0.9.1-alpha.3');
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { db } = this.context;
|
||||||
|
const NodeRepo = db.getRepository('flow_nodes');
|
||||||
|
const UserJobRepo = db.getRepository('users_jobs');
|
||||||
|
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(() => {
|
||||||
|
const { forms, schema, actions, ...config } = node.config;
|
||||||
|
if (forms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return node.update({
|
||||||
|
config: {
|
||||||
|
...config,
|
||||||
|
...migrateConfig({ schema, actions })
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
silent: true,
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}), Promise.resolve());
|
||||||
|
|
||||||
|
const usersJobs = await UserJobRepo.find({
|
||||||
|
filter: {
|
||||||
|
nodeId: nodes.map(item => item.id)
|
||||||
|
},
|
||||||
|
appends: ['job', 'node'],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
// update all results
|
||||||
|
await usersJobs.reduce((promise, userJob) => promise.then(async () => {
|
||||||
|
const { result, job, node } = userJob;
|
||||||
|
const { forms } = node.config;
|
||||||
|
const [formId] = Object.keys(forms);
|
||||||
|
if (result) {
|
||||||
|
await userJob.update({
|
||||||
|
result: { [formId]: result }
|
||||||
|
}, {
|
||||||
|
silent: true,
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (job.result) {
|
||||||
|
await job.update({
|
||||||
|
result: { [formId]: result }
|
||||||
|
}, {
|
||||||
|
silent: true,
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}), Promise.resolve());
|
||||||
|
|
||||||
|
const usedNodes = await NodeRepo.find({
|
||||||
|
filter: {
|
||||||
|
type: {
|
||||||
|
$notIn: ['delay', 'parallel']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeForms = {};
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const [form] = Object.keys(node.config.forms);
|
||||||
|
if (form) {
|
||||||
|
nodeForms[node.id] = form;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await usedNodes.reduce((promise, node) => promise.then(async () => {
|
||||||
|
await node.update({
|
||||||
|
config: migrateUsedConfig(cloneDeep(node.config), nodeForms)
|
||||||
|
}, {
|
||||||
|
silent: true,
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}), Promise.resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user