import { useCollectionManager, useCompile } from '@nocobase/client'; import { useFlowContext } from './FlowContext'; import { NAMESPACE } from './locale'; import { instructions, useAvailableUpstreams, useNodeContext } from './nodes'; import { triggers } from './triggers'; export type VariableOption = { key?: string; value: string; label: string; children?: VariableOptions; }; export type VariableOptions = VariableOption[] | null; const VariableTypes = [ { title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, value: '$jobsMapByNodeId', options(types) { const current = useNodeContext(); const upstreams = useAvailableUpstreams(current); const options: VariableOption[] = []; upstreams.forEach((node) => { const instruction = instructions.get(node.type); const subOptions = instruction.getOptions?.(node.config, types); if (subOptions) { options.push({ key: node.id.toString(), value: node.id.toString(), label: node.title ?? `#${node.id}`, children: subOptions, }); } }); return options; }, }, { title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`, value: '$context', options(types) { const { workflow } = useFlowContext(); const trigger = triggers.get(workflow.type); return trigger?.getOptions?.(workflow.config, types) ?? null; }, }, { title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`, value: '$system', options(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): Boolean { const inputType = typeof type; if (inputType === 'string') { return Boolean(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); } 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)); }); } export function useWorkflowVariableOptions(types?) { const compile = useCompile(); const options = VariableTypes.map((item: any) => { const opts = typeof item.options === 'function' ? item.options(types).filter(Boolean) : item.options; return { label: compile(item.title), value: item.value, key: item.value, children: compile(opts), disabled: opts && !opts.length, }; }); return options; } function useNormalizedFields(collectionName) { const compile = useCompile(); const { getCollection } = useCollectionManager(); const collection = getCollection(collectionName); if (!collection) { return []; } const { fields } = collection; 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 = fields ?? useNormalizedFields(collection); const result: VariableOption[] = filterTypedFields(normalizedFields, 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 }; }); return result; }