robobyte-front-builder 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@
26
26
  * - Adds search term filters based on searchTermRef and searchFieldsRef metadata.
27
27
  * - Translates group/pivot/value columns, sort model, and pagination into the expected backend format.
28
28
  * 3) Data is requested via ReportBuilderEndpoints.Post.GenericGet. Responses may contain rows and aggregations.
29
- * 4) Grid updates, status bar shows counts/aggregations, and export functions can read current viewer state.
29
+ * 4) Grid updates, status bar shows counts/aggregations, and export functions can read current view state.
30
30
  *
31
31
  * Quick search behavior (important)
32
32
  * - searchTermRef holds the current string. If it parses to a finite number, we search numeric (Int) fields with a numeric value.
@@ -59,6 +59,24 @@ import Grid from '@mui/material/Grid'
59
59
  // ** Store Imports
60
60
  // ** Custom Components Imports
61
61
  import {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'
62
+
63
+ // Global styles for flashing effect
64
+ if (typeof document !== 'undefined') {
65
+ const styleId = 'ag-grid-flashing-effect';
66
+ if (!document.getElementById(styleId)) {
67
+ const style = document.createElement('style');
68
+ style.id = styleId;
69
+ style.innerHTML = `
70
+ @keyframes flashing-bg {
71
+ 0% { opacity: 1; }
72
+ 50% { opacity: 0.5; }
73
+ 100% { opacity: 1; }
74
+ }
75
+ `;
76
+ document.head.appendChild(style);
77
+ }
78
+ }
79
+
62
80
  import {Endpoints, Services} from 'src/services/Endpoints'
63
81
  import {
64
82
  Autocomplete, Checkbox,
@@ -76,7 +94,8 @@ import {
76
94
  Paper,
77
95
  List,
78
96
  ListItemButton,
79
- ClickAwayListener
97
+ ClickAwayListener,
98
+ Menu
80
99
  } from '@mui/material'
81
100
  import {AgGridReact} from "ag-grid-react";
82
101
  import {Edit, FilterAlt, PrintOutlined, RefreshOutlined, Save, SaveAs, Close} from "@mui/icons-material";
@@ -101,6 +120,22 @@ import jsPDF from "jspdf";
101
120
  import "jspdf-autotable";
102
121
  import arabicFontBase64 from "../../../public/fonts/font";
103
122
  import {FilterFormat} from "services/helper/FilterFormat";
123
+ import {saveReportSession} from 'src/services/helper/reportSessionHelper'
124
+ import {
125
+ setUpdateRefValue,
126
+ setUpdateRefRow,
127
+ getUpdateRefValue,
128
+ hasUpdateRefValue,
129
+ clearUpdateRefRow,
130
+ clearAllUpdateRef,
131
+ getAllUpdates,
132
+ removeUpdateRefByRowId,
133
+ getRowId as getRowIdFromSettings,
134
+ normalizeUpdateRef,
135
+ cloneUpdateRefToOriginal,
136
+ restoreUpdateRefFromOriginal
137
+ } from './updateRefHelpers'
138
+ import {processColumnDefinitions, processColumnsConfig} from './convertStringFunctions'
104
139
 
105
140
  // ** Utils Import
106
141
 
@@ -137,6 +172,7 @@ const SGrid = props => {
137
172
  setOutGridApi,
138
173
  setEventRowSelected,
139
174
  pageName,
175
+ pageId,
140
176
  paramsPage,
141
177
  fixedTIncludes,
142
178
  groupBy,
@@ -144,14 +180,17 @@ const SGrid = props => {
144
180
  reportTitle,
145
181
  uniqueIdPath,
146
182
  columnsConfig,
147
- dataAsObject = false
183
+ dataAsObject = false,
184
+ isRerender = false,
185
+ updateRef
148
186
  } = props
149
187
  const groupEndPoint = ReportBuilderEndpoints.Post.GenericGet;
150
188
  const streamEndPoint = ReportBuilderEndpoints.Post.GenericGet;
151
189
  const pagedEndPoint = ReportBuilderEndpoints.Post.GenericGet;
152
190
  const [responseType, setResponseType] = useState()
153
191
  const [pagedAgg, setPagedAgg] = useState()
154
- const [colDefs, setColDefs] = useState([])
192
+ const colDefs = useRef([])
193
+ const originalRefData = useRef([])
155
194
  const [gridApi, setGridApi] = useState(null)
156
195
  const [includes, setIncludes] = useState([])
157
196
  const [isPagination, setIsPagination] = useState(true)
@@ -163,6 +202,7 @@ const SGrid = props => {
163
202
  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
164
203
  const router = useRouter()
165
204
  const authValues = useContext(AuthContext)
205
+ const contextValues = useContext(SystemContext)
166
206
  const [isTemplateEditing, setIsTemplateEditing] = useState(false)
167
207
  const [isDownloading, setIsDownloading] = useState(false)
168
208
  const [finalRequestObject, setFinalRequestObject] = useState(defaultFinalRequest)
@@ -191,6 +231,87 @@ const SGrid = props => {
191
231
  Tfilter: [],
192
232
  LocalTfilter: [],
193
233
  })
234
+
235
+ // Persistence Key helper
236
+ const getSessionKey = useCallback(() => {
237
+ const reportId = builderData?.id || builderModel?.id;
238
+ const pId = pageId || router.query?.pageId;
239
+ if (pId) return `RB_SESSION_FILTERS_PAGE_${pId}`;
240
+ if (reportId) return `RB_SESSION_FILTERS_REP_${reportId}`;
241
+ return null;
242
+ }, [builderData?.id, builderModel?.id, pageId, router.query?.pageId]);
243
+
244
+ // Clone updateRef to originalRefData when it first gets data
245
+ useEffect(() => {
246
+ if (updateRef && updateRef.current && Array.isArray(updateRef.current) && updateRef.current.length > 0) {
247
+ // Only clone if originalRefData is empty (first time updateRef has data)
248
+ if (!originalRefData.current || originalRefData.current.length === 0) {
249
+ cloneUpdateRefToOriginal(updateRef, originalRefData);
250
+ }
251
+ }
252
+ }, [updateRef?.current?.length]); // Watch for when updateRef gets data
253
+
254
+ // Load filters from sessionStorage on mount
255
+ useEffect(() => {
256
+ const key = getSessionKey();
257
+ if (!key) return;
258
+
259
+ try {
260
+ // Check if it's a refresh
261
+ let isRefresh = false;
262
+ try {
263
+ const navEntries = performance.getEntriesByType('navigation');
264
+ isRefresh = navEntries.length > 0 && navEntries[0].type === 'reload';
265
+ } catch (e) {
266
+ console.warn('SGrid: performance.getEntriesByType not supported', e);
267
+ }
268
+
269
+ if (isRefresh) {
270
+ sessionStorage.removeItem(key);
271
+ return;
272
+ }
273
+
274
+ const stored = sessionStorage.getItem(key);
275
+ if (stored) {
276
+ // If externalFilter is provided via props (routing payload), it should probably take precedence
277
+ // but let's see if it's actually passed. In ReportViewer, it's passed as 'filter' prop.
278
+ if (externalFilter) {
279
+ return;
280
+ }
281
+
282
+ const parsed = JSON.parse(stored);
283
+
284
+ if (parsed.Filter) setFilter(parsed.Filter);
285
+ if (parsed.selectedSearchObjects) setSelectedSearchObjects(parsed.selectedSearchObjects);
286
+ if (parsed.selectParams) setSelectParams(parsed.selectParams);
287
+ }
288
+ } catch (e) {
289
+ console.error('SGrid: Failed to restore session filters', e);
290
+ }
291
+ }, [getSessionKey, externalFilter]);
292
+
293
+ // Save filters to sessionStorage whenever they change
294
+ useEffect(() => {
295
+ const key = getSessionKey();
296
+ if (!key) return;
297
+
298
+ const timeout = setTimeout(() => {
299
+ try {
300
+ const toStore = {
301
+ Filter,
302
+ selectedSearchObjects,
303
+ selectParams
304
+ };
305
+ // Always store current state. If user cleared all filters, we want to remember it's empty in this session.
306
+ // We only avoid storing if the state is exactly the default/initial state and we haven't modified it yet.
307
+ sessionStorage.setItem(key, JSON.stringify(toStore));
308
+ } catch (e) {
309
+ console.error('SGrid: Failed to save session filters', e);
310
+ }
311
+ }, 1000); // debounce saving
312
+
313
+ return () => clearTimeout(timeout);
314
+ }, [Filter, selectedSearchObjects, selectParams, getSessionKey]);
194
315
  const [openDialogs, setOpenDialogs] = useState({
195
316
  addTemplate: false,
196
317
  CustomFilter: false,
@@ -277,7 +398,7 @@ const SGrid = props => {
277
398
  if (c.colId === "IZ_groupCount") {
278
399
  return {field: "IZ_groupCount", headerName: "Group Count"};
279
400
  }
280
- return colDefs.find(cd => cd.field === c.colId);
401
+ return colDefs.current.find(cd => cd.field === c.colId);
281
402
  })
282
403
  .filter(Boolean);
283
404
 
@@ -552,7 +673,15 @@ const SGrid = props => {
552
673
  async function handleResetColsToStudio() {
553
674
  try {
554
675
  setBuilderTFilter(builderData?.filter?.Tfilter ?? [])
555
- console.log(builderModel)
676
+ setFilter(pre => ({
677
+ ...pre,
678
+ LocalTfilter: [
679
+ ...(builderData?.filter?.fixedTFilter || []).map(f => ({...f})),
680
+ ...(builderData?.filter?.LocalTfilter || []).map(f => ({...f}))
681
+ ],
682
+ Tfilter: builderData?.filter?.Tfilter || [],
683
+ customFilterCode: builderData?.filter?.customFilterCode || ''
684
+ }))
556
685
  const allowedAggFuncs = ["sum", "avg", "min", "max"];
557
686
  const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
558
687
  let defaultCols = getDefaultColDefs()
@@ -567,7 +696,8 @@ const SGrid = props => {
567
696
  const propertyType = col.propertyType ?? col.field?.propertyType;
568
697
  const propPath = col.headerName ?? col.path;
569
698
  const fieldName = col.headerName ?? col.path.replace(/\./g, '_');
570
- const externalColConfig = (columnsConfig ?? []).find(x => x.field === fieldName)?.config ?? {}
699
+ const processedColumnsConfig = processColumnsConfig(columnsConfig ?? []);
700
+ const externalColConfig = processedColumnsConfig.find(x => x.field === fieldName)?.config ?? {}
571
701
  const headerName = col.title ?? col.headerName ?? col.path;
572
702
  const hasRouting = routingSettings.some(r => r.fieldName === fieldName);
573
703
  let valueFormatter = null;
@@ -583,8 +713,15 @@ const SGrid = props => {
583
713
  }
584
714
 
585
715
  if (propertyType === 'Double') {
716
+ const decimals = col.formatting?.decimals ?? 0;
717
+ valueFormatter =
718
+ `return params?.value != null ? numeral(params.value.toFixed(${decimals})).format('0,0.${'0'.repeat(decimals)}') : ''`;
719
+ }
720
+
721
+ if (propertyType === 'Int' && col.formatting?.decimals > 0) {
722
+ const decimals = col.formatting.decimals;
586
723
  valueFormatter =
587
- `return params?.value != null ? numeral(params.value.toFixed(2)).format('0,0') : ''`;
724
+ `return params?.value != null ? numeral(params.value.toFixed(${decimals})).format('0,0.${'0'.repeat(decimals)}') : ''`;
588
725
  }
589
726
 
590
727
  if (propertyType === 'Enum') {
@@ -593,7 +730,8 @@ const SGrid = props => {
593
730
  `return params?.value != null ? enumTrans('${enumName}', params.value) : ''`;
594
731
  }
595
732
 
596
- const isNumericAgg = (propertyType === 'Int' || propertyType === 'Double') &&
733
+ const isNumeric = propertyType === 'Int' || propertyType === 'Double';
734
+ const isNumericAgg = isNumeric &&
597
735
  !col.field?.path?.toLowerCase().endsWith('id');
598
736
 
599
737
  const isImage = col.image === true;
@@ -608,7 +746,173 @@ const SGrid = props => {
608
746
  ? "agCheckboxCellRenderer"
609
747
  : null)),
610
748
  width: isImage ? 90 : undefined,
611
- cellStyle: isImage ? {display: 'flex', alignItems: 'center', justifyContent: 'center'} : undefined,
749
+ cellStyle: (params) => {
750
+ let style = isImage ? {display: 'flex', alignItems: 'center', justifyContent: 'center'} : {};
751
+ const formatting = col.formatting;
752
+ if (formatting) {
753
+ if (formatting.fixedStyle) {
754
+ style = {...style, ...formatting.fixedStyle};
755
+ if (formatting.fixedStyle.flashing) {
756
+ style.animation = 'flashing-bg 1s infinite';
757
+ }
758
+ }
759
+ if (formatting.conditions && formatting.conditions.length > 0) {
760
+ const val = params.value;
761
+ for (const cond of formatting.conditions) {
762
+ let match = false;
763
+ const baseField = cond.baseField || 'self';
764
+ let currentVal;
765
+ if (baseField === 'self') {
766
+ currentVal = val;
767
+ } else {
768
+ // Resolve field path from data
769
+ // First try as is
770
+ currentVal = params.node.data[baseField];
771
+
772
+ // If not found and contains dots, try nested path
773
+ if ((currentVal === undefined || currentVal === null) && baseField.includes('.')) {
774
+ currentVal = baseField.split('.').reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : undefined, params.node.data);
775
+ }
776
+ }
777
+
778
+ const isNumericValue = typeof currentVal === 'number' || (!isNaN(parseFloat(currentVal)) && isFinite(currentVal));
779
+ let condVal;
780
+ if (cond.valueType === 'field' && cond.compareField) {
781
+ const compareField = cond.compareField;
782
+ // Resolve field path from data
783
+ // First try as is
784
+ condVal = params.node.data[compareField];
785
+
786
+ // If not found and contains dots, try nested path
787
+ if ((condVal === undefined || condVal === null) && compareField.includes('.')) {
788
+ condVal = compareField.split('.').reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : undefined, params.node.data);
789
+ }
790
+ } else if (cond.valueType === 'expression' && cond.expression) {
791
+ try {
792
+ // Math Expression evaluation
793
+ // Replace "FieldPath" with actual values from row data
794
+ let expr = cond.expression;
795
+ const fieldRegex = /"([^"]+)"/g;
796
+ let match;
797
+ let hasMissingValue = false;
798
+ const fieldValues = {};
799
+
800
+ // Find all fields in quotes
801
+ while ((match = fieldRegex.exec(cond.expression)) !== null) {
802
+ const fullMatch = match[0];
803
+ const path = match[1];
804
+ let val;
805
+ let fieldFound = false;
806
+
807
+ if (path === 'self') {
808
+ val = params.value;
809
+ fieldFound = true;
810
+ } else {
811
+ // Resolve field path from data
812
+ // First try as is (handles fieldName with underscores)
813
+ val = params.node.data[path];
814
+
815
+ if (val !== undefined) {
816
+ fieldFound = true;
817
+ } else if (path.includes('.')) {
818
+ // If not found and contains dots, try nested path
819
+ val = path.split('.').reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : undefined, params.node.data);
820
+ if (val !== undefined) {
821
+ fieldFound = true;
822
+ }
823
+ }
824
+ }
825
+
826
+ if (fieldFound) {
827
+ // If it's a string that is not a number, wrap it in single quotes to make it a string literal in the expression
828
+ // unless it's already a number
829
+ if (typeof val === 'string' && isNaN(parseFloat(val))) {
830
+ fieldValues[fullMatch] = `'${val.replace(/'/g, "\\'")}'`;
831
+ } else if (typeof val === 'number') {
832
+ fieldValues[fullMatch] = val;
833
+ } else if (!isNaN(parseFloat(val)) && isFinite(val)) {
834
+ fieldValues[fullMatch] = parseFloat(val);
835
+ } else if (val === null) {
836
+ fieldValues[fullMatch] = 'null';
837
+ } else if (typeof val === 'boolean') {
838
+ fieldValues[fullMatch] = val;
839
+ } else {
840
+ // Default to string if we can't be sure
841
+ fieldValues[fullMatch] = `'${String(val).replace(/'/g, "\\'")}'`;
842
+ }
843
+ }
844
+ }
845
+
846
+ // Replace fields with their values
847
+ // Sort keys by length descending to avoid partial replacements (e.g., "Field" before "FieldLonger")
848
+ const sortedKeys = Object.keys(fieldValues).sort((a, b) => b.length - a.length);
849
+ sortedKeys.forEach(key => {
850
+ // Escape special characters in key for RegExp
851
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
852
+ expr = expr.replace(new RegExp(escapedKey, 'g'), fieldValues[key]);
853
+ });
854
+
855
+ // Use Function constructor for evaluation
856
+ // Strip potentially dangerous characters while allowing math, comparison, ternary, and basic logical operators
857
+ // We allow: numbers, operators (+-*/), comparison (><=!|&), ternary (?:), parens, dots, spaces, commas, and some math words
858
+ // Updated to allow keywords for if and switch statements
859
+ // Also added ' for string literals
860
+ const safeExpr = expr.replace(/[^0-9+\-*/().\s><=!|&?:,;{}'a-zA-Z\\]/g, '');
861
+ // Check if it already contains 'return', otherwise wrap it if it looks like a simple expression
862
+ const finalCode = safeExpr.trim().startsWith('if') || safeExpr.trim().startsWith('switch') || safeExpr.includes('return')
863
+ ? safeExpr
864
+ : `return ${safeExpr}`;
865
+ condVal = new Function(finalCode)();
866
+ } catch (e) {
867
+ console.error("Math expression evaluation failed:", e);
868
+ condVal = undefined;
869
+ }
870
+ } else {
871
+ condVal = cond.value;
872
+ }
873
+
874
+ const isCondNumeric = typeof condVal === 'number' || (!isNaN(parseFloat(condVal)) && isFinite(condVal));
875
+ const finalCondVal = isCondNumeric ? parseFloat(condVal) : condVal;
876
+ const compareVal = isNumericValue ? parseFloat(currentVal) : currentVal;
877
+
878
+ if (cond.valueType === 'expression') {
879
+ match = !!condVal;
880
+ } else {
881
+ switch (cond.operator) {
882
+ case '==':
883
+ match = compareVal == finalCondVal;
884
+ break;
885
+ case '!=':
886
+ match = compareVal != finalCondVal;
887
+ break;
888
+ case '>':
889
+ match = compareVal > finalCondVal;
890
+ break;
891
+ case '<':
892
+ match = compareVal < finalCondVal;
893
+ break;
894
+ case '>=':
895
+ match = compareVal >= finalCondVal;
896
+ break;
897
+ case '<=':
898
+ match = compareVal <= finalCondVal;
899
+ break;
900
+ case 'contains':
901
+ match = String(compareVal).toLowerCase().includes(String(finalCondVal).toLowerCase());
902
+ break;
903
+ }
904
+ }
905
+ if (match) {
906
+ style = {...style, ...cond.style};
907
+ if (cond.style.flashing) {
908
+ style.animation = 'flashing-bg 1s infinite';
909
+ }
910
+ }
911
+ }
912
+ }
913
+ }
914
+ return style;
915
+ },
612
916
  allowedAggFuncs: isNumericAgg ? allowedAggFuncs : [],
613
917
  valueFormatter: valueFormatter
614
918
  ? applyValueFormatter(valueFormatter)
@@ -655,10 +959,11 @@ const SGrid = props => {
655
959
  }))
656
960
  setSelectParams(localSelectParams);
657
961
  if (extraCols != null) {
658
- localColDefs = [...extraCols, ...localColDefs];
962
+ const processedExtraCols = processColumnDefinitions(extraCols);
963
+ localColDefs = [...processedExtraCols, ...localColDefs];
659
964
  }
660
965
  localColDefs = [...localColDefs, ...defaultCols];
661
- setColDefs([
966
+ colDefs.current = ([
662
967
  {
663
968
  field: 'IZ_groupCount',
664
969
  headerName: 'Group Count',
@@ -705,7 +1010,8 @@ const SGrid = props => {
705
1010
  tableCols = processColumns(tableCols)
706
1011
  let localColDefs = [...tableCols, ...defaultCols]
707
1012
  if (extraCols != null) {
708
- localColDefs = [...extraCols, ...localColDefs]
1013
+ const processedExtraCols = processColumnDefinitions(extraCols);
1014
+ localColDefs = [...processedExtraCols, ...localColDefs]
709
1015
  }
710
1016
  const localSelectTFilter = response.data.map(x => ({
711
1017
  friendlyName: x.fieldName,
@@ -714,9 +1020,9 @@ const SGrid = props => {
714
1020
  propertyType: x.type,
715
1021
  }))
716
1022
  setSelectTFilter(localSelectTFilter)
717
- setColDefs(localColDefs)
1023
+ colDefs.current = localColDefs
718
1024
  } else {
719
- setColDefs([])
1025
+ colDefs.current = []
720
1026
  }
721
1027
  } catch (error) {
722
1028
 
@@ -753,92 +1059,161 @@ const SGrid = props => {
753
1059
  const {tRouting} = context;
754
1060
  const fieldName = colDef.field;
755
1061
 
756
- const routing = tRouting?.find(r => r.fieldName === fieldName || r.headerName === fieldName);
757
- if (!routing) return <span>{value}</span>;
1062
+ const [anchorEl, setAnchorEl] = useState(null);
1063
+ const open = Boolean(anchorEl);
758
1064
 
759
- // Determine behavior based on routing.type
760
- const type = routing.type || 'Route';
1065
+ const routes = tRouting?.filter(r => r.fieldName === fieldName || r.headerName === fieldName) || [];
1066
+ if (routes.length === 0) return <span>{value}</span>;
761
1067
 
762
- // Build finalRoute for Route type
763
- let finalRoute = null;
764
- if (type === 'Route') {
765
- if (routing.script) {
766
- try {
767
- const fn = new Function("row", routing.script);
768
- finalRoute = fn(data);
769
- } catch (err) {
770
- console.error("Routing script error:", err);
771
- finalRoute = "#";
772
- }
773
- } else if (routing.pattern) {
774
- finalRoute = routing.pattern.replace(/\{\{(.*?)\}\}/g, (_, key) => {
775
- const val = data?.[key];
776
- return val != null ? val : `:${key}`;
777
- });
778
- }
779
- }
1068
+ const handleRouteClick = (routing) => {
1069
+ setAnchorEl(null);
1070
+ // Determine behavior based on routing.type
1071
+ const type = routing.type || 'Route';
780
1072
 
781
- const handleClick = () => {
782
1073
  if (type === 'Filter') {
783
1074
  try {
784
1075
  // Prepare payload to pass via window.name (works across new tabs without session)
785
- const raw = routing.externalFilterJson || '';
786
- const replaced = raw.replace(/\{\{(.*?)\}\}/g, (_, key) => {
787
- const v = data?.[key];
788
- return v != null ? String(v) : '';
789
- });
790
1076
  let externalFilter = null;
791
- try {
792
- externalFilter = replaced ? JSON.parse(replaced) : null;
793
- } catch (e) {
794
- console.error('Invalid externalFilterJson after replacement', e);
1077
+
1078
+ if (routing.customFilterCode) {
1079
+ try {
1080
+ // Create function with access to authContext (if needed) and row data
1081
+ const executeCode = new Function('context', 'authContext', 'row', `
1082
+ let customFilter = [];
1083
+ try {
1084
+ ${routing.customFilterCode}
1085
+ } catch (e) {
1086
+ console.error('Inner error in routing customFilterCode:', e);
1087
+ }
1088
+ return customFilter;
1089
+ `);
1090
+ const customFilterResult = executeCode(contextValues, authValues, data);
1091
+
1092
+ if (customFilterResult && Array.isArray(customFilterResult)) {
1093
+ externalFilter = {Tfilter: FilterFormat(customFilterResult)};
1094
+ } else if (customFilterResult && typeof customFilterResult === 'object') {
1095
+ // If it returned { Tfilter: [...] } directly
1096
+ externalFilter = customFilterResult;
1097
+ }
1098
+ } catch (error) {
1099
+ console.error('Error executing routing custom filter code:', error);
1100
+ }
1101
+ }
1102
+
1103
+ if (!externalFilter) {
1104
+ const raw = routing.externalFilterJson || '';
1105
+ const replaced = raw.replace(/\{\{(.*?)\}\}/g, (_, key) => {
1106
+ const v = data?.[key];
1107
+ return v != null ? String(v) : '';
1108
+ });
1109
+ try {
1110
+ externalFilter = replaced ? JSON.parse(replaced) : null;
1111
+ } catch (e) {
1112
+ console.error('Invalid externalFilterJson after replacement', e);
1113
+ }
1114
+ }
1115
+
1116
+ if (externalFilter && routing.customFilterCode) {
1117
+ externalFilter.customFilterCode = routing.customFilterCode;
795
1118
  }
796
1119
 
797
1120
  const builderId = context?.builderId;
1121
+ const targetPageId = routing.pageId;
798
1122
  const payload = {
799
- pageId: routing.pageId ?? null,
1123
+ pageId: targetPageId ?? null,
800
1124
  externalFilter,
801
- id: builderId ?? null
1125
+ id: targetPageId ? null : (builderId ?? null)
802
1126
  };
803
1127
 
804
1128
  const base = '/reportBuilder/reportViewer';
805
- const url = builderId ? `${base}?id=${encodeURIComponent(builderId)}&useWindowName=1` : `${base}?useWindowName=1`;
806
- // Open the new tab and set its window.name with a prefixed base64 JSON payload
807
- const child = window.open(url, "_blank"); // do not use noopener because we need to set name
808
- if (child) {
809
- try {
1129
+ let url = base;
1130
+ if (targetPageId) {
1131
+ url += `?pageId=${encodeURIComponent(targetPageId)}`;
1132
+ } else if (builderId) {
1133
+ url += `?id=${encodeURIComponent(builderId)}`;
1134
+ }
1135
+
1136
+ try {
1137
+ const sessionId = saveReportSession(payload);
1138
+ if (sessionId) {
1139
+ url += (url.includes('?') ? '&' : '?') + `sessionId=${sessionId}`;
1140
+ } else {
1141
+ // fallback to session storage if localStorage fails
810
1142
  const json = JSON.stringify(payload);
811
- const b64 = typeof btoa !== 'undefined' ? btoa(unescape(encodeURIComponent(json))) : json;
812
- child.name = `RB:${b64}`;
813
- } catch (e) {
814
- // as a fallback, put minimal info in URL (may be long for large filters)
815
- try {
816
- const fallback = encodeURIComponent(JSON.stringify(payload));
817
- child.location.href = builderId ? `${base}?id=${encodeURIComponent(builderId)}&payload=${fallback}` : `${base}?payload=${fallback}`;
818
- } catch {}
1143
+ sessionStorage.setItem('RB_PAYLOAD', json);
819
1144
  }
1145
+ } catch (e) {
1146
+ console.error('Failed to set session payload', e);
820
1147
  }
1148
+ router.push(url).then(() => {
1149
+ });
821
1150
  } catch (e) {
822
1151
  console.error('Filter routing error', e);
823
1152
  }
824
1153
  } else {
1154
+ // Build finalRoute for Route type
1155
+ let finalRoute = null;
1156
+ if (routing.script) {
1157
+ try {
1158
+ const fn = new Function("row", routing.script);
1159
+ finalRoute = fn(data);
1160
+ } catch (err) {
1161
+ console.error("Routing script error:", err);
1162
+ finalRoute = "#";
1163
+ }
1164
+ } else if (routing.pattern) {
1165
+ finalRoute = routing.pattern.replace(/\{\{(.*?)\}\}/g, (_, key) => {
1166
+ const val = data?.[key];
1167
+ return val != null ? val : `:${key}`;
1168
+ });
1169
+ }
1170
+
825
1171
  if (finalRoute && finalRoute !== '#') {
826
- window.open(finalRoute, "_blank", "noopener,noreferrer");
1172
+ const target = routing.target || '_self';
1173
+
1174
+ if (target === '_blank' || target === '_parent' || target === '_top') {
1175
+ // Open in new tab/window or specific frame
1176
+ window.open(finalRoute, target);
1177
+ } else {
1178
+ // Default: navigate in same tab using Next.js router
1179
+ router.push(finalRoute);
1180
+ }
827
1181
  }
828
1182
  }
829
1183
  };
830
1184
 
1185
+ const handleClick = (event) => {
1186
+ if (routes.length > 1) {
1187
+ setAnchorEl(event.currentTarget);
1188
+ } else {
1189
+ handleRouteClick(routes[0]);
1190
+ }
1191
+ };
1192
+
831
1193
  return (
832
- <span
833
- style={{
834
- color: '#1976d2',
835
- fontWeight: 600,
836
- cursor: 'pointer',
837
- }}
838
- onClick={handleClick}
839
- >
840
- {value}
841
- </span>
1194
+ <>
1195
+ <span
1196
+ style={{
1197
+ color: '#1976d2',
1198
+ fontWeight: 600,
1199
+ cursor: 'pointer',
1200
+ }}
1201
+ onClick={handleClick}
1202
+ >
1203
+ {value}
1204
+ </span>
1205
+ <Menu
1206
+ anchorEl={anchorEl}
1207
+ open={open}
1208
+ onClose={() => setAnchorEl(null)}
1209
+ >
1210
+ {routes.map((r, idx) => (
1211
+ <MenuItem key={idx} onClick={() => handleRouteClick(r)}>
1212
+ {r.name || `Route ${idx + 1}`}
1213
+ </MenuItem>
1214
+ ))}
1215
+ </Menu>
1216
+ </>
842
1217
  );
843
1218
  };
844
1219
 
@@ -1059,13 +1434,30 @@ const SGrid = props => {
1059
1434
  setFinalRequestObject(defaultFinalRequest)
1060
1435
  const fixedTFilter = externalFilter?.fixedTFilter ?? externalFilter?.fixedTfilter ?? []
1061
1436
  const localFixedTFilter = Filter?.fixedTFilter ?? []
1062
- let localFilter = externalFilter ? {...externalFilter} : {}
1063
- localFilter = {...localFilter, ...Filter}
1064
-
1065
- let localTFilter = localFilter?.Tfilter ? [...localFilter?.Tfilter] : []
1066
- localTFilter = localFilter?.TFilter ? [...localTFilter, ...localFilter?.TFilter] : [...localTFilter]
1437
+ let localFilter = externalFilter && !Array.isArray(externalFilter) ? {...externalFilter} : {}
1438
+ // Merge with Filter but be careful not to overwrite Tfilter/TFilter with empty ones
1439
+ const {Tfilter: _tf, TFilter: _tF, customFilterCode: _cfc, ...otherInternalFilter} = Filter || {}
1440
+ localFilter = {...localFilter, ...otherInternalFilter}
1441
+
1442
+ let localTFilter = []
1443
+ if (externalFilter?.Tfilter) localTFilter.push(...externalFilter.Tfilter)
1444
+ if (externalFilter?.TFilter) localTFilter.push(...externalFilter.TFilter)
1445
+ if (Filter?.Tfilter) localTFilter.push(...Filter.Tfilter)
1446
+ if (Filter?.TFilter) localTFilter.push(...Filter.TFilter)
1447
+
1448
+ // Also support LocalTfilter (this is where CustomFilterDialog often stores its filters)
1449
+ if (Filter?.LocalTfilter) localTFilter.push(...Filter.LocalTfilter)
1450
+ if (externalFilter?.LocalTfilter) localTFilter.push(...externalFilter.LocalTfilter)
1451
+
1452
+ // Support for routing-passed externalFilter that might be directly inside externalFilter (not under Tfilter)
1453
+ if (Array.isArray(externalFilter) && localTFilter.length === 0) {
1454
+ localTFilter = [...externalFilter];
1455
+ }
1067
1456
  localTFilter = [...localTFilter, ...fixedTFilter, ...localFixedTFilter, ...builderTFilter]
1068
1457
 
1458
+ // Extract customFilterCode from externalFilter if present (e.g. from routing)
1459
+ let currentCustomFilterCode = Filter?.customFilterCode || externalFilter?.customFilterCode || "";
1460
+
1069
1461
  // Apply explicit user-selected search filters (selectedSearchObjects), grouped by path
1070
1462
  if ((selectedSearchObjects?.length ?? 0) > 0) {
1071
1463
  const byPath = selectedSearchObjects.reduce((acc, item) => {
@@ -1099,8 +1491,6 @@ const SGrid = props => {
1099
1491
  localTFilter = [...localTFilter, ...grouped];
1100
1492
  }
1101
1493
 
1102
- localTFilter = localTFilter.filter(x => x.value !== null && x.value !== undefined && x.value !== '')
1103
-
1104
1494
  delete localFilter.Tfilter
1105
1495
  delete localFilter.fixedTfilter
1106
1496
  delete localFilter.fixedTFilter
@@ -1112,8 +1502,8 @@ const SGrid = props => {
1112
1502
  const pageSize = request.endRow - request.startRow // Calculate the page size
1113
1503
  let aggregators = request.valueCols.filter(x => x.id != 'groupCount').map(agg => handleGetAggregationObject(agg))
1114
1504
  let updateAggregation = true
1115
- if (colDefs && gridApi) {
1116
- let resultColDef = [...colDefs]
1505
+ if (colDefs.current && gridApi) {
1506
+ let resultColDef = [...colDefs.current]
1117
1507
  if (request.rowGroupCols.length > 0) {
1118
1508
  }
1119
1509
  // gridApi.setGridOption('columnDefs', resultColDef)
@@ -1121,7 +1511,7 @@ const SGrid = props => {
1121
1511
  }
1122
1512
  // Build tFilter safely: remove fixed from preformatted lists and merge a single fixed source
1123
1513
  // 1) Strip fixed items from local Tfilter/TFilter (they may already be formatted by CustomFilterDialog)
1124
- let nonFixedLocal = Array.isArray(localTFilter) ? localTFilter.filter(it => !(it && it.fixed === true && it.value === null)) : []
1514
+ let nonFixedLocal = Array.isArray(localTFilter) ? localTFilter.filter(it => it != null && !(it.fixed === true && it.value === null)) : []
1125
1515
 
1126
1516
  // 2) Choose ONE fixed source by priority: component state -> external -> builder
1127
1517
  const extFixed = Array.isArray(externalFilter?.fixedTFilter)
@@ -1129,12 +1519,80 @@ const SGrid = props => {
1129
1519
  : (Array.isArray(externalFilter?.fixedTfilter) ? externalFilter.fixedTfilter : [])
1130
1520
  const fixedSource = (Array.isArray(Filter?.fixedTFilter) && Filter.fixedTFilter.length > 0)
1131
1521
  ? Filter.fixedTFilter
1132
- : (extFixed.length > 0 ? extFixed : (Array.isArray(builderTFilter) ? builderTFilter : []))
1522
+ : (extFixed.length > 0 ? extFixed : (Array.isArray(builderTFilter) && builderTFilter.length > 0 ? builderTFilter : []))
1133
1523
 
1134
1524
  // 3) Merge and format once
1135
- const mergedForFormat = [...nonFixedLocal]
1136
- console.log("Merged for format:", mergedForFormat)
1137
- let formattedT = FilterFormat(mergedForFormat)
1525
+ const mergedForFormat = [...nonFixedLocal, ...fixedSource]
1526
+ let formattedT = FilterFormat(mergedForFormat).filter(x => !x?.isCustom)
1527
+
1528
+ // Dynamic placeholder replacement for authValues
1529
+ if (formattedT && Array.isArray(formattedT)) {
1530
+ formattedT = formattedT.map(f => {
1531
+ if (typeof f.value === 'string' && f.value.includes('{{auth.')) {
1532
+ const replaced = String(f.value).replace(/\{\{auth\.(.*?)\}\}/g, (_, path) => {
1533
+ return path.split('.').reduce((acc, key) => acc && acc[key], authValues) ?? '';
1534
+ });
1535
+ return {...f, value: replaced};
1536
+ }
1537
+ return f;
1538
+ });
1539
+ }
1540
+
1541
+ // Dynamic placeholder replacement for authValues
1542
+ if (formattedT && Array.isArray(formattedT)) {
1543
+ formattedT = formattedT.map(f => {
1544
+ if (typeof f.value === 'string' && f.value.includes('{{context.')) {
1545
+ const replaced = String(f.value).replace(/\{\{context\.(.*?)\}\}/g, (_, path) => {
1546
+ return path.split('.').reduce((acc, key) => acc && acc[key], contextValues) ?? '';
1547
+ });
1548
+ return {...f, value: replaced};
1549
+ }
1550
+ return f;
1551
+ });
1552
+ }
1553
+
1554
+ // 4) Execute custom filter code if present
1555
+ let customFilterResult = null
1556
+ if (currentCustomFilterCode && currentCustomFilterCode.trim()) {
1557
+ try {
1558
+ // Wrap code in a function that provides contextValues
1559
+ const executeCode = new Function('context', 'authContext', 'filters', 'localFilter', `
1560
+ let customFilter = [];
1561
+ try {
1562
+ ${currentCustomFilterCode}
1563
+ } catch (e) {
1564
+ console.error('Inner error in customFilterCode:', e);
1565
+ }
1566
+ return customFilter;
1567
+ `);
1568
+ customFilterResult = executeCode(contextValues, authValues, formattedT, localFilter)
1569
+
1570
+ // Validate that result is an array
1571
+ if (customFilterResult && !Array.isArray(customFilterResult)) {
1572
+ console.error('Custom filter must return an array, got:', typeof customFilterResult)
1573
+ customFilterResult = null
1574
+ }
1575
+ } catch (error) {
1576
+ console.error('Error executing custom filter code in SGrid:', error)
1577
+ customFilterResult = null
1578
+ }
1579
+ }
1580
+
1581
+ // Merge custom filter array with formattedT if present
1582
+ if (customFilterResult && Array.isArray(customFilterResult)) {
1583
+ formattedT = [...formattedT, ...customFilterResult]
1584
+ }
1585
+
1586
+ // Final Deduplication after all merges and custom code execution
1587
+ if (Array.isArray(formattedT)) {
1588
+ formattedT = formattedT.filter((f, idx, self) =>
1589
+ idx === self.findIndex(t => (
1590
+ t.path === f.path &&
1591
+ t.method === f.method &&
1592
+ JSON.stringify(t.value) === JSON.stringify(f.value)
1593
+ ))
1594
+ );
1595
+ }
1138
1596
 
1139
1597
  let data = {
1140
1598
  ...localFilter,
@@ -1144,7 +1602,7 @@ const SGrid = props => {
1144
1602
  }
1145
1603
  // Exclude any tFilter entries with null value before sending the request (per requirement)
1146
1604
  if (Array.isArray(data.tFilter)) {
1147
- data.tFilter = data.tFilter.filter(f => f == null ? false : f.value !== null);
1605
+ data.tFilter = data.tFilter.filter(f => f == null ? false : (f.value !== null && !f.isCustom));
1148
1606
  }
1149
1607
  try {
1150
1608
  if (request.rowGroupCols.length !== request.groupKeys.length) {
@@ -1175,6 +1633,33 @@ const SGrid = props => {
1175
1633
  }));
1176
1634
  const aggIds = request.valueCols.map(vc => vc.id);
1177
1635
 
1636
+ // Dynamic placeholder replacement for authValues in selectParams
1637
+ let dynamicSelectParams = selectParams;
1638
+ if (Array.isArray(dynamicSelectParams)) {
1639
+ dynamicSelectParams = dynamicSelectParams.map(p => {
1640
+ if (typeof p.value === 'string' && p.value.includes('{{auth.')) {
1641
+ const replaced = p.value.replace(/\{\{auth\.(.*?)\}\}/g, (_, path) => {
1642
+ return path.split('.').reduce((acc, key) => acc && acc[key], authValues) ?? '';
1643
+ });
1644
+ return {...p, value: replaced};
1645
+ }
1646
+ return p;
1647
+ });
1648
+ }
1649
+
1650
+ // Dynamic placeholder replacement for contextValues in selectParams
1651
+ if (Array.isArray(dynamicSelectParams)) {
1652
+ dynamicSelectParams = dynamicSelectParams.map(p => {
1653
+ if (typeof p.value === 'string' && p.value.includes('{{context.')) {
1654
+ const replaced = p.value.replace(/\{\{context\.(.*?)\}\}/g, (_, path) => {
1655
+ return path.split('.').reduce((acc, key) => acc && acc[key], contextValues) ?? '';
1656
+ });
1657
+ return {...p, value: replaced};
1658
+ }
1659
+ return p;
1660
+ });
1661
+ }
1662
+
1178
1663
  const groupOrderColdIds = request.sortModel.filter(x => groupOrderCols.includes(x.colId) || ((x.colId == "IZ_groupCount" || x.colId.endsWith("groupCount") || aggIds.some(id => x.colId.toLowerCase().endsWith(id.toLowerCase()))) && isGroupCountOrder))
1179
1664
  let orderBy = groupOrderColdIds.map(sort => handleGetSortObject(sort.colId, sort.sort, true, aggIds.some(id => sort.colId.toLowerCase().endsWith(id.toLowerCase()))))
1180
1665
  data = {
@@ -1183,11 +1668,22 @@ const SGrid = props => {
1183
1668
  orderBy: orderBy
1184
1669
  }
1185
1670
 
1671
+ // Final deduplication for group keys vs base filters
1672
+ if (Array.isArray(data.tFilter)) {
1673
+ data.tFilter = data.tFilter.filter((f, idx, self) =>
1674
+ idx === self.findIndex(t => (
1675
+ t.path === f.path &&
1676
+ t.method === f.method &&
1677
+ JSON.stringify(t.value) === JSON.stringify(f.value)
1678
+ ))
1679
+ );
1680
+ }
1681
+
1186
1682
  // if (updateAggregation !== true) {
1187
1683
  // data.aggregators = []
1188
1684
  // }
1189
1685
  data.selectFields = selectedFields
1190
- data.selectParams = selectParams
1686
+ data.selectParams = dynamicSelectParams
1191
1687
  data.rawSql = builderData.rawSql
1192
1688
 
1193
1689
  const response = await Services.PostService(groupEndPoint, false, data, {
@@ -1259,8 +1755,46 @@ const SGrid = props => {
1259
1755
  ...data,
1260
1756
  tFilter: [...tFilters, ...data.tFilter]
1261
1757
  }
1758
+
1759
+ // Final deduplication for group keys vs base filters
1760
+ if (Array.isArray(data.tFilter)) {
1761
+ data.tFilter = data.tFilter.filter((f, idx, self) =>
1762
+ idx === self.findIndex(t => (
1763
+ t.path === f.path &&
1764
+ t.method === f.method &&
1765
+ JSON.stringify(t.value) === JSON.stringify(f.value)
1766
+ ))
1767
+ );
1768
+ }
1262
1769
  }
1263
1770
  let orderBy = request.sortModel.filter(x => !x.colId.endsWith("IZ_groupCount")).map(sort => handleGetSortObject(sort.colId, sort.sort, false))
1771
+
1772
+ // Dynamic placeholder replacement for authValues in selectParams
1773
+ let dynamicSelectParams = selectParams;
1774
+ if (Array.isArray(dynamicSelectParams)) {
1775
+ dynamicSelectParams = dynamicSelectParams.map(p => {
1776
+ if (typeof p.value === 'string' && p.value.includes('{{auth.')) {
1777
+ const replaced = p.value.replace(/\{\{auth\.(.*?)\}\}/g, (_, path) => {
1778
+ return path.split('.').reduce((acc, key) => acc && acc[key], authValues) ?? '';
1779
+ });
1780
+ return {...p, value: replaced};
1781
+ }
1782
+ return p;
1783
+ });
1784
+ }
1785
+
1786
+ if (Array.isArray(dynamicSelectParams)) {
1787
+ dynamicSelectParams = dynamicSelectParams.map(p => {
1788
+ if (typeof p.value === 'string' && p.value.includes('{{context.')) {
1789
+ const replaced = p.value.replace(/\{\{context\.(.*?)\}\}/g, (_, path) => {
1790
+ return path.split('.').reduce((acc, key) => acc && acc[key], contextValues) ?? '';
1791
+ });
1792
+ return {...p, value: replaced};
1793
+ }
1794
+ return p;
1795
+ });
1796
+ }
1797
+
1264
1798
  if (data.tIncludes != null)
1265
1799
  data = {
1266
1800
  ...data,
@@ -1281,7 +1815,7 @@ const SGrid = props => {
1281
1815
  data.aggregators = []
1282
1816
  }
1283
1817
  data.selectFields = selectedFields
1284
- data.selectParams = selectParams
1818
+ data.selectParams = dynamicSelectParams
1285
1819
  data.lastRowParams = lastRowRef.current[pageNumber];
1286
1820
 
1287
1821
  data.isPaginationByFilter = builderData?.isRaw === false
@@ -1292,7 +1826,6 @@ const SGrid = props => {
1292
1826
  rowCount: undefined // <== This keeps the grid in "loading" state
1293
1827
  }
1294
1828
  data.rawSql = builderData?.rawSql
1295
- console.log(builderData)
1296
1829
  const response = await Services.PostService(pagedEndPoint, false, data, {
1297
1830
  ...paramsPage,
1298
1831
  page: isSingle ? 1 : pageNumber,
@@ -1341,7 +1874,7 @@ const SGrid = props => {
1341
1874
  return {success: false}
1342
1875
  }
1343
1876
  },
1344
- [externalFilter, includes, paramsPage, Filter, selectedFields, isPagination, selectedSearchObjects, builderData]
1877
+ [externalFilter, includes, paramsPage, Filter, selectedFields, isPagination, selectedSearchObjects, builderData, authValues, contextValues]
1345
1878
  )
1346
1879
  const datasource = useMemo(() => createServerSideDatasource(getRowsFromApi), [getRowsFromApi]);
1347
1880
 
@@ -1416,7 +1949,19 @@ const SGrid = props => {
1416
1949
  }
1417
1950
 
1418
1951
  function handleFilterChange(field, value) {
1419
- handleChange(setFilter, field, value)
1952
+ if (field === 'LocalTfilter') {
1953
+ // When updating LocalTfilter, preserve main filters from builderModel
1954
+ setFilter(preValue => {
1955
+ const mainFilters = (preValue.LocalTfilter || []).filter(f => f.isMainFilter === true)
1956
+ const newFilters = Array.isArray(value) ? value : []
1957
+ return {
1958
+ ...preValue,
1959
+ LocalTfilter: [...mainFilters, ...newFilters]
1960
+ }
1961
+ })
1962
+ } else {
1963
+ handleChange(setFilter, field, value)
1964
+ }
1420
1965
  }
1421
1966
 
1422
1967
  async function handleGetTemplates() {
@@ -1524,14 +2069,53 @@ const SGrid = props => {
1524
2069
  }
1525
2070
 
1526
2071
  const restoreState = () => {
1527
- if (gridApi !== null && colDefs !== null && selectedTemplate != null) {
2072
+ let builderMainFilter = builderModel?.filter?.LocalTfilter ?? []
2073
+
2074
+ if (gridApi !== null && colDefs.current !== null && selectedTemplate != null) {
2075
+ console.log(selectedTemplate)
1528
2076
  gridApi.applyColumnState({
1529
2077
  state: selectedTemplate.value,
1530
2078
  applyOrder: true,
1531
2079
  });
1532
- }
1533
- if (selectedTemplate.filterValue != null && (Filter.Tfilter == null || Filter.Tfilter.length == 0)) {
1534
- setFilter(selectedTemplate.filterValue);
2080
+ if (selectedTemplate.filterValue != null) {
2081
+ let fixedLocalFilters = builderModel?.filter?.fixedTFilter ?? []
2082
+ fixedLocalFilters = fixedLocalFilters.filter(x => x.fixed === true)
2083
+ fixedLocalFilters = fixedLocalFilters.map(x => ({
2084
+ ...x,
2085
+ isMainFilter: x.fixed === true ? false : x.isMainFilter
2086
+ }))
2087
+ let templateFilter = selectedTemplate?.filterValue?.LocalTfilter ?? []
2088
+ let nonExistsFilter = fixedLocalFilters.filter(x => !templateFilter.some(y => y.path === x.path && y.method === x.method && y.isOrList === x.isOrList))
2089
+ let finalLocalFilter = [...templateFilter, ...nonExistsFilter]
2090
+
2091
+ // Handle Tfilter from template
2092
+ const templateTfilter = selectedTemplate?.filterValue?.Tfilter ?? []
2093
+ const templateCustomFilterCode = selectedTemplate?.filterValue?.customFilterCode ?? ''
2094
+
2095
+ setFilter(preValue => ({
2096
+ ...preValue,
2097
+ LocalTfilter: [...finalLocalFilter, ...builderMainFilter],
2098
+ Tfilter: templateTfilter,
2099
+ customFilterCode: templateCustomFilterCode
2100
+ }));
2101
+ }
2102
+ } else {
2103
+ let fixedLocalFilters = builderModel?.filter?.fixedTFilter ?? []
2104
+ fixedLocalFilters = fixedLocalFilters.filter(x => x.fixed === true)
2105
+
2106
+ let finalLocalFilter = [...fixedLocalFilters]
2107
+
2108
+ // When no template, use builderModel filters
2109
+ const builderTfilter = builderModel?.filter?.Tfilter ?? []
2110
+ const builderCustomFilterCode = builderModel?.filter?.customFilterCode ?? ''
2111
+
2112
+ setFilter(preValue => ({
2113
+ ...preValue,
2114
+ LocalTfilter: [...finalLocalFilter, ...builderMainFilter],
2115
+ Tfilter: builderTfilter,
2116
+ customFilterCode: builderCustomFilterCode
2117
+ }));
2118
+
1535
2119
  }
1536
2120
  };
1537
2121
 
@@ -1566,7 +2150,6 @@ const SGrid = props => {
1566
2150
  };
1567
2151
 
1568
2152
  setSelectedSearchObjects(prev => [...prev, newObj]);
1569
- console.log(newObj)
1570
2153
  setSearchTerm('');
1571
2154
  setSearchPopoverOpen(false);
1572
2155
  debouncedRefresh();
@@ -1575,18 +2158,16 @@ const SGrid = props => {
1575
2158
 
1576
2159
  useEffect(() => {
1577
2160
  if (builderData) {
1578
-
1579
2161
  if (builderData.isRaw) {
1580
2162
  handleGetRawColumns(builderData.rawSql)
1581
2163
  } else {
1582
2164
  handleResetColsToStudio()
1583
2165
  }
1584
2166
  }
1585
- }, [builderData]);
2167
+ }, [builderData, isRerender]);
1586
2168
 
1587
2169
  useEffect(() => {
1588
2170
  if (builderModel != null) {
1589
- console.log(builderModel)
1590
2171
  let rawSql = builderModel.rawSql;
1591
2172
  if (builderModel.isRaw === true) {
1592
2173
  const localSelectParams = builderModel.selectionParams.map((x, index) => ({
@@ -1607,11 +2188,44 @@ const SGrid = props => {
1607
2188
  return param.value;
1608
2189
  });
1609
2190
  }
1610
- setBuilderData({...builderModel, rawSql: rawSql})
1611
- setFilter(preValue => ({
1612
- ...preValue,
1613
- LocalTfilter: [...(builderModel?.filter?.fixedTFilter || [])]
1614
- }))
2191
+
2192
+ setBuilderData(prev => {
2193
+ // Only update if builderModel data changed or rawSql changed
2194
+ const isSameModel = prev && JSON.stringify(prev) === JSON.stringify({...builderModel, rawSql});
2195
+ if (isSameModel) return prev;
2196
+ return {...builderModel, rawSql: rawSql};
2197
+ });
2198
+
2199
+ // We explicitly skip updating setFilter if builderModel triggered it but we just restored from session.
2200
+ // We rely on the fact that if a session restore happened, Filter.LocalTfilter will likely be populated.
2201
+
2202
+ setFilter(preValue => {
2203
+ const fixedTFilter = builderModel?.filter?.fixedTFilter || [];
2204
+ const localTFilter = builderModel?.filter?.LocalTfilter || [];
2205
+ const tFilter = builderModel?.filter?.Tfilter || [];
2206
+ const customFilterCode = builderModel?.filter?.customFilterCode || '';
2207
+
2208
+ // If we already have filters (possibly restored from session), don't overwrite with just fixed filters
2209
+ // unless it's a completely fresh state or fixed filters changed.
2210
+ const isSameFilter = JSON.stringify(preValue.LocalTfilter) === JSON.stringify([...fixedTFilter, ...localTFilter]);
2211
+ const isSameCode = preValue.customFilterCode === customFilterCode;
2212
+
2213
+ if (preValue.LocalTfilter.length > (fixedTFilter.length + localTFilter.length) && isSameCode) {
2214
+ return preValue;
2215
+ }
2216
+
2217
+ if (isSameFilter && isSameCode) return preValue;
2218
+
2219
+ return {
2220
+ ...preValue,
2221
+ Tfilter: tFilter,
2222
+ LocalTfilter: [
2223
+ ...fixedTFilter.map(f => ({...f})),
2224
+ ...localTFilter.map(f => ({...f}))
2225
+ ],
2226
+ customFilterCode: customFilterCode
2227
+ };
2228
+ })
1615
2229
  }
1616
2230
  }, [builderModel]);
1617
2231
 
@@ -1640,39 +2254,38 @@ const SGrid = props => {
1640
2254
  }, [selectParams, builderModel?.isRaw]);
1641
2255
 
1642
2256
  useEffect(() => {
1643
-
1644
2257
  if (gridApi !== null) {
1645
2258
  gridApi.refreshServerSide({purge: !noPurge})
1646
2259
  lastRowRef.current = {}
1647
2260
  }
1648
- }, [refresh, externalFilter, localRefresh, Filter]);
2261
+ }, [refresh, externalFilter, localRefresh, Filter.Tfilter, Filter.LocalTfilter, Filter.customFilterCode, externalFilter?.customFilterCode]);
1649
2262
 
1650
2263
  useEffect(() => {
1651
- if (gridApi != null && colDefs != null)
2264
+ if (gridApi != null && colDefs.current != null)
1652
2265
  handleGetTemplates()
1653
- }, [gridApi, colDefs])
2266
+ }, [gridApi, colDefs.current])
1654
2267
 
1655
2268
  useEffect(() => {
1656
- if (colDefs.length > 0 && selectedTemplate != null) {
2269
+ if (colDefs.current.length > 0) {
1657
2270
  restoreState()
1658
2271
  }
1659
2272
  }, [selectedTemplate])
1660
- useEffect(() => {
1661
- if (selectedTemplate == null && templates.length > 0 && colDefs.length > 0) {
1662
- const userDefined = templates.find(x => x.isDefault === true)
1663
- if (userDefined != null)
1664
- setSelectedTemplate(userDefined)
1665
- else {
1666
- const systemDefined = templates.find(x => x.type === "UserPredefined")
1667
- if (systemDefined != null)
1668
- setSelectedTemplate(systemDefined)
1669
- else {
1670
- const anyTemplate = templates[0]
1671
- setSelectedTemplate(anyTemplate)
1672
- }
1673
- }
1674
- }
1675
- }, [templates, colDefs])
2273
+ // useEffect(() => {
2274
+ // if (templates.length > 0 && colDefs.length > 0) {
2275
+ // const userDefined = templates.find(x => x.isDefault === true)
2276
+ // if (userDefined != null)
2277
+ // setSelectedTemplate(userDefined)
2278
+ // else {
2279
+ // const systemDefined = templates.find(x => x.type === "UserPredefined")
2280
+ // if (systemDefined != null)
2281
+ // setSelectedTemplate(systemDefined)
2282
+ // else {
2283
+ // const anyTemplate = templates[0]
2284
+ // setSelectedTemplate(anyTemplate)
2285
+ // }
2286
+ // }
2287
+ // }
2288
+ // }, [])
1676
2289
 
1677
2290
  useEffect(() => {
1678
2291
  if (!timerValue || isNaN(timerValue) || timerValue == 0) {
@@ -1705,11 +2318,10 @@ const SGrid = props => {
1705
2318
  }, [externalTimer]); // re// n after each refresh or change in value
1706
2319
 
1707
2320
  return (
1708
- <Grid item container size={12}>
1709
- <Grid container item
2321
+ <Grid container size={{ xs: 12 }}>
2322
+ <Grid container
1710
2323
  sx={{backgroundColor: '#fafafb', justifyContent: 'space-between'}} padding={2}
1711
- size={12}
1712
- >
2324
+ size={{ xs: 12 }}>
1713
2325
  <Box sx={{display: 'flex'}}>
1714
2326
  {(builderData?.id != null && minimized !== true) &&
1715
2327
  <Box sx={{minWidth: '250px'}}>
@@ -1719,12 +2331,11 @@ const SGrid = props => {
1719
2331
  size={'small'}
1720
2332
  value={selectedTemplate}
1721
2333
  fullWidth
1722
- disableClearable
1723
2334
  options={templates}
1724
2335
  onChange={(e, value) => {
1725
2336
  setSelectedTemplate(value)
1726
- }
1727
- }
2337
+ }}
2338
+
1728
2339
  getOptionLabel={option => option.name}
1729
2340
  renderInput={params => (
1730
2341
  <TextField
@@ -1745,7 +2356,7 @@ const SGrid = props => {
1745
2356
  defaultValue={0} size={'small'}
1746
2357
  color='primary' sx={{width: '60px'}}>
1747
2358
  <MenuItem key={1} value={0}>No</MenuItem>
1748
- <MenuItem key={2} value={0.1667}>10s</MenuItem>
2359
+ {/*<MenuItem key={2} value={0.1667}>10s</MenuItem>*/}
1749
2360
  <MenuItem key={2} value={0.5}>30s</MenuItem>
1750
2361
  <MenuItem key={2} value={1}>1m</MenuItem>
1751
2362
  <MenuItem key={2} value={2}>2m</MenuItem>
@@ -1973,7 +2584,7 @@ const SGrid = props => {
1973
2584
  columnHoverHighlight={true}
1974
2585
  theme={agTheme}
1975
2586
  enableRtl={true}
1976
- columnDefs={colDefs}
2587
+ columnDefs={colDefs.current}
1977
2588
  rowModelType={"serverSide"}
1978
2589
  onGridReady={onGridReady}
1979
2590
  maxConcurrentDatasourceRequests={0}
@@ -1981,10 +2592,95 @@ const SGrid = props => {
1981
2592
  onRowSelected={onRowSelected}
1982
2593
  // getChildCount={getChildCount}
1983
2594
  sideBar={sideBarConfig}
1984
- context={{tRouting, builderId: builderData?.id}} // pass routing and builder id
2595
+ context={{
2596
+ tRouting,
2597
+ builderId: builderData?.id,
2598
+ updateRef: updateRef,
2599
+ settings: builderModel?.settings
2600
+ }} // pass routing, builder id, updateRef and settings
1985
2601
  gridOptions={{
1986
2602
  enableRangeSelection: true,
1987
2603
  enableCharts: true,
2604
+ getContextMenuItems: (params) => {
2605
+ const rowUniqueId = builderModel?.settings?.rowUniqueId
2606
+ const rowData = params.node?.data
2607
+ const defaultItems = params.defaultItems || []
2608
+
2609
+ if (!updateRef || !rowUniqueId || !rowData) {
2610
+ return defaultItems
2611
+ }
2612
+
2613
+ const rowIdValue = getRowIdFromSettings(rowData, rowUniqueId)
2614
+ const hasUpdate = updateRef.current && Array.isArray(updateRef.current) &&
2615
+ updateRef.current.some(item => item.rowId === rowIdValue)
2616
+
2617
+ const customItems = []
2618
+
2619
+ if (hasUpdate) {
2620
+ customItems.push({
2621
+ name: 'Remove Row from Update Ref',
2622
+ icon: '<span class="ag-icon ag-icon-cancel" unselectable="on" role="presentation"></span>',
2623
+ action: () => {
2624
+ removeUpdateRefByRowId(updateRef, rowData, rowUniqueId)
2625
+ params.api.refreshCells({force: true})
2626
+ }
2627
+ })
2628
+ }
2629
+
2630
+ customItems.push({
2631
+ name: 'Clone Update Ref (Backup)',
2632
+ icon: '<span class="ag-icon ag-icon-copy" unselectable="on" role="presentation"></span>',
2633
+ action: () => {
2634
+ cloneUpdateRefToOriginal(updateRef, originalRefData)
2635
+ params.api.refreshCells({force: true})
2636
+ },
2637
+ disabled: !updateRef.current || updateRef.current.length === 0
2638
+ })
2639
+
2640
+ customItems.push({
2641
+ name: 'Restore Update Ref (from Backup)',
2642
+ icon: '<span class="ag-icon ag-icon-undo" unselectable="on" role="presentation"></span>',
2643
+ action: () => {
2644
+ restoreUpdateRefFromOriginal(updateRef, originalRefData)
2645
+ params.api.refreshCells({force: true})
2646
+ },
2647
+ disabled: !originalRefData.current || originalRefData.current.length === 0
2648
+ })
2649
+
2650
+ customItems.push({
2651
+ name: 'Normalize Update Ref (Merge Duplicates)',
2652
+ icon: '<span class="ag-icon ag-icon-columns" unselectable="on" role="presentation"></span>',
2653
+ action: () => {
2654
+ normalizeUpdateRef(updateRef)
2655
+ params.api.refreshCells({force: true})
2656
+ },
2657
+ disabled: !updateRef.current || updateRef.current.length === 0
2658
+ })
2659
+
2660
+ customItems.push({
2661
+ name: 'Clear All Update Ref',
2662
+ icon: '<span class="ag-icon ag-icon-cancel" unselectable="on" role="presentation"></span>',
2663
+ action: () => {
2664
+ if (updateRef.current && Array.isArray(updateRef.current)) {
2665
+ const count = updateRef.current.length
2666
+ clearAllUpdateRef(updateRef)
2667
+ params.api.refreshCells({force: true})
2668
+ console.log(`Cleared ${count} rows from updateRef`)
2669
+ }
2670
+ },
2671
+ disabled: !updateRef.current || updateRef.current.length === 0
2672
+ })
2673
+
2674
+ if (customItems.length > 0) {
2675
+ return [
2676
+ ...customItems,
2677
+ 'separator',
2678
+ ...defaultItems
2679
+ ]
2680
+ }
2681
+
2682
+ return defaultItems
2683
+ }
1988
2684
  }}
1989
2685
  rowSelection="multiple"
1990
2686
  statusBar={{
@@ -2073,29 +2769,34 @@ const SGrid = props => {
2073
2769
  scroll='body'
2074
2770
  // onClose={() => handleToggleDialogs('CustomFilter')}
2075
2771
  >
2076
- <CustomFilterDialog
2077
- handleToggleDialogs={handleToggleDialogs}
2078
- Filter={[...(Filter?.LocalTfilter || [])]}
2079
- handleFilterChange={handleFilterChange}
2080
- className={builderData?.reportSource?.fullName}
2081
- LocalFilter={false}
2082
- selectTFilter={selectTFilter}
2083
- selectParamsMeta={builderData?.selectionParams || []}
2084
- selectParamsValues={selectParams}
2085
- onSelectParamsSave={(list) => {
2086
- // list is array of { index, value, type }
2087
- setSelectParams(prev => {
2088
- const arr = [...(prev || [])];
2089
- (list || []).forEach(item => {
2090
- const i = item.index;
2091
- const type = item.type;
2092
- const current = arr[i] || {id: i, PropertyType: type, value: null};
2093
- arr[i] = {...current, PropertyType: current.PropertyType || type, value: item.value};
2772
+ {openDialogs.CustomFilter && (
2773
+ <CustomFilterDialog
2774
+ handleToggleDialogs={handleToggleDialogs}
2775
+ Filter={(Filter?.LocalTfilter || []).filter(f => (f.isMainFilter !== true))}
2776
+ customFilterCode={Filter?.customFilterCode}
2777
+ handleFilterChange={handleFilterChange}
2778
+ className={builderData?.reportSource?.fullName}
2779
+ LocalFilter={false}
2780
+
2781
+ isViewer={router.pathname.includes('reportViewer')}
2782
+ selectTFilter={selectTFilter}
2783
+ selectParamsMeta={builderData?.selectionParams || []}
2784
+ selectParamsValues={selectParams}
2785
+ onSelectParamsSave={(list) => {
2786
+ // list is array of { index, value, type }
2787
+ setSelectParams(prev => {
2788
+ const arr = [...(prev || [])];
2789
+ (list || []).forEach(item => {
2790
+ const i = item.index;
2791
+ const type = item.type;
2792
+ const current = arr[i] || {id: i, PropertyType: type, value: null};
2793
+ arr[i] = {...current, PropertyType: current.PropertyType || type, value: item.value};
2794
+ });
2795
+ return arr;
2094
2796
  });
2095
- return arr;
2096
- });
2097
- }}
2098
- />
2797
+ }}
2798
+ />
2799
+ )}
2099
2800
  </Dialog>
2100
2801
 
2101
2802
 
@@ -2166,3 +2867,15 @@ function toNestedByUnderscore(input) {
2166
2867
  return result;
2167
2868
  }
2168
2869
 
2870
+ // Export helper functions for use in column configurations
2871
+ export {
2872
+ setUpdateRefValue,
2873
+ setUpdateRefRow,
2874
+ getUpdateRefValue,
2875
+ hasUpdateRefValue,
2876
+ clearUpdateRefRow,
2877
+ clearAllUpdateRef,
2878
+ getAllUpdates,
2879
+ removeUpdateRefByRowId
2880
+ }
2881
+