fix(plugin-workflow): fix node form values when closed (#2978)

* fix(plugin-workflow): fix node form values when closed

* fix(plugin-workflow): fix manual schema config refresh
This commit is contained in:
Junyi 2023-11-06 23:50:25 +08:00 committed by GitHub
parent 10a1f65284
commit 2747e7640f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 228 additions and 198 deletions

View File

@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css';
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { Input } from 'antd'; import { Input } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import sanitizeHTML from 'sanitize-html'; import sanitizeHTML from 'sanitize-html';
import { error } from '@nocobase/utils/client'; import { error } from '@nocobase/utils/client';
@ -228,26 +228,30 @@ export function TextArea(props) {
} }
const sel = window.getSelection?.(); const sel = window.getSelection?.();
if (sel) { if (sel) {
const children = Array.from(current.childNodes) as HTMLElement[]; try {
if (children.length) { const children = Array.from(current.childNodes) as HTMLElement[];
if (range[0] === -1) { if (children.length) {
if (range[1]) { if (range[0] === -1) {
nextRange.setStartAfter(children[range[1] - 1]); if (range[1]) {
nextRange.setStartAfter(children[range[1] - 1]);
}
} else {
nextRange.setStart(children[range[0]], range[1]);
} }
} else { if (range[2] === -1) {
nextRange.setStart(children[range[0]], range[1]); if (range[3]) {
} nextRange.setEndAfter(children[range[3] - 1]);
if (range[2] === -1) { }
if (range[3]) { } else {
nextRange.setEndAfter(children[range[3] - 1]); nextRange.setEnd(children[range[2]], range[3]);
} }
} else {
nextRange.setEnd(children[range[2]], range[3]);
} }
nextRange.collapse(true);
sel.removeAllRanges();
sel.addRange(nextRange);
} catch (ex) {
// console.error(ex);
} }
nextRange.collapse(true);
sel.removeAllRanges();
sel.addRange(nextRange);
} }
} else { } else {
const { lastChild } = current; const { lastChild } = current;
@ -262,39 +266,45 @@ export function TextArea(props) {
} }
}, [html]); }, [html]);
function onInsert(paths: string[]) { const onInsert = useCallback(
const variable: string[] = paths.filter((key) => Boolean(key.trim())); function (paths: string[]) {
const { current } = inputRef; const variable: string[] = paths.filter((key) => Boolean(key.trim()));
if (!current || !variable) { const { current } = inputRef;
return; if (!current || !variable) {
} return;
}
current.focus(); current.focus();
const content = createVariableTagHTML(variable.join('.'), keyLabelMap); const content = createVariableTagHTML(variable.join('.'), keyLabelMap);
pasteHTML(current, content, { pasteHTML(current, content, {
range, range,
}); });
setChanged(true); setChanged(true);
setRange(getCurrentRange(current)); setRange(getCurrentRange(current));
onChange(getValue(current)); onChange(getValue(current));
} },
[keyLabelMap, onChange, range],
);
function onInput({ currentTarget }) { const onInput = useCallback(
if (ime) { function ({ currentTarget }) {
return; if (ime) {
} return;
setChanged(true); }
setChanged(true);
setRange(getCurrentRange(currentTarget));
onChange(getValue(currentTarget));
},
[ime, onChange],
);
const onBlur = useCallback(function ({ currentTarget }) {
setRange(getCurrentRange(currentTarget)); setRange(getCurrentRange(currentTarget));
onChange(getValue(currentTarget)); }, []);
}
function onBlur({ currentTarget }) { const onKeyDown = useCallback(function (ev) {
setRange(getCurrentRange(currentTarget));
}
function onKeyDown(ev) {
if (ev.key === 'Enter') { if (ev.key === 'Enter') {
ev.preventDefault(); ev.preventDefault();
} }
@ -305,37 +315,40 @@ export function TextArea(props) {
// if (ev.key === 'Alt') { // if (ev.key === 'Alt') {
// console.debug(getCurrentRange(ev.currentTarget)); // console.debug(getCurrentRange(ev.currentTarget));
// } // }
} }, []);
function onPaste(ev) { const onPaste = useCallback(
ev.preventDefault(); function (ev) {
const input = ev.clipboardData.getData('text/html') || ev.clipboardData.getData('text'); ev.preventDefault();
const sanitizedHTML = sanitizeHTML(input, { const input = ev.clipboardData.getData('text/html') || ev.clipboardData.getData('text');
allowedTags: ['span'], const sanitizedHTML = sanitizeHTML(input, {
allowedAttributes: { allowedTags: ['span'],
span: ['data-variable', 'contenteditable'], allowedAttributes: {
}, span: ['data-variable', 'contenteditable'],
allowedClasses: {
span: ['ant-tag', 'ant-tag-*'],
},
transformTags: {
span(tagName, attribs) {
return attribs['data-variable']
? {
tagName: tagName,
attribs,
}
: {};
}, },
}, allowedClasses: {
}).replace(/\n/g, ' '); span: ['ant-tag', 'ant-tag-*'],
// ev.clipboardData.setData('text/html', sanitizedHTML); },
// console.log(input, sanitizedHTML); transformTags: {
setChanged(true); span(tagName, attribs) {
pasteHTML(ev.currentTarget, sanitizedHTML); return attribs['data-variable']
setRange(getCurrentRange(ev.currentTarget)); ? {
onChange(getValue(ev.currentTarget)); tagName: tagName,
} attribs,
}
: {};
},
},
}).replace(/\n/g, ' ');
// ev.clipboardData.setData('text/html', sanitizedHTML);
// console.log(input, sanitizedHTML);
setChanged(true);
pasteHTML(ev.currentTarget, sanitizedHTML);
setRange(getCurrentRange(ev.currentTarget));
onChange(getValue(ev.currentTarget));
},
[onChange],
);
const disabled = props.disabled || form.disabled; const disabled = props.disabled || form.disabled;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useContext, useMemo, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
import { import {
ActionContextProvider, ActionContextProvider,
FormProvider,
SchemaComponent, SchemaComponent,
SchemaInitializerItemOptions, SchemaInitializerItemOptions,
css, css,
@ -273,6 +274,10 @@ export function JobButton() {
); );
} }
function useNodeFormProps() {
return { form: useForm() };
}
export function NodeDefaultView(props) { export function NodeDefaultView(props) {
const { data, children } = props; const { data, children } = props;
const compile = useCompile(); const compile = useCompile();
@ -292,15 +297,14 @@ export function NodeDefaultView(props) {
const values = cloneDeep(data.config); const values = cloneDeep(data.config);
return createForm({ return createForm({
initialValues: values, initialValues: values,
values,
disabled: workflow.executed, disabled: workflow.executed,
}); });
}, [data, workflow]); }, [data, workflow]);
const resetForm = useCallback( const resetForm = useCallback(
(changed) => { (editing) => {
setFormValueChanged(changed); setEditingConfig(editing);
if (!changed) { if (!editing) {
form.reset(); form.reset();
} }
}, },
@ -366,123 +370,129 @@ export function NodeDefaultView(props) {
<ActionContextProvider <ActionContextProvider
value={{ value={{
visible: editingConfig, visible: editingConfig,
setVisible: setEditingConfig, setVisible: resetForm,
formValueChanged, formValueChanged,
setFormValueChanged: resetForm, setFormValueChanged,
}} }}
> >
<SchemaComponent <FormProvider form={form}>
scope={instruction.scope} <SchemaComponent
components={instruction.components} scope={{
schema={{ ...instruction.scope,
type: 'void', useNodeFormProps,
properties: { }}
...(instruction.view ? { view: instruction.view } : {}), components={instruction.components}
button: { schema={{
type: 'void', type: 'void',
'x-content': detailText, properties: {
'x-component': Button, ...(instruction.view ? { view: instruction.view } : {}),
'x-component-props': { button: {
type: 'link', type: 'void',
className: 'workflow-node-config-button', 'x-content': detailText,
}, 'x-component': Button,
}, 'x-component-props': {
[`${instruction.type}_${data.id}`]: { type: 'link',
type: 'void', className: 'workflow-node-config-button',
title: (
<div
className={css`
display: flex;
justify-content: space-between;
strong {
font-weight: bold;
}
.ant-tag {
margin-inline-end: 0;
}
code {
font-weight: normal;
}
`}
>
<strong>{data.title}</strong>
<Tooltip title={lang('Variable key of node')}>
<Tag>
<code>{data.key}</code>
</Tag>
</Tooltip>
</div>
),
'x-decorator': 'FormV2',
'x-decorator-props': {
form,
},
'x-component': 'Action.Drawer',
properties: {
...(instruction.description
? {
description: {
type: 'void',
'x-component': DrawerDescription,
'x-component-props': {
label: lang('Node type'),
title: instruction.title,
description: instruction.description,
},
},
}
: {}),
fieldset: {
type: 'void',
'x-component': 'fieldset',
'x-component-props': {
className: css`
.ant-input,
.ant-select,
.ant-cascader-picker,
.ant-picker,
.ant-input-number,
.ant-input-affix-wrapper {
&.auto-width {
width: auto;
min-width: 6em;
}
}
`,
},
properties: instruction.fieldset,
}, },
actions: workflow.executed },
? null [`${instruction.type}_${data.id}`]: {
: { type: 'void',
type: 'void', title: (
'x-component': 'Action.Drawer.Footer', <div
properties: { className={css`
cancel: { display: flex;
title: '{{t("Cancel")}}', justify-content: space-between;
'x-component': 'Action',
strong {
font-weight: bold;
}
.ant-tag {
margin-inline-end: 0;
}
code {
font-weight: normal;
}
`}
>
<strong>{data.title}</strong>
<Tooltip title={lang('Variable key of node')}>
<Tag>
<code>{data.key}</code>
</Tag>
</Tooltip>
</div>
),
'x-decorator': 'FormV2',
'x-decorator-props': {
// form,
useProps: '{{ useNodeFormProps }}',
},
'x-component': 'Action.Drawer',
properties: {
...(instruction.description
? {
description: {
type: 'void',
'x-component': DrawerDescription,
'x-component-props': { 'x-component-props': {
useAction: '{{ cm.useCancelAction }}', label: lang('Node type'),
title: instruction.title,
description: instruction.description,
}, },
}, },
submit: { }
title: '{{t("Submit")}}', : {}),
'x-component': 'Action', fieldset: {
'x-component-props': { type: 'void',
type: 'primary', 'x-component': 'fieldset',
useAction: useUpdateAction, 'x-component-props': {
className: css`
.ant-input,
.ant-select,
.ant-cascader-picker,
.ant-picker,
.ant-input-number,
.ant-input-affix-wrapper {
&.auto-width {
width: auto;
min-width: 6em;
}
}
`,
},
properties: instruction.fieldset,
},
actions: workflow.executed
? null
: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: useUpdateAction,
},
}, },
}, },
}, },
}, },
}, } as ISchema,
} as ISchema, },
}, }}
}} />
/> </FormProvider>
</ActionContextProvider> </ActionContextProvider>
</div> </div>
{children} {children}

View File

@ -1,8 +1,8 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { FormProvider, ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { Alert, Button, Modal, Space } from 'antd'; import { Alert, Button, Modal, Space } from 'antd';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@ -450,22 +450,27 @@ export function SchemaConfig({ value, onChange }) {
[], [],
); );
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 ( return (
<SchemaComponentContext.Provider <SchemaComponentContext.Provider
value={{ value={{
...ctx, ...ctx,
designable: !workflow.executed, designable: !workflow.executed,
refresh() { 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);
},
}} }}
> >
<SchemaInitializerProvider <SchemaInitializerProvider
@ -520,7 +525,9 @@ export function SchemaConfigButton(props) {
<Button type="primary" onClick={() => setVisible(true)}> <Button type="primary" onClick={() => setVisible(true)}>
{workflow.executed ? lang('View user interface') : lang('Configure user interface')} {workflow.executed ? lang('View user interface') : lang('Configure user interface')}
</Button> </Button>
<ActionContextProvider value={{ visible, setVisible }}>{props.children}</ActionContextProvider> <ActionContextProvider value={{ visible, setVisible, formValueChanged: false }}>
{props.children}
</ActionContextProvider>
</> </>
); );
} }