Руководство по миграции клиентского кода с v1 на v2

Данное руководство описывает, как перенести клиентский код плагина расширения рабочего процесса с v1 на v2. Основное изменение в клиенте v2 — замена декларативных интерфейсов настройки на основе Formily Schema подходом с Loader и чистыми React/antd-компонентами.

Обзор

Основные изменения

  1. Изменение путей импорта: @nocobase/plugin-workflow/client@nocobase/plugin-workflow/client-v2, базовый класс плагина @nocobase/client@nocobase/client-v2
  2. Изменение паттерна интерфейса настройки: от объектов Formily Schema (fieldset) к React-компонентам с отложенной загрузкой через Loader (FieldsetLoader)
  3. Удалены свойства scope/components: больше нет необходимости внедрять объекты scope или компоненты в Schema; просто импортируйте и используйте их напрямую в React-компонентах

Таблица соответствия путей импорта

// v1
import { Plugin } from '@nocobase/client';
import WorkflowPlugin, { Trigger, Instruction } from '@nocobase/plugin-workflow/client';

// v2
import { Plugin } from '@nocobase/client-v2';
import WorkflowPlugin, { Trigger, Instruction } from '@nocobase/plugin-workflow/client-v2';

Общие правила

Паттерн Loader

В v2 свойства типа LoaderOf заменяют объекты Formily Schema, такие как fieldset в v1. Loader — это по сути функция, возвращающая Promise<{ default: ComponentType }>, реализующая разделение кода и отложенную загрузку через динамический import():

// v1: Formily Schema object
fieldset = {
  interval: {
    type: 'number',
    title: 'Interval',
    'x-decorator': 'FormItem',
    'x-component': 'InputNumber',
    default: 60000,
  },
};

// v2: Loader pointing to a React component
FieldsetLoader = () => import('./MyConfig');

Когда Loader должен указывать на именованный экспорт (а не экспорт по умолчанию), используйте .then() для переназначения:

FieldsetLoader = () => import('./MyConfig').then((m) => ({ default: m.MyPresetConfig }));

Синтаксис компонента настройки

Компонент, загружаемый Loader, — стандартный функциональный React-компонент, использующий Form.Item из antd для построения форм. Пути полей неизменно используют формат вложенного массива ['config', 'fieldName']:

// v1: Formily Schema
fieldset = {
  interval: {
    type: 'number',
    title: '{{t("Interval")}}',
    name: 'config.interval',
    'x-decorator': 'FormItem',
    'x-component': 'InputNumber',
    default: 60000,
  },
};

// v2: React component
import { Form, InputNumber } from 'antd';

export default function MyConfig() {
  const { t } = useWorkflowTranslation();

  return (
    <Form.Item
      name={['config', 'interval']}
      label={t('Interval')}
      initialValue={60000}
    >
      <InputNumber />
    </Form.Item>
  );
}

Миграция триггеров

Таблица соответствия свойств

Свойство v1Свойство v2Описание
fieldsetFieldsetLoaderФорма настройки триггера
presetFieldsetPresetFieldsetLoaderПредустановленная форма при создании
triggerFieldsetTriggerFieldsetLoaderФорма ввода для ручного запуска
scopeУдаленоБольше не нужно; импортируйте напрямую в компоненте
componentsУдаленоБольше не нужно; импортируйте напрямую в компоненте
viewУдалено
validate(config)Новое; валидация конфигурации
createDefaultConfig()Новое; предоставляет значения конфигурации по умолчанию

Пример миграции

Синтаксис v1:

import { Trigger } from '@nocobase/plugin-workflow/client';

class MyTrigger extends Trigger {
  title = '{{t("My Trigger")}}';
  fieldset = {
    collection: {
      type: 'string',
      title: '{{t("Collection")}}',
      'x-decorator': 'FormItem',
      'x-component': 'CollectionSelect',
      required: true,
    },
    mode: {
      type: 'number',
      title: '{{t("Mode")}}',
      'x-decorator': 'FormItem',
      'x-component': 'Select',
      'x-component-props': {
        options: [
          { label: '{{t("Created")}}', value: 1 },
          { label: '{{t("Updated")}}', value: 2 },
        ],
      },
    },
  };
  scope = { /* ... */ };
  components = { CollectionSelect };
}

Синтаксис v2:

import { Trigger } from '@nocobase/plugin-workflow/client-v2';

class MyTrigger extends Trigger {
  title = '{{t("My Trigger")}}';

  PresetFieldsetLoader = () =>
    import('./MyTriggerConfig').then((m) => ({ default: m.MyPresetConfig }));
  FieldsetLoader = () => import('./MyTriggerConfig');
  TriggerFieldsetLoader = () => import('./TriggerMyConfig');

