/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { CloseOutlined, DeleteOutlined } from '@ant-design/icons'; import { createForm, Field } from '@formily/core'; import { toJS } from '@formily/reactive'; import { ISchema, observer, useField, useForm } from '@formily/react'; import { Alert, App, Button, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd'; import { cloneDeep, get, set } from 'lodash'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ActionContextProvider, FormProvider, SchemaComponent, SchemaInitializerItemType, Variable, css, cx, useAPIClient, useActionContext, useCancelAction, useCompile, usePlugin, useResourceActionContext, } from '@nocobase/client'; import { parse, str2moment } from '@nocobase/utils/client'; import WorkflowPlugin from '..'; import { AddButton } from '../AddNodeContext'; import { useFlowContext } from '../FlowContext'; import { DrawerDescription } from '../components/DrawerDescription'; import { StatusButton } from '../components/StatusButton'; import { JobStatusOptionsMap } from '../constants'; import { useGetAriaLabelOfAddButton } from '../hooks/useGetAriaLabelOfAddButton'; import { lang } from '../locale'; import useStyles from '../style'; import { UseVariableOptions, VariableOption, WorkflowVariableInput } from '../variable'; export type NodeAvailableContext = { engine: WorkflowPlugin; workflow: object; upstream: object; branchIndex: number; }; type Config = Record; type Options = { label: string; value: any }[]; export abstract class Instruction { title: string; type: string; group: string; description?: string; /** * @deprecated migrate to `presetFieldset` instead */ options?: { label: string; value: any; key: string }[]; fieldset: Record; /** * @experimental */ presetFieldset?: Record; /** * To presentation if the instruction is creating a branch * @experimental */ branching?: boolean | Options | ((config: Config) => boolean | Options); /** * @experimental */ view?: ISchema; scope?: Record; components?: Record; Component?(props): JSX.Element; /** * @experimental */ createDefaultConfig?(): Config { return {}; } useVariables?(node, options?: UseVariableOptions): VariableOption; useScopeVariables?(node, options?): VariableOption[]; useInitializers?(node): SchemaInitializerItemType | null; /** * @experimental */ isAvailable?(ctx: NodeAvailableContext): boolean; end?: boolean | ((node) => boolean); testable?: boolean; } function useUpdateAction() { const form = useForm(); const api = useAPIClient(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const data = useNodeContext(); const { workflow } = useFlowContext(); return { async run() { if (workflow.executed) { message.error(lang('Node in executed workflow cannot be modified')); return; } await form.submit(); await api.resource('flow_nodes').update?.({ filterByTk: data.id, values: { config: form.values, }, }); form.setInitialValues(toJS(form.values)); ctx.setFormValueChanged(false); ctx.setVisible(false); refresh(); }, }; } export const NodeContext = React.createContext({}); export function useNodeContext() { return useContext(NodeContext); } export function useNodeSavedConfig(keys = []) { const node = useNodeContext(); return keys.some((key) => get(node.config, key) != null); } /** * @experimental */ export function useAvailableUpstreams(node, filter?) { const stack: any[] = []; if (!node) { return []; } for (let current = node.upstream; current; current = current.upstream) { if (typeof filter !== 'function' || filter(current)) { stack.push(current); } } return stack; } /** * @experimental */ export function useUpstreamScopes(node) { const stack: any[] = []; if (!node) { return []; } for (let current = node; current; current = current.upstream) { if (current.upstream && current.branchIndex != null) { stack.push(current.upstream); } } return stack; } export function Node({ data }) { const { styles } = useStyles(); const { getAriaLabel } = useGetAriaLabelOfAddButton(data); const workflowPlugin = usePlugin(WorkflowPlugin); const { Component = NodeDefaultView, end } = workflowPlugin.instructions.get(data.type) ?? {}; return (
{!end || (typeof end === 'function' && !end(data)) ? ( ) : (
)}
); } export function RemoveButton() { const { t } = useTranslation(); const api = useAPIClient(); const { workflow, nodes, refresh } = useFlowContext() ?? {}; const current = useNodeContext(); const { modal } = App.useApp(); const resource = api.resource('flow_nodes'); const onOk = useCallback(async () => { await resource.destroy?.({ filterByTk: current.id, }); refresh(); }, [current.id, refresh, resource]); const onRemove = useCallback(async () => { const usingNodes = nodes.filter((node) => { if (node === current) { return false; } const template = parse(node.config); const refs = template.parameters.filter( ({ key }) => key.startsWith(`$jobsMapByNodeKey.${current.key}.`) || key === `$jobsMapByNodeKey.${current.key}`, ); return refs.length; }); if (usingNodes.length) { modal.error({ title: lang('Can not delete'), content: lang( 'The result of this node has been referenced by other nodes ({{nodes}}), please remove the usage before deleting.', { nodes: usingNodes.map((item) => `#${item.id}`).join(', ') }, ), }); return; } const hasBranches = !nodes.find((item) => item.upstream === current && item.branchIndex != null); const message = hasBranches ? t('Are you sure you want to delete it?') : lang('This node contains branches, deleting will also be preformed to them, are you sure?'); modal.confirm({ title: t('Delete'), content: message, onOk, }); }, [current, modal, nodes, onOk, t]); if (!workflow) { return null; } return workflow.executed ? null : (