robobyte-front-builder 1.0.21 → 1.0.23
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/INTEGRATION.md +1586 -0
- package/README.md +791 -74
- package/RoboByteBuilder_User_Manual.docx +0 -0
- package/package.json +4 -2
- package/src/context/BuilderContext.jsx +63 -7
- package/src/pages/api/ai.js +20 -0
- package/src/pages/printBuilder/index.jsx +3 -3
- package/src/pages/reportModule/reportBuilder/reportViewer/index.js +59 -3
- package/src/pages/viewBuilder/index.jsx +2 -2
- package/src/services/Endpoints/ReportBuilderEndpoints.js +7 -7
- package/src/services/sessionLog.js +171 -0
- package/src/views/builder/SessionLogDialog.jsx +350 -0
- package/src/views/builder/UnsavedChangesGuard.jsx +103 -0
- package/src/views/builder/inspector/definitions/reportViewer/main.js +13 -0
- package/src/views/builder/sidebar/tabs/AiTab/aiProvider.js +7 -3
- package/src/views/builder/sidebar/tabs/AiTab/schemaTransformer.js +65 -0
- package/src/views/builder/sidebar/tabs/AiTab/trainingExport.js +131 -0
- package/src/views/builder/sidebar/tabs/ViewTab.jsx +173 -13
- package/src/views/builder/viewer/ViewerComponentWrapper.jsx +9 -5
- package/src/views/builder/viewer/ViewerToolbar.jsx +18 -3
- package/src/views/builder/viewer/renderers/ReportViewerRenderer.jsx +46 -2
- package/src/views/genericTable/SGrid.js +656 -384
- package/src/views/printBuilder/PrintBuilderViewer.jsx +22 -2
|
@@ -98,7 +98,9 @@ import {
|
|
|
98
98
|
Menu
|
|
99
99
|
} from '@mui/material'
|
|
100
100
|
import {AgGridReact} from "ag-grid-react";
|
|
101
|
-
import {Edit, FilterAlt, PrintOutlined, RefreshOutlined, Save, SaveAs, Close} from "@mui/icons-material";
|
|
101
|
+
import {Edit, FilterAlt, PrintOutlined, RefreshOutlined, Save, SaveAs, Close, SearchOutlined} from "@mui/icons-material";
|
|
102
|
+
// Lazy MUI icon resolver for viewer-action button icons (icon name string → component)
|
|
103
|
+
import * as MuiIcons from "@mui/icons-material";
|
|
102
104
|
import {getRandomInt, Helper} from "services/helper/helper";
|
|
103
105
|
import PostService from 'services/PostService'
|
|
104
106
|
import handleChange from 'services/helper/handleChange'
|
|
@@ -183,6 +185,14 @@ function TemplatesToolPanel() {
|
|
|
183
185
|
handleGetTemplates, handleSaveTemplate,
|
|
184
186
|
handleToggleDialogs, setIsTemplateEditing,
|
|
185
187
|
builderData,
|
|
188
|
+
// Display settings (moved out of the in-grid toolbar) — see toolbar render
|
|
189
|
+
// below. These used to live next to the search field; they were moved to
|
|
190
|
+
// make the toolbar more compact and to follow the same disclosure pattern
|
|
191
|
+
// as Templates (advanced/less-used settings live in the side panel).
|
|
192
|
+
timerValue, setTimerValue,
|
|
193
|
+
isPagination, setIsPagination,
|
|
194
|
+
handleCSVExport, isDownloading,
|
|
195
|
+
isExcelExportAvailable,
|
|
186
196
|
} = ctx
|
|
187
197
|
|
|
188
198
|
return (
|
|
@@ -199,6 +209,57 @@ function TemplatesToolPanel() {
|
|
|
199
209
|
</Typography>
|
|
200
210
|
</Box>
|
|
201
211
|
|
|
212
|
+
{/* Display settings — auto-refresh timer, full-data toggle, Excel export */}
|
|
213
|
+
<SettingsSection title='Display Settings'>
|
|
214
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, width: '100%' }}>
|
|
215
|
+
<Box>
|
|
216
|
+
<Typography variant='caption' sx={{ color: 'text.secondary', display: 'block', mb: 0.5 }}>
|
|
217
|
+
Auto-refresh
|
|
218
|
+
</Typography>
|
|
219
|
+
<Select
|
|
220
|
+
fullWidth
|
|
221
|
+
size='small'
|
|
222
|
+
value={timerValue ?? 0}
|
|
223
|
+
onChange={(e) => setTimerValue?.(e.target.value)}
|
|
224
|
+
>
|
|
225
|
+
<MenuItem value={0}>Off</MenuItem>
|
|
226
|
+
<MenuItem value={0.5}>Every 30s</MenuItem>
|
|
227
|
+
<MenuItem value={1}>Every 1m</MenuItem>
|
|
228
|
+
<MenuItem value={2}>Every 2m</MenuItem>
|
|
229
|
+
<MenuItem value={5}>Every 5m</MenuItem>
|
|
230
|
+
<MenuItem value={15}>Every 15m</MenuItem>
|
|
231
|
+
<MenuItem value={30}>Every 30m</MenuItem>
|
|
232
|
+
</Select>
|
|
233
|
+
</Box>
|
|
234
|
+
|
|
235
|
+
<FormControlLabel
|
|
236
|
+
control={
|
|
237
|
+
<Checkbox
|
|
238
|
+
size='small'
|
|
239
|
+
checked={!isPagination}
|
|
240
|
+
onChange={e => setIsPagination?.(!e.target.checked)}
|
|
241
|
+
color='primary'
|
|
242
|
+
/>
|
|
243
|
+
}
|
|
244
|
+
label={<Typography variant='body2'>Load all data (no pagination)</Typography>}
|
|
245
|
+
/>
|
|
246
|
+
|
|
247
|
+
{isExcelExportAvailable && (
|
|
248
|
+
<Button
|
|
249
|
+
size='small'
|
|
250
|
+
variant='outlined'
|
|
251
|
+
startIcon={isDownloading ? <CircularProgress size={16}/> : <FileExcelOutline fontSize='small'/>}
|
|
252
|
+
onClick={handleCSVExport}
|
|
253
|
+
disabled={isDownloading}
|
|
254
|
+
fullWidth
|
|
255
|
+
sx={{ justifyContent: 'flex-start' }}
|
|
256
|
+
>
|
|
257
|
+
Export to Excel
|
|
258
|
+
</Button>
|
|
259
|
+
)}
|
|
260
|
+
</Box>
|
|
261
|
+
</SettingsSection>
|
|
262
|
+
|
|
202
263
|
{/* Template section */}
|
|
203
264
|
<SettingsSection title='Template'>
|
|
204
265
|
{builderData?.id == null ? (
|
|
@@ -283,6 +344,13 @@ const SGrid = props => {
|
|
|
283
344
|
expireReport,
|
|
284
345
|
filter: externalFilter,
|
|
285
346
|
actions,
|
|
347
|
+
// Page-level toolbar buttons rendered after Filter/Refresh.
|
|
348
|
+
// Built by ReportViewerRenderer; each item already has a bound onClick.
|
|
349
|
+
viewerActions,
|
|
350
|
+
// Title / caption rendered on the left side of the toolbar, sharing the
|
|
351
|
+
// row with the search and action buttons. See attached design reference.
|
|
352
|
+
title: viewerTitle,
|
|
353
|
+
caption: viewerCaption,
|
|
286
354
|
refresh,
|
|
287
355
|
height,
|
|
288
356
|
extraCols,
|
|
@@ -348,6 +416,17 @@ const SGrid = props => {
|
|
|
348
416
|
const searchInputRef = useRef(null);
|
|
349
417
|
const [searchPopoverOpen, setSearchPopoverOpen] = useState(false);
|
|
350
418
|
const [searchPopoverAnchor, setSearchPopoverAnchor] = useState(null);
|
|
419
|
+
// Search expansion: collapsed → just an icon button; expanded → full TextField.
|
|
420
|
+
// Auto-expand if the user is typing or already has filters in chips, so the
|
|
421
|
+
// visual state always shows what's active.
|
|
422
|
+
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
|
|
423
|
+
// Keyboard navigation in the search popover. Index points into
|
|
424
|
+
// searchFieldsRef.current. Reset whenever the popover opens or the term
|
|
425
|
+
// changes so the highlight always starts at the top of the list.
|
|
426
|
+
const [highlightedFieldIndex, setHighlightedFieldIndex] = useState(0);
|
|
427
|
+
// refs to ListItemButton DOM nodes so we can scrollIntoView the highlighted
|
|
428
|
+
// item when the user arrows past the visible part of the (capped 240px) list.
|
|
429
|
+
const searchOptionRefs = useRef([]);
|
|
351
430
|
|
|
352
431
|
const searchTermRef = useRef('');
|
|
353
432
|
const searchFieldsRef = useRef([]); // metadata only, no value
|
|
@@ -2454,397 +2533,593 @@ const SGrid = props => {
|
|
|
2454
2533
|
|
|
2455
2534
|
return (
|
|
2456
2535
|
<Grid container size={{ xs: 12 }}>
|
|
2457
|
-
<Grid
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
{
|
|
2479
|
-
<Box sx={{
|
|
2480
|
-
{
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
setSearchPopoverAnchor(e.currentTarget);
|
|
2491
|
-
setSearchPopoverOpen(true);
|
|
2492
|
-
}
|
|
2493
|
-
}}
|
|
2494
|
-
onChange={(e) => {
|
|
2495
|
-
const v = e.target.value;
|
|
2496
|
-
setSearchTerm(v);
|
|
2497
|
-
setSearchPopoverAnchor(e.currentTarget);
|
|
2498
|
-
setSearchPopoverOpen((v || '').length > 0);
|
|
2499
|
-
}}
|
|
2500
|
-
onKeyDown={(e) => {
|
|
2501
|
-
if (e.key === 'Enter') {
|
|
2502
|
-
e.preventDefault();
|
|
2503
|
-
const term = (searchTerm || '').trim();
|
|
2504
|
-
if (!term) return;
|
|
2505
|
-
const fields = searchFieldsRef?.current ?? [];
|
|
2506
|
-
if (fields.length === 0) return;
|
|
2507
|
-
handlePickSearchField(fields[0]);
|
|
2508
|
-
} else if (e.key === 'Escape') {
|
|
2509
|
-
setSearchPopoverOpen(false);
|
|
2510
|
-
}
|
|
2536
|
+
<Grid
|
|
2537
|
+
container
|
|
2538
|
+
sx={{ justifyContent: 'space-between', alignItems: 'center', flexWrap: 'nowrap', gap: 2 }}
|
|
2539
|
+
padding={2}
|
|
2540
|
+
size={{ xs: 12 }}
|
|
2541
|
+
>
|
|
2542
|
+
{/*
|
|
2543
|
+
── Toolbar layout ───────────────────────────────────────────────────
|
|
2544
|
+
One row, split into:
|
|
2545
|
+
|
|
2546
|
+
LEFT : title (h6) + caption (caption) stacked vertically.
|
|
2547
|
+
Both are optional — the block is hidden if neither is set.
|
|
2548
|
+
RIGHT : collapsible search (icon → TextField on focus / when it
|
|
2549
|
+
has a value), Filter (outlined Button), Refresh (outlined
|
|
2550
|
+
Button), then any user-defined viewerActions in order.
|
|
2551
|
+
|
|
2552
|
+
Timer / Select-All / Excel-Export live in the Templates side panel
|
|
2553
|
+
under "Display Settings" — see TemplatesToolPanel above.
|
|
2554
|
+
*/}
|
|
2555
|
+
|
|
2556
|
+
{/* ── LEFT: title + caption ─────────────────────────────────────── */}
|
|
2557
|
+
{viewerTitle || viewerCaption ? (
|
|
2558
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', minWidth: 0, flexShrink: 1 }}>
|
|
2559
|
+
{viewerTitle && (
|
|
2560
|
+
<Typography
|
|
2561
|
+
variant='h6'
|
|
2562
|
+
sx={{
|
|
2563
|
+
fontWeight: 600,
|
|
2564
|
+
lineHeight: 1.2,
|
|
2565
|
+
color: 'text.primary',
|
|
2566
|
+
whiteSpace: 'nowrap',
|
|
2567
|
+
overflow: 'hidden',
|
|
2568
|
+
textOverflow: 'ellipsis'
|
|
2511
2569
|
}}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
const valueLabel = g.values.reduce((acc, val, idx) => {
|
|
2526
|
-
if (idx > 0) acc.push(<strong key={`sep-${idx}`}> أو </strong>);
|
|
2527
|
-
acc.push(val);
|
|
2528
|
-
return acc;
|
|
2529
|
-
}, []);
|
|
2530
|
-
return (
|
|
2531
|
-
<Chip
|
|
2532
|
-
key={`group-${path}`}
|
|
2533
|
-
size="small"
|
|
2534
|
-
label={
|
|
2535
|
-
<span>
|
|
2536
|
-
<strong>{labelBase}</strong>: {valueLabel}
|
|
2537
|
-
</span>
|
|
2538
|
-
} onDelete={() => {
|
|
2539
|
-
setSelectedSearchObjects(prev => prev.filter(it => it.path !== path));
|
|
2540
|
-
debouncedRefresh();
|
|
2541
|
-
}}
|
|
2542
|
-
deleteIcon={
|
|
2543
|
-
<Box sx={{
|
|
2544
|
-
display: 'flex',
|
|
2545
|
-
alignItems: 'center',
|
|
2546
|
-
px: 0.5,
|
|
2547
|
-
bgcolor: 'error.main',
|
|
2548
|
-
color: (theme) => theme.palette.error.contrastText,
|
|
2549
|
-
height: '100%',
|
|
2550
|
-
alignSelf: 'stretch',
|
|
2551
|
-
borderTopRightRadius: '4px',
|
|
2552
|
-
borderBottomRightRadius: '4px'
|
|
2553
|
-
}}>
|
|
2554
|
-
<Close fontSize="small"/>
|
|
2555
|
-
</Box>
|
|
2556
|
-
}
|
|
2557
|
-
sx={{
|
|
2558
|
-
mr: 0.5,
|
|
2559
|
-
borderRadius: '8px',
|
|
2560
|
-
'& .MuiChip-deleteIcon': {
|
|
2561
|
-
margin: 0
|
|
2562
|
-
}
|
|
2563
|
-
}}
|
|
2564
|
-
/>
|
|
2565
|
-
);
|
|
2566
|
-
});
|
|
2567
|
-
})()}
|
|
2568
|
-
</Box>
|
|
2569
|
-
)
|
|
2570
|
+
>
|
|
2571
|
+
{viewerTitle}
|
|
2572
|
+
</Typography>
|
|
2573
|
+
)}
|
|
2574
|
+
{viewerCaption && (
|
|
2575
|
+
<Typography
|
|
2576
|
+
variant='caption'
|
|
2577
|
+
sx={{
|
|
2578
|
+
color: 'text.secondary',
|
|
2579
|
+
lineHeight: 1.2,
|
|
2580
|
+
whiteSpace: 'nowrap',
|
|
2581
|
+
overflow: 'hidden',
|
|
2582
|
+
textOverflow: 'ellipsis'
|
|
2570
2583
|
}}
|
|
2571
|
-
|
|
2584
|
+
>
|
|
2585
|
+
{viewerCaption}
|
|
2586
|
+
</Typography>
|
|
2587
|
+
)}
|
|
2588
|
+
</Box>
|
|
2589
|
+
) : (
|
|
2590
|
+
// Empty spacer keeps the right-side controls right-aligned even when
|
|
2591
|
+
// no title is set, so layout looks consistent across reports.
|
|
2592
|
+
<Box />
|
|
2593
|
+
)}
|
|
2572
2594
|
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2595
|
+
{/* ── RIGHT: search + filter + refresh + viewer actions ──────────── */}
|
|
2596
|
+
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1, flexShrink: 0 }}>
|
|
2597
|
+
{(searchFieldsRef?.current?.length ?? []) > 0 &&
|
|
2598
|
+
(() => {
|
|
2599
|
+
const hasValue = (searchTerm || '').length > 0 || (selectedSearchObjects?.length ?? 0) > 0
|
|
2600
|
+
const expanded = isSearchExpanded || hasValue
|
|
2601
|
+
if (!expanded) {
|
|
2602
|
+
return (
|
|
2603
|
+
<Tooltip title='بحث' placement='top'>
|
|
2604
|
+
<IconButton
|
|
2605
|
+
size='small'
|
|
2606
|
+
onClick={e => {
|
|
2607
|
+
setIsSearchExpanded(true)
|
|
2608
|
+
setTimeout(() => searchInputRef.current?.focus(), 0)
|
|
2609
|
+
}}
|
|
2610
|
+
sx={{
|
|
2611
|
+
border: '1px solid',
|
|
2612
|
+
borderColor: 'divider',
|
|
2613
|
+
borderRadius: 1,
|
|
2614
|
+
color: 'text.secondary',
|
|
2615
|
+
'&:hover': { borderColor: 'primary.main', color: 'primary.main' }
|
|
2616
|
+
}}
|
|
2617
|
+
>
|
|
2618
|
+
<SearchOutlined fontSize='small' />
|
|
2619
|
+
</IconButton>
|
|
2620
|
+
</Tooltip>
|
|
2621
|
+
)
|
|
2622
|
+
}
|
|
2623
|
+
return (
|
|
2624
|
+
<Box sx={{ px: 0, minWidth: minimized ? 200 : 320, display: 'flex', alignItems: 'center' }}>
|
|
2625
|
+
{/*<Tooltip title={'اكتب كلمة البحث ثم اختر الحقل'}>*/}
|
|
2626
|
+
<Box sx={{ position: 'relative', width: '100%' }}>
|
|
2627
|
+
<TextField
|
|
2628
|
+
placeholder={'بحث...'}
|
|
2629
|
+
fullWidth
|
|
2630
|
+
size={'small'}
|
|
2631
|
+
value={searchTerm}
|
|
2632
|
+
inputRef={searchInputRef}
|
|
2633
|
+
// ── Stylized look matching design reference ────────────────
|
|
2634
|
+
// - Rounded ~12px border on a paper background.
|
|
2635
|
+
// - Soft divider colour at rest, primary on focus/hover.
|
|
2636
|
+
// - Search icon sits at the end of the input (endAdornment).
|
|
2637
|
+
// - No floating label — just a placeholder, so the field
|
|
2638
|
+
// doesn't shift vertically when it gains a value.
|
|
2639
|
+
sx={{
|
|
2640
|
+
'& .MuiOutlinedInput-root': {
|
|
2641
|
+
borderRadius: 3,
|
|
2642
|
+
bgcolor: 'background.paper',
|
|
2643
|
+
pr: 1.25
|
|
2644
|
+
},
|
|
2645
|
+
'& .MuiOutlinedInput-notchedOutline': {
|
|
2646
|
+
borderColor: 'divider'
|
|
2647
|
+
},
|
|
2648
|
+
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
2649
|
+
borderColor: 'text.disabled'
|
|
2650
|
+
},
|
|
2651
|
+
'& .Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
2652
|
+
borderWidth: 1
|
|
2653
|
+
}
|
|
2654
|
+
}}
|
|
2655
|
+
onFocus={e => {
|
|
2656
|
+
setIsSearchExpanded(true)
|
|
2657
|
+
if ((e.target.value || '').length > 0) {
|
|
2658
|
+
setSearchPopoverAnchor(e.currentTarget)
|
|
2659
|
+
setSearchPopoverOpen(true)
|
|
2660
|
+
// Reset to top of list whenever the popover opens.
|
|
2661
|
+
setHighlightedFieldIndex(0)
|
|
2662
|
+
}
|
|
2663
|
+
}}
|
|
2664
|
+
onBlur={() => {
|
|
2665
|
+
// Collapse back to the icon only when there's nothing to show.
|
|
2666
|
+
const hasValue = (searchTerm || '').length > 0 || (selectedSearchObjects?.length ?? 0) > 0
|
|
2667
|
+
if (!hasValue) setIsSearchExpanded(false)
|
|
2668
|
+
}}
|
|
2669
|
+
onChange={e => {
|
|
2670
|
+
const v = e.target.value
|
|
2671
|
+
setSearchTerm(v)
|
|
2672
|
+
setSearchPopoverAnchor(e.currentTarget)
|
|
2673
|
+
setSearchPopoverOpen((v || '').length > 0)
|
|
2674
|
+
// Term changed → list order semantically resets, so put
|
|
2675
|
+
// the highlight back at the top.
|
|
2676
|
+
setHighlightedFieldIndex(0)
|
|
2677
|
+
}}
|
|
2678
|
+
onKeyDown={e => {
|
|
2679
|
+
const fields = searchFieldsRef?.current ?? []
|
|
2680
|
+
|
|
2681
|
+
if (e.key === 'ArrowDown') {
|
|
2682
|
+
e.preventDefault()
|
|
2683
|
+
if (!searchPopoverOpen) {
|
|
2684
|
+
// Open the popover if the user starts arrowing
|
|
2685
|
+
// before typing — common pattern in autocompletes.
|
|
2686
|
+
setSearchPopoverAnchor(searchInputRef.current)
|
|
2687
|
+
setSearchPopoverOpen(true)
|
|
2688
|
+
setHighlightedFieldIndex(0)
|
|
2689
|
+
return
|
|
2690
|
+
}
|
|
2691
|
+
if (fields.length === 0) return
|
|
2692
|
+
const next = (highlightedFieldIndex + 1) % fields.length
|
|
2693
|
+
setHighlightedFieldIndex(next)
|
|
2694
|
+
// Keep the highlighted row visible inside the 240px-capped list.
|
|
2695
|
+
searchOptionRefs.current[next]?.scrollIntoView({ block: 'nearest' })
|
|
2696
|
+
return
|
|
2697
|
+
}
|
|
2635
2698
|
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2699
|
+
if (e.key === 'ArrowUp') {
|
|
2700
|
+
e.preventDefault()
|
|
2701
|
+
if (!searchPopoverOpen) return
|
|
2702
|
+
if (fields.length === 0) return
|
|
2703
|
+
const prev = (highlightedFieldIndex - 1 + fields.length) % fields.length
|
|
2704
|
+
setHighlightedFieldIndex(prev)
|
|
2705
|
+
searchOptionRefs.current[prev]?.scrollIntoView({ block: 'nearest' })
|
|
2706
|
+
return
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
if (e.key === 'Home') {
|
|
2710
|
+
if (!searchPopoverOpen || fields.length === 0) return
|
|
2711
|
+
e.preventDefault()
|
|
2712
|
+
setHighlightedFieldIndex(0)
|
|
2713
|
+
searchOptionRefs.current[0]?.scrollIntoView({ block: 'nearest' })
|
|
2714
|
+
return
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
if (e.key === 'End') {
|
|
2718
|
+
if (!searchPopoverOpen || fields.length === 0) return
|
|
2719
|
+
e.preventDefault()
|
|
2720
|
+
const last = fields.length - 1
|
|
2721
|
+
setHighlightedFieldIndex(last)
|
|
2722
|
+
searchOptionRefs.current[last]?.scrollIntoView({ block: 'nearest' })
|
|
2723
|
+
return
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
if (e.key === 'Enter') {
|
|
2727
|
+
e.preventDefault()
|
|
2728
|
+
const term = (searchTerm || '').trim()
|
|
2729
|
+
if (!term) return
|
|
2730
|
+
if (fields.length === 0) return
|
|
2731
|
+
// Pick the highlighted field — defaults to the first
|
|
2732
|
+
// (highlightedFieldIndex starts at 0), so users who
|
|
2733
|
+
// never touched the arrow keys still get the same
|
|
2734
|
+
// behaviour as before.
|
|
2735
|
+
const idx = Math.min(Math.max(highlightedFieldIndex, 0), fields.length - 1)
|
|
2736
|
+
handlePickSearchField(fields[idx])
|
|
2737
|
+
return
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
if (e.key === 'Escape') {
|
|
2741
|
+
setSearchPopoverOpen(false)
|
|
2742
|
+
}
|
|
2743
|
+
}}
|
|
2744
|
+
InputProps={{
|
|
2745
|
+
startAdornment: (
|
|
2746
|
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
|
2747
|
+
{(() => {
|
|
2748
|
+
const byPath = selectedSearchObjects.reduce((acc, it) => {
|
|
2749
|
+
const key = it.path
|
|
2750
|
+
if (!acc[key]) acc[key] = { meta: it, values: [] }
|
|
2751
|
+
acc[key].values.push(String(it.value))
|
|
2752
|
+
return acc
|
|
2753
|
+
}, {})
|
|
2754
|
+
return Object.keys(byPath).map(path => {
|
|
2755
|
+
const g = byPath[path]
|
|
2756
|
+
const labelBase = g.meta.friendlyName || g.meta.path
|
|
2757
|
+
const valueLabel = g.values.reduce((acc, val, idx) => {
|
|
2758
|
+
if (idx > 0) acc.push(<strong key={`sep-${idx}`}> أو </strong>)
|
|
2759
|
+
acc.push(val)
|
|
2760
|
+
return acc
|
|
2761
|
+
}, [])
|
|
2762
|
+
return (
|
|
2763
|
+
<Chip
|
|
2764
|
+
key={`group-${path}`}
|
|
2765
|
+
size='small'
|
|
2766
|
+
label={
|
|
2767
|
+
<span>
|
|
2768
|
+
<strong>{labelBase}</strong>: {valueLabel}
|
|
2769
|
+
</span>
|
|
2770
|
+
}
|
|
2771
|
+
onDelete={() => {
|
|
2772
|
+
setSelectedSearchObjects(prev => prev.filter(it => it.path !== path))
|
|
2773
|
+
debouncedRefresh()
|
|
2774
|
+
}}
|
|
2775
|
+
deleteIcon={
|
|
2776
|
+
<Box
|
|
2777
|
+
sx={{
|
|
2778
|
+
display: 'flex',
|
|
2779
|
+
alignItems: 'center',
|
|
2780
|
+
px: 0.5,
|
|
2781
|
+
bgcolor: 'error.main',
|
|
2782
|
+
color: theme => theme.palette.error.contrastText,
|
|
2783
|
+
height: '100%',
|
|
2784
|
+
alignSelf: 'stretch',
|
|
2785
|
+
borderTopRightRadius: '4px',
|
|
2786
|
+
borderBottomRightRadius: '4px'
|
|
2787
|
+
}}
|
|
2788
|
+
>
|
|
2789
|
+
<Close fontSize='small' />
|
|
2790
|
+
</Box>
|
|
2791
|
+
}
|
|
2792
|
+
sx={{
|
|
2793
|
+
mr: 0.5,
|
|
2794
|
+
borderRadius: '8px',
|
|
2795
|
+
'& .MuiChip-deleteIcon': {
|
|
2796
|
+
margin: 0
|
|
2797
|
+
}
|
|
2798
|
+
}}
|
|
2799
|
+
/>
|
|
2800
|
+
)
|
|
2801
|
+
})
|
|
2802
|
+
})()}
|
|
2803
|
+
</Box>
|
|
2804
|
+
),
|
|
2805
|
+
endAdornment: <SearchOutlined sx={{ color: 'text.disabled', fontSize: 20, mr: 0.25 }} />
|
|
2806
|
+
}}
|
|
2807
|
+
/>
|
|
2808
|
+
|
|
2809
|
+
<ClickAwayListener onClickAway={() => setSearchPopoverOpen(false)}>
|
|
2810
|
+
<Popper
|
|
2811
|
+
open={searchPopoverOpen}
|
|
2812
|
+
anchorEl={searchPopoverAnchor}
|
|
2813
|
+
placement='bottom-start'
|
|
2814
|
+
style={{ zIndex: 1300, width: searchInputRef?.current?.offsetWidth || undefined }}
|
|
2815
|
+
>
|
|
2816
|
+
<Paper elevation={3} sx={{ mt: 0.5, maxHeight: 240, overflowY: 'auto' }}>
|
|
2817
|
+
<List dense>
|
|
2818
|
+
{(searchFieldsRef?.current ?? []).map((option, i) => (
|
|
2819
|
+
<ListItemButton
|
|
2820
|
+
key={`${option.path}-${i}`}
|
|
2821
|
+
ref={el => { searchOptionRefs.current[i] = el }}
|
|
2822
|
+
selected={i === highlightedFieldIndex}
|
|
2823
|
+
onMouseEnter={() => setHighlightedFieldIndex(i)}
|
|
2824
|
+
// Mouse-down (rather than click) so the input
|
|
2825
|
+
// doesn't lose focus before the handler runs.
|
|
2826
|
+
onMouseDown={(e) => {
|
|
2827
|
+
e.preventDefault()
|
|
2828
|
+
handlePickSearchField(option)
|
|
2829
|
+
}}
|
|
2830
|
+
>
|
|
2831
|
+
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
|
2832
|
+
<Box sx={{ fontSize: 14, fontWeight: 500 }}>{option.friendlyName || option.path}</Box>
|
|
2833
|
+
{/*<Box sx={{fontSize: 12, color: 'text.secondary'}}>{option.path}</Box>*/}
|
|
2834
|
+
</Box>
|
|
2835
|
+
</ListItemButton>
|
|
2836
|
+
))}
|
|
2837
|
+
{(searchFieldsRef?.current ?? []).length === 0 && (
|
|
2838
|
+
<ListItemButton disabled>
|
|
2839
|
+
<Box sx={{ px: 1, py: 0.5, color: 'text.secondary' }}>لا توجد حقول للبحث</Box>
|
|
2840
|
+
</ListItemButton>
|
|
2841
|
+
)}
|
|
2842
|
+
</List>
|
|
2843
|
+
</Paper>
|
|
2844
|
+
</Popper>
|
|
2845
|
+
</ClickAwayListener>
|
|
2846
|
+
</Box>
|
|
2847
|
+
{/*</Tooltip>*/}
|
|
2848
|
+
</Box>
|
|
2849
|
+
)
|
|
2850
|
+
})()}
|
|
2851
|
+
|
|
2852
|
+
{/* PDF export — only visible when the user has chosen "Load all data"
|
|
2853
|
+
(otherwise the grid is paginated and PDF would only print the
|
|
2854
|
+
current page). Kept as a compact IconButton — the primary
|
|
2855
|
+
actions in this toolbar are Filter / Refresh / viewerActions. */}
|
|
2856
|
+
{minimized !== true && !isPagination && (
|
|
2857
|
+
<Tooltip title='تصدير PDF'>
|
|
2858
|
+
<IconButton onClick={handlePDFExport} color='primary' size='small'>
|
|
2859
|
+
<PrintOutlined fontSize='small' />
|
|
2645
2860
|
</IconButton>
|
|
2646
2861
|
</Tooltip>
|
|
2647
|
-
|
|
2648
|
-
|
|
2862
|
+
)}
|
|
2863
|
+
<Button
|
|
2864
|
+
size='medium'
|
|
2865
|
+
color='primary'
|
|
2866
|
+
variant='outlined'
|
|
2867
|
+
onClick={() => setLocalRefresh(!localRefresh)}
|
|
2868
|
+
// sx={{ border: '1px solid' }}
|
|
2869
|
+
>
|
|
2870
|
+
<RefreshOutlined fontSize='small' />
|
|
2871
|
+
</Button>
|
|
2872
|
+
<Button
|
|
2873
|
+
size='small'
|
|
2874
|
+
variant='outlined'
|
|
2875
|
+
color='primary'
|
|
2876
|
+
startIcon={<FilterAlt fontSize='small' />}
|
|
2877
|
+
onClick={() => handleToggleDialogs('CustomFilter')}
|
|
2878
|
+
>
|
|
2879
|
+
Filter
|
|
2880
|
+
</Button>
|
|
2881
|
+
|
|
2882
|
+
{/* Viewer actions — rendered after Filter/Refresh in declared order.
|
|
2883
|
+
Each item is already bound by ReportViewerRenderer:
|
|
2884
|
+
{ key, label, icon, color, variant, onClick, disabled, confirmation }
|
|
2885
|
+
Icon is a string name resolved against @mui/icons-material. */}
|
|
2886
|
+
{Array.isArray(viewerActions) &&
|
|
2887
|
+
viewerActions.map(action => {
|
|
2888
|
+
const IconCmp = action.icon ? MuiIcons[action.icon] : null
|
|
2889
|
+
const variant = action.variant ?? 'outlined'
|
|
2890
|
+
const color = action.color ?? 'primary'
|
|
2891
|
+
return (
|
|
2892
|
+
<Button
|
|
2893
|
+
key={action.key}
|
|
2894
|
+
size='small'
|
|
2895
|
+
variant={variant}
|
|
2896
|
+
color={color}
|
|
2897
|
+
startIcon={IconCmp ? <IconCmp fontSize='small' /> : null}
|
|
2898
|
+
onClick={action.onClick}
|
|
2899
|
+
disabled={Boolean(action.disabled)}
|
|
2900
|
+
>
|
|
2901
|
+
{action.label}
|
|
2902
|
+
</Button>
|
|
2903
|
+
)
|
|
2904
|
+
})}
|
|
2649
2905
|
</Box>
|
|
2650
2906
|
</Grid>
|
|
2651
|
-
<TemplateStateContext.Provider
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
//
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2907
|
+
<TemplateStateContext.Provider
|
|
2908
|
+
value={{
|
|
2909
|
+
templates,
|
|
2910
|
+
selectedTemplate,
|
|
2911
|
+
setSelectedTemplate,
|
|
2912
|
+
handleGetTemplates,
|
|
2913
|
+
handleSaveTemplate,
|
|
2914
|
+
handleToggleDialogs,
|
|
2915
|
+
setIsTemplateEditing,
|
|
2916
|
+
builderData,
|
|
2917
|
+
// ── Display settings (rendered in the side panel) ────────────────────
|
|
2918
|
+
timerValue,
|
|
2919
|
+
setTimerValue,
|
|
2920
|
+
isPagination,
|
|
2921
|
+
setIsPagination,
|
|
2922
|
+
handleCSVExport,
|
|
2923
|
+
isDownloading,
|
|
2924
|
+
// Hide the Excel button in the side panel for raw / minimized reports.
|
|
2925
|
+
isExcelExportAvailable: Boolean(streamEndPoint && !builderData?.isRaw === true && minimized !== true)
|
|
2926
|
+
}}
|
|
2927
|
+
>
|
|
2928
|
+
<Box
|
|
2929
|
+
sx={{
|
|
2930
|
+
width: '100%',
|
|
2931
|
+
height: minimized === true ? '0px' : height ?? '70vh',
|
|
2932
|
+
direction: 'ltr',
|
|
2933
|
+
// Top corners flush with the toolbar above. AG Grid's quartz theme
|
|
2934
|
+
// rounds .ag-root-wrapper by default; we force the two top corners
|
|
2935
|
+
// to 0 so the grid sits seamlessly under the toolbar regardless of
|
|
2936
|
+
// the toolbar's own bottom edge. Bottom corners keep their theme
|
|
2937
|
+
// radius so the grid still looks like a contained surface.
|
|
2938
|
+
'& .ag-root-wrapper': {
|
|
2939
|
+
borderTopLeftRadius: 0,
|
|
2940
|
+
borderTopRightRadius: 0
|
|
2941
|
+
}
|
|
2942
|
+
}}
|
|
2943
|
+
>
|
|
2944
|
+
<AgGridReact
|
|
2945
|
+
debug={false}
|
|
2946
|
+
columnHoverHighlight={true}
|
|
2947
|
+
theme={agTheme}
|
|
2948
|
+
enableRtl={true}
|
|
2949
|
+
columnDefs={colDefs.current}
|
|
2950
|
+
rowModelType={'serverSide'}
|
|
2951
|
+
onGridReady={onGridReady}
|
|
2952
|
+
maxConcurrentDatasourceRequests={0}
|
|
2953
|
+
cacheBlockSize={100}
|
|
2954
|
+
onRowSelected={onRowSelected}
|
|
2955
|
+
// getChildCount={getChildCount}
|
|
2956
|
+
sideBar={sideBarConfig}
|
|
2957
|
+
context={{
|
|
2958
|
+
tRouting,
|
|
2959
|
+
builderId: builderData?.id,
|
|
2960
|
+
updateRef: updateRef,
|
|
2961
|
+
settings: builderModel?.settings,
|
|
2962
|
+
// Node identity — available inside all column functions as params.context.nodeId / nodeName
|
|
2963
|
+
nodeId: nodeId ?? null,
|
|
2964
|
+
nodeName: nodeId ?? null, // alias for convenience
|
|
2965
|
+
// Shared registry — access any report's ref: params.context.reportRefs.current['nodeId']
|
|
2691
2966
|
reportRefs: reportRefs ?? null,
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
updateRef
|
|
2967
|
+
// Returns the latest viewer state snapshot — used by MultiSelectEditor to commit values
|
|
2968
|
+
getViewerContext: () => ({
|
|
2969
|
+
data: viewerData,
|
|
2970
|
+
setData: setData ?? (() => {}),
|
|
2971
|
+
dataRef: viewerDataRef,
|
|
2972
|
+
reportRefs: reportRefs ?? null
|
|
2973
|
+
})
|
|
2974
|
+
}} // pass routing, builder id, updateRef, node identity and reportRefs
|
|
2975
|
+
gridOptions={{
|
|
2976
|
+
enableRangeSelection: true,
|
|
2977
|
+
enableCharts: true,
|
|
2978
|
+
getContextMenuItems: params => {
|
|
2979
|
+
const rowUniqueId = builderModel?.settings?.rowUniqueId
|
|
2980
|
+
const rowData = params.node?.data
|
|
2981
|
+
const defaultItems = params.defaultItems || []
|
|
2982
|
+
|
|
2983
|
+
if (!updateRef || !rowUniqueId || !rowData) {
|
|
2984
|
+
return defaultItems
|
|
2985
|
+
}
|
|
2709
2986
|
|
|
2710
|
-
|
|
2987
|
+
const rowIdValue = getRowIdFromSettings(rowData, rowUniqueId)
|
|
2988
|
+
const hasUpdate =
|
|
2989
|
+
updateRef.current &&
|
|
2990
|
+
Array.isArray(updateRef.current) &&
|
|
2991
|
+
updateRef.current.some(item => item.rowId === rowIdValue)
|
|
2992
|
+
|
|
2993
|
+
const customItems = []
|
|
2994
|
+
|
|
2995
|
+
if (hasUpdate) {
|
|
2996
|
+
customItems.push({
|
|
2997
|
+
name: 'Remove Row from Update Ref',
|
|
2998
|
+
icon: '<span class="ag-icon ag-icon-cancel" unselectable="on" role="presentation"></span>',
|
|
2999
|
+
action: () => {
|
|
3000
|
+
removeUpdateRefByRowId(updateRef, rowData, rowUniqueId)
|
|
3001
|
+
params.api.refreshCells({ force: true })
|
|
3002
|
+
}
|
|
3003
|
+
})
|
|
3004
|
+
}
|
|
2711
3005
|
|
|
2712
|
-
if (hasUpdate) {
|
|
2713
3006
|
customItems.push({
|
|
2714
|
-
name: '
|
|
2715
|
-
icon: '<span class="ag-icon ag-icon-
|
|
3007
|
+
name: 'Clone Update Ref (Backup)',
|
|
3008
|
+
icon: '<span class="ag-icon ag-icon-copy" unselectable="on" role="presentation"></span>',
|
|
2716
3009
|
action: () => {
|
|
2717
|
-
|
|
2718
|
-
params.api.refreshCells({force: true})
|
|
2719
|
-
}
|
|
3010
|
+
cloneUpdateRefToOriginal(updateRef, originalRefData)
|
|
3011
|
+
params.api.refreshCells({ force: true })
|
|
3012
|
+
},
|
|
3013
|
+
disabled: !updateRef.current || updateRef.current.length === 0
|
|
2720
3014
|
})
|
|
2721
|
-
}
|
|
2722
3015
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
3016
|
+
customItems.push({
|
|
3017
|
+
name: 'Restore Update Ref (from Backup)',
|
|
3018
|
+
icon: '<span class="ag-icon ag-icon-undo" unselectable="on" role="presentation"></span>',
|
|
3019
|
+
action: () => {
|
|
3020
|
+
restoreUpdateRefFromOriginal(updateRef, originalRefData)
|
|
3021
|
+
params.api.refreshCells({ force: true })
|
|
3022
|
+
},
|
|
3023
|
+
disabled: !originalRefData.current || originalRefData.current.length === 0
|
|
3024
|
+
})
|
|
2732
3025
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
3026
|
+
customItems.push({
|
|
3027
|
+
name: 'Normalize Update Ref (Merge Duplicates)',
|
|
3028
|
+
icon: '<span class="ag-icon ag-icon-columns" unselectable="on" role="presentation"></span>',
|
|
3029
|
+
action: () => {
|
|
3030
|
+
normalizeUpdateRef(updateRef)
|
|
3031
|
+
params.api.refreshCells({ force: true })
|
|
3032
|
+
},
|
|
3033
|
+
disabled: !updateRef.current || updateRef.current.length === 0
|
|
3034
|
+
})
|
|
2742
3035
|
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
3036
|
+
customItems.push({
|
|
3037
|
+
name: 'Clear All Update Ref',
|
|
3038
|
+
icon: '<span class="ag-icon ag-icon-cancel" unselectable="on" role="presentation"></span>',
|
|
3039
|
+
action: () => {
|
|
3040
|
+
if (updateRef.current && Array.isArray(updateRef.current)) {
|
|
3041
|
+
const count = updateRef.current.length
|
|
3042
|
+
clearAllUpdateRef(updateRef)
|
|
3043
|
+
params.api.refreshCells({ force: true })
|
|
3044
|
+
console.log(`Cleared ${count} rows from updateRef`)
|
|
3045
|
+
}
|
|
3046
|
+
},
|
|
3047
|
+
disabled: !updateRef.current || updateRef.current.length === 0
|
|
3048
|
+
})
|
|
2752
3049
|
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
action: () => {
|
|
2757
|
-
if (updateRef.current && Array.isArray(updateRef.current)) {
|
|
2758
|
-
const count = updateRef.current.length
|
|
2759
|
-
clearAllUpdateRef(updateRef)
|
|
2760
|
-
params.api.refreshCells({force: true})
|
|
2761
|
-
console.log(`Cleared ${count} rows from updateRef`)
|
|
2762
|
-
}
|
|
2763
|
-
},
|
|
2764
|
-
disabled: !updateRef.current || updateRef.current.length === 0
|
|
2765
|
-
})
|
|
3050
|
+
if (customItems.length > 0) {
|
|
3051
|
+
return [...customItems, 'separator', ...defaultItems]
|
|
3052
|
+
}
|
|
2766
3053
|
|
|
2767
|
-
|
|
2768
|
-
return [
|
|
2769
|
-
...customItems,
|
|
2770
|
-
'separator',
|
|
2771
|
-
...defaultItems
|
|
2772
|
-
]
|
|
3054
|
+
return defaultItems
|
|
2773
3055
|
}
|
|
3056
|
+
}}
|
|
3057
|
+
rowSelection='multiple'
|
|
3058
|
+
statusBar={{
|
|
3059
|
+
statusPanels: [
|
|
3060
|
+
// { statusPanel: 'agTotalRowCountComponent' },
|
|
3061
|
+
{
|
|
3062
|
+
statusPanel: CustomStatusBar,
|
|
3063
|
+
align: 'left'
|
|
3064
|
+
}
|
|
2774
3065
|
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
headerHeight={25}
|
|
2794
|
-
autoGroupColumnDef={autoGroupColumnDef}
|
|
2795
|
-
getRowId={getRowId}
|
|
2796
|
-
serverSideDatasource={datasource}
|
|
2797
|
-
blockLoadDebounceMillis={200}
|
|
2798
|
-
pinnedBottomRowData={pagedAgg}
|
|
2799
|
-
suppressRowClickSelection={true}
|
|
2800
|
-
serverSidePivotResultFieldSeparator={"_"}
|
|
2801
|
-
pivotKeySeparator={"_"}
|
|
2802
|
-
getRowStyle={(row) => {
|
|
2803
|
-
if (row.node.group) {
|
|
2804
|
-
return {fontWeight: 'bold'};
|
|
2805
|
-
}
|
|
2806
|
-
if (expireReport) {
|
|
2807
|
-
if (row.node?.data?.expireDate && new Date(row.node.data.expireDate) <= new Date()) {
|
|
2808
|
-
return {background: '#FF625F'};
|
|
3066
|
+
// { statusPanel: 'agFilteredRowCountComponent' },
|
|
3067
|
+
// { statusPanel: 'agSelectedRowCountComponent' },
|
|
3068
|
+
]
|
|
3069
|
+
}}
|
|
3070
|
+
groupHeaderHeight={25}
|
|
3071
|
+
// onChartCreated={x => console.log(x)}
|
|
3072
|
+
headerHeight={25}
|
|
3073
|
+
autoGroupColumnDef={autoGroupColumnDef}
|
|
3074
|
+
getRowId={getRowId}
|
|
3075
|
+
serverSideDatasource={datasource}
|
|
3076
|
+
blockLoadDebounceMillis={200}
|
|
3077
|
+
pinnedBottomRowData={pagedAgg}
|
|
3078
|
+
suppressRowClickSelection={true}
|
|
3079
|
+
serverSidePivotResultFieldSeparator={'_'}
|
|
3080
|
+
pivotKeySeparator={'_'}
|
|
3081
|
+
getRowStyle={row => {
|
|
3082
|
+
if (row.node.group) {
|
|
3083
|
+
return { fontWeight: 'bold' }
|
|
2809
3084
|
}
|
|
2810
|
-
if (
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
3085
|
+
if (expireReport) {
|
|
3086
|
+
if (row.node?.data?.expireDate && new Date(row.node.data.expireDate) <= new Date()) {
|
|
3087
|
+
return { background: '#FF625F' }
|
|
3088
|
+
}
|
|
3089
|
+
if (row.node?.data?.expireDate) {
|
|
3090
|
+
const expireDate = new Date(row.node.data.expireDate)
|
|
3091
|
+
const currentDate = new Date()
|
|
3092
|
+
const diffInDays = (expireDate - currentDate) / (1000 * 60 * 60 * 24) // Convert milliseconds to days
|
|
3093
|
+
|
|
3094
|
+
if (diffInDays > 10) {
|
|
3095
|
+
return { background: '#8AFF8A' }
|
|
3096
|
+
} else if (diffInDays > 0) {
|
|
3097
|
+
return { background: '#fcfd74' }
|
|
3098
|
+
}
|
|
2819
3099
|
}
|
|
2820
3100
|
}
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
params.api.refreshServerSide({purge: true})
|
|
2844
|
-
}
|
|
2845
|
-
}}
|
|
2846
|
-
/>
|
|
2847
|
-
</div>
|
|
3101
|
+
}}
|
|
3102
|
+
components={{
|
|
3103
|
+
...AG_COMPONENTS,
|
|
3104
|
+
routingCell: RoutingCell,
|
|
3105
|
+
imageCell: ImageCell,
|
|
3106
|
+
customStatusBar: CustomStatusBar,
|
|
3107
|
+
templatesToolPanel: TemplatesToolPanel
|
|
3108
|
+
}}
|
|
3109
|
+
onCellValueChanged={params => {
|
|
3110
|
+
// Guard: skip undefined phantom reverts produced by MultiSelectEditor's stopEditing(true) cancel path
|
|
3111
|
+
if (params.newValue === undefined && params.colDef?.cellEditor === 'MultiSelectEditor') {
|
|
3112
|
+
return
|
|
3113
|
+
}
|
|
3114
|
+
}}
|
|
3115
|
+
onSortChanged={params => {
|
|
3116
|
+
const sm = params.columns
|
|
3117
|
+
if (sm.length == 1 && sm.some(s => s.colId === 'IZ_groupCount')) {
|
|
3118
|
+
params.api.refreshServerSide({ purge: true })
|
|
3119
|
+
}
|
|
3120
|
+
}}
|
|
3121
|
+
/>
|
|
3122
|
+
</Box>
|
|
2848
3123
|
</TemplateStateContext.Provider>
|
|
2849
3124
|
<Dialog
|
|
2850
3125
|
fullWidth
|
|
@@ -2861,7 +3136,8 @@ const SGrid = props => {
|
|
|
2861
3136
|
pageName={'Builder' + builderData?.id}
|
|
2862
3137
|
item={isTemplateEditing === true ? selectedTemplate : null}
|
|
2863
3138
|
template={gridApi?.getColumnState()}
|
|
2864
|
-
userId={authValues?.user?.id}
|
|
3139
|
+
userId={authValues?.user?.id}
|
|
3140
|
+
/>
|
|
2865
3141
|
</Dialog>
|
|
2866
3142
|
<Dialog
|
|
2867
3143
|
fullWidth
|
|
@@ -2873,36 +3149,32 @@ const SGrid = props => {
|
|
|
2873
3149
|
{openDialogs.CustomFilter && (
|
|
2874
3150
|
<CustomFilterDialog
|
|
2875
3151
|
handleToggleDialogs={handleToggleDialogs}
|
|
2876
|
-
Filter={(Filter?.LocalTfilter || []).filter(f =>
|
|
3152
|
+
Filter={(Filter?.LocalTfilter || []).filter(f => f.isMainFilter !== true)}
|
|
2877
3153
|
customFilterCode={Filter?.customFilterCode}
|
|
2878
3154
|
handleFilterChange={handleFilterChange}
|
|
2879
3155
|
className={builderData?.reportSource?.fullName}
|
|
2880
3156
|
LocalFilter={false}
|
|
2881
|
-
|
|
2882
3157
|
isViewer={router.pathname.includes('report/viewer')}
|
|
2883
3158
|
selectTFilter={selectTFilter}
|
|
2884
3159
|
selectParamsMeta={builderData?.selectionParams || []}
|
|
2885
3160
|
selectParamsValues={selectParams}
|
|
2886
|
-
onSelectParamsSave={
|
|
3161
|
+
onSelectParamsSave={list => {
|
|
2887
3162
|
// list is array of { index, value, type }
|
|
2888
3163
|
setSelectParams(prev => {
|
|
2889
|
-
const arr = [...(prev || [])]
|
|
2890
|
-
(list || []).forEach(item => {
|
|
2891
|
-
const i = item.index
|
|
2892
|
-
const type = item.type
|
|
2893
|
-
const current = arr[i] || {id: i, PropertyType: type, value: null}
|
|
2894
|
-
arr[i] = {...current, PropertyType: current.PropertyType || type, value: item.value}
|
|
2895
|
-
})
|
|
2896
|
-
return arr
|
|
2897
|
-
})
|
|
3164
|
+
const arr = [...(prev || [])]
|
|
3165
|
+
;(list || []).forEach(item => {
|
|
3166
|
+
const i = item.index
|
|
3167
|
+
const type = item.type
|
|
3168
|
+
const current = arr[i] || { id: i, PropertyType: type, value: null }
|
|
3169
|
+
arr[i] = { ...current, PropertyType: current.PropertyType || type, value: item.value }
|
|
3170
|
+
})
|
|
3171
|
+
return arr
|
|
3172
|
+
})
|
|
2898
3173
|
}}
|
|
2899
3174
|
/>
|
|
2900
3175
|
)}
|
|
2901
3176
|
</Dialog>
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
3177
|
</Grid>
|
|
2905
|
-
|
|
2906
3178
|
)
|
|
2907
3179
|
}
|
|
2908
3180
|
|