refactor(plugin-workflow): manual collection block migration (#2064)

* refactor(plugin-workflow): change all collection node initializer in manual config

* refactor(plugin-workflow): clean code

* fix(plugin-workflow): fix custom form field in manual config

* refactor(plugin-workflow): add migration

* fix(plugin-workflow): fix custom form block migration

* fix(plugin-workflow): fix detail read-pretty for association fields
This commit is contained in:
Junyi 2023-06-16 13:06:47 +07:00 committed by GitHub
parent 4faf64f905
commit e5bab3249a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 345 additions and 139 deletions

View File

@ -1,44 +1,71 @@
import React from 'react';
import { SchemaInitializer, useCollectionManager } from '@nocobase/client';
import {
CollectionProvider,
SchemaInitializer,
SchemaInitializerItemOptions,
useCollectionManager,
useRecordCollectionDataSourceItems,
useSchemaTemplateManager,
} from '@nocobase/client';
import { traverseSchema } from '../nodes/manual/utils';
export function CollectionBlockInitializer({ insert, collection, dataSource, ...props }) {
function InnerCollectionBlockInitializer({ insert, collection, dataSource, ...props }) {
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const { getCollection } = useCollectionManager();
const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemOptions[];
const resovledCollection = getCollection(collection);
return (
<SchemaInitializer.Item
{...props}
onClick={() => {
insert({
async function onConfirm({ item }) {
const template = item.template ? await getTemplateSchemaByMode(item) : null;
const result = {
type: 'void',
name: resovledCollection.name,
title: resovledCollection.title,
'x-decorator': 'CollectionProvider',
'x-decorator': 'DetailsBlockProvider',
'x-decorator-props': {
collection,
dataSource,
},
'x-component': 'CardItem',
'x-component-props': {
// title: props.title
title: props.title,
},
'x-designer': 'SimpleDesigner',
'x-designer-props': {
type: 'record',
},
properties: {
grid: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ useFlowRecordFromBlock }}',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{useDetailsBlockProps}}',
},
'x-read-pretty': true,
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'CollectionFieldInitializers',
'x-context-datasource': dataSource,
'x-initializer': 'ReadPrettyFormItemInitializers',
properties: {},
},
},
},
},
};
traverseSchema(result, (node) => {
if (node['x-uid']) {
delete node['x-uid'];
}
});
}}
/>
insert(result);
}
return <SchemaInitializer.Item {...props} onClick={onConfirm} items={items} />;
}
export function CollectionBlockInitializer(props) {
return (
<CollectionProvider collection={props.collection}>
<InnerCollectionBlockInitializer {...props} />
</CollectionProvider>
);
}

View File

@ -1,39 +0,0 @@
import React from 'react';
import { cloneDeep } from 'lodash';
import { SchemaInitializer, useCollection, InitializerWithSwitch, gridRowColWrap } from '@nocobase/client';
function CollectionFieldInitializer({ field, ...props }) {
const uiSchema = cloneDeep(field.uiSchema);
delete uiSchema['x-uid'];
return (
<InitializerWithSwitch
{...props}
schema={{
...uiSchema,
name: field.name,
title: uiSchema.title ?? field.name,
'x-decorator': 'FormItem',
'x-read-pretty': true,
'x-designer': 'FormItem.Designer',
'x-collection-field': field.name,
}}
type="x-collection-field"
/>
);
}
export function CollectionFieldInitializers(props) {
const { fields } = useCollection();
const items = fields
.filter((field) => !['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type) && field.uiSchema)
.map((field) => ({
key: field.name,
type: 'item',
title: field.uiSchema.title,
component: CollectionFieldInitializer,
field,
}));
return <SchemaInitializer.Button {...props} items={items} title="{{t('Configure fields')}}" wrap={gridRowColWrap} />;
}

View File

@ -4,7 +4,6 @@ import { appends, collection, values } from '../schemas/collection';
import CollectionFieldset from '../components/CollectionFieldset';
import { NAMESPACE } from '../locale';
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
import { useCollectionFieldOptions } from '../variable';
import { FieldsSelect } from '../components/FieldsSelect';
@ -63,7 +62,5 @@ export default {
dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
};
},
initializers: {
CollectionFieldInitializers,
},
initializers: {},
};

View File

