refactor(client): refactor variable components and variables in workflow (#2157)
* refactor(plugin-workflow): change collection variables to lazy load * fix(plugin-workflow): avoid to-many reverse loading for association field * fix(client): fix variable components * chore(client): fix type * fix(client): fix current user lazy load options * refactor(client): remove compile from variable components which potencially causing bug * fix(plugin-workflow): fix scope argument for new api * fix(client): fix constant type options * fix(client): fix infinity rerendering * fix: avoid closure problem * fix(client): should use no children when lazy load * refactor(client): refactor AssignedField to use Variable component * fix(client): fix type * fix(plugin-workflow): fix variable options in some node not changes * fix(plugin-workflow): fix select variable for operand crash (T-815) * fix(plugin-workflow): variable types detect * fix(plugin-workflow): detect association to match types * fix(plugin-workflow): fix variable type filter logic * fix(plugin-workflow): fix optional types * fix(plugin-workflow): make changeOnSelect configurable in TextArea and JSONInput --------- Co-authored-by: Rairn <958414905@qq.com>
This commit is contained in:
parent
25a3a8affa
commit
c9b726916c
@ -2,19 +2,23 @@ import { CloseCircleFilled } from '@ant-design/icons';
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { error } from '@nocobase/utils/client';
|
import { error } from '@nocobase/utils/client';
|
||||||
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Tag } from 'antd';
|
import { Cascader, DatePicker, Input as AntInput, InputNumber, Select, Tag } from 'antd';
|
||||||
|
import type { DefaultOptionType } from 'antd/lib/cascader';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCompile } from '../..';
|
import { useCompile } from '../../hooks';
|
||||||
import { Option } from '../../../schema-settings/VariableInput/type';
|
|
||||||
import { XButton } from './XButton';
|
import { XButton } from './XButton';
|
||||||
|
|
||||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||||
const groupClass = css`
|
const groupClass = css`
|
||||||
width: auto;
|
width: auto;
|
||||||
display: flex !important;
|
display: flex;
|
||||||
|
&.ant-input-group-compact {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.ant-input-disabled {
|
.ant-input-disabled {
|
||||||
.ant-tag {
|
.ant-tag {
|
||||||
color: #bfbfbf;
|
color: #bfbfbf;
|
||||||
@ -115,69 +119,93 @@ const ConstantTypes = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getTypedConstantOption(type: string, types?: true | string[]) {
|
function getTypedConstantOption(type: string, types: true | string[], fieldNames) {
|
||||||
const allTypes = Object.values(ConstantTypes);
|
const allTypes = Object.values(ConstantTypes);
|
||||||
|
const children = (
|
||||||
|
types ? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true) : allTypes
|
||||||
|
).map((item) =>
|
||||||
|
Object.keys(item).reduce(
|
||||||
|
(result, key) =>
|
||||||
|
fieldNames[key] in item
|
||||||
|
? result
|
||||||
|
: Object.assign(result, {
|
||||||
|
[fieldNames[key]]: item[key],
|
||||||
|
}),
|
||||||
|
item,
|
||||||
|
),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
value: '',
|
value: '',
|
||||||
label: '{{t("Constant")}}',
|
label: '{{t("Constant")}}',
|
||||||
children: types
|
children,
|
||||||
? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true)
|
[fieldNames.value]: '',
|
||||||
: allTypes,
|
[fieldNames.label]: '{{t("Constant")}}',
|
||||||
|
[fieldNames.children]: children,
|
||||||
component: ConstantTypes[type]?.component,
|
component: ConstantTypes[type]?.component,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type VariableOptions = {
|
|
||||||
value: string;
|
|
||||||
label?: string;
|
|
||||||
children?: VariableOptions[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Input(props) {
|
export function Input(props) {
|
||||||
|
const {
|
||||||
|
value = '',
|
||||||
|
scope,
|
||||||
|
onChange,
|
||||||
|
children,
|
||||||
|
button,
|
||||||
|
useTypedConstant,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
changeOnSelect,
|
||||||
|
fieldNames,
|
||||||
|
} = props;
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
const [options, setOptions] = React.useState<DefaultOptionType[]>([]);
|
||||||
|
const [variableText, setVariableText] = React.useState('');
|
||||||
|
|
||||||
const { value = '', scope, onChange, children, button, useTypedConstant, style, className } = props;
|
|
||||||
const parsed = useMemo(() => parseValue(value), [value]);
|
const parsed = useMemo(() => parseValue(value), [value]);
|
||||||
const isConstant = typeof parsed === 'string';
|
const isConstant = typeof parsed === 'string';
|
||||||
const type = isConstant ? parsed : '';
|
const type = isConstant ? parsed : '';
|
||||||
const variable = isConstant ? null : parsed;
|
const variable = isConstant ? null : parsed;
|
||||||
|
const names = Object.assign(
|
||||||
|
{
|
||||||
|
label: 'label',
|
||||||
|
value: 'value',
|
||||||
|
children: 'children',
|
||||||
|
},
|
||||||
|
fieldNames ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
// 当 scope 是一个函数时,可能是一个 hook,所以不能使用 useMemo
|
const { component: ConstantComponent, ...constantOption }: DefaultOptionType & { component?: React.FC<any> } =
|
||||||
const variableOptions = typeof scope === 'function' ? scope() : scope ?? [];
|
|
||||||
|
|
||||||
const [variableText, setVariableText] = React.useState('');
|
|
||||||
|
|
||||||
const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC<any> } =
|
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (children) {
|
if (children) {
|
||||||
return {
|
return {
|
||||||
value: '',
|
value: '',
|
||||||
label: '{{t("Constant")}}',
|
label: t('Constant'),
|
||||||
|
[names.value]: '',
|
||||||
|
[names.label]: t('Constant'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (useTypedConstant) {
|
if (useTypedConstant) {
|
||||||
return getTypedConstantOption(type, useTypedConstant);
|
return getTypedConstantOption(type, useTypedConstant, names);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
value: '',
|
value: '',
|
||||||
label: '{{t("Null")}}',
|
label: t('Null'),
|
||||||
|
[names.value]: '',
|
||||||
|
[names.label]: t('Null'),
|
||||||
component: ConstantTypes.null.component,
|
component: ConstantTypes.null.component,
|
||||||
};
|
};
|
||||||
}, [type, useTypedConstant]);
|
}, [type, useTypedConstant]);
|
||||||
|
|
||||||
const [options, setOptions] = React.useState<Option[]>(() => {
|
|
||||||
return compile([constantOption, ...variableOptions]);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newOptions: Option[] = [constantOption, ...variableOptions];
|
setOptions([compile(constantOption), ...(scope ? cloneDeep(scope) : [])]);
|
||||||
setOptions(deepCompileLabel(newOptions, compile));
|
}, [scope]);
|
||||||
}, [variableOptions]);
|
|
||||||
|
|
||||||
const loadData = async (selectedOptions: Option[]) => {
|
const loadData = async (selectedOptions: DefaultOptionType[]) => {
|
||||||
const option = selectedOptions[selectedOptions.length - 1];
|
const option = selectedOptions[selectedOptions.length - 1];
|
||||||
if (option.loadChildren) {
|
if (!option.children && !option.isLeaf && option.loadChildren) {
|
||||||
await option.loadChildren(option);
|
await option.loadChildren(option);
|
||||||
setOptions((prev) => [...prev]);
|
setOptions((prev) => [...prev]);
|
||||||
}
|
}
|
||||||
@ -199,29 +227,29 @@ export function Input(props) {
|
|||||||
}
|
}
|
||||||
onChange(`{{${next.join('.')}}}`);
|
onChange(`{{${next.join('.')}}}`);
|
||||||
},
|
},
|
||||||
[type, variable],
|
[type, variable, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
if (!variable || options.length <= 1) {
|
if (!variable || !options.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let prevOption: Option = null;
|
let prevOption: DefaultOptionType = null;
|
||||||
const labels = [];
|
const labels = [];
|
||||||
|
|
||||||
for (let i = 0; i < variable.length; i++) {
|
for (let i = 0; i < variable.length; i++) {
|
||||||
const key = variable[i];
|
const key = variable[i];
|
||||||
try {
|
try {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
prevOption = options.find((item) => item.value === key);
|
prevOption = options.find((item) => item[names.value] === key);
|
||||||
} else {
|
} else {
|
||||||
if (prevOption.loadChildren && !prevOption.children?.length) {
|
if (prevOption.loadChildren && !prevOption.children?.length) {
|
||||||
await prevOption.loadChildren(prevOption);
|
await prevOption.loadChildren(prevOption);
|
||||||
}
|
}
|
||||||
prevOption = prevOption.children.find((item) => item.value === key);
|
prevOption = prevOption.children.find((item) => item[names.value] === key);
|
||||||
}
|
}
|
||||||
labels.push(prevOption.label);
|
labels.push(prevOption[names.label]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(err);
|
error(err);
|
||||||
}
|
}
|
||||||
@ -230,17 +258,19 @@ export function Input(props) {
|
|||||||
setVariableText(labels.join(' / '));
|
setVariableText(labels.join(' / '));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果没有这个延迟,会导致选择父节点时不展开子节点
|
run();
|
||||||
setTimeout(run);
|
// NOTE: watch `options.length` and it only happens once
|
||||||
}, [variable, options.length]);
|
}, [variable, options.length]);
|
||||||
|
|
||||||
const disabled = props.disabled || form.disabled;
|
const disabled = props.disabled || form.disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AntInput.Group compact style={style} className={classNames(className, groupClass)}>
|
<AntInput.Group compact style={style} className={classNames(groupClass, className)}>
|
||||||
{variable ? (
|
{variable ? (
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={cx(
|
||||||
|
'variable',
|
||||||
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
|
||||||
@ -263,7 +293,8 @@ export function Input(props) {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`}
|
`,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onInput={(e) => e.preventDefault()}
|
onInput={(e) => e.preventDefault()}
|
||||||
@ -297,13 +328,14 @@ export function Input(props) {
|
|||||||
) : (
|
) : (
|
||||||
children ?? <ConstantComponent value={value} onChange={onChange} />
|
children ?? <ConstantComponent value={value} onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
{options.length > 1 ? (
|
{options.length > 1 && !disabled ? (
|
||||||
<Cascader
|
<Cascader
|
||||||
options={options}
|
options={options}
|
||||||
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
||||||
onChange={onSwitch}
|
onChange={onSwitch}
|
||||||
loadData={loadData as any}
|
loadData={loadData as any}
|
||||||
changeOnSelect
|
changeOnSelect={changeOnSelect}
|
||||||
|
fieldNames={fieldNames}
|
||||||
>
|
>
|
||||||
{button ?? <XButton type={variable ? 'primary' : 'default'} />}
|
{button ?? <XButton type={variable ? 'primary' : 'default'} />}
|
||||||
</Cascader>
|
</Cascader>
|
||||||
@ -311,14 +343,3 @@ export function Input(props) {
|
|||||||
</AntInput.Group>
|
</AntInput.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepCompileLabel(list: Option[], compile) {
|
|
||||||
return list.map((item) => {
|
|
||||||
const children = item.children ? deepCompileLabel(item.children, compile) : null;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
label: compile(item.label),
|
|
||||||
children,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { VariableSelect } from './VariableSelect';
|
import { VariableSelect } from './VariableSelect';
|
||||||
@ -18,8 +19,8 @@ function setNativeInputValue(input, value) {
|
|||||||
|
|
||||||
export function JSONInput(props) {
|
export function JSONInput(props) {
|
||||||
const inputRef = useRef<any>(null);
|
const inputRef = useRef<any>(null);
|
||||||
const { scope } = props;
|
const { scope, changeOnSelect, ...others } = props;
|
||||||
const options = typeof scope === 'function' ? scope() : scope ?? [];
|
const [options, setOptions] = useState(scope ? cloneDeep(scope) : []);
|
||||||
|
|
||||||
function onInsert(selected) {
|
function onInsert(selected) {
|
||||||
if (!inputRef.current) {
|
if (!inputRef.current) {
|
||||||
@ -45,7 +46,7 @@ export function JSONInput(props) {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Input.JSON {...props} ref={inputRef} />
|
<Input.JSON {...others} ref={inputRef} />
|
||||||
<Button.Group
|
<Button.Group
|
||||||
className={css`
|
className={css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -56,7 +57,7 @@ export function JSONInput(props) {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<VariableSelect options={options} onInsert={onInsert} />
|
<VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} changeOnSelect={changeOnSelect} />
|
||||||
</Button.Group>
|
</Button.Group>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,10 +2,12 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|||||||
import { Input } from 'antd';
|
import { Input } from 'antd';
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { cx, css } from '@emotion/css';
|
import { cx, css } from '@emotion/css';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import * as sanitizeHTML from 'sanitize-html';
|
import * as sanitizeHTML from 'sanitize-html';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
import { EllipsisWithTooltip, useCompile } from '../..';
|
import { error } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
import { EllipsisWithTooltip } from '../..';
|
||||||
import { VariableSelect } from './VariableSelect';
|
import { VariableSelect } from './VariableSelect';
|
||||||
|
|
||||||
type RangeIndexes = [number, number, number, number];
|
type RangeIndexes = [number, number, number, number];
|
||||||
@ -112,9 +114,9 @@ function createOptionsValueLabelMap(options: any[]) {
|
|||||||
|
|
||||||
function createVariableTagHTML(variable, keyLabelMap) {
|
function createVariableTagHTML(variable, keyLabelMap) {
|
||||||
const labels = keyLabelMap.get(variable);
|
const labels = keyLabelMap.get(variable);
|
||||||
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-variable="${variable}">${labels?.join(
|
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-variable="${variable}">${
|
||||||
' / ',
|
labels ? labels.join(' / ') : '...'
|
||||||
)}</span>`;
|
}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [#, <>, #, #, <>]
|
// [#, <>, #, #, <>]
|
||||||
@ -185,29 +187,31 @@ function getCurrentRange(element: HTMLElement): RangeIndexes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TextArea(props) {
|
export function TextArea(props) {
|
||||||
const { value = '', scope, onChange, multiline = true } = props;
|
const { value = '', scope, onChange, multiline = true, changeOnSelect } = props;
|
||||||
const compile = useCompile();
|
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
const options = compile((typeof scope === 'function' ? scope() : scope) ?? []);
|
const [options, setOptions] = useState([]);
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
||||||
const [ime, setIME] = useState<boolean>(false);
|
const [ime, setIME] = useState<boolean>(false);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
||||||
// NOTE: e.g. [startElementIndex, startOffset, endElementIndex, endOffset]
|
// NOTE: e.g. [startElementIndex, startOffset, endElementIndex, endOffset]
|
||||||
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
||||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedVar([]);
|
preloadOptions(scope, value)
|
||||||
}, [scope]);
|
.then((preloaded) => {
|
||||||
|
setOptions(preloaded);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error);
|
||||||
|
}, [scope, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHtml(renderHTML(value ?? '', keyLabelMap));
|
setHtml(renderHTML(value ?? '', keyLabelMap));
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
setRange([-1, 0, -1, 0]);
|
setRange([-1, 0, -1, 0]);
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value, keyLabelMap]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current } = inputRef;
|
const { current } = inputRef;
|
||||||
@ -375,16 +379,51 @@ export function TextArea(props) {
|
|||||||
contentEditable={!disabled}
|
contentEditable={!disabled}
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
{!disabled ? <VariableSelect options={options} onInsert={onInsert} /> : null}
|
{!disabled ? (
|
||||||
|
<VariableSelect options={options} setOptions={setOptions} onInsert={onInsert} changeOnSelect={changeOnSelect} />
|
||||||
|
) : null}
|
||||||
</Input.Group>
|
</Input.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function preloadOptions(scope, value) {
|
||||||
|
const options = cloneDeep(scope ?? []);
|
||||||
|
for (let matcher; (matcher = VARIABLE_RE.exec(value ?? '')); ) {
|
||||||
|
const keys = matcher[1].split('.');
|
||||||
|
|
||||||
|
let prevOption = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
try {
|
||||||
|
if (i === 0) {
|
||||||
|
prevOption = options.find((item) => item.value === key);
|
||||||
|
} else {
|
||||||
|
if (prevOption.loadChildren && !prevOption.children?.length) {
|
||||||
|
await prevOption.loadChildren(prevOption);
|
||||||
|
}
|
||||||
|
prevOption = prevOption.children.find((item) => item.value === key);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
|
TextArea.ReadPretty = function ReadPretty(props): JSX.Element {
|
||||||
const { value, multiline = true, scope } = props;
|
const { value, scope } = props;
|
||||||
const compile = useCompile();
|
const [options, setOptions] = useState([]);
|
||||||
const options = compile((typeof scope === 'function' ? scope() : scope) ?? []);
|
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [options]);
|
||||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
|
||||||
|
useEffect(() => {
|
||||||
|
preloadOptions(scope, value)
|
||||||
|
.then((preloaded) => {
|
||||||
|
setOptions(preloaded);
|
||||||
|
})
|
||||||
|
.catch(error);
|
||||||
|
}, [scope, value]);
|
||||||
const html = renderHTML(value ?? '', keyLabelMap);
|
const html = renderHTML(value ?? '', keyLabelMap);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { Button, Cascader } from 'antd';
|
import { Button, Cascader } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function VariableSelect(props) {
|
export function VariableSelect({ options, setOptions, onInsert, changeOnSelect = false }): JSX.Element {
|
||||||
const { options, onInsert } = props;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
async function loadData(selectedOptions) {
|
||||||
setSelectedVar([]);
|
const option = selectedOptions[selectedOptions.length - 1];
|
||||||
}, [options]);
|
if (!option.children && !option.isLeaf && option.loadChildren) {
|
||||||
|
await option.loadChildren(option);
|
||||||
|
setOptions((prev) => [...prev]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -44,6 +47,7 @@ export function VariableSelect(props) {
|
|||||||
placeholder={t('Select a variable')}
|
placeholder={t('Select a variable')}
|
||||||
value={[]}
|
value={[]}
|
||||||
options={options}
|
options={options}
|
||||||
|
loadData={loadData}
|
||||||
onChange={(keyPaths = [], selectedOptions = []) => {
|
onChange={(keyPaths = [], selectedOptions = []) => {
|
||||||
setSelectedVar(keyPaths as string[]);
|
setSelectedVar(keyPaths as string[]);
|
||||||
if (!keyPaths.length) {
|
if (!keyPaths.length) {
|
||||||
@ -54,9 +58,9 @@ export function VariableSelect(props) {
|
|||||||
onInsert(keyPaths);
|
onInsert(keyPaths);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
changeOnSelect
|
changeOnSelect={changeOnSelect}
|
||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
if (e.detail !== 2) {
|
if (e.detail !== 2 || !changeOnSelect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let n = e.target; n && n !== e.currentTarget; n = n.parentNode) {
|
for (let n = e.target; n && n !== e.currentTarget; n = n.parentNode) {
|
||||||
@ -70,7 +74,9 @@ export function VariableSelect(props) {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
dropdownRender={(menu) => (
|
dropdownRender={
|
||||||
|
changeOnSelect
|
||||||
|
? (menu) => (
|
||||||
<>
|
<>
|
||||||
{menu}
|
{menu}
|
||||||
<div
|
<div
|
||||||
@ -83,7 +89,9 @@ export function VariableSelect(props) {
|
|||||||
{t('Double click to choose entire object')}
|
{t('Double click to choose entire object')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { connect, useField, useFieldSchema } from '@formily/react';
|
import { connect, useField, useFieldSchema } from '@formily/react';
|
||||||
import { merge } from '@formily/shared';
|
import { merge } from '@formily/shared';
|
||||||
import { Cascader, Select, Space } from 'antd';
|
import React, { useEffect, useState } from 'react';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFormBlockContext } from '../../../block-provider';
|
import { useFormBlockContext } from '../../../block-provider';
|
||||||
import {
|
import {
|
||||||
@ -11,17 +10,14 @@ import {
|
|||||||
useCollectionField,
|
useCollectionField,
|
||||||
useCollectionFilterOptions,
|
useCollectionFilterOptions,
|
||||||
} from '../../../collection-manager';
|
} from '../../../collection-manager';
|
||||||
import { useCompile, useComponent } from '../../../schema-component';
|
import { Variable, useCompile, useComponent } from '../../../schema-component';
|
||||||
import { DeletedField } from '../DeletedField';
|
import { DeletedField } from '../DeletedField';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
const DYNAMIC_RECORD_REG = /\{\{\s*currentRecord\.(.*)\s*\}\}/;
|
|
||||||
const DYNAMIC_USER_REG = /\{\{\s*currentUser\.(.*)\s*\}\}/;
|
|
||||||
const DYNAMIC_TIME_REG = /\{\{\s*currentTime\s*\}\}/;
|
|
||||||
|
|
||||||
const InternalField: React.FC = (props) => {
|
const InternalField: React.FC = (props) => {
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { name, interface: interfaceType, uiSchema } = useCollectionField();
|
const { uiSchema } = useCollectionField();
|
||||||
const component = useComponent(uiSchema?.['x-component']);
|
const component = useComponent(uiSchema?.['x-component']);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const setFieldProps = (key, value) => {
|
const setFieldProps = (key, value) => {
|
||||||
@ -87,162 +83,55 @@ export enum AssignedFieldValueType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AssignedField = (props: any) => {
|
export const AssignedField = (props: any) => {
|
||||||
|
const { value, onChange } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const collection = useCollection();
|
|
||||||
const field = useField<Field>();
|
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const isDynamicValue =
|
|
||||||
DYNAMIC_RECORD_REG.test(field.value) || DYNAMIC_USER_REG.test(field.value) || DYNAMIC_TIME_REG.test(field.value);
|
|
||||||
const initType = isDynamicValue ? AssignedFieldValueType.DynamicValue : AssignedFieldValueType.ConstantValue;
|
|
||||||
const [type, setType] = useState<string>(initType);
|
|
||||||
const initFieldType = {
|
|
||||||
[`${DYNAMIC_TIME_REG.test(field.value)}`]: 'currentTime',
|
|
||||||
[`${DYNAMIC_USER_REG.test(field.value)}`]: 'currentUser',
|
|
||||||
[`${DYNAMIC_RECORD_REG.test(field.value)}`]: 'currentRecord',
|
|
||||||
};
|
|
||||||
const [fieldType, setFieldType] = useState<string>(initFieldType['true']);
|
|
||||||
const initRecordValue = DYNAMIC_RECORD_REG.exec(field.value)?.[1]?.split('.') ?? [];
|
|
||||||
const [recordValue, setRecordValue] = useState<any>(initRecordValue);
|
|
||||||
const initUserValue = DYNAMIC_USER_REG.exec(field.value)?.[1]?.split('.') ?? [];
|
|
||||||
const [userValue, setUserValue] = useState<any>(initUserValue);
|
|
||||||
const initValue = isDynamicValue ? '' : field.value;
|
|
||||||
const [value, setValue] = useState(initValue);
|
|
||||||
const [options, setOptions] = useState<any[]>([]);
|
|
||||||
const { getField } = useCollection();
|
const { getField } = useCollection();
|
||||||
const collectionField = getField(fieldSchema.name);
|
const collectionField = getField(fieldSchema.name);
|
||||||
const fields = useCollectionFilterOptions(collection?.name);
|
const [options, setOptions] = useState<any[]>([]);
|
||||||
const userFields = useCollectionFilterOptions('users');
|
const collection = useCollection();
|
||||||
const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt'];
|
const fields = compile(useCollectionFilterOptions(collection?.name));
|
||||||
|
const userFields = compile(useCollectionFilterOptions('users'));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const opt = [
|
const opt = [
|
||||||
{
|
{
|
||||||
name: 'currentRecord',
|
name: 'currentRecord',
|
||||||
title: t('Current record'),
|
title: t('Current record'),
|
||||||
|
children: fields,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'currentUser',
|
name: 'currentUser',
|
||||||
title: t('Current user'),
|
title: t('Current user'),
|
||||||
|
children: userFields,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if (dateTimeFields.includes(collectionField?.interface)) {
|
if (['createdAt', 'datetime', 'time', 'updatedAt'].includes(collectionField?.interface)) {
|
||||||
opt.unshift({
|
opt.unshift({
|
||||||
name: 'currentTime',
|
name: 'currentTime',
|
||||||
title: t('Current time'),
|
title: t('Current time'),
|
||||||
|
children: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setOptions(compile(opt));
|
setOptions(opt);
|
||||||
}, []);
|
}, [fields, userFields]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (type === AssignedFieldValueType.ConstantValue) {
|
|
||||||
field.value = value;
|
|
||||||
} else {
|
|
||||||
if (fieldType === 'currentTime') {
|
|
||||||
field.value = '{{currentTime}}';
|
|
||||||
} else if (fieldType === 'currentUser') {
|
|
||||||
userValue?.length > 0 && (field.value = `{{currentUser.${userValue.join('.')}}}`);
|
|
||||||
} else if (fieldType === 'currentRecord') {
|
|
||||||
recordValue?.length > 0 && (field.value = `{{currentRecord.${recordValue.join('.')}}}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [type, value, fieldType, userValue, recordValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (type === AssignedFieldValueType.ConstantValue) {
|
|
||||||
setFieldType(null);
|
|
||||||
setUserValue([]);
|
|
||||||
setRecordValue([]);
|
|
||||||
}
|
|
||||||
}, [type]);
|
|
||||||
|
|
||||||
const typeChangeHandler = (val) => {
|
|
||||||
setType(val);
|
|
||||||
if (val === AssignedFieldValueType.DynamicValue) {
|
|
||||||
field.validator = null;
|
|
||||||
field.form.clearErrors();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const valueChangeHandler = (val) => {
|
|
||||||
setValue(val?.target?.value ?? val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldTypeChangeHandler = (val) => {
|
|
||||||
setFieldType(val);
|
|
||||||
};
|
|
||||||
const recordChangeHandler = (val) => {
|
|
||||||
setRecordValue(val);
|
|
||||||
};
|
|
||||||
const userChangeHandler = (val) => {
|
|
||||||
setUserValue(val);
|
|
||||||
};
|
|
||||||
const useFieldMemo = useMemo(() => {
|
|
||||||
if (!collectionField) {
|
|
||||||
return <DeletedField />;
|
|
||||||
}
|
|
||||||
if (type === AssignedFieldValueType.ConstantValue) {
|
|
||||||
return <CollectionField {...props} value={value} onChange={valueChangeHandler} style={{ minWidth: 150 }} />;
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Variable.Input
|
||||||
dropdownMatchSelectWidth={false}
|
value={value}
|
||||||
defaultValue={fieldType}
|
onChange={onChange}
|
||||||
value={fieldType}
|
scope={options}
|
||||||
style={{ minWidth: 150 }}
|
className={css`
|
||||||
onChange={fieldTypeChangeHandler}
|
.variable {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
fieldNames={{
|
||||||
|
label: 'title',
|
||||||
|
value: 'name',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{options?.map((opt) => {
|
<CollectionField value={value} onChange={onChange} />
|
||||||
return (
|
</Variable.Input>
|
||||||
<Select.Option key={opt.name} value={opt.name}>
|
|
||||||
{opt.title}
|
|
||||||
</Select.Option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [collectionField, type, value, fieldType, options]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<Select defaultValue={type} value={type} style={{ width: 150 }} onChange={typeChangeHandler}>
|
|
||||||
<Select.Option value={AssignedFieldValueType.ConstantValue}>{t('Constant value')}</Select.Option>
|
|
||||||
<Select.Option value={AssignedFieldValueType.DynamicValue}>{t('Dynamic value')}</Select.Option>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{useFieldMemo}
|
|
||||||
|
|
||||||
{fieldType === 'currentRecord' && (
|
|
||||||
<Cascader
|
|
||||||
fieldNames={{
|
|
||||||
label: 'title',
|
|
||||||
value: 'name',
|
|
||||||
children: 'children',
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
minWidth: 150,
|
|
||||||
}}
|
|
||||||
options={compile(fields)}
|
|
||||||
onChange={recordChangeHandler}
|
|
||||||
defaultValue={recordValue}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{fieldType === 'currentUser' && (
|
|
||||||
<Cascader
|
|
||||||
fieldNames={{
|
|
||||||
label: 'title',
|
|
||||||
value: 'name',
|
|
||||||
children: 'children',
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
minWidth: 150,
|
|
||||||
}}
|
|
||||||
options={compile(userFields)}
|
|
||||||
onChange={userChangeHandler}
|
|
||||||
defaultValue={userValue}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
||||||
import { FieldOption, Option } from '../type';
|
import { FieldOption, Option } from '../type';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface GetOptionsParams {
|
interface GetOptionsParams {
|
||||||
depth: number;
|
depth: number;
|
||||||
@ -35,7 +36,6 @@ const getChildren = (
|
|||||||
key: option.name,
|
key: option.name,
|
||||||
value: option.name,
|
value: option.name,
|
||||||
label: compile(option.title),
|
label: compile(option.title),
|
||||||
children: [],
|
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
field: option,
|
field: option,
|
||||||
depth,
|
depth,
|
||||||
@ -57,6 +57,7 @@ export const useFormVariable = ({
|
|||||||
level?: number;
|
level?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
const getFilterOptions = useGetFilterOptions();
|
const getFilterOptions = useGetFilterOptions();
|
||||||
const loadChildren = (option: any): Promise<void> => {
|
const loadChildren = (option: any): Promise<void> => {
|
||||||
if (!option.field?.target) {
|
if (!option.field?.target) {
|
||||||
@ -94,13 +95,15 @@ export const useFormVariable = ({
|
|||||||
}, 5);
|
}, 5);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const label = t('Current form');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
blockForm && {
|
blockForm && {
|
||||||
label: `{{t("Current form")}}`,
|
label,
|
||||||
value: '$form',
|
value: '$form',
|
||||||
key: '$form',
|
key: '$form',
|
||||||
children: [],
|
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
field: {
|
field: {
|
||||||
target: rootCollection,
|
target: rootCollection,
|
||||||
|
@ -38,7 +38,6 @@ const getChildren = (
|
|||||||
key: option.name,
|
key: option.name,
|
||||||
value: option.name,
|
value: option.name,
|
||||||
label: compile(option.title),
|
label: compile(option.title),
|
||||||
children: [],
|
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
field: option,
|
field: option,
|
||||||
depth,
|
depth,
|
||||||
@ -93,7 +92,6 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept
|
|||||||
label: t('Current user'),
|
label: t('Current user'),
|
||||||
value: '$user',
|
value: '$user',
|
||||||
key: '$user',
|
key: '$user',
|
||||||
children: [],
|
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
field: {
|
field: {
|
||||||
target: 'users',
|
target: 'users',
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Schema } from '@formily/react';
|
import { Schema } from '@formily/react';
|
||||||
|
import type { DefaultOptionType } from 'antd/lib/cascader';
|
||||||
|
|
||||||
export interface Option {
|
export interface Option extends DefaultOptionType {
|
||||||
key?: string | number;
|
key?: string | number;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
label?: React.ReactNode;
|
label: React.ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
children?: Option[];
|
children?: Option[];
|
||||||
// 标记是否为叶子节点,设置了 `loadData` 时有效
|
// 标记是否为叶子节点,设置了 `loadData` 时有效
|
||||||
|
@ -4,16 +4,18 @@ import { Tag } from 'antd';
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useRecord, Variable } from '@nocobase/client';
|
import { useCollectionManager, useCompile, useRecord, Variable } from '@nocobase/client';
|
||||||
|
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { useCollectionFieldOptions } from '../variable';
|
import { getCollectionFieldOptions } from '../variable';
|
||||||
|
|
||||||
const InternalExpression = observer(
|
const InternalExpression = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const { onChange } = props;
|
const { onChange } = props;
|
||||||
const { values } = useForm();
|
const { values } = useForm();
|
||||||
const [collection, setCollection] = useState(values?.sourceCollection);
|
const [collection, setCollection] = useState(values?.sourceCollection);
|
||||||
|
const compile = useCompile();
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
|
||||||
useFormEffects(() => {
|
useFormEffects(() => {
|
||||||
onFormInitialValuesChange((form) => {
|
onFormInitialValuesChange((form) => {
|
||||||
@ -25,7 +27,7 @@ const InternalExpression = observer(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = useCollectionFieldOptions({ collection: collection });
|
const options = getCollectionFieldOptions({ collection, compile, getCollectionFields });
|
||||||
|
|
||||||
return <Variable.TextArea {...props} scope={options} />;
|
return <Variable.TextArea {...props} scope={options} />;
|
||||||
},
|
},
|
||||||
@ -35,8 +37,10 @@ const InternalExpression = observer(
|
|||||||
function Result(props) {
|
function Result(props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const values = useRecord();
|
const values = useRecord();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() => useCollectionFieldOptions({ collection: values.sourceCollection }),
|
() => getCollectionFieldOptions({ collection: values.sourceCollection, compile, getCollectionFields }),
|
||||||
[values.sourceCollection, values.sourceCollection],
|
[values.sourceCollection, values.sourceCollection],
|
||||||
);
|
);
|
||||||
return props.value ? (
|
return props.value ? (
|
||||||
|
@ -18,8 +18,12 @@ import { FieldsSelect } from '../components/FieldsSelect';
|
|||||||
import { ValueBlock } from '../components/ValueBlock';
|
import { ValueBlock } from '../components/ValueBlock';
|
||||||
import { useNodeContext } from '.';
|
import { useNodeContext } from '.';
|
||||||
|
|
||||||
function matchToManyField(field, depth): boolean {
|
function matchToManyField(field, appends): boolean {
|
||||||
return ['hasMany', 'belongsToMany'].includes(field.type) && depth;
|
const fieldPrefix = `${field.name}.`;
|
||||||
|
return (
|
||||||
|
['hasMany', 'belongsToMany'].includes(field.type) &&
|
||||||
|
(appends.includes(field.name) || appends.some((item) => item.startsWith(fieldPrefix)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssociatedConfig({ value, onChange, ...props }): JSX.Element {
|
function AssociatedConfig({ value, onChange, ...props }): JSX.Element {
|
||||||
|
@ -14,18 +14,20 @@ import { BaseTypeSets, useWorkflowVariableOptions } from '../variable';
|
|||||||
import { ValueBlock } from '../components/ValueBlock';
|
import { ValueBlock } from '../components/ValueBlock';
|
||||||
|
|
||||||
function useDynamicExpressionCollectionFieldMatcher(field): boolean {
|
function useDynamicExpressionCollectionFieldMatcher(field): boolean {
|
||||||
const { getCollectionFields } = useCollectionManager();
|
if (!['belongsTo', 'hasOne'].includes(field.type)) {
|
||||||
if (field.type !== 'belongsTo') {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = getCollectionFields(field.target);
|
const fields = this.getCollectionFields(field.target);
|
||||||
return fields.some((f) => f.interface === 'expression');
|
return fields.some((f) => f.interface === 'expression');
|
||||||
}
|
}
|
||||||
|
|
||||||
const DynamicConfig = ({ value, onChange }) => {
|
const DynamicConfig = ({ value, onChange }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const scope = useWorkflowVariableOptions({ types: [useDynamicExpressionCollectionFieldMatcher] });
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
const scope = useWorkflowVariableOptions({
|
||||||
|
types: [useDynamicExpressionCollectionFieldMatcher.bind({ getCollectionFields })],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormLayout layout="vertical">
|
<FormLayout layout="vertical">
|
||||||
@ -55,10 +57,6 @@ const DynamicConfig = ({ value, onChange }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function useWorkflowVariableEntityOptions() {
|
|
||||||
return useWorkflowVariableOptions({ types: [{ type: 'reference', options: { collection: '*', entity: true } }] });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: `{{t("Calculation", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Calculation", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'calculation',
|
type: 'calculation',
|
||||||
@ -94,10 +92,13 @@ export default {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Variable.TextArea',
|
'x-component': 'CalculationExpression',
|
||||||
'x-component-props': {
|
// NOTE: can not use Variable.Input and scope directly as below,
|
||||||
scope: '{{useWorkflowVariableOptions}}',
|
// because the scope will be cached.
|
||||||
},
|
// 'x-component': 'Variable.Input',
|
||||||
|
// 'x-component-props': {
|
||||||
|
// scope: '{{useWorkflowVariableOptions()}}',
|
||||||
|
// },
|
||||||
['x-validator'](value, rules, { form }) {
|
['x-validator'](value, rules, { form }) {
|
||||||
const { values } = form;
|
const { values } = form;
|
||||||
const { evaluate } = evaluators.get(values.engine) as Evaluator;
|
const { evaluate } = evaluators.get(values.engine) as Evaluator;
|
||||||
@ -133,15 +134,15 @@ export default {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t("Variable datasource", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Variable datasource", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Variable.Input',
|
'x-component': 'ScopeSelect',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: '{{useWorkflowVariableEntityOptions}}',
|
changeOnSelect: true,
|
||||||
},
|
},
|
||||||
'x-reactions': {
|
'x-reactions': {
|
||||||
dependencies: ['dynamic'],
|
dependencies: ['dynamic'],
|
||||||
fulfill: {
|
fulfill: {
|
||||||
state: {
|
state: {
|
||||||
visible: '{{$deps[0] !== false}}',
|
visible: '{{$deps[0]}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -149,11 +150,20 @@ export default {
|
|||||||
},
|
},
|
||||||
view: {},
|
view: {},
|
||||||
scope: {
|
scope: {
|
||||||
useWorkflowVariableOptions,
|
|
||||||
useWorkflowVariableEntityOptions,
|
|
||||||
renderEngineReference,
|
renderEngineReference,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
CalculationExpression(props) {
|
||||||
|
const scope = useWorkflowVariableOptions();
|
||||||
|
|
||||||
|
return <Variable.TextArea scope={scope} {...props} />;
|
||||||
|
},
|
||||||
|
ScopeSelect(props) {
|
||||||
|
const scope = useWorkflowVariableOptions({
|
||||||
|
types: [{ type: 'reference', options: { collection: '*', entity: true } }],
|
||||||
|
});
|
||||||
|
return <Variable.Input scope={scope} {...props} />;
|
||||||
|
},
|
||||||
CalculationResult({ dataSource }) {
|
CalculationResult({ dataSource }) {
|
||||||
const { execution } = useFlowContext();
|
const { execution } = useFlowContext();
|
||||||
if (!execution) {
|
if (!execution) {
|
||||||
|
@ -6,6 +6,7 @@ import { Registry } from '@nocobase/utils/client';
|
|||||||
import { Button, Select } from 'antd';
|
import { Button, Select } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import { NodeDefaultView } from '.';
|
import { NodeDefaultView } from '.';
|
||||||
import { Branch } from '../Branch';
|
import { Branch } from '../Branch';
|
||||||
import { useFlowContext } from '../FlowContext';
|
import { useFlowContext } from '../FlowContext';
|
||||||
@ -371,10 +372,7 @@ export default {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Variable.TextArea',
|
'x-component': 'CalculationExpression',
|
||||||
'x-component-props': {
|
|
||||||
scope: '{{useWorkflowVariableOptions}}',
|
|
||||||
},
|
|
||||||
['x-validator'](value, rules, { form }) {
|
['x-validator'](value, rules, { form }) {
|
||||||
const { values } = form;
|
const { values } = form;
|
||||||
const { evaluate } = evaluators.get(values.engine);
|
const { evaluate } = evaluators.get(values.engine);
|
||||||
@ -483,6 +481,11 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
CalculationConfig,
|
CalculationConfig,
|
||||||
|
CalculationExpression(props) {
|
||||||
|
const scope = useWorkflowVariableOptions();
|
||||||
|
|
||||||
|
return <Variable.TextArea scope={scope} {...props} />;
|
||||||
|
},
|
||||||
RadioWithTooltip,
|
RadioWithTooltip,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { appends, collection, values } from '../schemas/collection';
|
import { appends, collection, values } from '../schemas/collection';
|
||||||
import CollectionFieldset from '../components/CollectionFieldset';
|
import CollectionFieldset from '../components/CollectionFieldset';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||||
import { useCollectionFieldOptions } from '../variable';
|
import { getCollectionFieldOptions } from '../variable';
|
||||||
import { FieldsSelect } from '../components/FieldsSelect';
|
import { FieldsSelect } from '../components/FieldsSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -41,10 +41,18 @@ export default {
|
|||||||
FieldsSelect,
|
FieldsSelect,
|
||||||
},
|
},
|
||||||
useVariables({ config }, options) {
|
useVariables({ config }, options) {
|
||||||
const result = useCollectionFieldOptions({
|
const compile = useCompile();
|
||||||
collection: config?.collection,
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
// const depth = config?.params?.appends?.length
|
||||||
|
// ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
|
||||||
|
// : 0;
|
||||||
|
const result = getCollectionFieldOptions({
|
||||||
|
collection: config.collection,
|
||||||
...options,
|
...options,
|
||||||
depth: options?.depth ?? config?.params?.appends?.length ? 1 : 0,
|
// depth: options?.depth ?? depth,
|
||||||
|
appends: config.params?.appends,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result?.length ? result : null;
|
return result?.length ? result : null;
|
||||||
|
@ -40,8 +40,20 @@ export default {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Variable.Input',
|
'x-component': 'Variable.Input',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: '{{useWorkflowVariableOptions}}',
|
scope: '{{useWorkflowVariableOptions()}}',
|
||||||
|
changeOnSelect: true,
|
||||||
useTypedConstant: ['string', 'number', 'null'],
|
useTypedConstant: ['string', 'number', 'null'],
|
||||||
|
className: css`
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.variable {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input.null-value {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager } from '@nocobase/client';
|
import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||||
import { useCollectionFieldOptions } from '../../variable';
|
import { getCollectionFieldOptions } from '../../variable';
|
||||||
import { NAMESPACE } from '../../locale';
|
import { NAMESPACE } from '../../locale';
|
||||||
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
import { SchemaConfig, SchemaConfigButton } from './SchemaConfig';
|
||||||
import { ModeConfig } from './ModeConfig';
|
import { ModeConfig } from './ModeConfig';
|
||||||
@ -86,6 +86,8 @@ export default {
|
|||||||
AssigneesSelect,
|
AssigneesSelect,
|
||||||
},
|
},
|
||||||
useVariables({ config }, { types }) {
|
useVariables({ config }, { types }) {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const formKeys = Object.keys(config.forms ?? {});
|
const formKeys = Object.keys(config.forms ?? {});
|
||||||
if (!formKeys.length) {
|
if (!formKeys.length) {
|
||||||
return null;
|
return null;
|
||||||
@ -96,10 +98,12 @@ export default {
|
|||||||
const form = config.forms[formKey];
|
const form = config.forms[formKey];
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const options = useCollectionFieldOptions({
|
const options = getCollectionFieldOptions({
|
||||||
fields: form.collection?.fields,
|
fields: form.collection?.fields,
|
||||||
collection: form.collection,
|
collection: form.collection,
|
||||||
types,
|
types,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
});
|
});
|
||||||
return options.length
|
return options.length
|
||||||
? {
|
? {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { appends, collection, filter } from '../schemas/collection';
|
import { appends, collection, filter } from '../schemas/collection';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||||
import { useCollectionFieldOptions } from '../variable';
|
import { getCollectionFieldOptions } from '../variable';
|
||||||
import { FieldsSelect } from '../components/FieldsSelect';
|
import { FieldsSelect } from '../components/FieldsSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -44,10 +44,18 @@ export default {
|
|||||||
FieldsSelect,
|
FieldsSelect,
|
||||||
},
|
},
|
||||||
useVariables({ config }, options) {
|
useVariables({ config }, options) {
|
||||||
const result = useCollectionFieldOptions({
|
const compile = useCompile();
|
||||||
collection: config?.collection,
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
// const depth = config?.params?.appends?.length
|
||||||
|
// ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
|
||||||
|
// : 0;
|
||||||
|
const result = getCollectionFieldOptions({
|
||||||
|
collection: config.collection,
|
||||||
...options,
|
...options,
|
||||||
depth: options?.depth ?? config?.params?.appends?.length ? 1 : 0,
|
// depth: options?.depth ?? depth,
|
||||||
|
appends: config.params?.appends,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result?.length ? result : null;
|
return result?.length ? result : null;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
import { ArrayItems } from '@formily/antd';
|
import { ArrayItems } from '@formily/antd';
|
||||||
|
|
||||||
|
import { Variable } from '@nocobase/client';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { useWorkflowVariableOptions } from '../variable';
|
import { useWorkflowVariableOptions } from '../variable';
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ export default {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Variable.Input',
|
'x-component': 'Variable.Input',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: useWorkflowVariableOptions,
|
scope: '{{useWorkflowVariableOptions()}}',
|
||||||
useTypedConstant: true,
|
useTypedConstant: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -112,7 +114,7 @@ export default {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Variable.Input',
|
'x-component': 'Variable.Input',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: useWorkflowVariableOptions,
|
scope: '{{useWorkflowVariableOptions()}}',
|
||||||
useTypedConstant: true,
|
useTypedConstant: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -138,9 +140,9 @@ export default {
|
|||||||
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-decorator-props': {},
|
'x-decorator-props': {},
|
||||||
'x-component': 'Variable.JSON',
|
'x-component': 'RequestBody',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: useWorkflowVariableOptions,
|
changeOnSelect: true,
|
||||||
autoSize: {
|
autoSize: {
|
||||||
minRows: 10,
|
minRows: 10,
|
||||||
},
|
},
|
||||||
@ -170,8 +172,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
view: {},
|
view: {},
|
||||||
scope: {},
|
scope: {
|
||||||
|
useWorkflowVariableOptions,
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
ArrayItems,
|
ArrayItems,
|
||||||
|
RequestBody(props) {
|
||||||
|
const scope = useWorkflowVariableOptions();
|
||||||
|
return <Variable.JSON scope={scope} {...props} />;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
import { SchemaInitializerItemOptions, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||||
import { FieldsSelect } from '../components/FieldsSelect';
|
import { FieldsSelect } from '../components/FieldsSelect';
|
||||||
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
import { NAMESPACE, lang } from '../locale';
|
||||||
import { appends, collection, filter } from '../schemas/collection';
|
import { appends, collection, filter } from '../schemas/collection';
|
||||||
import { useCollectionFieldOptions } from '../variable';
|
import { getCollectionFieldOptions } from '../variable';
|
||||||
|
|
||||||
const COLLECTION_TRIGGER_MODE = {
|
const COLLECTION_TRIGGER_MODE = {
|
||||||
CREATED: 1,
|
CREATED: 1,
|
||||||
@ -45,6 +45,15 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
target: 'appends',
|
||||||
|
effects: ['onFieldValueChange'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
mode: {
|
mode: {
|
||||||
@ -133,7 +142,8 @@ export default {
|
|||||||
FieldsSelect,
|
FieldsSelect,
|
||||||
},
|
},
|
||||||
useVariables(config, options) {
|
useVariables(config, options) {
|
||||||
const { t } = useWorkflowTranslation();
|
const compile = useCompile();
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const rootFields = [
|
const rootFields = [
|
||||||
{
|
{
|
||||||
collectionName: config.collection,
|
collectionName: config.collection,
|
||||||
@ -141,14 +151,20 @@ export default {
|
|||||||
type: 'hasOne',
|
type: 'hasOne',
|
||||||
target: config.collection,
|
target: config.collection,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: t('Trigger data'),
|
title: lang('Trigger data'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const result = useCollectionFieldOptions({
|
// const depth = config.appends?.length
|
||||||
|
// ? config.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) + 1
|
||||||
|
// : 1;
|
||||||
|
const result = getCollectionFieldOptions({
|
||||||
|
// depth,
|
||||||
...options,
|
...options,
|
||||||
fields: rootFields,
|
fields: rootFields,
|
||||||
depth: options?.depth ?? (config.appends?.length ? 2 : 1),
|
appends: ['data', ...(config.appends?.map((item) => `data.${item}`) || [])],
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { useCollectionDataSource, SchemaInitializerItemOptions } from '@nocobase/client';
|
import { useCollectionDataSource, SchemaInitializerItemOptions, useCompile, useCollectionManager } from '@nocobase/client';
|
||||||
|
|
||||||
import { ScheduleConfig } from './ScheduleConfig';
|
import { ScheduleConfig } from './ScheduleConfig';
|
||||||
import { SCHEDULE_MODE } from './constants';
|
import { SCHEDULE_MODE } from './constants';
|
||||||
import { NAMESPACE, useWorkflowTranslation } from '../../locale';
|
import { NAMESPACE, lang } from '../../locale';
|
||||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||||
import { useCollectionFieldOptions } from '../../variable';
|
import { getCollectionFieldOptions } from '../../variable';
|
||||||
import { FieldsSelect } from '../../components/FieldsSelect';
|
import { FieldsSelect } from '../../components/FieldsSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -24,20 +24,31 @@ export default {
|
|||||||
ScheduleConfig,
|
ScheduleConfig,
|
||||||
FieldsSelect,
|
FieldsSelect,
|
||||||
},
|
},
|
||||||
useVariables(config, { types }) {
|
useVariables(config, opts) {
|
||||||
const { t } = useWorkflowTranslation();
|
const compile = useCompile();
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const options: any[] = [];
|
const options: any[] = [];
|
||||||
if (!types || types.includes('date')) {
|
if (!opts?.types || opts.types.includes('date')) {
|
||||||
options.push({ key: 'date', value: 'date', label: t('Trigger time') });
|
options.push({ key: 'date', value: 'date', label: lang('Trigger time') });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldOptions = useCollectionFieldOptions({ collection: config.collection });
|
const depth = config.appends?.length
|
||||||
|
? config.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1) + 1
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const fieldOptions = getCollectionFieldOptions({
|
||||||
|
depth,
|
||||||
|
...opts,
|
||||||
|
collection: config.collection,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
|
});
|
||||||
if (config.mode === SCHEDULE_MODE.COLLECTION_FIELD) {
|
if (config.mode === SCHEDULE_MODE.COLLECTION_FIELD) {
|
||||||
if (fieldOptions.length) {
|
if (fieldOptions.length) {
|
||||||
options.push({
|
options.push({
|
||||||
key: 'data',
|
key: 'data',
|
||||||
value: 'data',
|
value: 'data',
|
||||||
label: t('Trigger data'),
|
label: lang('Trigger data'),
|
||||||
children: fieldOptions,
|
children: fieldOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCollectionManager, useCompile } from '@nocobase/client';
|
import { useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
import { useFlowContext } from './FlowContext';
|
import { useFlowContext } from './FlowContext';
|
||||||
import { NAMESPACE } from './locale';
|
import { NAMESPACE, lang } from './locale';
|
||||||
import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
|
import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
|
||||||
import { triggers } from './triggers';
|
import { triggers } from './triggers';
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export const systemOptions = {
|
|||||||
{
|
{
|
||||||
key: 'now',
|
key: 'now',
|
||||||
value: 'now',
|
value: 'now',
|
||||||
label: `{{t("System time")}}`,
|
label: lang('System time'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@ -110,7 +110,7 @@ export const BaseTypeSets = {
|
|||||||
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
|
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
|
||||||
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
|
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
|
||||||
|
|
||||||
function matchFieldType(field, type, depth): boolean {
|
function matchFieldType(field, type, appends): boolean {
|
||||||
const inputType = typeof type;
|
const inputType = typeof type;
|
||||||
if (inputType === 'string') {
|
if (inputType === 'string') {
|
||||||
return BaseTypeSets[type]?.has(field.interface);
|
return BaseTypeSets[type]?.has(field.interface);
|
||||||
@ -132,7 +132,7 @@ function matchFieldType(field, type, depth): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inputType === 'function') {
|
if (inputType === 'function') {
|
||||||
return type(field, depth);
|
return type(field, appends);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -142,31 +142,47 @@ function isAssociationField(field): boolean {
|
|||||||
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterTypedFields(fields, types, depth = 1) {
|
function getNextAppends(field, appends: string[]) {
|
||||||
if (!types) {
|
const fieldPrefix = `${field.name}.`;
|
||||||
return fields;
|
return appends.filter((item) => item.startsWith(fieldPrefix)).map((item) => item.replace(fieldPrefix, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterTypedFields({ fields, types, appends, compile, getCollectionFields }) {
|
||||||
return fields.filter((field) => {
|
return fields.filter((field) => {
|
||||||
if (
|
const match = types?.length ? types.some((type) => matchFieldType(field, type, appends)) : true;
|
||||||
isAssociationField(field) &&
|
if (isAssociationField(field)) {
|
||||||
depth &&
|
const nextAppends = getNextAppends(field, appends);
|
||||||
filterTypedFields(useNormalizedFields(field.target), types, depth - 1).length
|
const included = appends.includes(field.name);
|
||||||
) {
|
if (match) {
|
||||||
return true;
|
return included;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
(nextAppends.length || included) &&
|
||||||
|
filterTypedFields({
|
||||||
|
fields: getNormalizedFields(field.target, { compile, getCollectionFields }),
|
||||||
|
types,
|
||||||
|
// depth: depth - 1,
|
||||||
|
appends: nextAppends,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return match;
|
||||||
}
|
}
|
||||||
return types.some((type) => matchFieldType(field, type, depth));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkflowVariableOptions(options = {}) {
|
export function useWorkflowVariableOptions(options = {}) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
|
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
|
||||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(options).filter(Boolean) : null;
|
const opts = item.useOptions(options).filter(Boolean);
|
||||||
return {
|
return {
|
||||||
label: compile(item.label),
|
label: compile(item.label),
|
||||||
value: item.value,
|
value: item.value,
|
||||||
key: item.value,
|
key: item.value,
|
||||||
children: compile(opts),
|
children: opts,
|
||||||
disabled: opts && !opts.length,
|
disabled: opts && !opts.length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -174,9 +190,7 @@ export function useWorkflowVariableOptions(options = {}) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useNormalizedFields(collectionName) {
|
function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||||
const compile = useCompile();
|
|
||||||
const { getCollectionFields } = useCollectionManager();
|
|
||||||
const fields = getCollectionFields(collectionName);
|
const fields = getCollectionFields(collectionName);
|
||||||
const foreignKeyFields: any[] = [];
|
const foreignKeyFields: any[] = [];
|
||||||
const otherFields: any[] = [];
|
const otherFields: any[] = [];
|
||||||
@ -231,24 +245,55 @@ function useNormalizedFields(collectionName) {
|
|||||||
return otherFields.filter((field) => field.interface && !field.hidden);
|
return otherFields.filter((field) => field.interface && !field.hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCollectionFieldOptions(options): VariableOption[] {
|
async function loadChildren(option) {
|
||||||
const { fields, collection, types, depth = 1 } = options;
|
const result = getCollectionFieldOptions({
|
||||||
const compile = useCompile();
|
collection: option.field.target,
|
||||||
const normalizedFields = useNormalizedFields(collection);
|
types: option.types,
|
||||||
|
appends: getNextAppends(option.field, option.appends),
|
||||||
|
sourceKey: option.field.key,
|
||||||
|
compile: this.compile,
|
||||||
|
getCollectionFields: this.getCollectionFields,
|
||||||
|
});
|
||||||
|
if (result.length) {
|
||||||
|
option.children = result;
|
||||||
|
} else {
|
||||||
|
option.isLeaf = true;
|
||||||
|
option.loadChildren = null;
|
||||||
|
const matchingType = option.types?.some((type) => matchFieldType(option.field, type, 0));
|
||||||
|
if (!matchingType) {
|
||||||
|
option.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCollectionFieldOptions(options): VariableOption[] {
|
||||||
|
const { fields, collection, types, appends = [], compile, getCollectionFields } = options;
|
||||||
|
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
||||||
const computedFields = fields ?? normalizedFields;
|
const computedFields = fields ?? normalizedFields;
|
||||||
const result: VariableOption[] = filterTypedFields(computedFields, types, depth)
|
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields });
|
||||||
.filter((field) => !isAssociationField(field) || depth)
|
|
||||||
.map((field) => {
|
const result: VariableOption[] = filterTypedFields({
|
||||||
|
fields: computedFields,
|
||||||
|
types,
|
||||||
|
// depth,
|
||||||
|
appends,
|
||||||
|
compile,
|
||||||
|
getCollectionFields,
|
||||||
|
}).map((field) => {
|
||||||
const label = compile(field.uiSchema?.title || field.name);
|
const label = compile(field.uiSchema?.title || field.name);
|
||||||
|
const nextAppends = getNextAppends(field, appends);
|
||||||
|
// TODO: no matching fields in next appends should consider isLeaf as true
|
||||||
|
const isLeaf = !isAssociationField(field) || (!nextAppends.length && !appends.includes(field.name));
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
key: field.name,
|
key: field.name,
|
||||||
value: field.name,
|
value: field.name,
|
||||||
children:
|
isLeaf,
|
||||||
isAssociationField(field) && depth
|
loadChildren: isLeaf ? null : boundLoadChildren,
|
||||||
? useCollectionFieldOptions({ collection: field.target, types, depth: depth - 1 })
|
|
||||||
: null,
|
|
||||||
field,
|
field,
|
||||||
|
// depth,
|
||||||
|
appends,
|
||||||
|
types,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user