|
| 1 | +import React, { useEffect, useMemo, useState } from 'react'; |
| 2 | +import { Modal, Button, List, Tag, Tree, Tabs } from 'antd'; |
| 3 | +import type { DataNode } from 'antd/es/tree'; |
| 4 | +import { buildScopedTree, ScopeFilter } from '@/lib/helpers/global-data-tree'; |
| 5 | +import { getDeepConfigurationById } from '@/lib/data/db/machine-config'; |
| 6 | +import ProcessVariableForm from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/variable-definition/process-variable-form'; |
| 7 | +import { ProcessVariable, textFormatMap, typeLabelMap } from '@/lib/process-variable-schema'; |
| 8 | +import useEditorStateStore from '../use-editor-state-store'; |
| 9 | +import { useEnvironment } from '@/components/auth-can'; |
| 10 | +import { useQuery } from '@tanstack/react-query'; |
| 11 | + |
| 12 | +type AllowedTypes = React.ComponentProps<typeof ProcessVariableForm>['allowedTypes']; |
| 13 | + |
| 14 | +type Props = { |
| 15 | + open: boolean; |
| 16 | + onClose: () => void; |
| 17 | + onSelect: ( |
| 18 | + variable: string, |
| 19 | + variableType?: NonNullable<AllowedTypes>[number], |
| 20 | + variableTextFormat?: keyof typeof textFormatMap, |
| 21 | + ) => void; |
| 22 | + allowedTypes?: AllowedTypes; |
| 23 | + currentVariable?: string; |
| 24 | +}; |
| 25 | + |
| 26 | +const detectScope = (variable: string): ScopeFilter => { |
| 27 | + if (variable.includes('@process-initiator')) { |
| 28 | + return '@process-initiator'; |
| 29 | + } |
| 30 | + |
| 31 | + if (variable.includes('@organization')) { |
| 32 | + return '@organization'; |
| 33 | + } |
| 34 | + |
| 35 | + return '@worker'; |
| 36 | +}; |
| 37 | + |
| 38 | +const DataObjectSelectionModal: React.FC<Props> = ({ |
| 39 | + open, |
| 40 | + onClose, |
| 41 | + onSelect, |
| 42 | + currentVariable, |
| 43 | + allowedTypes, |
| 44 | +}) => { |
| 45 | + const environment = useEnvironment(); |
| 46 | + const [scope, setScope] = useState<ScopeFilter>('@worker'); |
| 47 | + const [selectedKey, setSelectedKey] = useState<string>(); |
| 48 | + const [showVariableForm, setShowVariableForm] = useState(false); |
| 49 | + const [activeTab, setActiveTab] = useState<'process' | 'global'>('process'); |
| 50 | + const [selectedProcessVar, setSelectedProcessVar] = useState<string>(); |
| 51 | + const { variables, updateVariables } = useEditorStateStore((state) => state); |
| 52 | + |
| 53 | + // fetch config |
| 54 | + const { data: config } = useQuery({ |
| 55 | + queryKey: ['deepConfig', environment.spaceId], |
| 56 | + queryFn: () => getDeepConfigurationById(environment.spaceId), |
| 57 | + enabled: open, |
| 58 | + }); |
| 59 | + |
| 60 | + // initialize with current variable on modal opening |
| 61 | + useEffect(() => { |
| 62 | + if (!open) return; |
| 63 | + if (currentVariable?.startsWith('@global')) { |
| 64 | + const detectedScope = detectScope(currentVariable); |
| 65 | + |
| 66 | + setActiveTab('global'); |
| 67 | + setScope(detectedScope); |
| 68 | + setSelectedKey(currentVariable); |
| 69 | + setSelectedProcessVar(undefined); |
| 70 | + } else { |
| 71 | + setActiveTab('process'); |
| 72 | + setSelectedProcessVar(currentVariable); |
| 73 | + setSelectedKey(undefined); |
| 74 | + } |
| 75 | + }, [open, currentVariable]); |
| 76 | + |
| 77 | + const treeData: DataNode[] = useMemo(() => { |
| 78 | + if (!config) return []; |
| 79 | + return buildScopedTree(config, scope); |
| 80 | + }, [config, scope]); |
| 81 | + |
| 82 | + const resetState = () => { |
| 83 | + setSelectedKey(undefined); |
| 84 | + setSelectedProcessVar(undefined); |
| 85 | + setActiveTab('process'); |
| 86 | + setScope('@worker'); |
| 87 | + }; |
| 88 | + |
| 89 | + const currentSelection = activeTab === 'process' ? selectedProcessVar : selectedKey; |
| 90 | + const isSelectionChanged = !!currentSelection && currentSelection !== currentVariable; |
| 91 | + |
| 92 | + const handleOk = () => { |
| 93 | + if (activeTab === 'process' && selectedProcessVar) { |
| 94 | + const variable = variables?.find((v) => v.name === selectedProcessVar); |
| 95 | + onSelect(selectedProcessVar, variable?.dataType, variable?.textFormat); |
| 96 | + } else if (activeTab === 'global' && selectedKey) { |
| 97 | + onSelect(selectedKey, 'string'); |
| 98 | + } |
| 99 | + resetState(); |
| 100 | + onClose(); |
| 101 | + }; |
| 102 | + |
| 103 | + const handleCancel = () => { |
| 104 | + resetState(); |
| 105 | + onClose(); |
| 106 | + }; |
| 107 | + |
| 108 | + const scopeFilters: { label: ScopeFilter; value: ScopeFilter }[] = [ |
| 109 | + { label: '@worker', value: '@worker' }, |
| 110 | + { label: '@process-initiator', value: '@process-initiator' }, |
| 111 | + { label: '@organization', value: '@organization' }, |
| 112 | + ]; |
| 113 | + |
| 114 | + return ( |
| 115 | + <> |
| 116 | + <Modal |
| 117 | + title="Add Variable" |
| 118 | + open={open} |
| 119 | + onCancel={handleCancel} |
| 120 | + onOk={handleOk} |
| 121 | + okText="OK" |
| 122 | + okButtonProps={{ |
| 123 | + disabled: !isSelectionChanged, |
| 124 | + }} |
| 125 | + width={500} |
| 126 | + > |
| 127 | + <Tabs |
| 128 | + activeKey={activeTab} |
| 129 | + onChange={(key) => { |
| 130 | + setActiveTab(key as 'process' | 'global'); |
| 131 | + }} |
| 132 | + items={[ |
| 133 | + { |
| 134 | + key: 'process', |
| 135 | + label: 'Process Variables', |
| 136 | + children: ( |
| 137 | + <> |
| 138 | + <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}> |
| 139 | + <Button onClick={() => setShowVariableForm(true)}>Add Variable</Button> |
| 140 | + </div> |
| 141 | + <List |
| 142 | + bordered |
| 143 | + style={{ maxHeight: 300, overflowY: 'auto' }} |
| 144 | + dataSource={ |
| 145 | + allowedTypes |
| 146 | + ? variables?.filter((v) => allowedTypes.includes(v.dataType)) ?? [] |
| 147 | + : variables ?? [] |
| 148 | + } |
| 149 | + renderItem={(v: ProcessVariable) => ( |
| 150 | + <List.Item |
| 151 | + style={{ |
| 152 | + cursor: 'pointer', |
| 153 | + backgroundColor: selectedProcessVar === v.name ? '#e6f4ff' : undefined, |
| 154 | + }} |
| 155 | + onClick={() => { |
| 156 | + setSelectedProcessVar(v.name); |
| 157 | + // clear global selection |
| 158 | + setSelectedKey(undefined); |
| 159 | + }} |
| 160 | + > |
| 161 | + <span>{v.name}</span> |
| 162 | + <Tag style={{ marginLeft: 8 }}>{typeLabelMap[v.dataType]}</Tag> |
| 163 | + </List.Item> |
| 164 | + )} |
| 165 | + /> |
| 166 | + </> |
| 167 | + ), |
| 168 | + }, |
| 169 | + { |
| 170 | + key: 'global', |
| 171 | + label: 'Global Data Object', |
| 172 | + children: ( |
| 173 | + <> |
| 174 | + <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}> |
| 175 | + {scopeFilters.map((f) => ( |
| 176 | + <Button |
| 177 | + key={f.value} |
| 178 | + type={scope === f.value ? 'primary' : 'default'} |
| 179 | + size="small" |
| 180 | + onClick={() => setScope(f.value)} |
| 181 | + > |
| 182 | + {f.label} |
| 183 | + </Button> |
| 184 | + ))} |
| 185 | + </div> |
| 186 | + <div |
| 187 | + style={{ |
| 188 | + maxHeight: 300, |
| 189 | + overflowY: 'auto', |
| 190 | + border: '1px solid #d9d9d9', |
| 191 | + borderRadius: 6, |
| 192 | + padding: 8, |
| 193 | + }} |
| 194 | + > |
| 195 | + <Tree |
| 196 | + treeData={treeData} |
| 197 | + selectedKeys={selectedKey ? [selectedKey] : []} |
| 198 | + onSelect={(keys) => { |
| 199 | + setSelectedKey(keys[0] as string | undefined); |
| 200 | + setSelectedProcessVar(undefined); |
| 201 | + }} |
| 202 | + defaultExpandAll |
| 203 | + /> |
| 204 | + </div> |
| 205 | + </> |
| 206 | + ), |
| 207 | + }, |
| 208 | + ]} |
| 209 | + /> |
| 210 | + </Modal> |
| 211 | + |
| 212 | + <ProcessVariableForm |
| 213 | + open={showVariableForm} |
| 214 | + variables={variables ?? []} |
| 215 | + allowedTypes={allowedTypes} |
| 216 | + onSubmit={(newVar) => { |
| 217 | + updateVariables([...(variables ?? []), newVar]); |
| 218 | + setSelectedProcessVar(newVar.name); |
| 219 | + setShowVariableForm(false); |
| 220 | + }} |
| 221 | + onCancel={() => setShowVariableForm(false)} |
| 222 | + /> |
| 223 | + </> |
| 224 | + ); |
| 225 | +}; |
| 226 | + |
| 227 | +export default DataObjectSelectionModal; |
0 commit comments