@ -0,0 +1,80 @@
import React, { useRef, useMemo, useContext } from 'react';
import { createForm } from '@formily/core';
import { useField } from '@formily/react';
import {
BlockRequestContext,
CollectionProvider,
FormBlockContext,
RecordProvider,
useAPIClient,
useAssociationNames,
} from '@nocobase/client';
import { useFlowContext } from '../../FlowContext';
import { parse } from '@nocobase/utils/client';
function useFlowContextData(dataSource) {
const { execution } = useFlowContext();
const data = useMemo(
() => ({
$context: execution?.context,
$jobsMapByNodeId: (execution?.jobs ?? []).reduce(
(map, job) => Object.assign(map, { [job.nodeId]: job.result }),
{},
),
}),
[execution],
);
const result = useMemo(() => parse(dataSource)(data), [execution]);
return result;
}
export function DetailsBlockProvider(props) {
const field = useField();
const formBlockRef = useRef(null);
const { appends, updateAssociationValues } = useAssociationNames();
const values = useFlowContextData(props.dataSource);
const form = useMemo(
() =>
createForm({
values,
readPretty: true,
}),
[values],
);
const params = {
appends,
};
const service = {
loading: false,
data: {
data: values,
},
};
const api = useAPIClient();
const resource = api.resource(props.collection);
const __parent = useContext(BlockRequestContext);
return (
<CollectionProvider collection={props.collection}>
<RecordProvider record={values} parent={false}>
<BlockRequestContext.Provider value={{ block: 'form', field, service, resource, __parent }}>
<FormBlockContext.Provider
value={{
params,
form,
field,
service,
updateAssociationValues,
formBlockRef,
}}
>
{props.children}
</FormBlockContext.Provider>
</BlockRequestContext.Provider>
</RecordProvider>
</CollectionProvider>
);
}

View File

@ -35,6 +35,9 @@ function InternalFormBlockInitializer({ insert, schema, ...others }) {
useAction: '{{ useSubmit }}',
},
'x-designer': 'Action.Designer',
'x-designer-props': {
type: 'record',
},
'x-action': `${JOB_STATUS.RESOLVED}`,
},
},

View File

