ebta-workflow-prod 1.1.3 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.eslintrc.js +7 -0
  2. package/.vscode/extensions.json +3 -0
  3. package/.vscode/settings.json +3 -0
  4. package/README.md +26 -0
  5. package/build/2983859c0d77824ec8db.svg +1 -0
  6. package/build/4168d659d55fd94368bc.svg +1 -0
  7. package/build/641b2c17e92b0e885b9f.svg +1 -0
  8. package/build/882445a26a4f3964786f.svg +1 -0
  9. package/build/activitypack.json +1 -1
  10. package/build/b280e967312a55101032.svg +1 -0
  11. package/build/main.js +2 -2
  12. package/build/main.js.LICENSE.txt +99 -0
  13. package/certs/localhost+2-key.pem +28 -0
  14. package/certs/localhost+2.pem +26 -0
  15. package/index.html +58 -0
  16. package/package.json +43 -7
  17. package/public/img/background.svg +3 -3
  18. package/public/img/compass.svg +3 -3
  19. package/public/img/grab.svg +3 -3
  20. package/public/img/grabbing.svg +3 -3
  21. package/public/img/sprites.svg +14 -14
  22. package/src/activities/AppendObjects.ts +40 -0
  23. package/src/activities/Base64ToFile.ts +56 -0
  24. package/src/activities/CreateGraphicLayer.ts +85 -0
  25. package/src/activities/DrapGeometry.ts +99 -0
  26. package/src/activities/GetElementByDataLayoutID.ts +58 -0
  27. package/src/activities/GetSesionStorage.ts +36 -0
  28. package/src/activities/GetTaskStatus.ts +56 -0
  29. package/src/activities/GetUserToken.ts +102 -0
  30. package/src/activities/GetvGISDetails copy.ts +110 -0
  31. package/src/activities/GetvGISDetails.ts +110 -0
  32. package/src/activities/HTMLextractor.ts +188 -0
  33. package/src/activities/LegendCreator.ts +155 -0
  34. package/src/activities/MergeObjects.ts +61 -0
  35. package/src/activities/PDFMapGenerator.ts +729 -0
  36. package/src/activities/RequestChecker.ts +94 -0
  37. package/src/activities/Sample.ts +41 -0
  38. package/src/activities/SetSessionStorage.ts +57 -0
  39. package/src/activities/SliceLine.ts +61 -0
  40. package/src/elements/AttributeFilter.tsx +1867 -0
  41. package/src/elements/CircularSlider.tsx +67 -0
  42. package/src/elements/CsColorPicker.tsx +140 -0
  43. package/src/elements/DropDownMenu.tsx +282 -0
  44. package/src/elements/HeaderwithHelp.tsx +178 -0
  45. package/src/elements/ItemSearcher.tsx +1035 -0
  46. package/src/elements/ItemSearcher_v2.tsx +1071 -0
  47. package/src/elements/LineChartMui.tsx +50 -0
  48. package/src/elements/MenueBar.tsx +118 -0
  49. package/src/elements/MultipleAutoCompleteCS.tsx +145 -0
  50. package/src/elements/SampleInput.tsx +38 -0
  51. package/src/elements/SingleAutoCompleteCS.tsx +133 -0
  52. package/src/elements/TableRadioGoup.tsx +163 -0
  53. package/src/elements/Timeline.tsx +104 -0
  54. package/src/elements/ToggleButtonGroup.tsx +129 -0
  55. package/src/elements/Viewer360.tsx +71 -0
  56. package/src/elements/modules/AttributeFilterEasyComp.tsx +253 -0
  57. package/src/index.ts +43 -0
  58. package/src/main.ts +15 -0
  59. package/src/styles/imageSlider.css +34 -0
  60. package/tsconfig.json +12 -0
  61. package/uuid.js +7 -0
