ebta-workflow-prod 4.0.17 → 4.0.19
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.
- package/build/main.js +1 -1
- package/package.json +1 -1
- package/src/elements/ItemSearcher_v2.tsx +209 -194
|
@@ -13,14 +13,15 @@ import ExpandMore from '@mui/icons-material/ExpandMore';
|
|
|
13
13
|
import SearchIcon from '@vertigis/web/ui/icons/Search';
|
|
14
14
|
import InputBase from '@mui/material/InputBase';
|
|
15
15
|
import Box from "@vertigis/web/ui/Box"
|
|
16
|
-
import Typography from
|
|
16
|
+
import Typography from '@vertigis/web/ui/Typography';
|
|
17
17
|
import { createTheme } from "@mui/material/styles";
|
|
18
18
|
import { ThemeProvider } from "@emotion/react";
|
|
19
19
|
import Checkbox from '@vertigis/web/ui/Checkbox';
|
|
20
|
-
import
|
|
20
|
+
import IconButton from '@vertigis/web/ui/IconButton';
|
|
21
21
|
import { Category, Clear as ClearIcon } from '@mui/icons-material';
|
|
22
22
|
import Chip from '@mui/material/Chip';
|
|
23
23
|
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
|
24
|
+
import { useRef } from "react";
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -33,15 +34,15 @@ import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
|
|
33
34
|
* runtime, and passed in as `props`. You can update the properties by using
|
|
34
35
|
* `props.setProperty(key, value)`.
|
|
35
36
|
*/
|
|
36
|
-
interface ItemSearcher_V2Props extends FormElementProps<string> {
|
|
37
|
+
interface ItemSearcher_V2Props extends FormElementProps<string> {
|
|
37
38
|
|
|
38
39
|
itemData: Category[]; // Structured data passed as prop
|
|
39
40
|
setting?: settingConfig;
|
|
40
41
|
request?: { type: string, timestamp: number, data?: any };
|
|
41
42
|
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
|
|
43
|
+
searchQuery: string;//Status storage property only
|
|
44
|
+
selection: { [key: string]: { [key: string]: Item[] } }//Status storage property only
|
|
45
|
+
loadDataTime: number;//provide cabability of loading a totally different data
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
|
|
@@ -111,7 +112,7 @@ interface Category {
|
|
|
111
112
|
checkbox?: boolean; //If true, show checkbox that makes all child items selected/unselected
|
|
112
113
|
index?: number; //as a child Category, indicate the position of this child category compared to other sibling items/categories
|
|
113
114
|
parentCate?: string;
|
|
114
|
-
description?:string;
|
|
115
|
+
description?: string;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
interface settingConfig {
|
|
@@ -128,9 +129,9 @@ interface settingConfig {
|
|
|
128
129
|
reset?: number; //If loading another totally different content, give timestamp number for reset parameter to reload itemData
|
|
129
130
|
showCount?: boolean; // display item count after category name
|
|
130
131
|
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.
|
|
132
|
+
matchAnyKeyword?: boolean;//When search keywords (detected seperation by many spliters), fulfil all keywords or any of keyword.
|
|
133
|
+
maxSelectableItems?: number;//maximum number of items that can be selected at the same time. If 0, no limit.
|
|
134
|
+
forceSort?: boolean;//if true, sort the items in the same order as the original data. If false, sort by name.
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
|
|
@@ -171,20 +172,32 @@ const defaultTheme = createTheme({
|
|
|
171
172
|
|
|
172
173
|
// Function to get all items from category and its subcategories
|
|
173
174
|
function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
174
|
-
const { raiseEvent
|
|
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)}
|
|
175
|
+
const { raiseEvent, openCategories, searchQuery, selection, loadDataTime, setValue, setProperty } = props;
|
|
176
|
+
if (openCategories == undefined) { setProperty("openCategories" as any, new Set()) }
|
|
177
|
+
if (searchQuery == undefined) { setProperty("searchQuery" as any, '') }
|
|
178
|
+
if (selection == undefined) { setProperty("selection" as any, {}) }
|
|
179
|
+
if (loadDataTime == undefined) { setProperty("loadDataTime" as any, 0) }
|
|
179
180
|
const [data, setData] = React.useState<Category[]>(props.itemData);
|
|
180
181
|
const preSearchOpenCategories = React.useRef<Set<string>>(new Set());
|
|
181
|
-
const
|
|
182
|
-
const prevSearchValue = React.useRef<string>('')
|
|
182
|
+
const hadSearchText = React.useRef<boolean>(false)
|
|
183
183
|
const [filteredData, setFilteredData] = React.useState<Category[]>([]); // New state for filtered data
|
|
184
|
-
const requestTime = React.useRef<number | null>(null)
|
|
184
|
+
const requestTime = React.useRef<number | null>(null)
|
|
185
|
+
const [inputValue, setInputValue] = React.useState<string>(searchQuery);
|
|
186
|
+
const debounceTimer = React.useRef<ReturnType<typeof setTimeout>>();
|
|
187
|
+
const latestOpenCategories = React.useRef<Set<string>>(openCategories);
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
185
192
|
// Handle search input changes
|
|
193
|
+
|
|
186
194
|
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
187
|
-
|
|
195
|
+
const value = event.target.value;
|
|
196
|
+
setInputValue(value); // updates the input instantly - no lag
|
|
197
|
+
clearTimeout(debounceTimer.current);
|
|
198
|
+
debounceTimer.current = setTimeout(() => {
|
|
199
|
+
setProperty("searchQuery" as any, value); // triggers the expensive effect
|
|
200
|
+
}, 400);
|
|
188
201
|
};
|
|
189
202
|
|
|
190
203
|
|
|
@@ -207,7 +220,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
207
220
|
height: 18,
|
|
208
221
|
width: 18,
|
|
209
222
|
marginLeft: '8px',
|
|
210
|
-
|
|
223
|
+
|
|
211
224
|
}}
|
|
212
225
|
alt="The category icon"
|
|
213
226
|
src={expandIcon} // Use the item's img property
|
|
@@ -225,7 +238,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
225
238
|
else if (!expandIcon && openCats.has(catName)) return (<ExpandLess />)
|
|
226
239
|
else if (!expandIcon && !openCats.has(catName)) return (<ExpandMore />)
|
|
227
240
|
}
|
|
228
|
-
|
|
241
|
+
|
|
229
242
|
|
|
230
243
|
function findCategory(item: Item) {
|
|
231
244
|
return props.itemData.find(category =>
|
|
@@ -235,7 +248,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
235
248
|
|
|
236
249
|
function tickItem(item: Item, category: Category) {
|
|
237
250
|
let selectCopy = structuredClone(selection)
|
|
238
|
-
|
|
251
|
+
|
|
239
252
|
if (isSelected(item, category.name)) {
|
|
240
253
|
//uncheck item
|
|
241
254
|
for (let key in selectCopy) {
|
|
@@ -266,7 +279,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
266
279
|
raiseEvent("error" as any, JSON.stringify({ type: "maxItemsReached", message: `You can only select up to ${props.setting.maxSelectableItems} items` }));
|
|
267
280
|
return;
|
|
268
281
|
}
|
|
269
|
-
|
|
282
|
+
|
|
270
283
|
// Add the item to the specified category
|
|
271
284
|
if (category) {
|
|
272
285
|
if (category.parentCate) {
|
|
@@ -316,7 +329,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
316
329
|
const handleCategoryCheck = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, category: Category) => {
|
|
317
330
|
event.stopPropagation();
|
|
318
331
|
let selectCopy = structuredClone(selection);
|
|
319
|
-
|
|
332
|
+
|
|
320
333
|
// For "allSelected" case, just remove all items (no limit check needed)
|
|
321
334
|
if (isChildSelected(category) === "allSelected") {
|
|
322
335
|
if (!category.parentCate) {
|
|
@@ -324,12 +337,12 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
324
337
|
} else {
|
|
325
338
|
delete selectCopy[category.parentCate][category.name];
|
|
326
339
|
}
|
|
327
|
-
}
|
|
340
|
+
}
|
|
328
341
|
// For adding items, check limit
|
|
329
342
|
else {
|
|
330
343
|
const currentTotal = getTotalSelectedItems();
|
|
331
344
|
let itemsToAdd = 0;
|
|
332
|
-
|
|
345
|
+
|
|
333
346
|
// Calculate how many items would be added
|
|
334
347
|
if (!category.parentCate) {
|
|
335
348
|
if (category.items) {
|
|
@@ -348,13 +361,13 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
348
361
|
}
|
|
349
362
|
}
|
|
350
363
|
}
|
|
351
|
-
|
|
364
|
+
|
|
352
365
|
// Check if adding these items would exceed the limit
|
|
353
366
|
if (props.setting?.maxSelectableItems && (currentTotal + itemsToAdd > props.setting.maxSelectableItems)) {
|
|
354
367
|
raiseEvent("error" as any, JSON.stringify({ type: "maxItemsReached", message: `You can only select up to ${props.setting.maxSelectableItems} items` }));
|
|
355
368
|
return;
|
|
356
369
|
}
|
|
357
|
-
|
|
370
|
+
|
|
358
371
|
// Proceed with adding items if within limit
|
|
359
372
|
if (!category.parentCate) {
|
|
360
373
|
//only one level
|
|
@@ -388,7 +401,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
388
401
|
}
|
|
389
402
|
}
|
|
390
403
|
}
|
|
391
|
-
|
|
404
|
+
|
|
392
405
|
// Update selection state
|
|
393
406
|
setProperty("selection" as any, selectCopy);
|
|
394
407
|
};
|
|
@@ -403,7 +416,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
403
416
|
} else {
|
|
404
417
|
updatedOpenCategories.add(categoryName); // Open category
|
|
405
418
|
}
|
|
406
|
-
|
|
419
|
+
|
|
407
420
|
setProperty("openCategories" as any, updatedOpenCategories); // Update with new value
|
|
408
421
|
};
|
|
409
422
|
|
|
@@ -445,15 +458,15 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
445
458
|
return true;
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
461
|
+
|
|
462
|
+
for (let key in selection) {
|
|
463
|
+
if (selection[key][cateName]) {
|
|
464
|
+
let index = selection[key][cateName].findIndex(a => a.name === item.name && a.value === item.value);
|
|
465
|
+
if (index !== -1) {
|
|
466
|
+
return true;
|
|
455
467
|
}
|
|
456
468
|
}
|
|
469
|
+
}
|
|
457
470
|
}
|
|
458
471
|
return false;
|
|
459
472
|
}
|
|
@@ -556,32 +569,32 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
556
569
|
|
|
557
570
|
function cleanSelection(selection: { [key1: string]: { [key2: string]: Item[] } }) {
|
|
558
571
|
for (const key1 in selection) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
572
|
+
if (Object.keys(selection[key1]).length > 0) {
|
|
573
|
+
const key2Object = selection[key1];
|
|
574
|
+
|
|
575
|
+
// Iterate over key2 within key1
|
|
576
|
+
for (const key2 in key2Object) {
|
|
577
|
+
if (key2Object.key2) {
|
|
578
|
+
// If key2 has an empty array, delete the key2
|
|
579
|
+
if (key2Object[key2].length === 0) {
|
|
580
|
+
delete key2Object[key2];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// If key1 has no more key2s left, delete key1
|
|
586
|
+
if (Object.keys(key2Object).length === 0) {
|
|
587
|
+
delete selection[key1];
|
|
568
588
|
}
|
|
569
|
-
}
|
|
570
589
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (Object.keys(key2Object).length === 0) {
|
|
574
|
-
delete selection[key1];
|
|
590
|
+
else {
|
|
591
|
+
delete selection[key1]
|
|
575
592
|
}
|
|
576
|
-
}
|
|
577
|
-
else{
|
|
578
|
-
delete selection[key1]
|
|
579
|
-
}
|
|
580
593
|
}
|
|
581
594
|
return selection;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Render categories with items and nested child categories
|
|
585
598
|
const renderCategory = (category: Category, level: number): React.ReactNode => {
|
|
586
599
|
|
|
587
600
|
function truncateString(str) {
|
|
@@ -602,11 +615,11 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
602
615
|
if (category.parentCate) {
|
|
603
616
|
const updatedSelection = { ...selection };
|
|
604
617
|
updatedSelection[category.parentCate][category.name] = [];
|
|
605
|
-
|
|
618
|
+
setProperty("selection" as any, updatedSelection);
|
|
606
619
|
} else {
|
|
607
620
|
const updatedSelection = { ...selection };
|
|
608
621
|
updatedSelection[category.name] = {};
|
|
609
|
-
|
|
622
|
+
setProperty("selection" as any, updatedSelection);
|
|
610
623
|
}
|
|
611
624
|
|
|
612
625
|
};
|
|
@@ -643,13 +656,13 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
643
656
|
<Checkbox sx={{ color: 'var(--accentIconBorder) !important' }} checked={isSelected(item, cateName)} />
|
|
644
657
|
</ListItemIcon> : null}
|
|
645
658
|
|
|
646
|
-
<ListItemText primary={item.name} secondary={item.description} sx={{ pl: item.img ? 2 : 0 }} primaryTypographyProps={{ style: { fontWeight: (900 - 200 * (level + 1)).toString()} }}
|
|
659
|
+
<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
660
|
</div>)
|
|
648
661
|
return (
|
|
649
662
|
<React.Fragment key={category.name}>
|
|
650
|
-
<ListItemButton onClick={() => handleCategoryToggle(category.name)} sx={{ pl: level * 2, py:0
|
|
663
|
+
<ListItemButton onClick={() => handleCategoryToggle(category.name)} sx={{ pl: level * 2, py: 0, minHeight: '35px' }}>
|
|
651
664
|
{props.setting?.checkbox && category.checkbox ?
|
|
652
|
-
<ListItemIcon sx={{ minHeight:'35px' }}>
|
|
665
|
+
<ListItemIcon sx={{ minHeight: '35px' }}>
|
|
653
666
|
<Checkbox sx={{ color: 'var(--accentIconBorder) !important' }}
|
|
654
667
|
checked={['allSelected', 'partSelected'].includes(isChildSelected(category))}
|
|
655
668
|
onClick={(event) => handleCategoryCheck(event, category)}
|
|
@@ -668,10 +681,10 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
668
681
|
src={category.img} // Use the item's img property
|
|
669
682
|
/>
|
|
670
683
|
) : (<ListItemIcon sx={{ minWidth: '20px' }}>
|
|
671
|
-
<FolderCopyIcon
|
|
684
|
+
<FolderCopyIcon sx={{ color: "var(--accentIconForeground)" }} />
|
|
672
685
|
</ListItemIcon>)
|
|
673
686
|
}
|
|
674
|
-
<ListItemText primary={props.setting?.showCount ? category.name + ` (${countAllItems(category)})` : category.name} secondary={category.description}
|
|
687
|
+
<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
688
|
{//selection chip
|
|
676
689
|
props.setting?.checkbox && sumSelection(category).length > 0 ?
|
|
677
690
|
<LightTooltip
|
|
@@ -687,28 +700,28 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
687
700
|
<List component="div" disablePadding>
|
|
688
701
|
{getMergedChildren(category).map((child, idx) => {
|
|
689
702
|
if (child.type === 'item') {
|
|
690
|
-
const item:Item = child.content;
|
|
703
|
+
const item: Item = child.content;
|
|
691
704
|
return (
|
|
692
705
|
<ListItemButton
|
|
693
|
-
sx={{ pl: (level + 1) * 2, py:0
|
|
706
|
+
sx={{ pl: (level + 1) * 2, py: 0, minHeight: '35px' }}
|
|
694
707
|
key={`item-${idx}`}
|
|
695
708
|
onClick={() => handleItemClick(item, category)}
|
|
696
709
|
>
|
|
697
710
|
{props.setting?.tooltip ? (
|
|
698
711
|
typeof props.setting?.tooltip == "string" ? (
|
|
699
|
-
<LightTooltip title={item[props.setting?.tooltip]}
|
|
700
|
-
{itemContent(item,category.name)}
|
|
712
|
+
<LightTooltip title={item[props.setting?.tooltip]} arrow>
|
|
713
|
+
{itemContent(item, category.name)}
|
|
701
714
|
</LightTooltip>
|
|
702
715
|
) : (
|
|
703
716
|
<LightTooltip
|
|
704
717
|
title={props.setting?.tooltip.map(key => item[key]).join(" ; ")}
|
|
705
718
|
arrow
|
|
706
719
|
>
|
|
707
|
-
{itemContent(item,category.name)}
|
|
720
|
+
{itemContent(item, category.name)}
|
|
708
721
|
</LightTooltip>
|
|
709
722
|
)
|
|
710
723
|
) : (
|
|
711
|
-
itemContent(item,category.name)
|
|
724
|
+
itemContent(item, category.name)
|
|
712
725
|
)}
|
|
713
726
|
</ListItemButton>
|
|
714
727
|
);
|
|
@@ -727,27 +740,27 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
727
740
|
const getMergedChildren = (category: Category) => {
|
|
728
741
|
const items = category.items || [];
|
|
729
742
|
const childCategories = category.childCategory || [];
|
|
730
|
-
|
|
743
|
+
|
|
731
744
|
// Convert items to an array with type marker
|
|
732
745
|
const itemsWithType = items.map(item => ({
|
|
733
746
|
type: 'item',
|
|
734
747
|
content: item
|
|
735
748
|
}));
|
|
736
|
-
|
|
749
|
+
|
|
737
750
|
// Convert child categories to an array with type marker
|
|
738
751
|
const categoriesWithType = childCategories.map(category => ({
|
|
739
752
|
type: 'category',
|
|
740
753
|
content: category
|
|
741
754
|
}));
|
|
742
|
-
|
|
755
|
+
|
|
743
756
|
// Sort based on forceSort setting
|
|
744
757
|
if (props.setting?.forceSort !== false) {
|
|
745
758
|
// Default behavior: sort alphabetically
|
|
746
|
-
categoriesWithType.sort((a, b) =>
|
|
759
|
+
categoriesWithType.sort((a, b) =>
|
|
747
760
|
a.content.name.localeCompare(b.content.name)
|
|
748
761
|
);
|
|
749
|
-
|
|
750
|
-
itemsWithType.sort((a, b) =>
|
|
762
|
+
|
|
763
|
+
itemsWithType.sort((a, b) =>
|
|
751
764
|
a.content.name.localeCompare(b.content.name)
|
|
752
765
|
);
|
|
753
766
|
} else {
|
|
@@ -759,10 +772,10 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
759
772
|
}
|
|
760
773
|
return 0; // Keep original order if no index
|
|
761
774
|
});
|
|
762
|
-
|
|
775
|
+
|
|
763
776
|
// For items, we keep the original order by not sorting
|
|
764
777
|
}
|
|
765
|
-
|
|
778
|
+
|
|
766
779
|
// Return categories first, then items
|
|
767
780
|
return [...categoriesWithType, ...itemsWithType];
|
|
768
781
|
};
|
|
@@ -782,104 +795,103 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
782
795
|
React.useEffect(() => {
|
|
783
796
|
function filterData(data: Category[], query: string): Category[] {
|
|
784
797
|
const keywords = query.split(/[\s,+;]+/).map((word) => word.trim().toLowerCase()).filter(Boolean);
|
|
785
|
-
|
|
798
|
+
|
|
786
799
|
function searchCategory(category: Category): Category | null {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
800
|
+
// Different matching logic based on the matchAnyKeyword setting
|
|
801
|
+
let categoryMatches: boolean;
|
|
802
|
+
|
|
803
|
+
if (props.setting?.matchAnyKeyword) {
|
|
804
|
+
// ANY keyword matching (original logic) - using regex
|
|
805
|
+
const keywordRegex = new RegExp(keywords.join("|"), "i");
|
|
806
|
+
categoryMatches = keywordRegex.test(category.name);
|
|
807
|
+
|
|
808
|
+
// Filter items based on name, description, or tags
|
|
809
|
+
const matchedItems = category.items?.filter(item =>
|
|
810
|
+
keywordRegex.test(item.name) ||
|
|
811
|
+
(item.description && keywordRegex.test(item.description)) ||
|
|
812
|
+
(item.tag && item.tag.some(tag => keywordRegex.test(tag)))
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
// Recursively filter child categories
|
|
816
|
+
const matchedChildCategories = category.childCategory
|
|
817
|
+
?.map(childCat => searchCategory(childCat))
|
|
818
|
+
.filter((childCat): childCat is Category => childCat !== null);
|
|
819
|
+
|
|
820
|
+
if (categoryMatches) {
|
|
821
|
+
return {
|
|
822
|
+
...category,
|
|
823
|
+
items: category.items || [],
|
|
824
|
+
childCategory: category.childCategory || []
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// If no match in the category name, check if items or child categories match
|
|
829
|
+
if ((matchedItems && matchedItems.length > 0) || (matchedChildCategories && matchedChildCategories.length > 0)) {
|
|
830
|
+
return {
|
|
831
|
+
...category,
|
|
832
|
+
items: matchedItems || [],
|
|
833
|
+
childCategory: matchedChildCategories || []
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
// ALL keywords matching logic
|
|
838
|
+
categoryMatches = keywords.every(keyword =>
|
|
839
|
+
category.name.toLowerCase().includes(keyword)
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
// Filter items that match ALL keywords
|
|
843
|
+
const matchedItems = category.items?.filter(item =>
|
|
844
|
+
keywords.every(keyword =>
|
|
845
|
+
item.name.toLowerCase().includes(keyword) ||
|
|
846
|
+
(item.description && item.description.toLowerCase().includes(keyword)) ||
|
|
847
|
+
(item.tag && item.tag.some(tag => tag.toLowerCase().includes(keyword)))
|
|
848
|
+
)
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
// Recursively filter child categories
|
|
852
|
+
const matchedChildCategories = category.childCategory
|
|
853
|
+
?.map(childCat => searchCategory(childCat))
|
|
854
|
+
.filter((childCat): childCat is Category => childCat !== null);
|
|
855
|
+
|
|
856
|
+
if (categoryMatches) {
|
|
857
|
+
return {
|
|
858
|
+
...category,
|
|
859
|
+
items: category.items || [],
|
|
860
|
+
childCategory: category.childCategory || []
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// If no match in the category name, check if items or child categories match
|
|
865
|
+
if ((matchedItems && matchedItems.length > 0) || (matchedChildCategories && matchedChildCategories.length > 0)) {
|
|
866
|
+
return {
|
|
867
|
+
...category,
|
|
868
|
+
items: matchedItems || [],
|
|
869
|
+
childCategory: matchedChildCategories || []
|
|
870
|
+
};
|
|
871
|
+
}
|
|
858
872
|
}
|
|
873
|
+
|
|
874
|
+
// Exclude this category if no matches found in items or child categories
|
|
875
|
+
return null;
|
|
859
876
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
return null;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
|
|
877
|
+
|
|
878
|
+
|
|
866
879
|
// Apply the search to each root category and filter out null results
|
|
867
880
|
const result = data.map(category => searchCategory(category)).filter((cat): cat is Category => cat !== null);
|
|
868
881
|
return result
|
|
869
|
-
|
|
882
|
+
|
|
870
883
|
}
|
|
871
|
-
|
|
884
|
+
|
|
872
885
|
|
|
873
886
|
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
887
|
|
|
888
|
+
if (searchQuery && !hadSearchText.current) {
|
|
889
|
+
// Store the current state when search begins
|
|
890
|
+
preSearchOpenCategories.current = new Set(latestOpenCategories.current);
|
|
891
|
+
hadSearchText.current = true;
|
|
892
|
+
}
|
|
880
893
|
const searchResults = filterData(data, searchQuery);
|
|
881
894
|
setFilteredData(searchResults);
|
|
882
|
-
|
|
883
895
|
// Expand categories with search results
|
|
884
896
|
const searchOpenCategories = new Set<string>();
|
|
885
897
|
const addToOpenCategories = (category: Category) => {
|
|
@@ -888,27 +900,26 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
888
900
|
};
|
|
889
901
|
|
|
890
902
|
searchResults.forEach(addToOpenCategories);
|
|
891
|
-
|
|
903
|
+
setProperty("openCategories" as any, searchOpenCategories);
|
|
892
904
|
}
|
|
893
905
|
if (!searchQuery) {
|
|
906
|
+
hadSearchText.current = false;
|
|
894
907
|
// Restore pre-search state if exists
|
|
895
|
-
|
|
896
|
-
currentOpenCategories.current = openCategories;
|
|
897
|
-
}
|
|
898
|
-
else {
|
|
908
|
+
|
|
899
909
|
if (preSearchOpenCategories.current.size > 0) {
|
|
900
|
-
setProperty("openCategories" as any,preSearchOpenCategories.current);
|
|
910
|
+
setProperty("openCategories" as any, preSearchOpenCategories.current);
|
|
901
911
|
setFilteredData([]);
|
|
902
912
|
preSearchOpenCategories.current = new Set()
|
|
903
913
|
}
|
|
904
914
|
return;
|
|
905
915
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}, [searchQuery, data, props.setting?.matchAnyKeyword, openCategories, setValue, setProperty]);
|
|
916
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
917
|
+
}, [searchQuery, data, props.setting?.matchAnyKeyword, setValue, setProperty]);
|
|
910
918
|
|
|
911
919
|
|
|
920
|
+
React.useEffect(() => {
|
|
921
|
+
latestOpenCategories.current = openCategories;
|
|
922
|
+
}, [openCategories]);
|
|
912
923
|
|
|
913
924
|
React.useEffect(() => {
|
|
914
925
|
|
|
@@ -926,12 +937,12 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
926
937
|
requestTime.current = props.request.timestamp;
|
|
927
938
|
}
|
|
928
939
|
if (props.setting?.checkbox && props.request && props.request.type == "clearSelection" && (requestTime.current) !== (props.request.timestamp)) {
|
|
929
|
-
setProperty("selection" as any,{})
|
|
940
|
+
setProperty("selection" as any, {})
|
|
930
941
|
requestTime.current = props.request.timestamp;
|
|
931
942
|
}
|
|
932
|
-
if (props.setting?.checkbox){
|
|
933
|
-
setValue(
|
|
934
|
-
setProperty("selectionResult" as any,props.setting.flatResut ? removeDirectItems() : cleanSelection(selection))
|
|
943
|
+
if (props.setting?.checkbox) {
|
|
944
|
+
setValue(JSON.stringify({ selectionResult: props.setting.flatResut ? removeDirectItems() : cleanSelection(selection) }))
|
|
945
|
+
setProperty("selectionResult" as any, props.setting.flatResut ? removeDirectItems() : cleanSelection(selection))
|
|
935
946
|
}
|
|
936
947
|
|
|
937
948
|
}, [props.request, props.setting, raiseEvent, selection, setProperty, setValue])
|
|
@@ -961,12 +972,12 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
961
972
|
}))
|
|
962
973
|
})
|
|
963
974
|
category.items = category.items.filter(item => item.name !== props.request?.data.title);
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
975
|
+
|
|
976
|
+
const newState = new Set(openCategories);
|
|
977
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
978
|
+
newState.add(props.request?.data.title || ''); // Open category if it's closed
|
|
979
|
+
setProperty("openCategories" as any, newState);
|
|
980
|
+
|
|
970
981
|
}
|
|
971
982
|
}
|
|
972
983
|
})
|
|
@@ -978,10 +989,10 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
978
989
|
React.useEffect(() => {
|
|
979
990
|
if (props.setting?.reset && (loadDataTime) !== (props.setting.reset)) {
|
|
980
991
|
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)
|
|
992
|
+
setProperty("selection" as any, {})
|
|
993
|
+
setProperty("openCategories" as any, new Set())
|
|
994
|
+
setProperty("searchQuery" as any, '')
|
|
995
|
+
setProperty("loadDataTime" as any, props.setting.reset)
|
|
985
996
|
}
|
|
986
997
|
}, [loadDataTime, props.itemData, props.setting?.reset, setProperty])
|
|
987
998
|
|
|
@@ -997,13 +1008,17 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
997
1008
|
<StyledInputBase
|
|
998
1009
|
placeholder={props.setting?.searchHolder || "Search…"}
|
|
999
1010
|
inputProps={{ 'aria-label': 'search' }}
|
|
1000
|
-
value={
|
|
1011
|
+
value={inputValue}
|
|
1001
1012
|
onChange={handleSearchChange}
|
|
1002
1013
|
sx={{ padding: '1px', fontSize: '1.4rem', color: 'var(--accentIconBorder) !important', width: '100%' }}
|
|
1003
1014
|
/>
|
|
1004
1015
|
{searchQuery && (
|
|
1005
1016
|
<IconButton
|
|
1006
|
-
onClick={() => {
|
|
1017
|
+
onClick={() => {
|
|
1018
|
+
clearTimeout(debounceTimer.current);
|
|
1019
|
+
setProperty("searchQuery" as any, '');
|
|
1020
|
+
setInputValue('');
|
|
1021
|
+
}}
|
|
1007
1022
|
size="large"
|
|
1008
1023
|
sx={{ position: 'absolute', right: '8px', color: 'var(--alertGrayBackground) !important' }}
|
|
1009
1024
|
>
|
|
@@ -1017,7 +1032,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
1017
1032
|
<List sx={{ width: '100%' }} component="nav">
|
|
1018
1033
|
{props.setting?.simpleSearch && searchQuery && filteredItems.length > 0 ? (
|
|
1019
1034
|
filteredItems.map((item, index) => (
|
|
1020
|
-
<ListItemButton key={index} sx={{minHeight:'35px'}}>
|
|
1035
|
+
<ListItemButton key={index} sx={{ minHeight: '35px' }}>
|
|
1021
1036
|
{item.img && typeof item.img === 'string' && item.img.trim() !== '' && (
|
|
1022
1037
|
<Box
|
|
1023
1038
|
component="img"
|
|
@@ -1063,8 +1078,8 @@ const ItemSearcher_V2ElementRegistration: FormElementRegistration<ItemSearcher_V
|
|
|
1063
1078
|
component: ItemSearcher_V2,
|
|
1064
1079
|
id: "ItemSearcher_V2",
|
|
1065
1080
|
getInitialProperties: () => ({
|
|
1066
|
-
itemData: [{name:"Cate1",items:[{name:"ItemA"}]},{name:"Cate2",checkbox:true,items:[{name:"ItemB"}]}],
|
|
1067
|
-
setting: {initialCollapse:true,checkbox:true,showCount:true},
|
|
1081
|
+
itemData: [{ name: "Cate1", items: [{ name: "ItemA" }] }, { name: "Cate2", checkbox: true, items: [{ name: "ItemB" }] }],
|
|
1082
|
+
setting: { initialCollapse: true, checkbox: true, showCount: true },
|
|
1068
1083
|
}),
|
|
1069
1084
|
};
|
|
1070
1085
|
|