@ -15,6 +15,7 @@ import {
GeneralSchemaDesigner,
SchemaSettings,
useCompile,
useFormBlockContext,
} from '@nocobase/client';
import { Registry } from '@nocobase/utils/client';
@ -27,6 +28,7 @@ import customForm from './forms/custom';
import createForm from './forms/create';
import updateForm from './forms/update';
import { FormBlockProvider } from './FormBlockProvider';
import { DetailsBlockProvider } from './DetailsBlockProvider';
type ValueOf<T> = T[keyof T];
@ -229,10 +231,6 @@ function useSubmit() {
};
}
function useFlowRecordFromBlock() {
return {};
}
export function SchemaConfig({ value, onChange }) {
const ctx = useContext(SchemaComponentContext);
const trigger = useTrigger();
@ -333,6 +331,7 @@ export function SchemaConfig({ value, onChange }) {
{},
),
FormBlockProvider,
DetailsBlockProvider,
// NOTE: fake provider component
ManualActionStatusProvider(props) {
return props.children;
@ -344,7 +343,7 @@ export function SchemaConfig({ value, onChange }) {
}}
scope={{
useSubmit,
useFlowRecordFromBlock,
useDetailsBlockProps: useFormBlockContext,
}}
/>
</SchemaInitializerProvider>

View File

@ -14,7 +14,6 @@ import {
useCollectionManager,
useCurrentUserContext,
useRecord,
useRequest,
useTableBlockContext,
FormBlockContext,
useFormBlockContext,
@ -28,6 +27,7 @@ import { instructions, useAvailableUpstreams } from '..';
import { linkNodes } from '../../utils';
import { manualFormTypes } from './SchemaConfig';
import { FormBlockProvider } from './FormBlockProvider';
import { DetailsBlockProvider } from './DetailsBlockProvider';
const nodeCollection = {
title: `{{t("Task", { ns: "${NAMESPACE}" })}}`,
@ -421,23 +421,6 @@ function useSubmit() {
};
}
// parse datasource block from execution context
function useFlowRecordFromBlock(opts) {
const { ['x-context-datasource']: dataSource } = useFieldSchema();
const { execution } = useFlowContext();
const result = parse(dataSource)({
$context: execution?.context,
$jobsMapByNodeId: (execution?.jobs ?? []).reduce(
(map, job) => Object.assign(map, { [job.nodeId]: job.result }),
{},
),
});
return useRequest(() => {
return Promise.resolve({ data: result });
}, opts);
}
function FlowContextProvider(props) {
const api = useAPIClient();
const { id } = useRecord();
@ -479,6 +462,7 @@ function FlowContextProvider(props) {
<SchemaComponent
components={{
FormBlockProvider,
DetailsBlockProvider,
ActionBarProvider,
ManualActionStatusProvider,
...Array.from(manualFormTypes.getValues()).reduce(
@ -490,6 +474,7 @@ function FlowContextProvider(props) {
scope={{
useSubmit,
useFormBlockProps,
useDetailsBlockProps,
...Array.from(manualFormTypes.getValues()).reduce(
(result, item) => Object.assign(result, item.block.scope),
{},
@ -529,6 +514,11 @@ function useFormBlockProps() {
return { form };
}
function useDetailsBlockProps() {
const { form } = useFormBlockContext();
return { form };
}
function Drawer() {
const ctx = useContext(SchemaComponentContext);
const { id, node, workflow, status, updatedAt } = useRecord();
@ -585,9 +575,6 @@ function Drawer() {
},
},
}}
scope={{
useFlowRecordFromBlock,
}}
/>
</SchemaComponentContext.Provider>
);

View File

@ -1,19 +1,22 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useMemo } from 'react';
import { cloneDeep, set } from 'lodash';
import { Field } from '@formily/core';
import { useForm } from '@formily/react';
import { Field, createForm } from '@formily/core';
import { useForm, useField, useFieldSchema } from '@formily/react';
import { ArrayTable } from '@formily/antd';
import {
ActionContextProvider,
CollectionContext,
CollectionProvider,
FormBlockContext,
gridRowColWrap,
RecordProvider,
SchemaComponent,
SchemaInitializer,
SchemaInitializerItemOptions,
useCollectionManager,
useRecord,
} from '@nocobase/client';
import { merge, uid } from '@nocobase/utils/client';
@ -22,22 +25,41 @@ import { lang, NAMESPACE } from '../../../locale';
import { findSchema } from '../utils';
import { ManualFormType } from '../SchemaConfig';
const FormCollectionContext = React.createContext<any>(null);
function FormCollectionProvider(props) {
function CustomFormBlockProvider(props) {
const [fields, setCollectionFields] = useState(props.collection?.fields ?? []);
const userJob = useRecord();
const field = useField();
const fieldSchema = useFieldSchema();
const [formKey] = Object.keys(fieldSchema.toJSON().properties ?? {});
const values = userJob?.result?.[formKey];
const form = useMemo(
() =>
createForm({
initialValues: values,
}),
[values],
);
return (
<FormCollectionContext.Provider value={{ setCollectionFields }}>
<CollectionProvider
collection={{
...props.collection,
fields,
}}
>
<RecordProvider record={values} parent={false}>
<FormBlockContext.Provider
value={{
form,
field,
setCollectionFields,
}}
>
{props.children}
</FormBlockContext.Provider>
</RecordProvider>
</CollectionProvider>
</FormCollectionContext.Provider>
);
}
@ -48,7 +70,7 @@ function CustomFormBlockInitializer({ insert, ...props }) {
onClick={() => {
insert({
type: 'void',
'x-decorator': 'FormCollectionProvider',
'x-decorator': 'CustomFormBlockProvider',
'x-decorator-props': {
collection: {
name: uid(),
@ -180,7 +202,7 @@ function AddCustomFormField(props) {
const collection = useContext(CollectionContext);
const [interfaceOptions, setInterface] = useState<any>(null);
const [insert, setCallback] = useState<any>();
const { setCollectionFields } = useContext(FormCollectionContext);
const { setCollectionFields } = useContext(FormBlockContext);
return (
<AddCustomFormFieldButtonContext.Provider
@ -271,7 +293,9 @@ function AddCustomFormField(props) {
type: options.uiSchema.type,
'x-decorator': 'FormItem',
'x-component': 'CollectionField',
'x-interface-options': newField,
'x-component-props': {
field: newField,
},
'x-collection-field': `${collection.name}.${options.name}`,
'x-designer': 'FormItem.Designer',
});
@ -329,11 +353,11 @@ export default {
AddCustomFormField,
},
components: {
FormCollectionProvider,
CustomFormBlockProvider,
},
parseFormOptions(root) {
const forms = {};
const formBlocks: any[] = findSchema(root, (item) => item['x-decorator'] === 'FormCollectionProvider');
const formBlocks: any[] = findSchema(root, (item) => item['x-decorator'] === 'CustomFormBlockProvider');
formBlocks.forEach((formBlock) => {
const [formKey] = Object.keys(formBlock.properties);
const formSchema = formBlock.properties[formKey];
@ -342,7 +366,9 @@ export default {
(item) => item['x-component'] === 'CollectionField',
true,
);
formBlock['x-decorator-props'].collection.fields = fields.map((field) => field['x-interface-options']);
formBlock['x-decorator-props'].collection.fields = fields.map(
(field) => field['x-component-props']?.field ?? field['x-interface-options'],
);
forms[formKey] = {
type: 'custom',
title: formBlock['x-component-props']?.title || formKey,
@ -358,7 +384,7 @@ export default {
block: {
scope: {},
components: {
FormCollectionProvider: CollectionProvider,
CustomFormBlockProvider,
},
},
} as ManualFormType;

View File

@ -1,8 +1,7 @@
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager } from '@nocobase/client';
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
import { CollectionFieldInitializers } from '../../components/CollectionFieldInitializers';
import { filterTypedFields, useCollectionFieldOptions } from '../../variable';
import { useCollectionFieldOptions } from '../../variable';
import { NAMESPACE } from '../../locale';
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
import { ModeConfig } from './ModeConfig';
@ -149,7 +148,5 @@ export default {
}
: null;
},
initializers: {
CollectionFieldInitializers,
},
initializers: {},
};

View File

@ -3,7 +3,6 @@ import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase
import { appends, collection, filter } from '../schemas/collection';
import { NAMESPACE } from '../locale';
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
import { useCollectionFieldOptions } from '../variable';
import { FieldsSelect } from '../components/FieldsSelect';
@ -54,7 +53,7 @@ export default {
return result?.length ? result : null;
},
useInitializers(node): SchemaInitializerItemOptions | null {
if (!node.config.collection) {
if (!node.config.collection || node.config.multiple) {
return null;
}
@ -66,7 +65,5 @@ export default {
dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
};
},
initializers: {
CollectionFieldInitializers,
},
initializers: {},
};

View File

@ -3,7 +3,6 @@ import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase
import { appends, collection, filter } from '../schemas/collection';
import { useCollectionFieldOptions } from '../variable';
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
import { NAMESPACE, useWorkflowTranslation } from '../locale';
import { FieldsSelect } from '../components/FieldsSelect';
@ -168,7 +167,5 @@ export default {
dataSource: '{{$context.data}}',
};
},
initializers: {
CollectionFieldInitializers,
},
initializers: {},
};

View File

@ -3,7 +3,6 @@ import { useCollectionDataSource, SchemaInitializerItemOptions } from '@nocobase
import { ScheduleConfig } from './ScheduleConfig';
import { SCHEDULE_MODE } from './constants';
import { NAMESPACE, useWorkflowTranslation } from '../../locale';
import { CollectionFieldInitializers } from '../../components/CollectionFieldInitializers';
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
import { useCollectionFieldOptions } from '../../variable';
import { appends } from '../../schemas/collection';
@ -72,7 +71,5 @@ export default {
dataSource: '{{$context.data}}',
};
},
initializers: {
CollectionFieldInitializers,
},
initializers: {},
};

View File

@ -0,0 +1,138 @@
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 migrateSchema(schema = {}): object {
const root = { properties: schema };
const collectionBlocks = findSchema(root, (item) => {
return (
item['x-component'] === 'CardItem' &&
item['x-designer'] === 'SimpleDesigner' &&
item['x-decorator'] === 'CollectionProvider'
);
});
collectionBlocks.forEach((block) => {
const id = uid();
const { grid } = block.properties;
delete block.properties.grid;
const fields = findSchema(grid, (item) => {
return item['x-decorator'] === 'FormItem';
});
fields.forEach((field) => {
Object.assign(field, {
'x-component': 'CollectionField',
'x-collection-field': `${block['x-decorator-props'].collection}.${field['x-collection-field']}`,
});
});
Object.assign(block, {
'x-decorator': 'DetailsBlockProvider',
'x-decorator-props': {
...block['x-decorator-props'],
dataSource: grid['x-context-datasource'],
},
properties: {
[id]: {
type: 'void',
name: id,
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{useDetailsBlockProps}}',
},
properties: {
grid: {
type: 'void',
name: 'grid',
'x-component': 'Grid',
'x-initializer': 'ReadPrettyFormItemInitializers',
properties: grid.properties,
},
},
},
},
});
});
const customForms = findSchema(root, (item) => {
return item['x-decorator'] === 'FormCollectionProvider';
});
customForms.forEach((item) => {
Object.assign(item, {
'x-decorator': 'CustomFormBlockProvider',
});
});
const customFormFields = findSchema(root, (item) => {
return item['x-interface-options'];
});
customFormFields.forEach((field) => {
const options = field['x-interface-options'];
delete field['x-interface-options'];
Object.assign(field, {
'x-component-props': {
field: options,
},
});
});
return schema;
}
export default class extends Migration {
async up() {
const match = await this.app.version.satisfies('<0.9.4-alpha.3');
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(() => {
const { schema, ...config } = node.config;
node.set('config', {
...config,
schema: {
...migrateSchema(schema),
},
});
node.changed('config', true);
return node.save({
silent: true,
transaction,
});
}),
Promise.resolve(),
);
});
}
}