@@ -0,0 +1,1071 @@
1
+ /* eslint-disable prefer-const */
2
+ import * as React from "react";
3
+ import { FormElementProps, FormElementRegistration } from "@vertigis/workflow";
4
+ import { styled } from '@mui/material/styles';
5
+ import List from '@vertigis/web/ui/List';
6
+ import ListItemButton from '@vertigis/web/ui/ListItemButton';
7
+ import ListItemIcon from '@vertigis/web/ui/ListItemIcon';
8
+ import ListItemText from '@vertigis/web/ui/ListItemText';
9
+ import Collapse from '@vertigis/web/ui/Collapse';
10
+ import FolderCopyIcon from '@mui/icons-material/FolderCopy';
11
+ import ExpandLess from '@mui/icons-material/ExpandLess';
12
+ import ExpandMore from '@mui/icons-material/ExpandMore';
13
+ import SearchIcon from '@vertigis/web/ui/icons/Search';
14
+ import InputBase from '@mui/material/InputBase';
15
+ import Box from "@vertigis/web/ui/Box"
16
+ import Typography from '@vertigis/web/ui/Typography';
17
+ import { createTheme } from "@mui/material/styles";
18
+ import { ThemeProvider } from "@emotion/react";
19
+ import Checkbox from '@vertigis/web/ui/Checkbox';
20
+ import IconButton from '@vertigis/web/ui/IconButton';
21
+ import { Category, Clear as ClearIcon } from '@mui/icons-material';
22
+ import Chip from '@mui/material/Chip';
23
+ import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
24
+
25
+
26
+ /**
27
+ * The generic type argument provided to `FormElementProps<T>` controls the type
28
+ * of `props.value` and `props.setValue(value)`. If your element doesn't need a
29
+ * `value`, you can omit the type argument.
30
+ *
31
+ * You can also declare additional public properties of your element by adding
32
+ * properties to this interface. The properties will be managed by the Workflow
33
+ * runtime, and passed in as `props`. You can update the properties by using
34
+ * `props.setProperty(key, value)`.
35
+ */
36
+ interface ItemSearcher_V2Props extends FormElementProps<string> {
37
+
38
+ itemData: Category[]; // Structured data passed as prop
39
+ setting?: settingConfig;
40
+ request?: { type: string, timestamp: number, data?: any };
41
+ openCategories: Set<string>;//Status storage property only
42
+ searchQuery:string;//Status storage property only
43
+ selection:{ [key: string]: { [key: string]: Item[] } }//Status storage property only
44
+ loadDataTime:number;//provide cabability of loading a totally different data
45
+ }
46
+
47
+
48
+ /**
49
+ * Provide strictly structured item data to display them and search.
50
+ * @displayName ItemSearcher_V2
51
+ * @description Display list of items and allow user to search based on name, description & tag.
52
+ * @param props collapseIcon?: string;
53
+ expandIcon?: string;//replacing the drop down icon on right
54
+ checkbox?: boolean;//If true, make items selectable checkbox rather than click
55
+ checkboxGroup?:boolean;//If true, make category itself selectable
56
+ triggerChange?:boolean;//If true, trigger changed event when being checkbox mode
57
+ simpleSearch?: boolean; // decide two mode in search result: clear Search show items only and rank them by relativity. If false, use filter on original Hierarchy, show items has keyword or group whose name has keyword and all its children.
58
+ initialCollapse?: boolean // decide whether collapse all group at beginning, default is collapse all
59
+ */
60
+
61
+ const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
62
+ <Tooltip {...props} classes={{ popper: className }} />
63
+ ))(({ theme }) => ({
64
+ [`& .${tooltipClasses.tooltip}`]: {
65
+ boxShadow: theme.shadows[1],
66
+ fontSize: 10,
67
+ },
68
+ }));
69
+
70
+ const Search = styled('div')(({ theme }) => ({
71
+ position: 'relative',
72
+ borderRadius: theme.shape.borderRadius,
73
+ backgroundColor: 'var(--alertGrayForeground)',
74
+ marginRight: theme.spacing(2),
75
+ marginLeft: 0,
76
+ width: '100%',
77
+ [theme.breakpoints.up('sm')]: {
78
+ marginLeft: theme.spacing(3),
79
+ width: 'auto',
80
+ },
81
+ }));
82
+
83
+ const SearchIconWrapper = styled('div')(({ theme }) => ({
84
+ padding: theme.spacing(0, 2),
85
+ height: '100%',
86
+ position: 'absolute',
87
+ pointerEvents: 'none',
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+
92
+ }));
93
+
94
+ interface Item {
95
+ name: string;
96
+ description?: string;
97
+ tag?: string[];
98
+ value?: any;
99
+ img?: string;
100
+ score?: number;
101
+ parentValue?: any;
102
+ action?: "displayLayer" | "tick";
103
+ }
104
+
105
+ interface Category {
106
+ name: string;
107
+ items?: Item[];
108
+ childCategory?: Category[];
109
+ collapsed?: boolean; //true to hide children. It's above setting Config initial collapse.
110
+ img?: string;
111
+ checkbox?: boolean; //If true, show checkbox that makes all child items selected/unselected
112
+ index?: number; //as a child Category, indicate the position of this child category compared to other sibling items/categories
113
+ parentCate?: string;
114
+ description?:string;
115
+ }
116
+
117
+ interface settingConfig {
118
+ checkbox?: boolean; //If true, show checkbox rather than raise click event
119
+ collapseIcon?: string;//replace the fold up icon on right
120
+ expandIcon?: string;//replacing the drop down icon on right
121
+ triggerChange?: boolean;//If true, trigger changed event when under checkbox mode
122
+ triggerClick?: boolean;//If true, trigger click event when under checkbox mode
123
+ simpleSearch?: boolean; // decide two mode in search result: clear Search show items only and rank them by relativity. If false, use filter on original Hierarchy, show items has keyword or group whose name has keyword and all its children.
124
+ initialCollapse?: boolean;// decide whether collapse all group at beginning, default is collapse all
125
+ tooltip?: "tag" | "description" | "value" | Array<"tag" | "description" | "value">;//tooltip of item content. Can be any combination of description, value (link etc), tag (property unvisible but used for searching)
126
+ searchHolder?: string; //Text placeholder used in search bar
127
+ requestLayer?: boolean;//true if the item is a layer group url and need further process
128
+ reset?: number; //If loading another totally different content, give timestamp number for reset parameter to reload itemData
129
+ showCount?: boolean; // display item count after category name
130
+ flatResut?: boolean;//decide the output to be result/layer or result/group/layer
131
+ matchAnyKeyword?:boolean;//When search keywords (detected seperation by many spliters), fulfil all keywords or any of keyword.
132
+ maxSelectableItems?:number;//maximum number of items that can be selected at the same time. If 0, no limit.
133
+ forceSort?:boolean;//if true, sort the items in the same order as the original data. If false, sort by name.
134
+ }
135
+
136
+
137
+
138
+ const StyledInputBase = styled(InputBase)(({ theme }) => ({
139
+ color: 'inherit',
140
+ '& .MuiInputBase-input': {
141
+ padding: theme.spacing(1, 1, 1, 0),
142
+ paddingLeft: `calc(1em + ${theme.spacing(4)})`,
143
+ transition: theme.transitions.create('width'),
144
+ width: '100%',
145
+ [theme.breakpoints.up('md')]: {
146
+ width: '100%',
147
+ },
148
+ },
149
+
150
+ }));
151
+
152
+ const defaultTheme = createTheme({
153
+ components: {
154
+ MuiListItemText: {
155
+ styleOverrides: {
156
+ secondary: {
157
+ color: 'inherit',
158
+ },
159
+ },
160
+ },
161
+ },
162
+
163
+ typography: {
164
+ fontSize: 18
165
+ },
166
+ });
167
+
168
+
169
+
170
+
171
+
172
+ // Function to get all items from category and its subcategories
173
+ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
174
+ const { raiseEvent ,openCategories,searchQuery,selection,loadDataTime, setValue, setProperty} = props;
175
+ if (openCategories ==undefined){setProperty("openCategories" as any,new Set())}
176
+ if (searchQuery ==undefined){setProperty("searchQuery" as any,'')}
177
+ if (selection ==undefined){setProperty("selection" as any,{})}
178
+ if (loadDataTime ==undefined){setProperty("loadDataTime" as any,0)}
179
+ const [data, setData] = React.useState<Category[]>(props.itemData);
180
+ const preSearchOpenCategories = React.useRef<Set<string>>(new Set());
181
+ const currentOpenCategories = React.useRef<Set<string> >(new Set());
182
+ const prevSearchValue = React.useRef<string>('')
183
+ const [filteredData, setFilteredData] = React.useState<Category[]>([]); // New state for filtered data
184
+ const requestTime = React.useRef<number | null>(null);
185
+ // Handle search input changes
186
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
187
+ setProperty("searchQuery" as any,event.target.value); // Update search query
188
+ };
189
+
190
+
191
+ function getTotalSelectedItems() {
192
+ let total = 0;
193
+ for (let catKey in selection) {
194
+ for (let subcatKey in selection[catKey]) {
195
+ total += selection[catKey][subcatKey].length;
196
+ }
197
+ }
198
+ return total;
199
+ }
200
+
201
+ function expandIcon(collapseIcon: string, expandIcon: string, openCats, catName: string) {
202
+
203
+ openCategories.has(catName) ? <ExpandLess /> : <ExpandMore />
204
+ if (Boolean(expandIcon) && openCats.has(catName)) return (<Box
205
+ component="img"
206
+ sx={{
207
+ height: 18,
208
+ width: 18,
209
+ marginLeft: '8px',
210
+
211
+ }}
212
+ alt="The category icon"
213
+ src={expandIcon} // Use the item's img property
214
+ />)
215
+ else if (Boolean(collapseIcon) && !openCats.has(catName)) return (<Box
216
+ component="img"
217
+ sx={{
218
+ height: 18,
219
+ width: 18,
220
+ marginLeft: '8px',
221
+ }}
222
+ alt="The category icon"
223
+ src={collapseIcon} // Use the item's img property
224
+ />)
225
+ else if (!expandIcon && openCats.has(catName)) return (<ExpandLess />)
226
+ else if (!expandIcon && !openCats.has(catName)) return (<ExpandMore />)
227
+ }
228
+
229
+
230
+ function findCategory(item: Item) {
231
+ return props.itemData.find(category =>
232
+ category.items?.some(existingItem => existingItem.value == item.value)
233
+ ) || data[0];
234
+ }
235
+
236
+ function tickItem(item: Item, category: Category) {
237
+ let selectCopy = structuredClone(selection)
238
+
239
+ if (isSelected(item, category.name)) {
240
+ //uncheck item
241
+ for (let key in selectCopy) {
242
+ let element = selectCopy[key]
243
+ // Check if the element is an object containing nested arrays of Items
244
+ for (let subKey in element) {
245
+ if (element[subKey]) {
246
+ let index = element[subKey].findIndex(a => a.name === item.name && (a.value === item.value || a.description === item.description));
247
+ if (index !== -1) {
248
+ element[subKey].splice(index, 1);
249
+ // If the subcategory is empty after removal, delete the subcategory
250
+ if (element[subKey].length === 0) {
251
+ delete element[subKey];
252
+ }
253
+ }
254
+ }
255
+ }
256
+ if (Object.values(selectCopy[key]).length == 0) {
257
+ delete selectCopy[key];
258
+ }// If the category is empty after removal, delete the category
259
+ }
260
+ }
261
+ else {
262
+ // Check if we've reached maximum selectable items limit
263
+ const totalSelectedItems = getTotalSelectedItems();
264
+ if (props.setting?.maxSelectableItems && totalSelectedItems >= props.setting.maxSelectableItems) {
265
+ // Prevent adding more items if limit is reached
266
+ raiseEvent("error" as any, JSON.stringify({ type: "maxItemsReached", message: `You can only select up to ${props.setting.maxSelectableItems} items` }));
267
+ return;
268
+ }
269
+
270
+ // Add the item to the specified category
271
+ if (category) {
272
+ if (category.parentCate) {
273
+ if (!selectCopy[category.parentCate]) {
274
+ selectCopy[category.parentCate] = {};
275
+ }
276
+ if (!selectCopy[category.parentCate][category.name]) {
277
+ selectCopy[category.parentCate][category.name] = [];
278
+ }
279
+ selectCopy[category.parentCate][category.name].push((delete item.score, item));
280
+ }
281
+ else {
282
+ if (!selectCopy[category.name]) {
283
+ selectCopy[category.name] = {};
284
+ }
285
+ if (!selectCopy[category.name]["directItem"]) {
286
+ selectCopy[category.name]["directItem"] = [];
287
+ }
288
+ selectCopy[category.name]["directItem"].push((delete item.score, item));
289
+ }
290
+ }
291
+ }
292
+ setProperty("selection" as any, selectCopy)
293
+ }
294
+
295
+ // Handle click on items to raise an event
296
+ const handleItemClick = (item: Item, category: Category) => {
297
+ if (props.setting?.checkbox) {
298
+ if (props.setting?.triggerChange) {
299
+ raiseEvent("changed", JSON.stringify(item)); // Pass the entire item data structure
300
+ }
301
+ if (props.setting?.triggerClick) {
302
+ raiseEvent("clicked", JSON.stringify(item)); // Pass the entire item data structure
303
+ }
304
+ if (props.setting?.requestLayer && item.action == "displayLayer") {
305
+ raiseEvent("clicked", JSON.stringify({ action: "openLayer", data: item })); // Pass the entire item data structure
306
+ }
307
+ if (!props.setting?.requestLayer || !(item.action == "displayLayer")) {
308
+ tickItem(item, category)
309
+ }
310
+ }
311
+ else {
312
+ raiseEvent("clicked", JSON.stringify(item)); // Pass the entire item data structure
313
+ }
314
+ };
315
+
316
+ const handleCategoryCheck = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, category: Category) => {
317
+ event.stopPropagation();
318
+ let selectCopy = structuredClone(selection);
319
+
320
+ // For "allSelected" case, just remove all items (no limit check needed)
321
+ if (isChildSelected(category) === "allSelected") {
322
+ if (!category.parentCate) {
323
+ delete selectCopy[category.name];
324
+ } else {
325
+ delete selectCopy[category.parentCate][category.name];
326
+ }
327
+ }
328
+ // For adding items, check limit
329
+ else {
330
+ const currentTotal = getTotalSelectedItems();
331
+ let itemsToAdd = 0;
332
+
333
+ // Calculate how many items would be added
334
+ if (!category.parentCate) {
335
+ if (category.items) {
336
+ for (let item of category.items) {
337
+ if (!isSelected(item, category.name)) {
338
+ itemsToAdd++;
339
+ }
340
+ }
341
+ }
342
+ } else {
343
+ if (category.items) {
344
+ for (let item of category.items) {
345
+ if (!isSelected(item, category.name)) {
346
+ itemsToAdd++;
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ // Check if adding these items would exceed the limit
353
+ if (props.setting?.maxSelectableItems && (currentTotal + itemsToAdd > props.setting.maxSelectableItems)) {
354
+ raiseEvent("error" as any, JSON.stringify({ type: "maxItemsReached", message: `You can only select up to ${props.setting.maxSelectableItems} items` }));
355
+ return;
356
+ }
357
+
358
+ // Proceed with adding items if within limit
359
+ if (!category.parentCate) {
360
+ //only one level
361
+ if (category.items) {
362
+ for (let item of category.items) {
363
+ if (!isSelected(item, category.name)) {
364
+ // If item is not selected, check it
365
+ if (!selectCopy[category.name]) {
366
+ selectCopy[category.name] = {}
367
+ selectCopy[category.name]["directItem"] = [];
368
+ }
369
+ selectCopy[category.name]["directItem"].push((delete item.score, item));
370
+ }
371
+ }
372
+ }
373
+ } else {
374
+ //two levels
375
+ if (category.items) {
376
+ for (let item of category.items) {
377
+ if (!isSelected(item, category.name)) {
378
+ if (!selectCopy[category.parentCate]) {
379
+ selectCopy[category.parentCate] = {};
380
+ }
381
+ // If item is not selected, check it
382
+ if (!selectCopy[category.parentCate][category.name]) {
383
+ selectCopy[category.parentCate][category.name] = [];
384
+ }
385
+ selectCopy[category.parentCate][category.name].push((delete item.score, item));
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ // Update selection state
393
+ setProperty("selection" as any, selectCopy);
394
+ };
395
+
396
+
397
+ // Handle category toggle open/close manually
398
+ const handleCategoryToggle = (categoryName: string) => {
399
+ const updatedOpenCategories = new Set(openCategories); // Clone current state
400
+
401
+ if (updatedOpenCategories.has(categoryName)) {
402
+ updatedOpenCategories.delete(categoryName); // Close category
403
+ } else {
404
+ updatedOpenCategories.add(categoryName); // Open category
405
+ }
406
+
407
+ setProperty("openCategories" as any, updatedOpenCategories); // Update with new value
408
+ };
409
+
410
+
411
+ function countAllItems(category: Category): number {
412
+ let itemCount = 0;
413
+
414
+ // Add items of the current category (if any)
415
+ if (category.items) {
416
+ itemCount += category.items.length;
417
+ }
418
+
419
+ // Recursively count items in child categories (if any)
420
+ if (category.childCategory) {
421
+ category.childCategory.forEach(childCategory => {
422
+ itemCount += countAllItems(childCategory); // Recursive call for each child category
423
+ });
424
+ }
425
+
426
+ return itemCount;
427
+ }
428
+
429
+ function isSelected(item: Item, cateName?: string) {
430
+ // Type guard to check if an element is an Item
431
+ if (!cateName) {
432
+ for (let key in selection) {
433
+ for (let subkey in selection[key]) {
434
+ let index = selection[key][subkey].findIndex(a => a.name === item.name && a.value === item.value);
435
+ if (index !== -1) {
436
+ return true;
437
+ }
438
+ }
439
+ }
440
+ }
441
+ else {
442
+ if (selection[cateName]?.["directItem"]) {
443
+ let index = selection[cateName]["directItem"].findIndex(a => a.name === item.name && a.value === item.value);
444
+ if (index !== -1) {
445
+ return true;
446
+ }
447
+ }
448
+
449
+ for (let key in selection) {
450
+ if (selection[key][cateName]) {
451
+ let index = selection[key][cateName].findIndex(a => a.name === item.name && a.value === item.value);
452
+ if (index !== -1) {
453
+ return true;
454
+ }
455
+ }
456
+ }
457
+ }
458
+ return false;
459
+ }
460
+
461
+
462
+ const isChildSelected = (category: Category) => {
463
+ let allSelected = true;
464
+ let partSelected = false;
465
+ let noneSelected = true;
466
+
467
+ // Check items of the current category
468
+ if (category.items) {
469
+ for (let item of category.items) {
470
+ const selected = isSelected(item, category.name);
471
+ if (selected) {
472
+ partSelected = true;
473
+ noneSelected = false;
474
+ } else {
475
+ allSelected = false;
476
+ }
477
+ }
478
+ }
479
+
480
+ // Recursively check child categories
481
+ if (category.childCategory) {
482
+ for (let child of category.childCategory) {
483
+ const childStatus = isChildSelected(child);
484
+ if (childStatus === 'notSelected') {
485
+ noneSelected = true;
486
+ } else if (childStatus === 'partSelected') {
487
+ partSelected = true;
488
+ } else if (childStatus === 'allSelected') {
489
+ allSelected = true;
490
+ }
491
+ }
492
+ }
493
+
494
+ // Determine the category selection state
495
+ if (allSelected) return 'allSelected';
496
+ if (partSelected) return 'partSelected';
497
+ if (noneSelected) return 'notSelected';
498
+ return "error"
499
+ }
500
+
501
+
502
+
503
+
504
+ // Get search rank for an item based on search query
505
+ const getSearchRank = (item: Item, query: string) => {
506
+ const keywords = query.split(/[\s,+;]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
507
+ let score = 0;
508
+
509
+ keywords.forEach((keyword) => {
510
+ if (item.name.toLowerCase().includes(keyword)) score += 2.5;
511
+ else if (item.description && item.description.toLowerCase().includes(keyword)) score += 2;
512
+ else if (item.tag && item.tag.some((tag) => tag.toLowerCase().includes(keyword))) score += 1.5;
513
+ });
514
+
515
+ return score;
516
+ };
517
+
518
+
519
+
520
+
521
+ // Recursively get all items from category and its child categories
522
+ function getAllItemsFromCategory(category: Category): Item[] {
523
+ let flattenItems: Item[] = [];
524
+
525
+ if (category.items) {
526
+ flattenItems = category.items.map((item) => ({
527
+ ...item,
528
+ score: getSearchRank(item, searchQuery),
529
+ }));
530
+ }
531
+
532
+ if (category.childCategory && Array.isArray(category.childCategory)) {
533
+ for (const child of category.childCategory) {
534
+ flattenItems = flattenItems.concat(getAllItemsFromCategory(child));
535
+ }
536
+ }
537
+
538
+ return flattenItems;
539
+ }
540
+
541
+ const sumSelection = (category: Category): Item[] => {
542
+ let result: Item[] = []
543
+ if (!category.parentCate) {
544
+ //category is first level, get all direct item and child cate items
545
+ for (let key in selection[category.name]) {
546
+ result = result.concat(selection[category.name][key])
547
+ }
548
+ }
549
+ else {
550
+ result = selection[category.parentCate]?.[category.name] || []
551
+ }
552
+
553
+ return result.length > 0 ? result : [];
554
+ };
555
+
556
+
557
+ function cleanSelection(selection: { [key1: string]: { [key2: string]: Item[] } }) {
558
+ for (const key1 in selection) {
559
+ if (Object.keys(selection[key1]).length>0) {
560
+ const key2Object = selection[key1];
561
+
562
+ // Iterate over key2 within key1
563
+ for (const key2 in key2Object) {
564
+ if (key2Object.key2) {
565
+ // If key2 has an empty array, delete the key2
566
+ if (key2Object[key2].length === 0) {
567
+ delete key2Object[key2];
568
+ }
569
+ }
570
+ }
571
+
572
+ // If key1 has no more key2s left, delete key1
573
+ if (Object.keys(key2Object).length === 0) {
574
+ delete selection[key1];
575
+ }
576
+ }
577
+ else{
578
+ delete selection[key1]
579
+ }
580
+ }
581
+ return selection;
582
+ }
583
+
584
+ // Render categories with items and nested child categories
585
+ const renderCategory = (category: Category, level: number): React.ReactNode => {
586
+
587
+ function truncateString(str) {
588
+ if (str.length > 10) {
589
+ return str.substring(0, 8) + '...';
590
+ }
591
+ return str;
592
+ }
593
+ const chipLabel = () => {
594
+ return sumSelection(category)?.length > 0 ?
595
+ sumSelection(category)?.length > 1 ?
596
+ sumSelection(category).length + " selected" :
597
+ truncateString(sumSelection(category)[0].name)
598
+ : ""
599
+ }
600
+
601
+ const handleDeleteSelection = (category: Category) => {
602
+ if (category.parentCate) {
603
+ const updatedSelection = { ...selection };
604
+ updatedSelection[category.parentCate][category.name] = [];
605
+ setProperty("selection" as any,updatedSelection);
606
+ } else {
607
+ const updatedSelection = { ...selection };
608
+ updatedSelection[category.name] = {};
609
+ setProperty("selection" as any,updatedSelection);
610
+ }
611
+
612
+ };
613
+
614
+ const chipElement = (
615
+ <Chip
616
+ label={chipLabel()}
617
+ sx={{
618
+ fontSize: '1.1rem',
619
+ color: 'var(--accentIconBackground)',
620
+ backgroundColor: 'var(--alertAmberBackground)',
621
+ '& .MuiChip-deleteIcon': { color: 'var(--accentIconBackground)' }
622
+
623
+ }}
624
+ onDelete={() => handleDeleteSelection(category)}
625
+ />
626
+ );
627
+
628
+ const itemContent = (item: Item, cateName?: string) => (<div style={{ display: 'flex', alignItems: 'center' }}>
629
+
630
+ {item.img && typeof item.img === 'string' && item.img.trim() !== '' && (
631
+ <Box
632
+ component="img"
633
+ sx={{
634
+ height: 18,
635
+ width: 18,
636
+ marginLeft: '8px',
637
+ }}
638
+ alt="The item icon"
639
+ src={item.img} // Use the item's img property
640
+ />
641
+ )}
642
+ {props.setting?.checkbox ? <ListItemIcon sx={{ minWidth: '35px' }}>
643
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }} checked={isSelected(item, cateName)} />
644
+ </ListItemIcon> : null}
645
+
646
+ <ListItemText primary={item.name} secondary={item.description} sx={{ pl: item.img ? 2 : 0 }} primaryTypographyProps={{ style: { fontWeight: (900 - 200 * (level + 1)).toString()} }} secondaryTypographyProps={{ style: { fontWeight: "300" } }}/>
647
+ </div>)
648
+ return (
649
+ <React.Fragment key={category.name}>
650
+ <ListItemButton onClick={() => handleCategoryToggle(category.name)} sx={{ pl: level * 2, py:0 , minHeight:'35px'}}>
651
+ {props.setting?.checkbox && category.checkbox ?
652
+ <ListItemIcon sx={{ minHeight:'35px' }}>
653
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }}
654
+ checked={['allSelected', 'partSelected'].includes(isChildSelected(category))}
655
+ onClick={(event) => handleCategoryCheck(event, category)}
656
+ />
657
+ </ListItemIcon> : null
658
+ }
659
+ {category.img && typeof category.img === 'string' && category.img.trim() !== '' ? (
660
+ <Box
661
+ component="img"
662
+ sx={{
663
+ height: 18,
664
+ width: 18,
665
+ marginLeft: '8px',
666
+ }}
667
+ alt="The category icon"
668
+ src={category.img} // Use the item's img property
669
+ />
670
+ ) : (<ListItemIcon sx={{ minWidth: '20px' }}>
671
+ <FolderCopyIcon sx={{ color: "var(--accentIconForeground)" }} />
672
+ </ListItemIcon>)
673
+ }
674
+ <ListItemText primary={props.setting?.showCount ? category.name + ` (${countAllItems(category)})` : category.name} secondary={category.description} sx={{ pl: 1 }} primaryTypographyProps={{ style: {fontWeight: (900 - level * 200).toString() ,fontSize:level==0?'1.2rem':undefined} }} secondaryTypographyProps={{ style: { fontWeight: "300" } }}/>
675
+ {//selection chip
676
+ props.setting?.checkbox && sumSelection(category).length > 0 ?
677
+ <LightTooltip
678
+ title={sumSelection(category).map(a => a.name).join(',')}
679
+ arrow>
680
+ {chipElement}
681
+ </LightTooltip>
682
+ : <></>
683
+ }
684
+ {expandIcon(props.setting?.collapseIcon ?? '', props.setting?.expandIcon ?? '', openCategories, category.name)}
685
+ </ListItemButton>
686
+ <Collapse in={openCategories && openCategories.has(category.name)} timeout="auto" unmountOnExit>
687
+ <List component="div" disablePadding>
688
+ {getMergedChildren(category).map((child, idx) => {
689
+ if (child.type === 'item') {
690
+ const item:Item = child.content;
691
+ return (
692
+ <ListItemButton
693
+ sx={{ pl: (level + 1) * 2, py:0 , minHeight:'35px'}}
694
+ key={`item-${idx}`}
695
+ onClick={() => handleItemClick(item, category)}
696
+ >
697
+ {props.setting?.tooltip ? (
698
+ typeof props.setting?.tooltip == "string" ? (
699
+ <LightTooltip title={item[props.setting?.tooltip]} arrow>
700
+ {itemContent(item,category.name)}
701
+ </LightTooltip>
702
+ ) : (
703
+ <LightTooltip
704
+ title={props.setting?.tooltip.map(key => item[key]).join(" ; ")}
705
+ arrow
706
+ >
707
+ {itemContent(item,category.name)}
708
+ </LightTooltip>
709
+ )
710
+ ) : (
711
+ itemContent(item,category.name)
712
+ )}
713
+ </ListItemButton>
714
+ );
715
+ } else {
716
+ // Render child category
717
+ const childCategory = child.content;
718
+ return renderCategory(childCategory, level + 1);
719
+ }
720
+ })}
721
+ </List>
722
+ </Collapse>
723
+ </React.Fragment>
724
+ );
725
+ };
726
+
727
+ const getMergedChildren = (category: Category) => {
728
+ const items = category.items || [];
729
+ const childCategories = category.childCategory || [];
730
+
731
+ // Convert items to an array with type marker
732
+ const itemsWithType = items.map(item => ({
733
+ type: 'item',
734
+ content: item
735
+ }));
736
+
737
+ // Convert child categories to an array with type marker
738
+ const categoriesWithType = childCategories.map(category => ({
739
+ type: 'category',
740
+ content: category
741
+ }));
742
+
743
+ // Sort based on forceSort setting
744
+ if (props.setting?.forceSort !== false) {
745
+ // Default behavior: sort alphabetically
746
+ categoriesWithType.sort((a, b) =>
747
+ a.content.name.localeCompare(b.content.name)
748
+ );
749
+
750
+ itemsWithType.sort((a, b) =>
751
+ a.content.name.localeCompare(b.content.name)
752
+ );
753
+ } else {
754
+ // When forceSort is false: respect original order
755
+ // For child categories, sort by index if available
756
+ categoriesWithType.sort((a, b) => {
757
+ if (a.content.index !== undefined && b.content.index !== undefined) {
758
+ return a.content.index - b.content.index;
759
+ }
760
+ return 0; // Keep original order if no index
761
+ });
762
+
763
+ // For items, we keep the original order by not sorting
764
+ }
765
+
766
+ // Return categories first, then items
767
+ return [...categoriesWithType, ...itemsWithType];
768
+ };
769
+
770
+
771
+ // Filter items based on the search query
772
+ const filteredItems = data
773
+ .flatMap((category) => getAllItemsFromCategory(category))
774
+ .map((item) => ({
775
+ ...item,
776
+ score: getSearchRank(item, searchQuery),
777
+ }))
778
+ .filter((item) => item.score > 0)
779
+ .sort((a, b) => b.score - a.score); // Sort by total score in descending order
780
+
781
+
782
+ React.useEffect(() => {
783
+ function filterData(data: Category[], query: string): Category[] {
784
+ const keywords = query.split(/[\s,+;]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
785
+
786
+ function searchCategory(category: Category): Category | null {
787
+ // Different matching logic based on the matchAnyKeyword setting
788
+ let categoryMatches: boolean;
789
+
790
+ if (props.setting?.matchAnyKeyword) {
791
+ // ANY keyword matching (original logic) - using regex
792
+ const keywordRegex = new RegExp(keywords.join("|"), "i");
793
+ categoryMatches = keywordRegex.test(category.name);
794
+
795
+ // Filter items based on name, description, or tags
796
+ const matchedItems = category.items?.filter(item =>
797
+ keywordRegex.test(item.name) ||
798
+ (item.description && keywordRegex.test(item.description)) ||
799
+ (item.tag && item.tag.some(tag => keywordRegex.test(tag)))
800
+ );
801
+
802
+ // Recursively filter child categories
803
+ const matchedChildCategories = category.childCategory
804
+ ?.map(childCat => searchCategory(childCat))
805
+ .filter((childCat): childCat is Category => childCat !== null);
806
+
807
+ if (categoryMatches) {
808
+ return {
809
+ ...category,
810
+ items: category.items || [],
811
+ childCategory: category.childCategory || []
812
+ };
813
+ }
814
+
815
+ // If no match in the category name, check if items or child categories match
816
+ if ((matchedItems && matchedItems.length > 0) || (matchedChildCategories && matchedChildCategories.length > 0)) {
817
+ return {
818
+ ...category,
819
+ items: matchedItems || [],
820
+ childCategory: matchedChildCategories || []
821
+ };
822
+ }
823
+ } else {
824
+ // ALL keywords matching logic
825
+ categoryMatches = keywords.every(keyword =>
826
+ category.name.toLowerCase().includes(keyword)
827
+ );
828
+
829
+ // Filter items that match ALL keywords
830
+ const matchedItems = category.items?.filter(item =>
831
+ keywords.every(keyword =>
832
+ item.name.toLowerCase().includes(keyword) ||
833
+ (item.description && item.description.toLowerCase().includes(keyword)) ||
834
+ (item.tag && item.tag.some(tag => tag.toLowerCase().includes(keyword)))
835
+ )
836
+ );
837
+
838
+ // Recursively filter child categories
839
+ const matchedChildCategories = category.childCategory
840
+ ?.map(childCat => searchCategory(childCat))
841
+ .filter((childCat): childCat is Category => childCat !== null);
842
+
843
+ if (categoryMatches) {
844
+ return {
845
+ ...category,
846
+ items: category.items || [],
847
+ childCategory: category.childCategory || []
848
+ };
849
+ }
850
+
851
+ // If no match in the category name, check if items or child categories match
852
+ if ((matchedItems && matchedItems.length > 0) || (matchedChildCategories && matchedChildCategories.length > 0)) {
853
+ return {
854
+ ...category,
855
+ items: matchedItems || [],
856
+ childCategory: matchedChildCategories || []
857
+ };
858
+ }
859
+ }
860
+
861
+ // Exclude this category if no matches found in items or child categories
862
+ return null;
863
+ }
864
+
865
+
866
+ // Apply the search to each root category and filter out null results
867
+ const result = data.map(category => searchCategory(category)).filter((cat): cat is Category => cat !== null);
868
+ return result
869
+
870
+ }
871
+
872
+
873
+ if (searchQuery) {
874
+
875
+ // Store the current state when search begins
876
+ if (searchQuery.length === 1) {
877
+ preSearchOpenCategories.current = new Set(currentOpenCategories.current);
878
+ }
879
+
880
+ const searchResults = filterData(data, searchQuery);
881
+ setFilteredData(searchResults);
882
+
883
+ // Expand categories with search results
884
+ const searchOpenCategories = new Set<string>();
885
+ const addToOpenCategories = (category: Category) => {
886
+ searchOpenCategories.add(category.name);
887
+ category.childCategory?.forEach(addToOpenCategories);
888
+ };
889
+
890
+ searchResults.forEach(addToOpenCategories);
891
+ setProperty("openCategories" as any,searchOpenCategories);
892
+ }
893
+ if (!searchQuery) {
894
+ // Restore pre-search state if exists
895
+ if (prevSearchValue.current==''){
896
+ currentOpenCategories.current = openCategories;
897
+ }
898
+ else {
899
+ if (preSearchOpenCategories.current.size > 0) {
900
+ setProperty("openCategories" as any,preSearchOpenCategories.current);
901
+ setFilteredData([]);
902
+ preSearchOpenCategories.current = new Set()
903
+ }
904
+ return;
905
+ }
906
+ }
907
+ prevSearchValue.current=searchQuery
908
+
909
+ }, [searchQuery, data, props.setting?.matchAnyKeyword, openCategories, setValue, setProperty]);
910
+
911
+
912
+
913
+ React.useEffect(() => {
914
+
915
+ function removeDirectItems() {
916
+ let selectCopy = structuredClone(selection)
917
+ let result = {}
918
+ for (let key in selection) {
919
+ result[key] = selectCopy[key].directItem;
920
+ }
921
+ return result;
922
+ }
923
+
924
+ if (props.setting?.checkbox && props.request && props.request.type == "submit" && (requestTime.current) !== (props.request.timestamp)) {
925
+ raiseEvent("clicked", JSON.stringify({ action: "submit", selectionResult: props.setting.flatResut ? removeDirectItems() : cleanSelection(selection) })); // Pass the entire item data structure
926
+ requestTime.current = props.request.timestamp;
927
+ }
928
+ if (props.setting?.checkbox && props.request && props.request.type == "clearSelection" && (requestTime.current) !== (props.request.timestamp)) {
929
+ setProperty("selection" as any,{})
930
+ requestTime.current = props.request.timestamp;
931
+ }
932
+ if (props.setting?.checkbox){
933
+ setValue( JSON.stringify( {selectionResult:props.setting.flatResut ? removeDirectItems() : cleanSelection(selection)}))
934
+ setProperty("selectionResult" as any,props.setting.flatResut ? removeDirectItems() : cleanSelection(selection))
935
+ }
936
+
937
+ }, [props.request, props.setting, raiseEvent, selection, setProperty, setValue])
938
+
939
+
940
+ React.useEffect(() => {
941
+ if (props.request && props.request.type == "displayLayer" && (requestTime.current) !== (props.request.timestamp)) {
942
+ let dataCopy = structuredClone(data)
943
+
944
+ dataCopy.forEach((category: Category) => {
945
+ if (category.items) {
946
+ const item = category.items.find(item => item.name === props.request?.data.title);
947
+ const foundindex = category.items.findIndex(item => item.name === props.request?.data.title) - 1;
948
+ if (item !== undefined) {
949
+ if (!category.childCategory) {
950
+ category.childCategory = [];
951
+ }
952
+ category.childCategory.push({
953
+ name: props.request?.data.title,
954
+ index: foundindex,
955
+ checkbox: true,
956
+ items: props.request?.data.layers.map(sublayer => ({
957
+ "name": sublayer.title,
958
+ "value": sublayer.id,
959
+ "parentName": item.name,
960
+ "parentValue": item.value,
961
+ }))
962
+ })
963
+ category.items = category.items.filter(item => item.name !== props.request?.data.title);
964
+
965
+ const newState = new Set(openCategories);
966
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
967
+ newState.add(props.request?.data.title || ''); // Open category if it's closed
968
+ setProperty("openCategories" as any, newState);
969
+
970
+ }
971
+ }
972
+ })
973
+ setData(dataCopy)
974
+ requestTime.current = props.request.timestamp
975
+ }
976
+ }, [data, openCategories, props.request, setProperty])
977
+
978
+ React.useEffect(() => {
979
+ if (props.setting?.reset && (loadDataTime) !== (props.setting.reset)) {
980
+ setData(props.itemData)
981
+ setProperty("selection" as any,{})
982
+ setProperty("openCategories" as any,new Set())
983
+ setProperty("searchQuery" as any,'')
984
+ setProperty("loadDataTime" as any,props.setting.reset)
985
+ }
986
+ }, [loadDataTime, props.itemData, props.setting?.reset, setProperty])
987
+
988
+
989
+
990
+
991
+ return (
992
+ <>
993
+ <Search sx={{ border: 2, borderColor: 'var(--alertGrayForegroundHover)', marginLeft: '0px !important', marginRight: '0px' }} >
994
+ <SearchIconWrapper>
995
+ <SearchIcon />
996
+ </SearchIconWrapper>
997
+ <StyledInputBase
998
+ placeholder={props.setting?.searchHolder || "Search…"}
999
+ inputProps={{ 'aria-label': 'search' }}
1000
+ value={searchQuery}
1001
+ onChange={handleSearchChange}
1002
+ sx={{ padding: '1px', fontSize: '1.4rem', color: 'var(--accentIconBorder) !important', width: '100%' }}
1003
+ />
1004
+ {searchQuery && (
1005
+ <IconButton
1006
+ onClick={() => { setProperty("searchQuery" as any,'') }}
1007
+ size="large"
1008
+ sx={{ position: 'absolute', right: '8px', color: 'var(--alertGrayBackground) !important' }}
1009
+ >
1010
+ <ClearIcon />
1011
+ </IconButton>
1012
+ )}
1013
+ </Search>
1014
+
1015
+ <ThemeProvider theme={defaultTheme}>
1016
+
1017
+ <List sx={{ width: '100%' }} component="nav">
1018
+ {props.setting?.simpleSearch && searchQuery && filteredItems.length > 0 ? (
1019
+ filteredItems.map((item, index) => (
1020
+ <ListItemButton key={index} sx={{minHeight:'35px'}}>
1021
+ {item.img && typeof item.img === 'string' && item.img.trim() !== '' && (
1022
+ <Box
1023
+ component="img"
1024
+ sx={{
1025
+ height: 18,
1026
+ width: 18,
1027
+ marginLeft: '8px',
1028
+ }}
1029
+ alt="The item icon"
1030
+ src={item.img} // Use the item's img property
1031
+ />
1032
+ )}
1033
+ {props.setting?.checkbox ? <ListItemIcon sx={{ minWidth: '35px' }}>
1034
+ <Checkbox sx={{ color: 'var(--accentIconBorder) !important' }} checked={isSelected(item)} />
1035
+ </ListItemIcon> : null}
1036
+ <ListItemText
1037
+ primary={item.name}
1038
+ secondary={item.description}
1039
+ onClick={() => handleItemClick(item, findCategory(item))}
1040
+ primaryTypographyProps={{ style: { fontWeight: '600' } }}
1041
+ />
1042
+
1043
+ </ListItemButton>
1044
+ ))
1045
+ ) : searchQuery && filteredData.length > 0 ?
1046
+ (
1047
+ filteredData.map((category) => renderCategory(category, 0))
1048
+ ) :
1049
+ searchQuery && filteredData.length === 0 ? (
1050
+ <Typography variant="body1" sx={{ padding: '10px', fontSize: '1.5rem' }}>No result found</Typography>
1051
+ ) : (
1052
+ data.map((category) => renderCategory(category, 0))
1053
+ )}
1054
+ </List>
1055
+
1056
+
1057
+ </ThemeProvider>
1058
+ </>
1059
+ );
1060
+ }
1061
+
1062
+ const ItemSearcher_V2ElementRegistration: FormElementRegistration<ItemSearcher_V2Props> = {
1063
+ component: ItemSearcher_V2,
1064
+ id: "ItemSearcher_V2",
1065
+ getInitialProperties: () => ({
1066
+ itemData: [{name:"Cate1",items:[{name:"ItemA"}]},{name:"Cate2",checkbox:true,items:[{name:"ItemB"}]}],
1067
+ setting: {initialCollapse:true,checkbox:true,showCount:true},
1068
+ }),
1069
+ };
1070
+
1071
+ export default ItemSearcher_V2ElementRegistration;