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/build/main.js +2 -2
- package/package.json +1 -1
- package/src/elements/AttributeFilter.tsx +152 -15
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
(
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
|
|
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) ?
|