ebta-workflow-prod 4.0.15 → 4.0.17

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ebta-workflow-prod",
3
3
  "private": false,
4
- "version": "4.0.15",
4
+ "version": "4.0.17",
5
5
  "description": "",
6
6
  "license": "UNLICENSED",
7
7
  "author": "",
@@ -8,6 +8,8 @@ import ListItemButton from '@vertigis/web/ui/ListItemButton';
8
8
  import ListItemIcon from '@vertigis/web/ui/ListItemIcon';
9
9
  import ListItemText from '@vertigis/web/ui/ListItemText';
10
10
  import Collapse from '@vertigis/web/ui/Collapse';
11
+ import Menu from '@vertigis/web/ui/Menu';
12
+ import MenuItem from '@vertigis/web/ui/MenuItem';
11
13
  import FolderOpenIcon from '@mui/icons-material/FolderOpen';
12
14
  import ExpandLess from '@mui/icons-material/ExpandLess';
13
15
  import ExpandMore from '@mui/icons-material/ExpandMore';
@@ -135,6 +137,9 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
135
137
  const maxDate = React.useRef(2209004187)
136
138
  //const [filteredValues, setFilteredValues] = React.useState<fieldValue[]>([]); // New state for filtered data
137
139
  const [showAll, setShowAll] = React.useState(false);
140
+ const [sortModes, setSortModes] = React.useState<Record<string, string>>({});
141
+ const [sortAnchor, setSortAnchor] = React.useState<null | { el: HTMLElement; name: string }>(null);
142
+
138
143
  // const [minValue, setMinValue] = React.useState<number>(-Infinity)
139
144
  // const [maxValue, setMaxValue] = React.useState<number>(Infinity)
140
145
  // const [startDate, setStartDate] = React.useState<Dayjs>(defaultStart)
@@ -269,7 +274,16 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
269
274
  const filteredAttributes = {};
270
275
  for (const key of attributeList) {
271
276
  if (key in feature.attributes) {
272
- filteredAttributes[key] = originalData.find(attri => attri.name === key)?.type == 'date' ? epoch2MelbDate(Number(feature.attributes[key]) / 1000) : feature.attributes[key];
277
+ const isDate = originalData.find(attri => attri.name === key)?.type == 'date';
278
+ const rawVal = feature.attributes[key];
279
+ if (isDate) {
280
+ const epoch = Number(rawVal);
281
+ filteredAttributes[key] = (rawVal !== null && rawVal !== undefined && epoch > 0)
282
+ ? epoch2MelbDate(epoch / 1000)
283
+ : null; // keep as null, generateAttributeSummary will skip it
284
+ } else {
285
+ filteredAttributes[key] = rawVal;
286
+ }
273
287
  }
274
288
  }
275
289
  return filteredAttributes;
@@ -890,7 +904,9 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
890
904
  // Iterate through the features array
