tachybase_todo/packages/plugins/workflow/src/client/variable.tsx
Junyi f4064767c6
feat(plugin-workflow): aggregate (#1852)
* feat(plugin-workflow): add aggregate instruction

* test(plugin-workflow): add test cases

* fix(plugin-workflow): fix types

* fix(plugin-workflow): fix double result type

* test(plugin-workflow): fix test cases in mysql

* refactor(plugin-workflow): consolidate variables api

* fix(plugin-workflow): fix create node variable

* fix(plugin-workflow): fix aggregate association name

* fix(plugin-workflow): fix test cases

* fix(plugin-workflow): fix aggregate node config for duplication

* fix(plugin-workflow): fix variable api

* fix(plugin-workflow): fix variable api caller

* fix(plugin-workflow): fix job button style
2023-05-17 13:37:03 +08:00

256 lines
7.4 KiB
TypeScript

import { useCollectionManager, useCompile } from '@nocobase/client';
import { useFlowContext } from './FlowContext';
import { NAMESPACE } from './locale';
import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
import { triggers } from './triggers';
export type VariableOption = {
key?: string;
value: string;
label: string;
children?: VariableOptions;
};
export type VariableOptions = VariableOption[] | null;
export const nodesOptions = {
label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
value: '$jobsMapByNodeId',
useOptions(current, options) {
const upstreams = useAvailableUpstreams(current);
const result: VariableOption[] = [];
upstreams.forEach((node) => {
const instruction = instructions.get(node.type);
const subOptions = instruction.useVariables?.(node, options);
if (subOptions) {
result.push({
key: node.id.toString(),
value: node.id.toString(),
label: node.title ?? `#${node.id}`,
children: subOptions,
});
}
});
return result;
},
};
export const triggerOptions = {
label: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
value: '$context',
useOptions(current, options) {
const { workflow } = useFlowContext();
const trigger = triggers.get(workflow.type);
return trigger?.useVariables?.(workflow.config, options) ?? null;
},
};
export const scopeOptions = {
label: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
value: '$scopes',
useOptions(current, options) {
const scopes = useUpstreamScopes(current);
const result: VariableOption[] = [];
scopes.forEach((node) => {
const instruction = instructions.get(node.type);
const subOptions = instruction.useScopeVariables?.(node, options);
if (subOptions) {
result.push({
key: node.id.toString(),
value: node.id.toString(),
label: node.title ?? `#${node.id}`,
children: subOptions,
});
}
});
return result;
},
};
export const systemOptions = {
label: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
value: '$system',
useOptions(current, { types }) {
return [
...(!types || types.includes('date')
? [
{
key: 'now',
value: 'now',
label: `{{t("System time")}}`,
},
]
: []),
];
},
};
export const BaseTypeSets = {
boolean: new Set(['checkbox']),
number: new Set(['number', 'percent']),
string: new Set([
'input',
'password',
'email',
'phone',
'select',
'radioGroup',
'text',
'markdown',
'richText',
'expression',
'time',
]),
date: new Set(['date', 'createdAt', 'updatedAt']),
};
// { type: 'reference', options: { collection: 'users', multiple: false } }
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
function matchFieldType(field, type, depth): boolean {
const inputType = typeof type;
if (inputType === 'string') {
return BaseTypeSets[type]?.has(field.interface);
}
if (inputType === 'object' && type.type === 'reference') {
if (isAssociationField(field)) {
return (
type.options?.entity && (field.collectionName === type.options?.collection || type.options?.collection === '*')
);
} else if (field.isForeignKey) {
return (
(field.collectionName === type.options?.collection && field.name === 'id') ||
field.target === type.options?.collection
);
} else {
return false;
}
}
if (inputType === 'function') {
return type(field, depth);
}
return false;
}
function isAssociationField(field): boolean {
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
}
export function filterTypedFields(fields, types, depth = 1) {
if (!types) {
return fields;
}
return fields.filter((field) => {
if (
isAssociationField(field) &&
depth &&
filterTypedFields(useNormalizedFields(field.target), types, depth - 1).length
) {
return true;
}
return types.some((type) => matchFieldType(field, type, depth));
});
}
export function useWorkflowVariableOptions(options = {}) {
const compile = useCompile();
const current = useNodeContext();
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
const opts = typeof item.useOptions === 'function' ? item.useOptions(current, options).filter(Boolean) : null;
return {
label: compile(item.label),
value: item.value,
key: item.value,
children: compile(opts),
disabled: opts && !opts.length,
};
});
return result;
}
function useNormalizedFields(collectionName) {
const compile = useCompile();
const { getCollectionFields } = useCollectionManager();
const fields = getCollectionFields(collectionName);
const foreignKeyFields: any[] = [];
const otherFields: any[] = [];
fields.forEach((field) => {
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') {
const foreignKeyField = foreignKeyFields.find((f) => f.name === field.foreignKey);
if (foreignKeyField) {
otherFields.splice(i, 0, {
...field,
...foreignKeyField,
uiSchema: {
...field.uiSchema,
title: field.uiSchema?.title ? `${compile(field.uiSchema?.title)} ID` : foreignKeyField.name,
},
});
} 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,
},
});
}
} else if (field.type === 'context' && field.collectionName === 'users') {
const belongsToField =
otherFields.find((f) => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
otherFields.splice(i, 0, {
...field,
type: field.dataType,
interface: belongsToField.interface,
uiSchema: {
...belongsToField.uiSchema,
title: belongsToField.uiSchema?.title ? `${compile(belongsToField.uiSchema?.title)} ID` : field.name,
},
});
}
}
return otherFields.filter((field) => field.interface && !field.hidden);
}
export function useCollectionFieldOptions(options): VariableOption[] {
const { fields, collection, types, depth = 1 } = options;
const compile = useCompile();
const normalizedFields = useNormalizedFields(collection);
const computedFields = fields ?? normalizedFields;
const result: VariableOption[] = filterTypedFields(computedFields, types, depth)
.filter((field) => !isAssociationField(field) || depth)
.map((field) => {
const label = compile(field.uiSchema?.title || field.name);
return {
label,
key: field.name,
value: field.name,
children:
isAssociationField(field) && depth
? useCollectionFieldOptions({ collection: field.target, types, depth: depth - 1 })
: null,
field,
};
});
return result;
}