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