ebta-workflow-prod 1.1.2 → 4.0.1

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