Zeke Zhang b750cab942
refactor: unify tab initailizer naming (#3932)
* refactor: rename dialog to popup

* feat: add tabPaneInitializers

* refactor: rename TabPaneInitializers to popup:addTab

* refactor: rename TabPaneInitializersForCreateFormBlock to popup:addTab

* refactor: rename TabPaneInitializersForBulkEditFormBlock to popup:addTab

* chore: fix unit tests

* chore: fix e2e
2024-04-05 23:19:09 +08:00

660 lines
19 KiB
TypeScript

import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { Alert, Button, Modal, Space, message } from 'antd';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Action,
ActionContextProvider,
CompatibleSchemaInitializer,
DefaultValueProvider,
FormActiveFieldsProvider,
GeneralSchemaDesigner,
InitializerWithSwitch,
SchemaComponent,
SchemaComponentContext,
SchemaInitializerItem,
SchemaInitializerItemType,
SchemaSettingsDivider,
SchemaSettingsItem,
SchemaSettingsRemove,
VariableScopeProvider,
css,
gridRowColWrap,
useDataSourceManager,
useFormActiveFields,
useFormBlockContext,
usePlugin,
useSchemaInitializer,
useSchemaInitializerItem,
useSchemaOptionsContext,
} from '@nocobase/client';
import WorkflowPlugin, {
DetailsBlockProvider,
JOB_STATUS,
SimpleDesigner,
useAvailableUpstreams,
useFlowContext,
useNodeContext,
useTrigger,
useWorkflowVariableOptions,
} from '@nocobase/plugin-workflow/client';
import { Registry, lodash } from '@nocobase/utils/client';
import { NAMESPACE, usePluginTranslation } from '../../locale';
import { FormBlockProvider } from './FormBlockProvider';
import createRecordForm from './forms/create';
import customRecordForm from './forms/custom';
import updateRecordForm from './forms/update';
type ValueOf<T> = T[keyof T];
export type FormType = {
type: 'create' | 'update' | 'custom';
title: string;
actions: ValueOf<typeof JOB_STATUS>[];
collection:
| string
| {
name: string;
fields: any[];
[key: string]: any;
};
};
export type ManualFormType = {
title: string;
config: {
useInitializer: ({ allCollections }?: { allCollections: any[] }) => SchemaInitializerItemType;
initializers?: {
[key: string]: React.FC;
};
components?: {
[key: string]: React.FC;
};
parseFormOptions(root: ISchema): { [key: string]: FormType };
};
block: {
scope?: {
[key: string]: () => any;
};
components?: {
[key: string]: React.FC;
};
};
validate?: (config: any) => string | null;
};
export const manualFormTypes = new Registry<ManualFormType>();
manualFormTypes.register('custom', customRecordForm);
manualFormTypes.register('create', createRecordForm);
manualFormTypes.register('update', updateRecordForm);
function useTriggerInitializers(): SchemaInitializerItemType | null {
const { workflow } = useFlowContext();
const trigger = useTrigger();
return trigger.useInitializers ? trigger.useInitializers(workflow.config) : null;
}
const blockTypeNames = {
customForm: customRecordForm.title,
record: `{{t("Data record", { ns: "${NAMESPACE}" })}}`,
};
/**
* @deprecated
* use `addBlockButton` instead
*/
export const addBlockButton_deprecated = new CompatibleSchemaInitializer({
name: 'AddBlockButton',
wrap: gridRowColWrap,
title: '{{t("Add block")}}',
items: [
{
type: 'itemGroup',
name: 'dataBlocks',
title: '{{t("Data blocks")}}',
hideIfNoChildren: true,
useChildren() {
const workflowPlugin = usePlugin(WorkflowPlugin);
const current = useNodeContext();
const nodes = useAvailableUpstreams(current);
const triggerInitializers = [useTriggerInitializers()].filter(Boolean);
const nodeBlockInitializers = nodes
.map((node) => {
const instruction = workflowPlugin.instructions.get(node.type);
return instruction?.useInitializers?.(node);
})
.filter(Boolean);
const dataBlockInitializers: any = [
...triggerInitializers,
...(nodeBlockInitializers.length
? [
{
name: 'nodes',
type: 'subMenu',
title: `{{t("Node result", { ns: "workflow" })}}`,
children: nodeBlockInitializers,
},
]
: []),
].filter(Boolean);
return dataBlockInitializers;
},
},
{
type: 'itemGroup',
name: 'form',
title: '{{t("Form")}}',
useChildren() {
const dm = useDataSourceManager();
const allCollections = dm.getAllCollections();
return Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => {
const { useInitializer: getInitializer } = item.config;
return getInitializer({ allCollections });
});
},
},
{
type: 'itemGroup',
name: 'otherBlocks',
title: '{{t("Other blocks")}}',
children: [
{
name: 'markdown',
title: '{{t("Markdown")}}',
Component: 'MarkdownBlockInitializer',
},
],
},
],
});
export const addBlockButton = new CompatibleSchemaInitializer(
{
name: 'workflowManual:popup:configureUserInterface:addBlock',
wrap: gridRowColWrap,
title: '{{t("Add block")}}',
items: [
{
type: 'itemGroup',
name: 'dataBlocks',
title: '{{t("Data blocks")}}',
hideIfNoChildren: true,
useChildren() {
const workflowPlugin = usePlugin(WorkflowPlugin);
const current = useNodeContext();
const nodes = useAvailableUpstreams(current);
const triggerInitializers = [useTriggerInitializers()].filter(Boolean);
const nodeBlockInitializers = nodes
.map((node) => {
const instruction = workflowPlugin.instructions.get(node.type);
return instruction?.useInitializers?.(node);
})
.filter(Boolean);
const dataBlockInitializers: any = [
...triggerInitializers,
...(nodeBlockInitializers.length
? [
{
name: 'nodes',
type: 'subMenu',
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
children: nodeBlockInitializers,
},
]
: []),
].filter(Boolean);
return dataBlockInitializers;
},
},
{
type: 'itemGroup',
name: 'form',
title: '{{t("Form")}}',
useChildren() {
const dm = useDataSourceManager();
const allCollections = dm.getAllCollections();
return Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => {
const { useInitializer: getInitializer } = item.config;
return getInitializer({ allCollections });
});
},
},
{
type: 'itemGroup',
name: 'otherBlocks',
title: '{{t("Other blocks")}}',
children: [
{
name: 'markdown',
title: '{{t("Markdown")}}',
Component: 'MarkdownBlockInitializer',
},
],
},
],
},
addBlockButton_deprecated,
);
function AssignedFieldValues() {
const ctx = useContext(SchemaComponentContext);
const { t: coreT } = useTranslation();
const { t } = usePluginTranslation();
const fieldSchema = useFieldSchema();
const scope = useWorkflowVariableOptions();
const [open, setOpen] = useState(false);
const [initialSchema, setInitialSchema] = useState(
fieldSchema?.['x-action-settings']?.assignedValues?.schema ?? {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'assignFieldValuesForm:configureFields',
properties: {},
},
);
const [schema, setSchema] = useState<Schema>(null);
const { components } = useSchemaOptionsContext();
useEffect(() => {
setSchema(
new Schema({
properties: {
grid: initialSchema,
},
}),
);
}, [initialSchema]);
const form = useMemo(() => {
const initialValues = fieldSchema?.['x-action-settings']?.assignedValues?.values;
return createForm({
initialValues: lodash.cloneDeep(initialValues),
values: lodash.cloneDeep(initialValues),
});
}, [fieldSchema]);
const upLevelActiveFields = useFormActiveFields();
const title = coreT('Assign field values');
function onCancel() {
setOpen(false);
}
function onSubmit() {
if (!fieldSchema['x-action-settings']) {
fieldSchema['x-action-settings'] = {};
}
if (!fieldSchema['x-action-settings'].assignedValues) {
fieldSchema['x-action-settings'].assignedValues = {};
}
fieldSchema['x-action-settings'].assignedValues.schema = initialSchema;
fieldSchema['x-action-settings'].assignedValues.values = form.values;
setOpen(false);
setTimeout(() => {
ctx.refresh?.();
}, 300);
}
return (
<>
<SchemaSettingsItem title={title} onClick={() => setOpen(true)}>
{title}
</SchemaSettingsItem>
<Modal
width={'50%'}
title={title}
open={open}
onCancel={onCancel}
footer={
<Space>
<Button onClick={onCancel}>{t('Cancel')}</Button>
<Button type="primary" onClick={onSubmit}>
{t('Submit')}
</Button>
</Space>
}
>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<VariableScopeProvider scope={scope}>
<FormActiveFieldsProvider name="form" getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName}>
<FormProvider form={form}>
<FormLayout layout={'vertical'}>
<Alert
message={t('Values preset in this form will override user submitted ones when continue or reject.')}
/>
<br />
{open && schema && (
<SchemaComponentContext.Provider
value={{
...ctx,
refresh() {
setInitialSchema(lodash.get(schema.toJSON(), 'properties.grid'));
},
}}
>
<SchemaComponent schema={schema} components={components} />
</SchemaComponentContext.Provider>
)}
</FormLayout>
</FormProvider>
</FormActiveFieldsProvider>
</VariableScopeProvider>
</DefaultValueProvider>
</Modal>
</>
);
}
function ManualActionDesigner(props) {
return (
<GeneralSchemaDesigner {...props} disableInitializer>
<Action.Designer.ButtonEditor />
<AssignedFieldValues />
<SchemaSettingsDivider />
<SchemaSettingsRemove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'ActionBar',
}}
/>
</GeneralSchemaDesigner>
);
}
function ContinueInitializer() {
const itemConfig = useSchemaInitializerItem();
const { action, actionProps, ...others } = itemConfig;
const { insert } = useSchemaInitializer();
return (
<SchemaInitializerItem
{...others}
onClick={() => {
insert({
type: 'void',
title: others.title,
'x-decorator': 'ManualActionStatusProvider',
'x-decorator-props': {
value: action,
},
'x-component': 'Action',
'x-component-props': {
...actionProps,
useAction: '{{ useSubmit }}',
},
'x-designer': 'ManualActionDesigner',
'x-action-settings': {},
});
}}
/>
);
}
function ActionInitializer() {
const itemConfig = useSchemaInitializerItem();
const { action, actionProps, ...others } = itemConfig;
return (
<InitializerWithSwitch
{...others}
item={itemConfig}
schema={{
type: 'void',
title: others.title,
'x-decorator': 'ManualActionStatusProvider',
'x-decorator-props': {
value: action,
},
'x-component': 'Action',
'x-component-props': {
...actionProps,
useAction: '{{ useSubmit }}',
},
'x-designer': 'Action.Designer',
'x-action': `${action}`,
}}
type="x-action"
/>
);
}
/**
* @deprecated
* use `addActionButton` instead
*/
export const addActionButton_deprecated = new CompatibleSchemaInitializer({
name: 'AddActionButton',
title: '{{t("Configure actions")}}',
items: [
{
name: 'jobStatusResolved',
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
Component: ContinueInitializer,
action: JOB_STATUS.RESOLVED,
actionProps: {
type: 'primary',
},
},
{
name: 'jobStatusRejected',
title: `{{t("Terminate the process", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.REJECTED,
actionProps: {
danger: true,
},
},
{
name: 'jobStatusPending',
title: `{{t("Save temporarily", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.PENDING,
},
],
});
export const addActionButton = new CompatibleSchemaInitializer(
{
name: 'workflowManual:form:configureActions',
title: '{{t("Configure actions")}}',
items: [
{
name: 'jobStatusResolved',
title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`,
Component: ContinueInitializer,
action: JOB_STATUS.RESOLVED,
actionProps: {
type: 'primary',
},
},
{
name: 'jobStatusRejected',
title: `{{t("Terminate the process", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.REJECTED,
actionProps: {
danger: true,
},
},
{
name: 'jobStatusPending',
title: `{{t("Save temporarily", { ns: "${NAMESPACE}" })}}`,
Component: ActionInitializer,
action: JOB_STATUS.PENDING,
},
],
},
addActionButton_deprecated,
);
// NOTE: fake useAction for ui configuration
function useSubmit() {
// const { values, submit, id: formId } = useForm();
// const formSchema = useFieldSchema();
return {
run() {},
};
}
export function SchemaConfig({ value, onChange }) {
const workflowPlugin = usePlugin(WorkflowPlugin);
const ctx = useContext(SchemaComponentContext);
const node = useNodeContext();
const nodes = useAvailableUpstreams(node);
const form = useForm();
const { workflow } = useFlowContext();
const nodeComponents = {};
nodes.forEach((item) => {
const instruction = workflowPlugin.instructions.get(item.type);
Object.assign(nodeComponents, instruction.components);
});
const schema = useMemo(
() =>
new Schema({
properties: {
drawer: {
type: 'void',
title: `{{t("User interface", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
'x-component-props': {
className: css`
.ant-drawer-body {
background: var(--nb-box-bg);
}
`,
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'popup:addTab',
'x-initializer-props': {
gridInitializer: 'workflowManual:popup:configureUserInterface:addBlock',
},
properties: value ?? {
tab1: {
type: 'void',
title: `{{t("Manual", { ns: "${NAMESPACE}" })}}`,
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'workflowManual:popup:configureUserInterface:addBlock',
properties: {},
},
},
},
},
},
},
},
},
}),
[],
);
const refresh = useCallback(
function refresh() {
// ctx.refresh?.();
const { tabs } = lodash.get(schema.toJSON(), 'properties.drawer.properties') as { tabs: ISchema };
const forms = Array.from(manualFormTypes.getValues()).reduce(
(result, item: ManualFormType) => Object.assign(result, item.config.parseFormOptions(tabs)),
{},
);
form.setValuesIn('forms', forms);
onChange(tabs.properties);
},
[form, onChange, schema],
);
return (
<SchemaComponentContext.Provider
value={{
...ctx,
designable: !workflow.executed,
refresh,
}}
>
<SchemaComponent
schema={schema}
components={{
...nodeComponents,
// @ts-ignore
...Array.from(manualFormTypes.getValues()).reduce(
(result, item: ManualFormType) => Object.assign(result, item.config.components),
{},
),
FormBlockProvider,
DetailsBlockProvider,
// NOTE: fake provider component
ManualActionStatusProvider(props) {
return props.children;
},
ActionBarProvider(props) {
return props.children;
},
SimpleDesigner,
ManualActionDesigner,
}}
scope={{
useSubmit,
useDetailsBlockProps: useFormBlockContext,
}}
/>
</SchemaComponentContext.Provider>
);
}
function validateForms(forms: Record<string, any> = {}) {
for (const form of Object.values(forms)) {
const formType = manualFormTypes.get(form.type);
if (typeof formType.validate === 'function') {
const msg = formType.validate(form);
if (msg) {
return msg;
}
}
}
}
export function SchemaConfigButton(props) {
const { workflow } = useFlowContext();
const [visible, setVisible] = useState(false);
const { values } = useForm();
const { t } = usePluginTranslation();
const onSetVisible = useCallback(
(v) => {
if (!v) {
const msg = validateForms(values.forms);
if (msg) {
message.error({
// eslint-disable-next-line react-hooks/rules-of-hooks
title: t('Validation failed'),
content: t(msg),
});
return;
}
}
setVisible(v);
},
[values.forms],
);
return (
<>
<Button type="primary" onClick={() => setVisible(true)} disabled={false}>
{t(workflow.executed ? 'View user interface' : 'Configure user interface')}
</Button>
<ActionContextProvider value={{ visible, setVisible: onSetVisible, formValueChanged: false }}>
{props.children}
</ActionContextProvider>
</>
);
}