  validate(config) {
    return Boolean(config?.collection && config?.mode);
  }
}
// MyTriggerConfig.tsx
import { Form, Select } from 'antd';
import { CollectionCascader } from '@nocobase/plugin-workflow/client-v2';

export function MyPresetConfig() {
  return (
    <Form.Item name={['config', 'collection']} label="Collection" rules={[{ required: true }]}>
      <CollectionCascader />
    </Form.Item>
  );
}

export default function MyTriggerConfig() {
  return (
    <>
      <Form.Item name={['config', 'collection']} label="Collection">
        <CollectionCascader disabled />
      </Form.Item>
      <Form.Item name={['config', 'mode']} label="Mode">
        <Select
          options={[
            { label: 'Created', value: 1 },
            { label: 'Updated', value: 2 },
          ]}
        />
      </Form.Item>
    </>
  );
}

Регистрация плагина

// v1
import { Plugin } from '@nocobase/client';
import WorkflowPlugin from '@nocobase/plugin-workflow/client';

export default class extends Plugin {
  async load() {
    const workflow = this.app.pm.get(WorkflowPlugin);
    workflow.registerTrigger('myTrigger', MyTrigger);
  }
}

// v2
import { Plugin } from '@nocobase/client-v2';

export default class extends Plugin {
  async load() {
    const workflow = this.app.pm.get('workflow');
    workflow.registerTrigger('myTrigger', MyTrigger);
  }
}

Миграция узлов

Таблица соответствия свойств

Свойство v1Свойство v2Описание
fieldsetFieldsetLoaderФорма настройки узла в панели
presetFieldsetPresetFieldsetLoaderПредустановленная форма при создании
ComponentComponentLoaderПользовательский рендеринг узла на холсте
scopeУдаленоБольше не нужно; импортируйте напрямую в компоненте
componentsУдаленоБольше не нужно; импортируйте напрямую в компоненте
viewУдалено
createDefaultConfig()Новое; предоставляет значения конфигурации по умолчанию

Пример миграции

Синтаксис v1:

import WorkflowPlugin, { Instruction } from '@nocobase/plugin-workflow/client';

class MyInstruction extends Instruction {
  title = 'Random string';
  type = 'randomString';
  group = 'extended';
  fieldset = {
    digit: {
      type: 'number',
      title: 'Digit',
      name: 'digit',
      'x-decorator': 'FormItem',
      'x-component': 'InputNumber',
      'x-component-props': { min: 1, max: 10 },
      default: 6,
    },
  };
  useVariables(node, options) {
    return { value: node.key, label: node.title };
  }
}

Синтаксис v2:

import { Instruction } from '@nocobase/plugin-workflow/client-v2';

class MyInstruction extends Instruction {
  title = 'Random string';
  type = 'randomString';
  group = 'extended';

  FieldsetLoader = () => import('./components/RandomStringConfig');

  useVariables(node, options) {
    return { value: node.key, label: node.title };
  }
}
// components/RandomStringConfig.tsx
import { Form, InputNumber } from 'antd';

export default function RandomStringConfig() {
  return (
    <Form.Item
      name={['config', 'digit']}
      label="Digit"
      initialValue={6}
      rules={[{ required: true }]}
    >
      <InputNumber min={1} max={10} />
    </Form.Item>
  );
}

Прочие замечания

Неизменённые части

Следующие свойства и методы имеют практически одинаковые сигнатуры в v1 и v2 и могут быть оставлены без изменений при миграции:

  • useVariables(node/config, options) — предоставляет варианты переменных
  • useScopeVariables(node, options) — предоставляет переменные области видимости ветви
  • isAvailable(ctx) — проверка доступности узла (в v2 NodeAvailableContext добавлено новое свойство engine)

Новые свойства в v2

  • getCreateModelMenuItem — определяет конфигурацию создания пунктов меню подмоделей для узлов/триггеров на холсте v2
  • useTempAssociationSource — предоставляет информацию о временном источнике ассоциативных данных
  • validate(config) — валидация конфигурации триггера (только для триггеров)
  • branching — объявляет, является ли узел узлом ветвления (только для узлов)
  • end — объявляет, является ли узел завершающим (только для узлов)
  • testable — объявляет, поддерживает ли узел тестовый запуск (только для узлов)

Согласованность семантики значений

При миграции убедитесь, что значения форм, генерируемые компонентами v2, согласованы с v1, особенно структура данных при ручном запуске. Например, если форма ручного запуска в v1 сохраняет полный объект записи, версия v2 должна сохранять ту же структуру значений, а не только первичный ключ.