From e5bab3249a527bdea68f393a1ac882c337335ef5 Mon Sep 17 00:00:00 2001 From: Junyi Date: Fri, 16 Jun 2023 13:06:47 +0700 Subject: [PATCH] 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 --- .../components/CollectionBlockInitializer.tsx | 85 +++++++---- .../CollectionFieldInitializers.tsx | 39 ----- .../workflow/src/client/nodes/create.tsx | 5 +- .../nodes/manual/DetailsBlockProvider.tsx | 80 ++++++++++ .../nodes/manual/FormBlockInitializer.tsx | 3 + .../src/client/nodes/manual/SchemaConfig.tsx | 9 +- .../src/client/nodes/manual/WorkflowTodo.tsx | 29 +--- .../src/client/nodes/manual/forms/custom.tsx | 72 ++++++--- .../src/client/nodes/manual/index.tsx | 7 +- .../workflow/src/client/nodes/query.tsx | 7 +- .../src/client/triggers/collection.tsx | 5 +- .../src/client/triggers/schedule/index.tsx | 5 +- .../20230612021134-manual-collection-block.ts | 138 ++++++++++++++++++ 13 files changed, 345 insertions(+), 139 deletions(-) delete mode 100644 packages/plugins/workflow/src/client/components/CollectionFieldInitializers.tsx create mode 100644 packages/plugins/workflow/src/client/nodes/manual/DetailsBlockProvider.tsx create mode 100644 packages/plugins/workflow/src/server/migrations/20230612021134-manual-collection-block.ts diff --git a/packages/plugins/workflow/src/client/components/CollectionBlockInitializer.tsx b/packages/plugins/workflow/src/client/components/CollectionBlockInitializer.tsx index acdbcf860..620f5a6c4 100644 --- a/packages/plugins/workflow/src/client/components/CollectionBlockInitializer.tsx +++ b/packages/plugins/workflow/src/client/components/CollectionBlockInitializer.tsx @@ -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 ( - { - 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': 'DetailsBlockProvider', + 'x-decorator-props': { + collection, + dataSource, + }, + 'x-component': 'CardItem', + 'x-component-props': { + title: props.title, + }, + 'x-designer': 'SimpleDesigner', + properties: { + grid: { type: 'void', - name: resovledCollection.name, - title: resovledCollection.title, - 'x-decorator': 'CollectionProvider', - 'x-decorator-props': { - collection, - }, - 'x-component': 'CardItem', + 'x-component': 'FormV2', 'x-component-props': { - // title: props.title - }, - 'x-designer': 'SimpleDesigner', - 'x-designer-props': { - type: 'record', + useProps: '{{useDetailsBlockProps}}', }, + 'x-read-pretty': true, properties: { - grid: { + grid: template || { type: 'void', - 'x-decorator': 'Form', - 'x-decorator-props': { - useValues: '{{ useFlowRecordFromBlock }}', - }, '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 ; +} + +export function CollectionBlockInitializer(props) { + return ( + + + ); } diff --git a/packages/plugins/workflow/src/client/components/CollectionFieldInitializers.tsx b/packages/plugins/workflow/src/client/components/CollectionFieldInitializers.tsx deleted file mode 100644 index f4cb93823..000000000 --- a/packages/plugins/workflow/src/client/components/CollectionFieldInitializers.tsx +++ /dev/null @@ -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 ( - - ); -} - -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 ; -} diff --git a/packages/plugins/workflow/src/client/nodes/create.tsx b/packages/plugins/workflow/src/client/nodes/create.tsx index 6698c3213..2d696e0b6 100644 --- a/packages/plugins/workflow/src/client/nodes/create.tsx +++ b/packages/plugins/workflow/src/client/nodes/create.tsx @@ -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: {}, }; diff --git a/packages/plugins/workflow/src/client/nodes/manual/DetailsBlockProvider.tsx b/packages/plugins/workflow/src/client/nodes/manual/DetailsBlockProvider.tsx new file mode 100644 index 000000000..fa69370de --- /dev/null +++ b/packages/plugins/workflow/src/client/nodes/manual/DetailsBlockProvider.tsx @@ -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 ( + + + + + {props.children} + + + + + ); +} diff --git a/packages/plugins/workflow/src/client/nodes/manual/FormBlockInitializer.tsx b/packages/plugins/workflow/src/client/nodes/manual/FormBlockInitializer.tsx index 851b32786..8d2a56504 100644 --- a/packages/plugins/workflow/src/client/nodes/manual/FormBlockInitializer.tsx +++ b/packages/plugins/workflow/src/client/nodes/manual/FormBlockInitializer.tsx @@ -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}`, }, }, diff --git a/packages/plugins/workflow/src/client/nodes/manual/SchemaConfig.tsx b/packages/plugins/workflow/src/client/nodes/manual/SchemaConfig.tsx index f2d84e511..07628d316 100644 --- a/packages/plugins/workflow/src/client/nodes/manual/SchemaConfig.tsx +++ b/packages/plugins/workflow/src/client/nodes/manual/SchemaConfig.tsx @@ -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[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, }} /> diff --git a/packages/plugins/workflow/src/client/nodes/manual/WorkflowTodo.tsx b/packages/plugins/workflow/src/client/nodes/manual/WorkflowTodo.tsx index fa1688c02..645e24a87 100644 --- a/packages/plugins/workflow/src/client/nodes/manual/WorkflowTodo.tsx +++ b/packages/plugins/workflow/src/client/nodes/manual/WorkflowTodo.tsx @@ -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) { 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, - }} /> ); diff --git a/packages/plugins/workflow/src/client/nodes/manual/forms/custom.tsx b/packages/plugins/workflow/src/client/nodes/manual/forms/custom.tsx index f3f60785c..d52d4779c 100644 --- a/packages/plugins/workflow/src/client/nodes/manual/forms/custom.tsx +++ b/packages/plugins/workflow/src/client/nodes/manual/forms/custom.tsx @@ -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(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 ( - - - {props.children} - - + + + + {props.children} + + + ); } @@ -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(null); const [insert, setCallback] = useState(); - const { setCollectionFields } = useContext(FormCollectionContext); + const { setCollectionFields } = useContext(FormBlockContext); return ( 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; diff --git a/packages/plugins/workflow/src/client/nodes/manual/index.tsx b/packages/plugins/workflow/src/client/nodes/manual/index.tsx index 94cedefdd..cb563e9cb 100644 --- a/packages/plugins/workflow/src/client/nodes/manual/index.tsx +++ b/packages/plugins/workflow/src/client/nodes/manual/index.tsx @@ -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: {}, }; diff --git a/packages/plugins/workflow/src/client/nodes/query.tsx b/packages/plugins/workflow/src/client/nodes/query.tsx index d3e5c264b..7ddacd372 100644 --- a/packages/plugins/workflow/src/client/nodes/query.tsx +++ b/packages/plugins/workflow/src/client/nodes/query.tsx @@ -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: {}, }; diff --git a/packages/plugins/workflow/src/client/triggers/collection.tsx b/packages/plugins/workflow/src/client/triggers/collection.tsx index 15d2ad752..02ea23b12 100644 --- a/packages/plugins/workflow/src/client/triggers/collection.tsx +++ b/packages/plugins/workflow/src/client/triggers/collection.tsx @@ -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: {}, }; diff --git a/packages/plugins/workflow/src/client/triggers/schedule/index.tsx b/packages/plugins/workflow/src/client/triggers/schedule/index.tsx index 7543ba1f2..775d4b21a 100644 --- a/packages/plugins/workflow/src/client/triggers/schedule/index.tsx +++ b/packages/plugins/workflow/src/client/triggers/schedule/index.tsx @@ -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: {}, }; diff --git a/packages/plugins/workflow/src/server/migrations/20230612021134-manual-collection-block.ts b/packages/plugins/workflow/src/server/migrations/20230612021134-manual-collection-block.ts new file mode 100644 index 000000000..14945a73e --- /dev/null +++ b/packages/plugins/workflow/src/server/migrations/20230612021134-manual-collection-block.ts @@ -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(), + ); + }); + } +}