2023-11-03 20:08:11 +08:00
|
|
|
import React from 'react';
|
|
|
|
|
|
|
|
import { Variable, useCompile } from '@nocobase/client';
|
|
|
|
|
2023-03-30 23:49:57 +08:00
|
|
|
import { useFlowContext } from './FlowContext';
|
2023-07-05 22:01:41 +08:00
|
|
|
import { NAMESPACE, lang } from './locale';
|
2023-05-16 09:45:45 +08:00
|
|
|
import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
|
2023-03-30 23:49:57 +08:00
|
|
|
import { triggers } from './triggers';
|
2023-02-20 11:52:06 +08:00
|
|
|
|
2023-03-10 16:36:58 +08:00
|
|
|
export type VariableOption = {
|
2023-04-08 10:52:31 +08:00
|
|
|
key?: string;
|
2023-07-18 12:50:24 +08:00
|
|
|
value?: string;
|
|
|
|
label?: string;
|
2023-04-08 10:52:31 +08:00
|
|
|
children?: VariableOptions;
|
2023-07-18 12:50:24 +08:00
|
|
|
[key: string]: any;
|
2023-03-10 16:36:58 +08:00
|
|
|
};
|
2023-02-20 11:52:06 +08:00
|
|
|
|
2023-04-08 10:52:31 +08:00
|
|
|
export type VariableOptions = VariableOption[] | null;
|
|
|
|
|
2023-07-18 12:50:24 +08:00
|
|
|
export type VariableDataType =
|
2023-08-04 17:56:12 +08:00
|
|
|
| string
|
|
|
|
| {
|
|
|
|
type: string;
|
|
|
|
options?: { entity?: boolean; collection?: string };
|
|
|
|
}
|
|
|
|
| ((field: any, appends?: string[]) => boolean);
|
2023-07-18 12:50:24 +08:00
|
|
|
|
|
|
|
export type OptionsOfUseVariableOptions = {
|
|
|
|
types?: VariableDataType[];
|
|
|
|
fieldNames?: {
|
|
|
|
label?: string;
|
|
|
|
value?: string;
|
|
|
|
children?: string;
|
|
|
|
};
|
2023-09-26 23:39:50 +08:00
|
|
|
appends?: string[] | null;
|
2023-10-23 00:05:15 +08:00
|
|
|
depth?: number;
|
2023-11-07 22:17:07 +08:00
|
|
|
current?: any;
|
2023-08-04 17:56:12 +08:00
|
|
|
};
|
2023-07-18 12:50:24 +08:00
|
|
|
|
|
|
|
export const defaultFieldNames = { label: 'label', value: 'value', children: 'children' } as const;
|
|
|
|
|
2023-05-17 13:37:03 +08:00
|
|
|
export const nodesOptions = {
|
|
|
|
label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
2023-10-25 23:14:54 +08:00
|
|
|
value: '$jobsMapByNodeKey',
|
2023-07-18 12:50:24 +08:00
|
|
|
useOptions(options: OptionsOfUseVariableOptions) {
|
2023-05-17 17:29:18 +08:00
|
|
|
const current = useNodeContext();
|
2023-05-17 13:37:03 +08:00
|
|
|
const upstreams = useAvailableUpstreams(current);
|
|
|
|
const result: VariableOption[] = [];
|
|
|
|
upstreams.forEach((node) => {
|
|
|
|
const instruction = instructions.get(node.type);
|
2023-07-06 12:27:34 +08:00
|
|
|
const subOption = instruction.useVariables?.(node, options);
|
|
|
|
if (subOption) {
|
|
|
|
result.push(subOption);
|
2023-05-17 13:37:03 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
2023-05-16 09:45:45 +08:00
|
|
|
},
|
2023-05-17 13:37:03 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const triggerOptions = {
|
|
|
|
label: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
|
|
|
|
value: '$context',
|
2023-07-18 12:50:24 +08:00
|
|
|
useOptions(options: OptionsOfUseVariableOptions) {
|
2023-05-17 13:37:03 +08:00
|
|
|
const { workflow } = useFlowContext();
|
|
|
|
const trigger = triggers.get(workflow.type);
|
|
|
|
return trigger?.useVariables?.(workflow.config, options) ?? null;
|
2023-02-20 11:52:06 +08:00
|
|
|
},
|
2023-05-17 13:37:03 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const scopeOptions = {
|
|
|
|
label: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
|
|
|
|
value: '$scopes',
|
2023-07-18 12:50:24 +08:00
|
|
|
useOptions(options: OptionsOfUseVariableOptions) {
|
2023-11-07 22:17:07 +08:00
|
|
|
const { fieldNames = defaultFieldNames, current } = options;
|
|
|
|
const source = useNodeContext();
|
|
|
|
const from = current ?? source;
|
|
|
|
const scopes = useUpstreamScopes(from);
|
2023-05-17 13:37:03 +08:00
|
|
|
const result: VariableOption[] = [];
|
|
|
|
scopes.forEach((node) => {
|
|
|
|
const instruction = instructions.get(node.type);
|
|
|
|
const subOptions = instruction.useScopeVariables?.(node, options);
|
|
|
|
if (subOptions) {
|
|
|
|
result.push({
|
2023-10-25 23:14:54 +08:00
|
|
|
key: node.key,
|
|
|
|
[fieldNames.value]: node.key,
|
2023-07-18 12:50:24 +08:00
|
|
|
[fieldNames.label]: node.title ?? `#${node.id}`,
|
|
|
|
[fieldNames.children]: subOptions,
|
2023-05-17 13:37:03 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return result;
|
2023-02-20 11:52:06 +08:00
|
|
|
},
|
2023-05-17 13:37:03 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
export const systemOptions = {
|
|
|
|
label: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
|
|
|
value: '$system',
|
2023-07-18 12:50:24 +08:00
|
|
|
useOptions({ types, fieldNames = defaultFieldNames }: OptionsOfUseVariableOptions) {
|
2023-05-17 13:37:03 +08:00
|
|
|
return [
|
|
|
|
...(!types || types.includes('date')
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
key: 'now',
|
2023-07-18 12:50:24 +08:00
|
|
|
[fieldNames.label]: lang('System time'),
|
|
|
|
[fieldNames.value]: 'now',
|
2023-05-17 13:37:03 +08:00
|
|
|
},
|
|
|
|
]
|
|
|
|
: []),
|
|
|
|
];
|
2023-03-30 23:49:57 +08:00
|
|
|
},
|
2023-05-17 13:37:03 +08:00
|
|
|
};
|
2023-02-20 11:52:06 +08:00
|
|
|
|
2023-04-08 10:52:31 +08:00
|
|
|
export const BaseTypeSets = {
|
|
|
|
boolean: new Set(['checkbox']),
|
2023-07-07 22:51:44 +08:00
|
|
|
number: new Set(['integer', 'number', 'percent']),
|
2023-04-25 13:12:14 +08:00
|
|
|
string: new Set([
|
|
|
|
'input',
|
|
|
|
'password',
|
|
|
|
'email',
|
|
|
|
'phone',
|
|
|
|
'select',
|
|
|
|
'radioGroup',
|
|
|
|
'text',
|
|
|
|
'markdown',
|
|
|
|
'richText',
|
|
|
|
'expression',
|
|
|
|
'time',
|
|
|
|
]),
|
2023-04-08 10:52:31 +08:00
|
|
|
date: new Set(['date', 'createdAt', 'updatedAt']),
|
2023-03-30 23:49:57 +08:00
|
|
|
};
|
2023-02-20 11:52:06 +08:00
|
|
|
|
2023-04-08 10:52:31 +08:00
|
|
|
// { type: 'reference', options: { collection: 'users', multiple: false } }
|
|
|
|
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
|
|
|
|
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
|
|
|
|
|
2023-09-26 23:39:50 +08:00
|
|
|
function matchFieldType(field, type): boolean {
|
2023-04-08 10:52:31 +08:00
|
|
|
const inputType = typeof type;
|
|
|
|
if (inputType === 'string') {
|
2023-05-08 13:50:43 +08:00
|
|
|
return BaseTypeSets[type]?.has(field.interface);
|
2023-02-20 11:52:06 +08:00
|
|
|
}
|
|
|
|
|
2023-04-08 10:52:31 +08:00
|
|
|
if (inputType === 'object' && type.type === 'reference') {
|
|
|
|
if (isAssociationField(field)) {
|
2023-04-25 13:12:14 +08:00
|
|
|
return (
|
|
|
|
type.options?.entity && (field.collectionName === type.options?.collection || type.options?.collection === '*')
|
|
|
|
);
|
2023-04-08 10:52:31 +08:00
|
|
|
} else if (field.isForeignKey) {
|
|
|
|
return (
|
|
|
|
(field.collectionName === type.options?.collection && field.name === 'id') ||
|
2023-04-25 13:12:14 +08:00
|
|
|
field.target === type.options?.collection
|
2023-04-08 10:52:31 +08:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputType === 'function') {
|
2023-09-26 23:39:50 +08:00
|
|
|
return type(field);
|
2023-02-20 11:52:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-04-08 10:52:31 +08:00
|
|
|
function isAssociationField(field): boolean {
|
|
|
|
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
2023-02-20 11:52:06 +08:00
|
|
|
}
|
|
|
|
|
2023-09-26 23:39:50 +08:00
|
|
|
function getNextAppends(field, appends: string[] | null): string[] | null {
|
|
|
|
if (appends == null) {
|
|
|
|
return null;
|
|
|
|
}
|
2023-07-05 22:01:41 +08:00
|
|
|
const fieldPrefix = `${field.name}.`;
|
|
|
|
return appends.filter((item) => item.startsWith(fieldPrefix)).map((item) => item.replace(fieldPrefix, ''));
|
|
|
|
}
|
|
|
|
|
2023-10-23 00:05:15 +08:00
|
|
|
function filterTypedFields({ fields, types, appends, depth = 1, compile, getCollectionFields }) {
|
2023-04-08 10:52:31 +08:00
|
|
|
return fields.filter((field) => {
|
2023-09-26 23:39:50 +08:00
|
|
|
const match = types?.length ? types.some((type) => matchFieldType(field, type)) : true;
|
2023-07-05 22:01:41 +08:00
|
|
|
if (isAssociationField(field)) {
|
2023-09-26 23:39:50 +08:00
|
|
|
if (appends === null) {
|
2023-10-23 00:05:15 +08:00
|
|
|
if (!depth) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-09-26 23:39:50 +08:00
|
|
|
return (
|
|
|
|
match ||
|
|
|
|
filterTypedFields({
|
|
|
|
fields: getNormalizedFields(field.target, { compile, getCollectionFields }),
|
|
|
|
types,
|
2023-10-23 00:05:15 +08:00
|
|
|
depth: depth - 1,
|
2023-09-26 23:39:50 +08:00
|
|
|
appends,
|
|
|
|
compile,
|
|
|
|
getCollectionFields,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2023-07-05 22:01:41 +08:00
|
|
|
const nextAppends = getNextAppends(field, appends);
|
|
|
|
const included = appends.includes(field.name);
|
2023-09-26 23:39:50 +08:00
|
|
|
if (match) {
|
|
|
|
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;
|
2023-04-08 10:52:31 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-03 20:08:11 +08:00
|
|
|
function useOptions(scope, opts) {
|
|
|
|
const compile = useCompile();
|
|
|
|
const children = scope.useOptions?.(opts)?.filter(Boolean);
|
|
|
|
const { fieldNames } = opts;
|
|
|
|
return {
|
|
|
|
[fieldNames.label]: compile(scope.label),
|
|
|
|
[fieldNames.value]: scope.value,
|
|
|
|
key: scope[fieldNames.value],
|
|
|
|
[fieldNames.children]: children,
|
|
|
|
disabled: !children || !children.length,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-07-18 12:50:24 +08:00
|
|
|
export function useWorkflowVariableOptions(options: OptionsOfUseVariableOptions = {}) {
|
|
|
|
const fieldNames = Object.assign({}, defaultFieldNames, options.fieldNames ?? {});
|
|
|
|
const opts = Object.assign(options, { fieldNames });
|
2023-11-03 20:08:11 +08:00
|
|
|
const result = [
|
|
|
|
useOptions(scopeOptions, opts),
|
|
|
|
useOptions(nodesOptions, opts),
|
|
|
|
useOptions(triggerOptions, opts),
|
|
|
|
useOptions(systemOptions, opts),
|
|
|
|
];
|
|
|
|
// const cache = useMemo(() => result, [result]);
|
2023-05-17 13:37:03 +08:00
|
|
|
|
|
|
|
return result;
|
2023-02-20 11:52:06 +08:00
|
|
|
}
|
|
|
|
|
2023-07-05 22:01:41 +08:00
|
|
|
function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
2023-05-17 13:37:03 +08:00
|
|
|
const fields = getCollectionFields(collectionName);
|
2023-04-08 10:52:31 +08:00
|
|
|
const foreignKeyFields: any[] = [];
|
|
|
|
const otherFields: any[] = [];
|
2023-04-25 13:12:14 +08:00
|
|
|
fields.forEach((field) => {
|
2023-04-08 10:52:31 +08:00
|
|
|
if (field.isForeignKey) {
|
|
|
|
foreignKeyFields.push(field);
|
|
|
|
} else {
|
|
|
|
otherFields.push(field);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
for (let i = otherFields.length - 1; i >= 0; i--) {
|
|
|
|
const field = otherFields[i];
|
|
|
|
if (field.type === 'belongsTo') {
|
2023-04-25 13:12:14 +08:00
|
|
|
const foreignKeyField = foreignKeyFields.find((f) => f.name === field.foreignKey);
|
2023-04-08 10:52:31 +08:00
|
|
|
if (foreignKeyField) {
|
|
|
|
otherFields.splice(i, 0, {
|
2023-04-21 10:52:13 +08:00
|
|
|
...field,
|
2023-04-08 10:52:31 +08:00
|
|
|
...foreignKeyField,
|
|
|
|
uiSchema: {
|
|
|
|
...field.uiSchema,
|
|
|
|
title: field.uiSchema?.title ? `${compile(field.uiSchema?.title)} ID` : foreignKeyField.name,
|
2023-04-25 13:12:14 +08:00
|
|
|
},
|
2023-04-08 10:52:31 +08:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
otherFields.splice(i, 0, {
|
|
|
|
...field,
|
|
|
|
name: field.foreignKey,
|
|
|
|
type: 'bigInt',
|
|
|
|
isForeignKey: true,
|
|
|
|
interface: field.interface,
|
|
|
|
uiSchema: {
|
|
|
|
...field.uiSchema,
|
|
|
|
title: field.uiSchema?.title ? `${compile(field.uiSchema?.title)} ID` : field.name,
|
2023-04-25 13:12:14 +08:00
|
|
|
},
|
2023-04-08 10:52:31 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (field.type === 'context' && field.collectionName === 'users') {
|
2023-04-25 13:12:14 +08:00
|
|
|
const belongsToField =
|
|
|
|
otherFields.find((f) => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
|
2023-04-08 10:52:31 +08:00
|
|
|
otherFields.splice(i, 0, {
|
|
|
|
...field,
|
|
|
|
type: field.dataType,
|
|
|
|
interface: belongsToField.interface,
|
|
|
|
uiSchema: {
|
|
|
|
...belongsToField.uiSchema,
|
2023-04-25 13:12:14 +08:00
|
|
|
title: belongsToField.uiSchema?.title ? `${compile(belongsToField.uiSchema?.title)} ID` : field.name,
|
|
|
|
},
|
2023-04-08 10:52:31 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-25 13:12:14 +08:00
|
|
|
return otherFields.filter((field) => field.interface && !field.hidden);
|
2023-04-02 13:24:25 +08:00
|
|
|
}
|
|
|
|
|
2023-09-12 14:29:05 +08:00
|
|
|
function loadChildren(option) {
|
2023-08-14 17:11:48 +08:00
|
|
|
const appends = getNextAppends(option.field, option.appends);
|
2023-07-05 22:01:41 +08:00
|
|
|
const result = getCollectionFieldOptions({
|
|
|
|
collection: option.field.target,
|
|
|
|
types: option.types,
|
2023-08-14 17:11:48 +08:00
|
|
|
appends,
|
2023-10-23 00:05:15 +08:00
|
|
|
depth: option.depth - 1,
|
2023-07-18 12:50:24 +08:00
|
|
|
...this,
|
2023-07-05 22:01:41 +08:00
|
|
|
});
|
2023-07-18 12:50:24 +08:00
|
|
|
option.loadChildren = null;
|
2023-07-05 22:01:41 +08:00
|
|
|
if (result.length) {
|
|
|
|
option.children = result;
|
|
|
|
} else {
|
|
|
|
option.isLeaf = true;
|
2023-09-26 23:39:50 +08:00
|
|
|
const matchingType = option.types ? option.types.some((type) => matchFieldType(option.field, type)) : true;
|
2023-07-05 22:01:41 +08:00
|
|
|
if (!matchingType) {
|
|
|
|
option.disabled = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getCollectionFieldOptions(options): VariableOption[] {
|
2023-08-04 17:56:12 +08:00
|
|
|
const {
|
|
|
|
fields,
|
|
|
|
collection,
|
|
|
|
types,
|
|
|
|
appends = [],
|
2023-10-23 00:05:15 +08:00
|
|
|
depth = 1,
|
2023-08-04 17:56:12 +08:00
|
|
|
compile,
|
|
|
|
getCollectionFields,
|
|
|
|
fieldNames = defaultFieldNames,
|
|
|
|
} = options;
|
2023-07-05 22:01:41 +08:00
|
|
|
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
2023-05-16 09:45:45 +08:00
|
|
|
const computedFields = fields ?? normalizedFields;
|
2023-07-18 12:50:24 +08:00
|
|
|
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields, fieldNames });
|
2023-07-05 22:01:41 +08:00
|
|
|
|
|
|
|
const result: VariableOption[] = filterTypedFields({
|
|
|
|
fields: computedFields,
|
|
|
|
types,
|
2023-10-23 00:05:15 +08:00
|
|
|
depth,
|
2023-07-05 22:01:41 +08:00
|
|
|
appends,
|
|
|
|
compile,
|
|
|
|
getCollectionFields,
|
|
|
|
}).map((field) => {
|
|
|
|
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
|
2023-09-26 23:39:50 +08:00
|
|
|
const isLeaf =
|
|
|
|
!isAssociationField(field) || (nextAppends && !nextAppends.length && !appends.includes(field.name)) || false;
|
|
|
|
|
2023-07-05 22:01:41 +08:00
|
|
|
return {
|
2023-07-18 12:50:24 +08:00
|
|
|
[fieldNames.label]: label,
|
2023-07-05 22:01:41 +08:00
|
|
|
key: field.name,
|
2023-07-18 12:50:24 +08:00
|
|
|
[fieldNames.value]: field.name,
|
2023-07-05 22:01:41 +08:00
|
|
|
isLeaf,
|
|
|
|
loadChildren: isLeaf ? null : boundLoadChildren,
|
|
|
|
field,
|
2023-10-23 00:05:15 +08:00
|
|
|
depth,
|
2023-07-05 22:01:41 +08:00
|
|
|
appends,
|
|
|
|
types,
|
|
|
|
};
|
|
|
|
});
|
2023-03-10 16:36:58 +08:00
|
|
|
|
|
|
|
return result;
|
2023-02-20 11:52:06 +08:00
|
|
|
}
|
2023-11-03 20:08:11 +08:00
|
|
|
|
|
|
|
export function WorkflowVariableInput({ variableOptions, ...props }): JSX.Element {
|
|
|
|
const scope = useWorkflowVariableOptions(variableOptions);
|
|
|
|
return <Variable.Input scope={scope} {...props} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function WorkflowVariableTextArea({ variableOptions, ...props }): JSX.Element {
|
|
|
|
const scope = useWorkflowVariableOptions(variableOptions);
|
|
|
|
return <Variable.TextArea scope={scope} {...props} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function WorkflowVariableRawTextArea({ variableOptions, ...props }): JSX.Element {
|
|
|
|
const scope = useWorkflowVariableOptions(variableOptions);
|
|
|
|
return <Variable.RawTextArea scope={scope} {...props} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function WorkflowVariableJSON({ variableOptions, ...props }): JSX.Element {
|
|
|
|
const scope = useWorkflowVariableOptions(variableOptions);
|
|
|
|
return <Variable.JSON scope={scope} {...props} />;
|
|
|
|
}
|