891
905
  features.forEach((feature) => {
892
906
  const value = feature[attributeName];
907
+ if (value === null || value === undefined || value === '') return;
893
908
  if (value) {
909
+
894
910
  // Count the occurrence of each value for the attribute
895
911
  if (!attributeSummary[value]) {
896
912
  attributeSummary[value] = 0;
@@ -1019,7 +1035,13 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
1019
1035
 
1020
1036
  }
1021
1037
  else if (attri.display) {
1038
+
1022
1039
  const dataCopy = structuredClone(data)
1040
+ setSortModes(prev => {
1041
+ const copy = { ...prev };
1042
+ delete copy[attri.name];
1043
+ return copy;
1044
+ });
1023
1045
  let result: Attribute[]
1024
1046
  if (recordTable !== undefined && recordTable.length > 0) {
1025
1047
  result = dataCopy.map(item =>
@@ -1204,6 +1226,48 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
1204
1226
 
1205
1227
 
1206
1228
  const renderAttribute = (attribute: Attribute): React.ReactNode => {
1229
+ // --- sort mode for this attribute ---
1230
+ const defaultSort = attribute.type === 'date' ? 'late-early' : 'count';
1231
+ const sortMode = sortModes[attribute.name] ?? defaultSort;
1232
+
1233
+ const sortOptions: Record<string, string[]> = {
1234
+ date: ['late-early', 'early-late', 'count'],
1235
+ number: ['count', 'big-small', 'small-big'],
1236
+ string: ['count', 'a-z', 'z-a'],
1237
+ };
1238
+ const fieldKind = attribute.type === 'date' ? 'date'
1239
+ : (attribute.type && ['integer', 'number', 'float', 'short', 'long', 'big integer'].includes(attribute.type)) || detectNumberAttribute(attribute) ? 'number'
1240
+ : 'string';
1241
+ const modes = sortOptions[fieldKind];
1242
+ const modeLabels: Record<string, string> = {
1243
+ 'count': 'By count',
1244
+ 'a-z': 'A → Z',
1245
+ 'z-a': 'Z → A',
1246
+ 'late-early': 'Latest first',
1247
+ 'early-late': 'Earliest first',
1248
+ 'big-small': 'Biggest first',
1249
+ 'small-big': 'Smallest first',
1250
+ };
1251
+
1252
+ function applySortMode(values: fieldValue[]): fieldValue[] {
1253
+ const copy = [...values];
1254
+ switch (sortMode) {
1255
+ case 'count': return copy.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
1256
+ case 'a-z': return copy.sort((a, b) => a.value.localeCompare(b.value));
1257
+ case 'z-a': return copy.sort((a, b) => b.value.localeCompare(a.value));
1258
+ case 'late-early': return copy.sort((a, b) => {
1259
+ const da = dayjs(a.value, 'DD/MM/YYYY'), db = dayjs(b.value, 'DD/MM/YYYY');
1260
+ return db.valueOf() - da.valueOf();
1261
+ });
1262
+ case 'early-late': return copy.sort((a, b) => {
1263
+ const da = dayjs(a.value, 'DD/MM/YYYY'), db = dayjs(b.value, 'DD/MM/YYYY');
1264
+ return da.valueOf() - db.valueOf();
1265
+ });
1266
+ case 'big-small': return copy.sort((a, b) => Number(b.value) - Number(a.value));
1267
+ case 'small-big': return copy.sort((a, b) => Number(a.value) - Number(b.value));
1268
+ default: return copy;
1269
+ }
1270
+ }
1207
1271
 
1208
1272
  let ISnumberAttribute = false
1209
1273
  let attributeMin = -Infinity
@@ -1217,6 +1281,9 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
1217
1281
  }
1218
1282
 
1219
1283
 
1284
+
1285
+
1286
+
1220
1287
  let displayedValues: fieldValue[] = []
1221
1288
  let displayedFilterValues: fieldValue[] = []
1222
1289
  let limitInUse
@@ -1227,13 +1294,19 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
1227
1294
  limitInUse = (attribute.type == 'date' && (dayjs.unix(minDate.current).format('DD/MM/YYYY') != startDate.format('DD/MM/YYYY') || (dayjs.unix(maxDate.current).format('DD/MM/YYYY') != endDate.format('DD/MM/YYYY')))) || searchValue.length > 0 || (minValue !== -Infinity || maxValue !== Infinity)
1228
1295
  }
1229
1296
  if (attribute.uniqValue && attribute.uniqValue.length > 0) {
1230
- displayedValues = (attribute.uniqValue.length < defaultNumber + 2) || showAll ? attribute.uniqValue : attribute.uniqValue.slice(0, defaultNumber);
1297
+ const sorted = applySortMode(attribute.uniqValue);
1298
+ displayedValues = (sorted.length < defaultNumber + 2) || showAll
1299
+ ? sorted
1300
+ : sorted.slice(0, defaultNumber);
1231
1301
  }
1232
-
1233
1302
  if (limitInUse && filteredValues.length > 0) {
1234
- displayedFilterValues = ((filteredValues.length < defaultNumber + 2) || showAll) ? filteredValues : filteredValues.slice(0, defaultNumber)
1303
+ const sorted = applySortMode(filteredValues);
1304
+ displayedFilterValues = ((sorted.length < defaultNumber + 2) || showAll)
1305
+ ? sorted
1306
+ : sorted.slice(0, defaultNumber)
1235
1307
  }
1236
1308
 
1309
+
1237
1310
  const handleDateRangeChange = (newStart: Dayjs, newEnd: Dayjs) => {
1238
1311
  setProperty("startDate" as any, newStart || defaultStart);
1239
1312
  setProperty("endDate" as any, newEnd || defaultEnd)
@@ -1424,21 +1497,85 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
1424
1497
  }
1425
1498
  {((!limitInUse && attribute.uniqValue && attribute.uniqValue.length > 2) ||
1426
1499
  (limitInUse && filteredValues.length > 2)) ?
1427
-
1428
- <ListItemButton sx={{ pl: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} onClick={handleShowAllClick}>
1500
+ <ListItemButton
1501
+ sx={{ pl: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
1502
+ onClick={handleShowAllClick}
1503
+ >
1504
+ {/* Show more/less label */}
1429
1505
  <div style={{ display: 'flex', alignItems: 'center', flexGrow: 1 }}>
1430
- {
1431
- // Display SHOW-ALL or not
1432
- ((!limitInUse && attribute.uniqValue && attribute.uniqValue.length >= defaultNumber + 2) ||
1433
- (limitInUse && filteredValues.length >= defaultNumber + 2)) ?
1434
- <ListItemText primary={`Show ${showAll ? 'less' : 'all others▼'} ${showAll ? '▲' : `(${filteredValues.length > defaultNumber && limitInUse ? filteredValues.length - defaultNumber : attribute.uniqValue.length - defaultNumber})`}`} primaryTypographyProps={{ style: { fontWeight: '500' } }} />
1435
- : null
1436
- }
1506
+ {((!limitInUse && attribute.uniqValue && attribute.uniqValue.length >= defaultNumber + 2) ||
1507
+ (limitInUse && filteredValues.length >= defaultNumber + 2)) ?
1508
+ <ListItemText
1509
+ primary={`Show ${showAll ? 'less' : 'all others▼'} ${showAll ? '▲' : `(${filteredValues.length > defaultNumber && limitInUse ? filteredValues.length - defaultNumber : attribute.uniqValue.length - defaultNumber})`}`}
1510
+ primaryTypographyProps={{ style: { fontWeight: '500' } }}
1511
+ />
1512
+ : null}
1437
1513
  </div>
1438
- <Button style={{ color: 'var(--accentIconBorder) !important' }} variant="text" onClick={(event) => handleSelectAll(event, attribute.name)}>
1514
+
1515
+ {/* Sort dropdown button */}
1516
+ {attribute.uniqValue && attribute.uniqValue.length > 1 && (() => {
1517
+ const open = sortAnchor?.name === attribute.name;
1518
+ return (
1519
+ <Box onClick={e => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center' }}>
1520
+ <LightTooltip title={`Sort: ${modeLabels[sortMode]}`} arrow>
1521
+ <IconButton
1522
+ size="small"
1523
+ onClick={e => {
1524
+ e.stopPropagation();
1525
+ setSortAnchor(open ? null : { el: e.currentTarget, name: attribute.name });
1526
+ }}
1527
+ sx={{ fontSize: '1rem', opacity: open ? 1 : 0.65, px: 0.5 }}
1528
+ >
1529
+ 📶
1530
+ </IconButton>
1531
+ </LightTooltip>
1532
+
1533
+ {/* Dropdown menu — needs Menu imported from @mui/material or @vertigis/web/ui */}
1534
+ <Menu
1535
+ anchorEl={sortAnchor?.el}
1536
+ open={open}
1537
+ onClose={() => setSortAnchor(null)}
1538
+ onClick={e => e.stopPropagation()}
1539
+ MenuListProps={{ dense: true }}
1540
+ PaperProps={{
1541
+ sx: {
1542
+ fontSize: '1.2rem',
1543
+ minWidth: 160,
1544
+ boxShadow: 3,
1545
+ }
1546
+ }}
1547
+ transformOrigin={{ horizontal: 'right', vertical: 'top' }}
1548
+ anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
1549
+ >
1550
+ {modes.map(mode => (
1551
+ <MenuItem
1552
+ key={mode}
1553
+ selected={sortMode === mode}
1554
+ onClick={e => {
1555
+ e.stopPropagation();
1556
+ setSortModes(prev => ({ ...prev, [attribute.name]: mode }));
1557
+ setSortAnchor(null);
1558
+ }}
1559
+ sx={{ fontSize: '1.2rem' }}
1560
+ >
1561
+ {modeLabels[mode]}
1562
+ </MenuItem>
1563
+ ))}
1564
+ </Menu>
1565
+ </Box>
1566
+ );
1567
+ })()}
1568
+
1569
+ {/* Select / Deselect All */}
1570
+ <Button
1571
+ style={{ color: 'var(--accentIconBorder) !important' }}
1572
+ variant="text"
1573
+ onClick={(event) => handleSelectAll(event, attribute.name)}
1574
+ >
1439
1575
  {allButtonText}
1440
1576
  </Button>
1441
- </ListItemButton> : <></>}
1577
+ </ListItemButton>
1578
+ : <></>}
1442
1579
  {
1443
1580
  // Situation1: Search or range & search result found, only show 'DISPLAYED FILTER VALUE' which is controlled by showall to be all or first 4
1444
1581
  (limitInUse && filteredValues.length > 0) ?