Skip to content

Commit a48b9d6

Browse files
authored
Merge pull request #739 from PROCEED-Labs/ms/global-data-object
Global Data Object Selector for User Tasks
2 parents 3cb08c6 + d260f48 commit a48b9d6

4 files changed

Lines changed: 404 additions & 17 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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;

src/management-system-v2/components/html-form-editor/_utils/ToolbarPlugin.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from '@lexical/list';
2828

2929
import {
30+
$createTextNode,
3031
$getSelection,
3132
$isRangeSelection,
3233
CAN_REDO_COMMAND,
@@ -42,6 +43,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
4243
import { VariableSetting } from '../elements/utils';
4344

4445
import { tokenize } from '@proceed/user-task-helper/src/tokenize';
46+
import DataObjectSelectionModal from './DataObjectSelectionModal';
4547

4648
const LowPriority = 1;
4749

@@ -58,6 +60,7 @@ export default function ToolbarPlugin() {
5860
const [linkType, setLinkType] = useState<'url' | 'variable'>('url');
5961
const [isOrderedList, setIsOrderedList] = useState(false);
6062
const [isUnorderedList, setIsUnorderedList] = useState(false);
63+
const [modalOpen, setModalOpen] = useState(false);
6164

6265
const $updateToolbar = useCallback(() => {
6366
const selection = $getSelection();
@@ -301,6 +304,30 @@ export default function ToolbarPlugin() {
301304
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')}
302305
/>
303306
</div>
307+
<Divider orientation="vertical" />
308+
<Button
309+
className="toolbar-item spaced"
310+
type="text"
311+
style={{ fontSize: '11px', fontWeight: 600 }}
312+
onClick={() => setModalOpen(true)}
313+
>
314+
Add Variable
315+
</Button>
316+
317+
<DataObjectSelectionModal
318+
open={modalOpen}
319+
onClose={() => setModalOpen(false)}
320+
onSelect={(variable) => {
321+
editor.update(() => {
322+
const selection = $getSelection();
323+
if ($isRangeSelection(selection)) {
324+
const token = `{%${variable}%}`;
325+
const textNode = $createTextNode(token);
326+
selection.insertNodes([textNode]);
327+
}
328+
});
329+
}}
330+
/>
304331
{isLink && (
305332
<div style={{ margin: '5px' }}>
306333
Link:

0 commit comments

Comments
 (0)