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.
@@ -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 '@vertigis/web/ui/Typography';
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 IconButton from '@vertigis/web/ui/IconButton';
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 ,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)}
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 currentOpenCategories = React.useRef<Set<string> >(new Set());
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
- setProperty("searchQuery" as any,event.target.value); // Update search query
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
- 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
- }
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
- 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];
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
- // If key1 has no more key2s left, delete key1
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
- // Render categories with items and nested child categories
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
- setProperty("selection" as any,updatedSelection);
618
+ setProperty("selection" as any, updatedSelection);
606
619
  } else {
607
620
  const updatedSelection = { ...selection };
608
621
  updatedSelection[category.name] = {};
609
- setProperty("selection" as any,updatedSelection);
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()} }} secondaryTypographyProps={{ style: { fontWeight: "300" } }}/>
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 , minHeight:'35px'}}>
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 sx={{ color: "var(--accentIconForeground)" }} />
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} sx={{ pl: 1 }} primaryTypographyProps={{ style: {fontWeight: (900 - level * 200).toString() ,fontSize:level==0?'1.2rem':undefined} }} secondaryTypographyProps={{ style: { fontWeight: "300" } }}/>
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 , minHeight:'35px'}}
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]} arrow>
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
- // 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
- };
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
- // Exclude this category if no matches found in items or child categories
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
- setProperty("openCategories" as any,searchOpenCategories);
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
- if (prevSearchValue.current==''){
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
- prevSearchValue.current=searchQuery
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( JSON.stringify( {selectionResult:props.setting.flatResut ? removeDirectItems() : cleanSelection(selection)}))
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
- 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
-
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={searchQuery}
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={() => { setProperty("searchQuery" as any,'') }}
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