robobyte-front-builder 1.0.16 → 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.
- package/package.json +1 -1
- package/src/lib/providers/RoboByteFrontBuilderProvider.jsx +26 -9
- package/src/pages/_app.js +0 -8
- package/src/pages/reportModule/reportBuilder/index.js +719 -561
- package/src/pages/reportModule/reportBuilder/reportViewer/index.js +135 -80
- package/src/pages/reportModule/reportBuilder/reports/index.js +51 -11
- package/src/pages/reportModule/reportBuilder/reportsPermissions/index.js +127 -0
- package/src/services/helper/multiSelectEditorByBuilder.js +245 -0
- package/src/services/helper/reportSessionHelper.js +83 -0
- package/src/views/genericTable/ColumnConfiguratorDialog.js +762 -0
- package/src/views/genericTable/FormattingSettingsDialog.js +546 -0
- package/src/views/genericTable/ReportSettingsDialog.js +151 -0
- package/src/views/genericTable/SGrid.js +872 -159
- package/src/views/genericTable/TAGGrid.js +83 -69
- package/src/views/genericTable/convertStringFunctions.js +200 -0
- package/src/views/genericTable/updateRefHelpers.js +421 -0
- package/src/views/rolePermissions/UpdateReportPermissionDialog.js +315 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import BlankLayout from
|
|
1
|
+
import BlankLayout from "src/@core/layouts/BlankLayout";
|
|
2
2
|
import {
|
|
3
3
|
Autocomplete,
|
|
4
4
|
Box,
|
|
@@ -16,68 +16,71 @@ import {
|
|
|
16
16
|
Link,
|
|
17
17
|
Chip,
|
|
18
18
|
Button,
|
|
19
|
+
DialogActions,
|
|
20
|
+
MenuItem,
|
|
21
|
+
Select,
|
|
22
|
+
InputLabel,
|
|
23
|
+
FormControl,
|
|
24
|
+
DialogContent,
|
|
25
|
+
DialogTitle,
|
|
19
26
|
Dialog,
|
|
20
27
|
Tooltip,
|
|
21
|
-
Accordion,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from '@mui/material'
|
|
26
|
-
import { useEffect, useRef, useState } from 'react'
|
|
27
|
-
import { Endpoints, Services } from 'services/Endpoints'
|
|
28
|
+
Checkbox, Accordion, AccordionSummary, AccordionDetails, FormControlLabel,
|
|
29
|
+
} from '@mui/material';
|
|
30
|
+
import {useEffect, useRef, useState} from "react";
|
|
31
|
+
import {Endpoints, Services} from "services/Endpoints";
|
|
28
32
|
import {
|
|
29
33
|
AddBox,
|
|
34
|
+
ArrowBackOutlined,
|
|
30
35
|
ArrowForwardSharp,
|
|
31
36
|
AssessmentOutlined,
|
|
32
|
-
|
|
33
|
-
FilterAlt,
|
|
37
|
+
BackupOutlined,
|
|
38
|
+
Expand, FilterAlt, FilterBAndW, FilterVintageOutlined,
|
|
39
|
+
KeyboardReturnOutlined,
|
|
34
40
|
PreviewOutlined,
|
|
35
41
|
SaveAsOutlined,
|
|
36
|
-
SaveOutlined,
|
|
37
|
-
Search,
|
|
38
|
-
Visibility,
|
|
39
|
-
VisibilityOff,
|
|
40
|
-
Image as ImageIcon,
|
|
42
|
+
SaveOutlined, Search, Visibility, VisibilityOff, Image as ImageIcon,
|
|
41
43
|
CloudDownloadOutlined,
|
|
42
44
|
CloudUploadOutlined
|
|
43
|
-
} from
|
|
44
|
-
import dayjs from 'dayjs'
|
|
45
|
-
import {
|
|
46
|
-
import SGrid from
|
|
47
|
-
import {
|
|
48
|
-
import ComputedTextEditor from
|
|
49
|
-
import ExpressionEditor from
|
|
50
|
-
import BuilderExpressionParams from
|
|
51
|
-
import handleChange from
|
|
52
|
-
import DynamicValueList from
|
|
53
|
-
import SqlEditor from
|
|
54
|
-
import Switch from
|
|
55
|
-
import {
|
|
56
|
-
import PermissionsSubjects from
|
|
57
|
-
import ReportBuilderSaveForm from
|
|
58
|
-
import {
|
|
59
|
-
import CustomFilterDialog from
|
|
60
|
-
|
|
61
|
-
import RoutingSettingDialog from
|
|
62
|
-
import
|
|
63
|
-
import
|
|
64
|
-
import
|
|
45
|
+
} from "@mui/icons-material";
|
|
46
|
+
import dayjs from 'dayjs';
|
|
47
|
+
import {BackspaceOutline, CheckCircleOutline, DeleteOutline, Filter, PencilOutline} from "mdi-material-ui";
|
|
48
|
+
import SGrid from "views/genericTable/SGrid";
|
|
49
|
+
import {Controller, useForm} from "react-hook-form";
|
|
50
|
+
import ComputedTextEditor from "views/genericTable/RegexTextEditor";
|
|
51
|
+
import ExpressionEditor from "views/genericTable/RegexTextEditor";
|
|
52
|
+
import BuilderExpressionParams from "views/genericTable/BuilderExpressionParams";
|
|
53
|
+
import handleChange from "services/helper/handleChange";
|
|
54
|
+
import DynamicValueList from "views/genericTable/BuilderExpressionParams";
|
|
55
|
+
import SqlEditor from "views/genericTable/QueryEditor";
|
|
56
|
+
import Switch from "@mui/material/Switch";
|
|
57
|
+
import {EditorState} from "draft-js";
|
|
58
|
+
import PermissionsSubjects from "src/configs/Permissions/PermissionsSubjects.json";
|
|
59
|
+
import ReportBuilderSaveForm from "views/genericTable/ReportBuilderSaveDialog";
|
|
60
|
+
import {useRouter} from "next/router";
|
|
61
|
+
import CustomFilterDialog from "views/customFilter/CustomFilterDialog";
|
|
62
|
+
import FixedFilterDialog from "views/customFilter/FixedFilterDialog";
|
|
63
|
+
import RoutingSettingDialog from "views/genericTable/RoutingSettingDialog";
|
|
64
|
+
import FormattingSettingsDialog from "views/genericTable/FormattingSettingsDialog";
|
|
65
|
+
import ColumnConfiguratorDialog from "views/genericTable/ColumnConfiguratorDialog";
|
|
66
|
+
import ReportSettingsDialog from "views/genericTable/ReportSettingsDialog";
|
|
67
|
+
import {DragDropContext, Droppable, Draggable} from 'react-beautiful-dnd';
|
|
65
68
|
|
|
66
69
|
const ReportBuilder = () => {
|
|
67
|
-
const [models, setModels] = useState([])
|
|
68
|
-
const [modelFields, setModelFields] = useState([])
|
|
69
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
70
|
-
const [selectedCollection, setSelectedCollection] = useState()
|
|
71
|
-
const [editingHeaderPath, setEditingHeaderPath] = useState(null)
|
|
72
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
70
|
+
const [models, setModels] = useState([]);
|
|
71
|
+
const [modelFields, setModelFields] = useState([]);
|
|
72
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
73
|
+
const [selectedCollection, setSelectedCollection] = useState();
|
|
74
|
+
const [editingHeaderPath, setEditingHeaderPath] = useState(null);
|
|
75
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
73
76
|
const router = useRouter()
|
|
74
|
-
const {
|
|
77
|
+
const {id} = router.query
|
|
75
78
|
const [builderMetadata, setBuilderMetadata] = useState({
|
|
76
79
|
id: 0,
|
|
77
80
|
name: '',
|
|
78
81
|
description: '',
|
|
79
|
-
isNew: true
|
|
80
|
-
})
|
|
82
|
+
isNew: true,
|
|
83
|
+
});
|
|
81
84
|
const [builderModel, setBuilderModel] = useState({
|
|
82
85
|
reportSource: null,
|
|
83
86
|
selectedFields: [],
|
|
@@ -85,14 +88,17 @@ const ReportBuilder = () => {
|
|
|
85
88
|
isRaw: false,
|
|
86
89
|
filter: {
|
|
87
90
|
Tfilter: [],
|
|
88
|
-
LocalTfilter: []
|
|
91
|
+
LocalTfilter: [],
|
|
89
92
|
},
|
|
90
93
|
searchFilter: [],
|
|
91
94
|
rawSql: '',
|
|
92
|
-
routingSettings: []
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
routingSettings: [],
|
|
96
|
+
settings: {
|
|
97
|
+
rowUniqueId: []
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const [breadcrumbs, setBreadcrumbs] = useState([]);
|
|
101
|
+
const [isPreview, setIsPreview] = useState(false);
|
|
96
102
|
const [openDialogs, setOpenDialogs] = useState({
|
|
97
103
|
expression: false,
|
|
98
104
|
params: false,
|
|
@@ -100,12 +106,19 @@ const ReportBuilder = () => {
|
|
|
100
106
|
filter: false,
|
|
101
107
|
searchFilter: false,
|
|
102
108
|
routing: false,
|
|
103
|
-
fixedFilter: false
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
fixedFilter: false,
|
|
110
|
+
formatting: false,
|
|
111
|
+
columnConfig: false,
|
|
112
|
+
settings: false
|
|
113
|
+
});
|
|
114
|
+
const [selectedRow, setSelectedRow] = useState(null);
|
|
115
|
+
const [columnConfigResult, setColumnConfigResult] = useState(null);
|
|
106
116
|
|
|
107
117
|
// File input ref for uploading builder model JSON
|
|
108
|
-
const fileInputRef = useRef(null)
|
|
118
|
+
const fileInputRef = useRef(null);
|
|
119
|
+
|
|
120
|
+
// UpdateRef for tracking field changes in the grid (array structure)
|
|
121
|
+
const updateRef = useRef([]);
|
|
109
122
|
|
|
110
123
|
// Download current builderModel as JSON file
|
|
111
124
|
const handleDownloadModel = () => {
|
|
@@ -113,121 +126,122 @@ const ReportBuilder = () => {
|
|
|
113
126
|
const replacer = (key, value) => {
|
|
114
127
|
// Convert Dayjs instances to Date so JSON.stringify uses ISO via toJSON
|
|
115
128
|
if (value && typeof value === 'object') {
|
|
116
|
-
if (dayjs.isDayjs(value)) return value.toDate()
|
|
117
|
-
if (value instanceof Date) return value // keep as Date
|
|
129
|
+
if (dayjs.isDayjs(value)) return value.toDate();
|
|
130
|
+
if (value instanceof Date) return value; // keep as Date
|
|
118
131
|
}
|
|
119
|
-
return value
|
|
120
|
-
}
|
|
121
|
-
const json = JSON.stringify(builderModel, replacer, 2)
|
|
122
|
-
const blob = new Blob([json], {
|
|
123
|
-
const url = URL.createObjectURL(blob)
|
|
124
|
-
const a = document.createElement('a')
|
|
125
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-')
|
|
126
|
-
a.href = url
|
|
127
|
-
a.download = `builderModel-${ts}.json
|
|
128
|
-
document.body.appendChild(a)
|
|
129
|
-
a.click()
|
|
130
|
-
document.body.removeChild(a)
|
|
131
|
-
URL.revokeObjectURL(url)
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
const json = JSON.stringify(builderModel, replacer, 2);
|
|
135
|
+
const blob = new Blob([json], {type: 'application/json'});
|
|
136
|
+
const url = URL.createObjectURL(blob);
|
|
137
|
+
const a = document.createElement('a');
|
|
138
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
139
|
+
a.href = url;
|
|
140
|
+
a.download = `builderModel-${ts}.json`;
|
|
141
|
+
document.body.appendChild(a);
|
|
142
|
+
a.click();
|
|
143
|
+
document.body.removeChild(a);
|
|
144
|
+
URL.revokeObjectURL(url);
|
|
132
145
|
} catch (err) {
|
|
133
|
-
console.error('Failed to download builder model:', err)
|
|
146
|
+
console.error('Failed to download builder model:', err);
|
|
134
147
|
}
|
|
135
|
-
}
|
|
148
|
+
};
|
|
136
149
|
|
|
137
150
|
// Trigger hidden file input
|
|
138
151
|
const handleUploadClick = () => {
|
|
139
152
|
try {
|
|
140
|
-
fileInputRef.current?.click()
|
|
153
|
+
fileInputRef.current?.click();
|
|
141
154
|
} catch (e) {
|
|
142
|
-
console.error(e)
|
|
155
|
+
console.error(e);
|
|
143
156
|
}
|
|
144
|
-
}
|
|
157
|
+
};
|
|
145
158
|
|
|
146
159
|
// Handle JSON file selection and load into builderModel
|
|
147
|
-
const handleFileChange = e => {
|
|
148
|
-
const file = e?.target?.files?.[0]
|
|
149
|
-
if (!file) return
|
|
150
|
-
const reader = new FileReader()
|
|
160
|
+
const handleFileChange = (e) => {
|
|
161
|
+
const file = e?.target?.files?.[0];
|
|
162
|
+
if (!file) return;
|
|
163
|
+
const reader = new FileReader();
|
|
151
164
|
reader.onload = () => {
|
|
152
165
|
try {
|
|
153
|
-
const text = reader.result || ''
|
|
154
|
-
const obj = JSON.parse(text)
|
|
166
|
+
const text = reader.result || '';
|
|
167
|
+
const obj = JSON.parse(text);
|
|
155
168
|
// Basic validation: ensure it's an object
|
|
156
169
|
if (obj && typeof obj === 'object') {
|
|
157
|
-
setBuilderModel(obj)
|
|
170
|
+
setBuilderModel(obj);
|
|
158
171
|
} else {
|
|
159
|
-
console.error('Uploaded file is not a valid builder model object')
|
|
172
|
+
console.error('Uploaded file is not a valid builder model object');
|
|
160
173
|
}
|
|
161
174
|
} catch (err) {
|
|
162
|
-
console.error('Failed to parse uploaded JSON:', err)
|
|
175
|
+
console.error('Failed to parse uploaded JSON:', err);
|
|
163
176
|
} finally {
|
|
164
|
-
if (e?.target) e.target.value = ''
|
|
177
|
+
if (e?.target) e.target.value = '';
|
|
165
178
|
}
|
|
166
|
-
}
|
|
167
|
-
reader.readAsText(file)
|
|
168
|
-
}
|
|
179
|
+
};
|
|
180
|
+
reader.readAsText(file);
|
|
181
|
+
};
|
|
169
182
|
|
|
170
|
-
const {
|
|
183
|
+
const {control, handleSubmit, reset} = useForm({
|
|
171
184
|
defaultValues: {
|
|
172
185
|
name: '',
|
|
173
186
|
type: '',
|
|
174
|
-
expression: ''
|
|
175
|
-
}
|
|
176
|
-
})
|
|
187
|
+
expression: '',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
177
190
|
|
|
178
191
|
const handleToggleDialogs = dialog => {
|
|
179
192
|
handleChange(setOpenDialogs, dialog, !openDialogs[dialog])
|
|
180
193
|
}
|
|
181
194
|
|
|
182
|
-
const isInSearchFilter = fullPath
|
|
195
|
+
const isInSearchFilter = (fullPath) =>
|
|
196
|
+
(builderModel.searchFilter ?? []).some(f => f.fullPath === fullPath);
|
|
183
197
|
|
|
184
|
-
const toggleSearchFilterByFullPath = payload => {
|
|
198
|
+
const toggleSearchFilterByFullPath = (payload) => {
|
|
185
199
|
setBuilderModel(prev => {
|
|
186
|
-
const exists = (prev.searchFilter ?? []).some(f => f.fullPath === payload.fullPath)
|
|
200
|
+
const exists = (prev.searchFilter ?? []).some(f => f.fullPath === payload.fullPath);
|
|
187
201
|
const updated = exists
|
|
188
202
|
? prev.searchFilter.filter(f => f.fullPath !== payload.fullPath)
|
|
189
|
-
: [...(prev.searchFilter ?? []), payload]
|
|
203
|
+
: [...(prev.searchFilter ?? []), payload];
|
|
190
204
|
|
|
191
205
|
return {
|
|
192
206
|
...prev,
|
|
193
207
|
searchFilter: updated
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
}
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
};
|
|
197
211
|
|
|
198
212
|
const handleGetModels = async () => {
|
|
199
213
|
try {
|
|
200
|
-
setIsLoading(true)
|
|
201
|
-
const response = await Services.PostService(Endpoints.ReportBuilder.Post.GetModels, false, {})
|
|
214
|
+
setIsLoading(true);
|
|
215
|
+
const response = await Services.PostService(Endpoints.ReportBuilder.Post.GetModels, false, {});
|
|
202
216
|
if (response) {
|
|
203
|
-
setModels(response)
|
|
217
|
+
setModels(response);
|
|
204
218
|
}
|
|
205
219
|
} catch (error) {
|
|
206
|
-
console.error(error)
|
|
220
|
+
console.error(error);
|
|
207
221
|
} finally {
|
|
208
|
-
setIsLoading(false)
|
|
222
|
+
setIsLoading(false);
|
|
209
223
|
}
|
|
210
|
-
}
|
|
224
|
+
};
|
|
211
225
|
|
|
212
226
|
const handleGetFields = async (modelFullName, friendlyName = null, propertyType, isPrevious) => {
|
|
213
227
|
try {
|
|
214
|
-
setIsLoading(true)
|
|
228
|
+
setIsLoading(true);
|
|
215
229
|
const response = await Services.PostService(
|
|
216
230
|
Endpoints.ReportBuilder.Post.GetModelFields,
|
|
217
231
|
false,
|
|
218
232
|
{},
|
|
219
|
-
{
|
|
220
|
-
)
|
|
233
|
+
{modelFullName}
|
|
234
|
+
);
|
|
221
235
|
if (response) {
|
|
222
|
-
setModelFields(response.data)
|
|
223
|
-
const modelObject = models.find(m => m.fullName === modelFullName)
|
|
236
|
+
setModelFields(response.data);
|
|
237
|
+
const modelObject = models.find(m => m.fullName === modelFullName);
|
|
224
238
|
|
|
225
|
-
console.log(modelObject)
|
|
239
|
+
console.log(modelObject);
|
|
226
240
|
setBreadcrumbs(prev => {
|
|
227
|
-
const exists = prev.find(b => b.fullName === modelFullName)
|
|
241
|
+
const exists = prev.find(b => b.fullName === modelFullName);
|
|
228
242
|
if (exists && isPrevious) {
|
|
229
|
-
const index = prev.findIndex(b => b.fullName === modelFullName)
|
|
230
|
-
return prev.slice(0, index + 1)
|
|
243
|
+
const index = prev.findIndex(b => b.fullName === modelFullName);
|
|
244
|
+
return prev.slice(0, index + 1);
|
|
231
245
|
}
|
|
232
246
|
console.log(prev)
|
|
233
247
|
return [
|
|
@@ -235,69 +249,73 @@ const ReportBuilder = () => {
|
|
|
235
249
|
{
|
|
236
250
|
fullName: modelFullName,
|
|
237
251
|
friendlyName: friendlyName ?? modelObject?.friendlyName ?? modelFullName,
|
|
238
|
-
propertyType: propertyType ?? modelObject?.propertyType ?? null
|
|
239
|
-
}
|
|
240
|
-
]
|
|
241
|
-
})
|
|
252
|
+
propertyType: propertyType ?? modelObject?.propertyType ?? null,
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
});
|
|
242
256
|
}
|
|
243
257
|
} catch (error) {
|
|
244
|
-
console.error(error)
|
|
258
|
+
console.error(error);
|
|
245
259
|
} finally {
|
|
246
|
-
setIsLoading(false)
|
|
260
|
+
setIsLoading(false);
|
|
247
261
|
}
|
|
248
|
-
}
|
|
262
|
+
};
|
|
249
263
|
|
|
250
264
|
// 1) Trim a full field‐path for *leaf* fields:
|
|
251
|
-
|
|
252
|
-
|
|
265
|
+
// • always drop the root segment
|
|
266
|
+
// • if there are any collections, drop everything up through the *last* collection
|
|
253
267
|
const getLeafTrimmedPath = (fullPath, collectionBreadcrumbs) => {
|
|
254
|
-
const parts = fullPath.split('.')
|
|
255
|
-
const afterRoot = parts.slice(1)
|
|
268
|
+
const parts = fullPath.split('.');
|
|
269
|
+
const afterRoot = parts.slice(1);
|
|
256
270
|
if (!collectionBreadcrumbs.length) {
|
|
257
|
-
return afterRoot.join('.')
|
|
271
|
+
return afterRoot.join('.');
|
|
258
272
|
}
|
|
259
|
-
const lastColl = collectionBreadcrumbs[collectionBreadcrumbs.length - 1].friendlyName
|
|
260
|
-
const idx = afterRoot.lastIndexOf(lastColl)
|
|
261
|
-
return idx >= 0
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
const lastColl = collectionBreadcrumbs[collectionBreadcrumbs.length - 1].friendlyName;
|
|
274
|
+
const idx = afterRoot.lastIndexOf(lastColl);
|
|
275
|
+
return idx >= 0
|
|
276
|
+
? afterRoot.slice(idx + 1).join('.')
|
|
277
|
+
: afterRoot.join('.');
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// 2) Trim a *collection* node’s path:
|
|
281
|
+
// • drop the root segment
|
|
282
|
+
// • then drop *exactly* the first collection (if present), keep everything else
|
|
267
283
|
const getCollectionTrimmedPath = (fullCollectionPath, collectionBreadcrumbs) => {
|
|
268
|
-
const parts = fullCollectionPath.split('.')
|
|
269
|
-
const afterRoot = parts.slice(1)
|
|
284
|
+
const parts = fullCollectionPath.split('.');
|
|
285
|
+
const afterRoot = parts.slice(1);
|
|
270
286
|
if (!collectionBreadcrumbs.length) {
|
|
271
|
-
return afterRoot.join('.')
|
|
287
|
+
return afterRoot.join('.');
|
|
272
288
|
}
|
|
273
|
-
const firstColl = collectionBreadcrumbs[0].friendlyName
|
|
289
|
+
const firstColl = collectionBreadcrumbs[0].friendlyName;
|
|
274
290
|
// if the very first segment after root is our first collection, drop it
|
|
275
291
|
if (afterRoot[0] === firstColl) {
|
|
276
|
-
const rest = afterRoot.slice(1)
|
|
292
|
+
const rest = afterRoot.slice(1);
|
|
277
293
|
// if that was the only segment, fall back to the collection name
|
|
278
|
-
return rest.length ? rest.join('.') : firstColl
|
|
294
|
+
return rest.length ? rest.join('.') : firstColl;
|
|
279
295
|
}
|
|
280
|
-
return afterRoot.join('.')
|
|
281
|
-
}
|
|
296
|
+
return afterRoot.join('.');
|
|
297
|
+
};
|
|
282
298
|
|
|
283
|
-
const handleAddStaticField = field => {
|
|
299
|
+
const handleAddStaticField = (field) => {
|
|
284
300
|
console.log(builderModel.selectedFields)
|
|
285
301
|
// build fullPath & identify collection crumbs
|
|
286
|
-
const fullPathParts = [
|
|
287
|
-
|
|
302
|
+
const fullPathParts = [
|
|
303
|
+
...breadcrumbs.map(b => b.friendlyName),
|
|
304
|
+
field.friendlyName
|
|
305
|
+
];
|
|
306
|
+
const fullPath = fullPathParts.join('.');
|
|
288
307
|
const collectionBreadcrumbs = breadcrumbs
|
|
289
308
|
.map((b, i) => ({
|
|
290
309
|
...b,
|
|
291
|
-
fullPath: breadcrumbs
|
|
292
|
-
.slice(0, i + 1)
|
|
310
|
+
fullPath: breadcrumbs.slice(0, i + 1)
|
|
293
311
|
.map(x => x.friendlyName)
|
|
294
312
|
.join('.')
|
|
295
313
|
}))
|
|
296
|
-
.filter(b => b.propertyType === 'Collection')
|
|
314
|
+
.filter(b => b.propertyType === 'Collection');
|
|
297
315
|
|
|
298
316
|
// compute leaf path & header
|
|
299
|
-
const trimmedPath = getLeafTrimmedPath(fullPath, collectionBreadcrumbs)
|
|
300
|
-
const headerName = trimmedPath.replace(/\./g, '_')
|
|
317
|
+
const trimmedPath = getLeafTrimmedPath(fullPath, collectionBreadcrumbs);
|
|
318
|
+
const headerName = trimmedPath.replace(/\./g, '_');
|
|
301
319
|
console.log(field)
|
|
302
320
|
const newLeafField = {
|
|
303
321
|
fullPath: fullPath,
|
|
@@ -309,19 +327,21 @@ const ReportBuilder = () => {
|
|
|
309
327
|
propertyType: field.propertyType,
|
|
310
328
|
enumName: field.enumName,
|
|
311
329
|
hidden: false,
|
|
312
|
-
image: false
|
|
313
|
-
}
|
|
330
|
+
image: false,
|
|
331
|
+
};
|
|
314
332
|
|
|
315
333
|
setBuilderModel(prev => {
|
|
316
|
-
const clone = [...prev.selectedFields]
|
|
334
|
+
const clone = [...prev.selectedFields];
|
|
317
335
|
|
|
318
336
|
const insertIntoCollectionsOnly = (nodes, crumbs, depth = 0) => {
|
|
319
|
-
const crumb = crumbs[depth]
|
|
320
|
-
const collFull = crumb.fullPath
|
|
321
|
-
const collTrimmed = getCollectionTrimmedPath(collFull, crumbs)
|
|
322
|
-
const collHeader = collTrimmed.replace(/\./g, '_')
|
|
323
|
-
|
|
324
|
-
let node = nodes.find(n =>
|
|
337
|
+
const crumb = crumbs[depth];
|
|
338
|
+
const collFull = crumb.fullPath;
|
|
339
|
+
const collTrimmed = getCollectionTrimmedPath(collFull, crumbs);
|
|
340
|
+
const collHeader = collTrimmed.replace(/\./g, '_');
|
|
341
|
+
|
|
342
|
+
let node = nodes.find(n =>
|
|
343
|
+
n.propertyType === 'Collection' && n.path === collTrimmed
|
|
344
|
+
);
|
|
325
345
|
if (!node) {
|
|
326
346
|
node = {
|
|
327
347
|
fullPath: fullPath,
|
|
@@ -329,33 +349,34 @@ const ReportBuilder = () => {
|
|
|
329
349
|
headerName: collHeader,
|
|
330
350
|
propertyType: 'Collection',
|
|
331
351
|
children: []
|
|
332
|
-
}
|
|
333
|
-
nodes.push(node)
|
|
352
|
+
};
|
|
353
|
+
nodes.push(node);
|
|
334
354
|
}
|
|
335
355
|
|
|
336
|
-
const isLast = depth === crumbs.length - 1
|
|
356
|
+
const isLast = depth === crumbs.length - 1;
|
|
337
357
|
if (isLast) {
|
|
338
358
|
if (!node.children.some(f => f.path === trimmedPath)) {
|
|
339
|
-
node.children.push(newLeafField)
|
|
359
|
+
node.children.push(newLeafField);
|
|
340
360
|
}
|
|
341
361
|
} else {
|
|
342
|
-
insertIntoCollectionsOnly(node.children, crumbs, depth + 1)
|
|
362
|
+
insertIntoCollectionsOnly(node.children, crumbs, depth + 1);
|
|
343
363
|
}
|
|
344
|
-
}
|
|
364
|
+
};
|
|
345
365
|
|
|
346
366
|
if (!collectionBreadcrumbs.length) {
|
|
347
367
|
// no collections ⇒ flat insert
|
|
348
368
|
if (!clone.some(f => f.path === trimmedPath)) {
|
|
349
|
-
return {
|
|
369
|
+
return {...prev, selectedFields: [...clone, newLeafField]};
|
|
350
370
|
}
|
|
351
|
-
return prev
|
|
371
|
+
return prev;
|
|
352
372
|
}
|
|
353
373
|
|
|
354
374
|
// otherwise build the nested collection node(s)
|
|
355
|
-
insertIntoCollectionsOnly(clone, collectionBreadcrumbs)
|
|
356
|
-
return {
|
|
357
|
-
})
|
|
358
|
-
}
|
|
375
|
+
insertIntoCollectionsOnly(clone, collectionBreadcrumbs);
|
|
376
|
+
return {...prev, selectedFields: clone};
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
359
380
|
|
|
360
381
|
// (1) Helper: insert a node into the right collection, by `collectionPath`
|
|
361
382
|
function insertIntoCollection(nodes, collectionPath, newField) {
|
|
@@ -366,19 +387,19 @@ const ReportBuilder = () => {
|
|
|
366
387
|
return {
|
|
367
388
|
...node,
|
|
368
389
|
children: [...(node.children || []), newField]
|
|
369
|
-
}
|
|
390
|
+
};
|
|
370
391
|
}
|
|
371
392
|
// otherwise recurse
|
|
372
393
|
return {
|
|
373
394
|
...node,
|
|
374
395
|
children: insertIntoCollection(node.children || [], collectionPath, newField)
|
|
375
|
-
}
|
|
396
|
+
};
|
|
376
397
|
}
|
|
377
|
-
return node
|
|
378
|
-
})
|
|
398
|
+
return node;
|
|
399
|
+
});
|
|
379
400
|
}
|
|
380
401
|
|
|
381
|
-
|
|
402
|
+
// (2) Add computed field — either top‐level or inside the selectedCollection
|
|
382
403
|
const handleAddComputedField = (expression, headerName, propertyType, title) => {
|
|
383
404
|
const newField = {
|
|
384
405
|
path: headerName,
|
|
@@ -388,221 +409,230 @@ const ReportBuilder = () => {
|
|
|
388
409
|
expression,
|
|
389
410
|
title,
|
|
390
411
|
hidden: false,
|
|
391
|
-
image: false
|
|
392
|
-
}
|
|
412
|
+
image: false,
|
|
413
|
+
};
|
|
393
414
|
|
|
394
415
|
setBuilderModel(prev => {
|
|
395
|
-
const {
|
|
416
|
+
const {selectedFields} = prev;
|
|
396
417
|
// if no collection selected, append at root
|
|
397
418
|
if (!selectedCollection) {
|
|
398
419
|
return {
|
|
399
420
|
...prev,
|
|
400
421
|
selectedFields: [...selectedFields, newField]
|
|
401
|
-
}
|
|
422
|
+
};
|
|
402
423
|
}
|
|
403
424
|
// otherwise insert under the chosen collection
|
|
404
|
-
const updated = insertIntoCollection(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
425
|
+
const updated = insertIntoCollection(
|
|
426
|
+
selectedFields,
|
|
427
|
+
selectedCollection.path,
|
|
428
|
+
newField
|
|
429
|
+
);
|
|
430
|
+
return {...prev, selectedFields: updated};
|
|
431
|
+
});
|
|
432
|
+
setSelectedCollection(null);
|
|
433
|
+
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// (3) Update computed field — likewise drill into the right spot
|
|
437
|
+
const handleUpdateComputedField = (
|
|
438
|
+
oldHeaderName,
|
|
439
|
+
expression,
|
|
440
|
+
headerName,
|
|
441
|
+
propertyType,
|
|
442
|
+
title
|
|
443
|
+
) => {
|
|
412
444
|
setBuilderModel(prev => {
|
|
413
445
|
const updateNode = node => {
|
|
414
446
|
if (node.type === 'computed' && node.headerName === oldHeaderName) {
|
|
415
|
-
return {
|
|
447
|
+
return {...node, headerName, expression, propertyType, title, path: headerName};
|
|
416
448
|
}
|
|
417
449
|
if (node.propertyType === 'Collection') {
|
|
418
450
|
return {
|
|
419
451
|
...node,
|
|
420
452
|
children: (node.children || []).map(updateNode)
|
|
421
|
-
}
|
|
453
|
+
};
|
|
422
454
|
}
|
|
423
|
-
return node
|
|
424
|
-
}
|
|
455
|
+
return node;
|
|
456
|
+
};
|
|
425
457
|
|
|
426
458
|
return {
|
|
427
459
|
...prev,
|
|
428
460
|
selectedFields: prev.selectedFields.map(updateNode)
|
|
429
|
-
}
|
|
430
|
-
})
|
|
431
|
-
setSelectedCollection(null)
|
|
432
|
-
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
setSelectedCollection(null);
|
|
464
|
+
|
|
465
|
+
};
|
|
466
|
+
|
|
433
467
|
|
|
434
468
|
function removeFieldByPath(fields, pathToRemove) {
|
|
435
469
|
return fields.reduce((acc, f) => {
|
|
436
470
|
if (f.propertyType === 'Collection') {
|
|
437
471
|
// first, recurse into its children
|
|
438
|
-
const filteredChildren = removeFieldByPath(f.children || [], pathToRemove)
|
|
472
|
+
const filteredChildren = removeFieldByPath(f.children || [], pathToRemove);
|
|
439
473
|
// only keep this collection if it still has children
|
|
440
474
|
if (filteredChildren.length > 0) {
|
|
441
|
-
acc.push({
|
|
475
|
+
acc.push({...f, children: filteredChildren});
|
|
442
476
|
}
|
|
443
477
|
} else {
|
|
444
478
|
// leaf field: only keep it if paths don't match
|
|
445
479
|
if (f.path !== pathToRemove) {
|
|
446
|
-
acc.push(f)
|
|
480
|
+
acc.push(f);
|
|
447
481
|
}
|
|
448
482
|
}
|
|
449
|
-
return acc
|
|
450
|
-
}, [])
|
|
483
|
+
return acc;
|
|
484
|
+
}, []);
|
|
451
485
|
}
|
|
452
486
|
|
|
453
487
|
function handleFilterChange(field, value) {
|
|
454
|
-
console.log(field, value)
|
|
488
|
+
console.log(field, value);
|
|
455
489
|
setBuilderModel(prev => ({
|
|
456
490
|
...prev,
|
|
457
491
|
filter: {
|
|
458
492
|
...prev.filter,
|
|
459
|
-
[field]: value
|
|
460
|
-
}
|
|
461
|
-
}))
|
|
493
|
+
[field]: value, // <-- computed key on the nested object
|
|
494
|
+
},
|
|
495
|
+
}));
|
|
462
496
|
}
|
|
463
497
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
498
|
+
|
|
499
|
+
const handleDragEnd = (result) => {
|
|
500
|
+
const {source, destination} = result || {};
|
|
501
|
+
if (!destination) return;
|
|
502
|
+
if (source.droppableId !== destination.droppableId) return;
|
|
503
|
+
const droppableId = source.droppableId;
|
|
469
504
|
|
|
470
505
|
setBuilderModel(prev => {
|
|
471
506
|
const reorderLeaves = (arr, from, to) => {
|
|
472
|
-
const leaves = arr.filter(n => !n.children)
|
|
473
|
-
const collections = arr.filter(n => n.children)
|
|
474
|
-
if (from < 0 || to < 0 || from >= leaves.length || to >= leaves.length) return arr
|
|
475
|
-
const leafMap = Object.fromEntries(leaves.map(n => [n.path, n]))
|
|
476
|
-
const order = leaves.map(n => n.path)
|
|
477
|
-
const [moved] = order.splice(from, 1)
|
|
478
|
-
order.splice(to, 0, moved)
|
|
479
|
-
const orderQueue = order.slice()
|
|
507
|
+
const leaves = arr.filter(n => !n.children);
|
|
508
|
+
const collections = arr.filter(n => n.children);
|
|
509
|
+
if (from < 0 || to < 0 || from >= leaves.length || to >= leaves.length) return arr;
|
|
510
|
+
const leafMap = Object.fromEntries(leaves.map(n => [n.path, n]));
|
|
511
|
+
const order = leaves.map(n => n.path);
|
|
512
|
+
const [moved] = order.splice(from, 1);
|
|
513
|
+
order.splice(to, 0, moved);
|
|
514
|
+
const orderQueue = order.slice();
|
|
480
515
|
// rebuild original array: keep collections in place, leaves in new order
|
|
481
516
|
return arr.map(node => {
|
|
482
|
-
if (node.children) return node
|
|
483
|
-
const nextPath = orderQueue.shift()
|
|
484
|
-
return leafMap[nextPath] || node
|
|
485
|
-
})
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const updateCollections = nodes =>
|
|
489
|
-
|
|
490
|
-
if (n.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return { ...n, children: newChildren }
|
|
494
|
-
}
|
|
495
|
-
return { ...n, children: updateCollections(n.children || []) }
|
|
517
|
+
if (node.children) return node;
|
|
518
|
+
const nextPath = orderQueue.shift();
|
|
519
|
+
return leafMap[nextPath] || node;
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const updateCollections = (nodes) => nodes.map(n => {
|
|
524
|
+
if (n.propertyType === 'Collection') {
|
|
525
|
+
if (n.path === droppableId) {
|
|
526
|
+
const newChildren = reorderLeaves(n.children || [], source.index, destination.index);
|
|
527
|
+
return {...n, children: newChildren};
|
|
496
528
|
}
|
|
497
|
-
return n
|
|
498
|
-
}
|
|
529
|
+
return {...n, children: updateCollections(n.children || [])};
|
|
530
|
+
}
|
|
531
|
+
return n;
|
|
532
|
+
});
|
|
499
533
|
|
|
500
534
|
if (droppableId === 'root') {
|
|
501
|
-
return {
|
|
535
|
+
return {...prev, selectedFields: reorderLeaves(prev.selectedFields, source.index, destination.index)};
|
|
502
536
|
} else {
|
|
503
|
-
return {
|
|
537
|
+
return {...prev, selectedFields: updateCollections(prev.selectedFields)};
|
|
504
538
|
}
|
|
505
|
-
})
|
|
506
|
-
}
|
|
539
|
+
});
|
|
540
|
+
};
|
|
507
541
|
|
|
508
542
|
const renderSelectedFields = (nodes, level = 0, droppableId = 'root') => {
|
|
509
543
|
// Count leaf-only index for Draggable indices
|
|
510
|
-
let leafIndexCounter = 0
|
|
544
|
+
let leafIndexCounter = 0;
|
|
511
545
|
return (
|
|
512
546
|
<Droppable droppableId={droppableId}>
|
|
513
|
-
{provided => (
|
|
514
|
-
<List dense disablePadding sx={{
|
|
515
|
-
{nodes.map(item =>
|
|
547
|
+
{(provided) => (
|
|
548
|
+
<List dense disablePadding sx={{pl: level * 2}} ref={provided.innerRef} {...provided.droppableProps}>
|
|
549
|
+
{nodes.map((item) => (
|
|
516
550
|
item.children ? (
|
|
517
|
-
<Accordion key={item.headerName} disableGutters sx={{
|
|
518
|
-
<AccordionSummary expandIcon={<Expand
|
|
551
|
+
<Accordion key={item.headerName} disableGutters sx={{mb: 1}}>
|
|
552
|
+
<AccordionSummary expandIcon={<Expand/>}>
|
|
519
553
|
<Box
|
|
520
554
|
sx={{
|
|
521
555
|
display: 'flex',
|
|
522
556
|
alignItems: 'center',
|
|
523
557
|
justifyContent: 'space-between',
|
|
524
|
-
width: '100%'
|
|
558
|
+
width: '100%',
|
|
525
559
|
}}
|
|
526
560
|
>
|
|
527
|
-
<Typography variant=
|
|
561
|
+
<Typography variant="subtitle2" fontWeight="bold">
|
|
528
562
|
{item.headerName ?? item.path}
|
|
529
563
|
</Typography>
|
|
530
564
|
<IconButton
|
|
531
|
-
size=
|
|
532
|
-
onClick={e => {
|
|
533
|
-
e.stopPropagation()
|
|
534
|
-
setSelectedCollection(item)
|
|
535
|
-
setSelectedRow(null)
|
|
536
|
-
setOpenDialogs(prev => ({
|
|
565
|
+
size="small"
|
|
566
|
+
onClick={(e) => {
|
|
567
|
+
e.stopPropagation();
|
|
568
|
+
setSelectedCollection(item);
|
|
569
|
+
setSelectedRow(null);
|
|
570
|
+
setOpenDialogs(prev => ({...prev, expression: true}));
|
|
537
571
|
}}
|
|
538
|
-
title=
|
|
572
|
+
title="Add computed field to this collection"
|
|
539
573
|
>
|
|
540
|
-
<AddBox fontSize=
|
|
574
|
+
<AddBox fontSize="small"/>
|
|
541
575
|
</IconButton>
|
|
542
576
|
</Box>
|
|
543
577
|
</AccordionSummary>
|
|
544
|
-
<AccordionDetails>
|
|
578
|
+
<AccordionDetails>
|
|
579
|
+
{renderSelectedFields(item.children, level + 1, item.path)}
|
|
580
|
+
</AccordionDetails>
|
|
545
581
|
</Accordion>
|
|
546
582
|
) : (
|
|
547
|
-
<Draggable
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
index={leafIndexCounter++}
|
|
551
|
-
>
|
|
552
|
-
{providedDrag => (
|
|
583
|
+
<Draggable key={`${droppableId}::${item.path}`} draggableId={`${droppableId}::${item.path}`}
|
|
584
|
+
index={leafIndexCounter++}>
|
|
585
|
+
{(providedDrag) => (
|
|
553
586
|
<ListItem
|
|
554
|
-
ref={providedDrag.innerRef}
|
|
555
|
-
|
|
556
|
-
{...providedDrag.dragHandleProps}
|
|
557
|
-
divider
|
|
558
|
-
>
|
|
587
|
+
ref={providedDrag.innerRef} {...providedDrag.draggableProps} {...providedDrag.dragHandleProps}
|
|
588
|
+
divider>
|
|
559
589
|
<ListItemText
|
|
560
590
|
primary={
|
|
561
591
|
editingHeaderPath === item.path ? (
|
|
562
592
|
<TextField
|
|
563
|
-
size=
|
|
593
|
+
size="small"
|
|
564
594
|
autoFocus
|
|
565
|
-
variant=
|
|
595
|
+
variant="standard"
|
|
566
596
|
value={item.title}
|
|
567
|
-
onChange={e => {
|
|
568
|
-
const newHeaderName = e.target.value
|
|
597
|
+
onChange={(e) => {
|
|
598
|
+
const newHeaderName = e.target.value;
|
|
569
599
|
|
|
570
600
|
setBuilderModel(prev => {
|
|
571
|
-
const updateNode = node => {
|
|
601
|
+
const updateNode = (node) => {
|
|
572
602
|
if (node.path === item.path && !node.children) {
|
|
573
|
-
return {
|
|
603
|
+
return {...node, title: newHeaderName};
|
|
574
604
|
}
|
|
575
605
|
if (node.propertyType === 'Collection') {
|
|
576
|
-
return {
|
|
606
|
+
return {...node, children: (node.children || []).map(updateNode)};
|
|
577
607
|
}
|
|
578
|
-
return node
|
|
579
|
-
}
|
|
608
|
+
return node;
|
|
609
|
+
};
|
|
580
610
|
|
|
581
611
|
return {
|
|
582
612
|
...prev,
|
|
583
613
|
selectedFields: prev.selectedFields.map(updateNode)
|
|
584
|
-
}
|
|
585
|
-
})
|
|
614
|
+
};
|
|
615
|
+
});
|
|
586
616
|
}}
|
|
587
617
|
onBlur={() => setEditingHeaderPath(null)}
|
|
588
|
-
onKeyDown={e => {
|
|
618
|
+
onKeyDown={(e) => {
|
|
589
619
|
if (e.key === 'Enter' || e.key === 'Escape') {
|
|
590
|
-
setEditingHeaderPath(null)
|
|
620
|
+
setEditingHeaderPath(null);
|
|
591
621
|
}
|
|
592
622
|
}}
|
|
593
623
|
inputProps={{
|
|
594
624
|
style: {
|
|
595
625
|
fontWeight: 'bold',
|
|
596
|
-
fontSize: '0.875rem'
|
|
626
|
+
fontSize: '0.875rem',
|
|
597
627
|
}
|
|
598
628
|
}}
|
|
599
629
|
/>
|
|
600
630
|
) : (
|
|
601
631
|
<Typography
|
|
602
|
-
variant=
|
|
603
|
-
fontWeight=
|
|
632
|
+
variant="subtitle2"
|
|
633
|
+
fontWeight="bold"
|
|
604
634
|
onDoubleClick={() => setEditingHeaderPath(item.path)}
|
|
605
|
-
sx={{
|
|
635
|
+
sx={{cursor: 'pointer'}}
|
|
606
636
|
>
|
|
607
637
|
{item.title ?? item.headerName}
|
|
608
638
|
</Typography>
|
|
@@ -610,153 +640,175 @@ const ReportBuilder = () => {
|
|
|
610
640
|
}
|
|
611
641
|
secondary={item.path}
|
|
612
642
|
/>
|
|
613
|
-
<Box sx={{
|
|
643
|
+
<Box sx={{display: 'flex', gap: 1}}>
|
|
614
644
|
{item.type === 'computed' && (
|
|
615
645
|
<IconButton
|
|
616
|
-
size=
|
|
646
|
+
size="small"
|
|
617
647
|
onClick={() => {
|
|
618
|
-
setSelectedRow(item)
|
|
619
|
-
setOpenDialogs(prev => ({
|
|
648
|
+
setSelectedRow(item);
|
|
649
|
+
setOpenDialogs(prev => ({...prev, expression: true}));
|
|
620
650
|
}}
|
|
621
|
-
title=
|
|
651
|
+
title="Edit computed field"
|
|
622
652
|
>
|
|
623
|
-
<PencilOutline fontSize=
|
|
653
|
+
<PencilOutline fontSize="small"/>
|
|
624
654
|
</IconButton>
|
|
625
655
|
)}
|
|
626
656
|
|
|
627
|
-
{(item?.field?.propertyType === 'String' ||
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
'
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
)}
|
|
657
|
+
{((item?.field?.propertyType === 'String' || item?.field?.propertyType === 'Int' || item?.field?.propertyType === 'Guid') && item?.type === 'static') && (
|
|
658
|
+
<IconButton
|
|
659
|
+
size="small"
|
|
660
|
+
onClick={() => {
|
|
661
|
+
console.log(item, level)
|
|
662
|
+
toggleSearchFilterByFullPath({
|
|
663
|
+
fullPath: item.fullPath,
|
|
664
|
+
path: item.path || item.headerName || item.fullPath,
|
|
665
|
+
propertyType: item.propertyType,
|
|
666
|
+
isWithinCollection: level !== 0
|
|
667
|
+
});
|
|
668
|
+
}}
|
|
669
|
+
title={
|
|
670
|
+
isInSearchFilter(item.fullPath)
|
|
671
|
+
? 'Remove from search filter'
|
|
672
|
+
: 'Add to search filter'
|
|
673
|
+
}
|
|
674
|
+
sx={{
|
|
675
|
+
color: isInSearchFilter(item.fullPath) ? 'primary.main' : 'text.secondary',
|
|
676
|
+
'&:hover': {
|
|
677
|
+
backgroundColor: 'transparent',
|
|
678
|
+
color: isInSearchFilter(item.fullPath) ? 'primary.main' : 'info.main',
|
|
679
|
+
},
|
|
680
|
+
}}
|
|
681
|
+
>
|
|
682
|
+
<Search fontSize="small"/>
|
|
683
|
+
</IconButton>
|
|
684
|
+
)}
|
|
656
685
|
<IconButton
|
|
657
|
-
size=
|
|
686
|
+
size="small"
|
|
658
687
|
onClick={() => {
|
|
659
688
|
setBuilderModel(prev => {
|
|
660
|
-
const toggleHidden = node => {
|
|
689
|
+
const toggleHidden = (node) => {
|
|
661
690
|
if (node.propertyType === 'Collection') {
|
|
662
691
|
return {
|
|
663
692
|
...node,
|
|
664
693
|
children: (node.children || []).map(toggleHidden)
|
|
665
|
-
}
|
|
694
|
+
};
|
|
666
695
|
}
|
|
667
696
|
if (node.path === item.path) {
|
|
668
|
-
return {
|
|
697
|
+
return {...node, hidden: !node.hidden};
|
|
669
698
|
}
|
|
670
|
-
return node
|
|
671
|
-
}
|
|
699
|
+
return node;
|
|
700
|
+
};
|
|
672
701
|
return {
|
|
673
702
|
...prev,
|
|
674
703
|
selectedFields: prev.selectedFields.map(toggleHidden)
|
|
675
|
-
}
|
|
676
|
-
})
|
|
704
|
+
};
|
|
705
|
+
});
|
|
677
706
|
}}
|
|
678
|
-
title={item.hidden ?
|
|
707
|
+
title={item.hidden ? "Show column" : "Hide column"}
|
|
679
708
|
sx={{
|
|
680
709
|
color: item.hidden ? 'warning.main' : 'text.secondary',
|
|
681
710
|
'&:hover': {
|
|
682
711
|
backgroundColor: 'transparent',
|
|
683
|
-
color: item.hidden ? 'warning.main' : 'info.main'
|
|
684
|
-
}
|
|
712
|
+
color: item.hidden ? 'warning.main' : 'info.main',
|
|
713
|
+
},
|
|
685
714
|
}}
|
|
686
715
|
>
|
|
687
|
-
{item.hidden ? <VisibilityOff fontSize=
|
|
716
|
+
{item.hidden ? <VisibilityOff fontSize="small"/> : <Visibility fontSize="small"/>}
|
|
688
717
|
</IconButton>
|
|
689
718
|
<IconButton
|
|
690
|
-
size=
|
|
719
|
+
size="small"
|
|
691
720
|
onClick={() => {
|
|
692
721
|
setBuilderModel(prev => {
|
|
693
|
-
const toggleImage = node => {
|
|
722
|
+
const toggleImage = (node) => {
|
|
694
723
|
if (node.propertyType === 'Collection') {
|
|
695
|
-
return {
|
|
724
|
+
return {...node, children: (node.children || []).map(toggleImage)};
|
|
696
725
|
}
|
|
697
726
|
if (node.path === item.path) {
|
|
698
|
-
return {
|
|
727
|
+
return {...node, image: !node.image};
|
|
699
728
|
}
|
|
700
|
-
return node
|
|
701
|
-
}
|
|
729
|
+
return node;
|
|
730
|
+
};
|
|
702
731
|
return {
|
|
703
732
|
...prev,
|
|
704
733
|
selectedFields: prev.selectedFields.map(toggleImage)
|
|
705
|
-
}
|
|
706
|
-
})
|
|
734
|
+
};
|
|
735
|
+
});
|
|
707
736
|
}}
|
|
708
|
-
title={item.image ?
|
|
737
|
+
title={item.image ? "Unset image" : "Render as image"}
|
|
709
738
|
sx={{
|
|
710
739
|
color: item.image ? 'success.main' : 'text.secondary',
|
|
711
740
|
'&:hover': {
|
|
712
741
|
backgroundColor: 'transparent',
|
|
713
|
-
color: item.image ? 'success.main' : 'info.main'
|
|
714
|
-
}
|
|
742
|
+
color: item.image ? 'success.main' : 'info.main',
|
|
743
|
+
},
|
|
744
|
+
}}
|
|
745
|
+
>
|
|
746
|
+
<ImageIcon fontSize="small"/>
|
|
747
|
+
</IconButton>
|
|
748
|
+
<IconButton
|
|
749
|
+
size="small"
|
|
750
|
+
onClick={() => {
|
|
751
|
+
setSelectedRow(item);
|
|
752
|
+
setOpenDialogs(prev => ({...prev, formatting: true}));
|
|
753
|
+
}}
|
|
754
|
+
title="Formatting Settings"
|
|
755
|
+
sx={{
|
|
756
|
+
color: item.formatting ? 'primary.main' : 'text.secondary',
|
|
757
|
+
'&:hover': {
|
|
758
|
+
backgroundColor: 'transparent',
|
|
759
|
+
color: item.formatting ? 'primary.main' : 'info.main',
|
|
760
|
+
},
|
|
715
761
|
}}
|
|
716
762
|
>
|
|
717
|
-
<
|
|
763
|
+
<FilterVintageOutlined fontSize="small"/>
|
|
718
764
|
</IconButton>
|
|
719
765
|
<IconButton
|
|
720
|
-
size=
|
|
766
|
+
size="small"
|
|
721
767
|
onClick={() =>
|
|
722
768
|
setBuilderModel(prev => ({
|
|
723
769
|
...prev,
|
|
724
770
|
selectedFields: removeFieldByPath(prev.selectedFields, item.path)
|
|
725
771
|
}))
|
|
726
772
|
}
|
|
727
|
-
title=
|
|
773
|
+
title="Remove field"
|
|
728
774
|
>
|
|
729
|
-
<DeleteOutline fontSize=
|
|
775
|
+
<DeleteOutline fontSize="small"/>
|
|
730
776
|
</IconButton>
|
|
731
777
|
</Box>
|
|
732
778
|
</ListItem>
|
|
733
779
|
)}
|
|
734
780
|
</Draggable>
|
|
735
781
|
)
|
|
736
|
-
)}
|
|
782
|
+
))}
|
|
737
783
|
{provided.placeholder}
|
|
738
784
|
</List>
|
|
739
785
|
)}
|
|
740
786
|
</Droppable>
|
|
741
|
-
)
|
|
742
|
-
}
|
|
787
|
+
);
|
|
788
|
+
};
|
|
743
789
|
|
|
744
790
|
const isFullPathSelected = (nodes, targetFullPath) => {
|
|
745
791
|
for (const node of nodes) {
|
|
746
|
-
if (node.fullPath === targetFullPath) return true
|
|
792
|
+
if (node.fullPath === targetFullPath) return true;
|
|
747
793
|
if (node.children && isFullPathSelected(node.children, targetFullPath)) {
|
|
748
|
-
return true
|
|
794
|
+
return true;
|
|
749
795
|
}
|
|
750
796
|
}
|
|
751
|
-
return false
|
|
752
|
-
}
|
|
797
|
+
return false;
|
|
798
|
+
};
|
|
753
799
|
|
|
754
|
-
|
|
800
|
+
|
|
801
|
+
const handleGetReport = async (id) => {
|
|
755
802
|
try {
|
|
756
|
-
setIsLoading(true)
|
|
757
|
-
const response = await Services.PostService(
|
|
803
|
+
setIsLoading(true);
|
|
804
|
+
const response = await Services.PostService(
|
|
805
|
+
Endpoints.ReportBuilder.Post.GetReportDetails,
|
|
806
|
+
false,
|
|
807
|
+
{},
|
|
808
|
+
{id: id}
|
|
809
|
+
)
|
|
758
810
|
if (response) {
|
|
759
|
-
setBuilderModel(JSON.parse(response.data.value))
|
|
811
|
+
setBuilderModel(JSON.parse(response.data.value));
|
|
760
812
|
setBuilderMetadata({
|
|
761
813
|
id: response.data.id,
|
|
762
814
|
name: response.data.name,
|
|
@@ -769,22 +821,24 @@ const ReportBuilder = () => {
|
|
|
769
821
|
} catch (error) {
|
|
770
822
|
console.error(error)
|
|
771
823
|
} finally {
|
|
772
|
-
setIsLoading(false)
|
|
824
|
+
setIsLoading(false);
|
|
773
825
|
}
|
|
774
826
|
}
|
|
775
827
|
|
|
776
828
|
|
|
777
829
|
useEffect(() => {
|
|
778
|
-
handleGetModels()
|
|
779
|
-
}, [])
|
|
830
|
+
handleGetModels();
|
|
831
|
+
}, []);
|
|
780
832
|
useEffect(() => {
|
|
781
833
|
if (id != null) {
|
|
782
|
-
|
|
834
|
+
|
|
835
|
+
handleGetReport(id);
|
|
783
836
|
}
|
|
784
|
-
}, [id])
|
|
837
|
+
}, [id]);
|
|
838
|
+
|
|
785
839
|
|
|
786
840
|
return (
|
|
787
|
-
<div dir=
|
|
841
|
+
<div dir="ltr" style={{direction: 'ltr', textAlign: 'left', width: '100%', height: '100%'}}>
|
|
788
842
|
<style>
|
|
789
843
|
{`
|
|
790
844
|
.MuiListItemText-primary,
|
|
@@ -796,8 +850,8 @@ const ReportBuilder = () => {
|
|
|
796
850
|
`}
|
|
797
851
|
</style>
|
|
798
852
|
|
|
799
|
-
<CssBaseline
|
|
800
|
-
<Box sx={{
|
|
853
|
+
<CssBaseline/>
|
|
854
|
+
<Box sx={{display: 'flex', height: '100vh', overflow: 'hidden'}}>
|
|
801
855
|
<Paper
|
|
802
856
|
elevation={3}
|
|
803
857
|
sx={{
|
|
@@ -807,84 +861,86 @@ const ReportBuilder = () => {
|
|
|
807
861
|
borderRight: '1px solid #eee',
|
|
808
862
|
borderRadius: '0px',
|
|
809
863
|
p: 2,
|
|
810
|
-
bgcolor: 'white'
|
|
864
|
+
bgcolor: 'white',
|
|
811
865
|
}}
|
|
812
866
|
>
|
|
813
|
-
<Box display=
|
|
814
|
-
<Box display=
|
|
815
|
-
<Typography variant=
|
|
816
|
-
|
|
867
|
+
<Box display="flex" alignItems="center" gap={1} justifyContent="space-between">
|
|
868
|
+
<Box display="flex" alignItems="center" gap={1}>
|
|
869
|
+
<Typography variant="h6">
|
|
870
|
+
Report Configuration
|
|
871
|
+
</Typography>
|
|
872
|
+
<Chip variant={'filled'} label="Beta" color="warning" size="small"/>
|
|
817
873
|
</Box>
|
|
818
874
|
<Box>
|
|
819
|
-
<IconButton
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
>
|
|
828
|
-
<SaveAsOutlined color={'primary'} />
|
|
875
|
+
<IconButton onClick={() => {
|
|
876
|
+
setBuilderMetadata(preValue => ({
|
|
877
|
+
...preValue,
|
|
878
|
+
isNew: (id == null),
|
|
879
|
+
}))
|
|
880
|
+
handleToggleDialogs('save')
|
|
881
|
+
}}>
|
|
882
|
+
<SaveAsOutlined color={'primary'}/>
|
|
829
883
|
</IconButton>
|
|
830
|
-
{builderMetadata.id !== 0 && (
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
)}
|
|
843
|
-
|
|
844
|
-
<IconButton
|
|
845
|
-
onClick={() => {
|
|
846
|
-
router.push('/reportModule/reportBuilder/reports')
|
|
847
|
-
}}
|
|
848
|
-
>
|
|
849
|
-
<AssessmentOutlined color={'primary'} />
|
|
884
|
+
{builderMetadata.id !== 0 && <IconButton onClick={() => {
|
|
885
|
+
setBuilderMetadata(preValue => ({
|
|
886
|
+
...preValue,
|
|
887
|
+
isNew: false,
|
|
888
|
+
}))
|
|
889
|
+
handleToggleDialogs('save')
|
|
890
|
+
}}><SaveOutlined color={'primary'}/></IconButton>}
|
|
891
|
+
|
|
892
|
+
<IconButton onClick={() => {
|
|
893
|
+
router.push('reportBuilder/reports')
|
|
894
|
+
}}>
|
|
895
|
+
<AssessmentOutlined color={'primary'}/>
|
|
850
896
|
</IconButton>
|
|
851
897
|
</Box>
|
|
898
|
+
|
|
852
899
|
</Box>
|
|
853
900
|
<Autocomplete
|
|
854
|
-
size=
|
|
901
|
+
size="small"
|
|
855
902
|
value={builderModel.reportSource}
|
|
856
903
|
options={models}
|
|
857
904
|
getOptionLabel={option => option.tableName || ''}
|
|
858
905
|
onChange={(e, value) => {
|
|
859
906
|
if (value) {
|
|
860
|
-
setBuilderModel(preValue => ({
|
|
861
|
-
setBreadcrumbs([])
|
|
862
|
-
handleGetFields(value.fullName, value.friendlyName, value.propertyType)
|
|
907
|
+
setBuilderModel(preValue => ({...preValue, reportSource: value}))
|
|
908
|
+
setBreadcrumbs([]);
|
|
909
|
+
handleGetFields(value.fullName, value.friendlyName, value.propertyType);
|
|
863
910
|
}
|
|
864
911
|
}}
|
|
865
912
|
renderOption={(props, option) => (
|
|
866
|
-
<Box sx={{
|
|
913
|
+
<Box sx={{direction: 'rtl'}} component="li" {...props}>
|
|
867
914
|
<Box>
|
|
868
|
-
<Typography sx={{
|
|
869
|
-
|
|
915
|
+
<Typography sx={{color: 'primary.dark'}}>
|
|
916
|
+
{option.tableName}
|
|
917
|
+
</Typography>
|
|
918
|
+
<Typography variant="body2" color="text.secondary">
|
|
870
919
|
{option.friendlyName}
|
|
871
920
|
</Typography>
|
|
872
921
|
</Box>
|
|
873
922
|
</Box>
|
|
874
923
|
)}
|
|
875
|
-
renderInput={params =>
|
|
876
|
-
|
|
924
|
+
renderInput={(params) => (
|
|
925
|
+
<TextField
|
|
926
|
+
{...params}
|
|
927
|
+
variant="outlined"
|
|
928
|
+
label="Select Source Model"
|
|
929
|
+
fullWidth
|
|
930
|
+
/>
|
|
931
|
+
)}
|
|
932
|
+
sx={{mb: 2, mt: 2}}
|
|
877
933
|
/>
|
|
878
934
|
|
|
879
935
|
{breadcrumbs.length > 0 && (
|
|
880
|
-
<Breadcrumbs separator=
|
|
936
|
+
<Breadcrumbs separator="›" sx={{mb: 2}} aria-label="breadcrumb">
|
|
881
937
|
{breadcrumbs.map((crumb, i) => (
|
|
882
938
|
<Link
|
|
883
939
|
key={crumb.fullName}
|
|
884
|
-
underline=
|
|
940
|
+
underline="hover"
|
|
885
941
|
color={i === breadcrumbs.length - 1 ? 'primary.main' : 'inherit'}
|
|
886
942
|
onClick={() => handleGetFields(crumb.fullName, crumb.friendlyName, crumb.propertyType, true)}
|
|
887
|
-
sx={{
|
|
943
|
+
sx={{cursor: 'pointer', fontSize: '0.875rem'}}
|
|
888
944
|
>
|
|
889
945
|
{crumb.friendlyName}
|
|
890
946
|
</Link>
|
|
@@ -892,45 +948,51 @@ const ReportBuilder = () => {
|
|
|
892
948
|
</Breadcrumbs>
|
|
893
949
|
)}
|
|
894
950
|
|
|
895
|
-
<Box sx={{
|
|
896
|
-
<Typography variant=
|
|
951
|
+
<Box sx={{display: 'flex', flexDirection: 'column', gap: 1, mb: 2}}>
|
|
952
|
+
<Typography variant="subtitle1" gutterBottom>
|
|
897
953
|
Available Fields
|
|
898
954
|
</Typography>
|
|
899
955
|
<TextField
|
|
900
|
-
variant=
|
|
901
|
-
size=
|
|
902
|
-
placeholder=
|
|
956
|
+
variant="outlined"
|
|
957
|
+
size="small"
|
|
958
|
+
placeholder="Search fields..."
|
|
903
959
|
fullWidth
|
|
904
960
|
value={searchQuery}
|
|
905
|
-
onChange={e => setSearchQuery(e.target.value)}
|
|
961
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
906
962
|
/>
|
|
907
963
|
</Box>
|
|
908
964
|
|
|
909
|
-
<Box sx={{
|
|
965
|
+
<Box sx={{flexGrow: 1, overflowY: 'auto'}}>
|
|
910
966
|
<List dense disablePadding>
|
|
911
967
|
{modelFields
|
|
912
|
-
.filter(
|
|
913
|
-
field
|
|
914
|
-
|
|
915
|
-
field.path?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
968
|
+
.filter(field =>
|
|
969
|
+
field.friendlyName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
970
|
+
field.path?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
916
971
|
)
|
|
917
972
|
.map((field, index) => {
|
|
973
|
+
|
|
918
974
|
// Now check against your selectedFields by their stored 'path'
|
|
919
|
-
const fullPath = [
|
|
975
|
+
const fullPath = [
|
|
976
|
+
...breadcrumbs.map(b => b.friendlyName),
|
|
977
|
+
field.friendlyName
|
|
978
|
+
].join('.');
|
|
920
979
|
|
|
921
980
|
// now check *that*:
|
|
922
|
-
const isSelected = isFullPathSelected(
|
|
981
|
+
const isSelected = isFullPathSelected(
|
|
982
|
+
builderModel.selectedFields,
|
|
983
|
+
fullPath
|
|
984
|
+
);
|
|
923
985
|
return (
|
|
924
986
|
<ListItem key={index} divider>
|
|
925
987
|
<ListItemText
|
|
926
988
|
primary={
|
|
927
|
-
<Box sx={{
|
|
989
|
+
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
|
|
928
990
|
{field.friendlyName}
|
|
929
991
|
{isSelected && (
|
|
930
992
|
<CheckCircleOutline
|
|
931
|
-
fontSize=
|
|
932
|
-
color=
|
|
933
|
-
titleAccess=
|
|
993
|
+
fontSize="small"
|
|
994
|
+
color="success"
|
|
995
|
+
titleAccess="Field already selected"
|
|
934
996
|
/>
|
|
935
997
|
)}
|
|
936
998
|
</Box>
|
|
@@ -940,7 +1002,7 @@ const ReportBuilder = () => {
|
|
|
940
1002
|
sx: {
|
|
941
1003
|
textAlign: 'left',
|
|
942
1004
|
direction: 'ltr',
|
|
943
|
-
display: 'block'
|
|
1005
|
+
display: 'block',
|
|
944
1006
|
}
|
|
945
1007
|
}}
|
|
946
1008
|
secondaryTypographyProps={{
|
|
@@ -949,7 +1011,7 @@ const ReportBuilder = () => {
|
|
|
949
1011
|
color: 'red',
|
|
950
1012
|
textAlign: 'left',
|
|
951
1013
|
direction: 'ltr',
|
|
952
|
-
display: 'block'
|
|
1014
|
+
display: 'block',
|
|
953
1015
|
}
|
|
954
1016
|
}}
|
|
955
1017
|
/>
|
|
@@ -960,12 +1022,12 @@ const ReportBuilder = () => {
|
|
|
960
1022
|
alignItems: 'center',
|
|
961
1023
|
gap: 1,
|
|
962
1024
|
ml: 1,
|
|
963
|
-
direction: 'ltr'
|
|
1025
|
+
direction: 'ltr',
|
|
964
1026
|
}}
|
|
965
1027
|
>
|
|
966
1028
|
{(field.propertyType === 'Class' || field.propertyType === 'Collection') && (
|
|
967
1029
|
<IconButton
|
|
968
|
-
size=
|
|
1030
|
+
size="small"
|
|
969
1031
|
onClick={() => {
|
|
970
1032
|
handleGetFields(field.modelClassName, field.friendlyName, field.propertyType)
|
|
971
1033
|
setSearchQuery('')
|
|
@@ -974,90 +1036,98 @@ const ReportBuilder = () => {
|
|
|
974
1036
|
color: 'text.secondary',
|
|
975
1037
|
'&:hover': {
|
|
976
1038
|
backgroundColor: 'transparent',
|
|
977
|
-
color: 'success.main'
|
|
978
|
-
}
|
|
1039
|
+
color: 'success.main',
|
|
1040
|
+
},
|
|
979
1041
|
}}
|
|
980
|
-
title=
|
|
1042
|
+
title="Navigate into nested fields"
|
|
981
1043
|
>
|
|
982
|
-
<ArrowForwardSharp fontSize=
|
|
1044
|
+
<ArrowForwardSharp fontSize="inherit"/>
|
|
983
1045
|
</IconButton>
|
|
984
1046
|
)}
|
|
985
1047
|
|
|
1048
|
+
|
|
986
1049
|
{field.propertyType !== 'Class' && field.propertyType !== 'Collection' && (
|
|
987
1050
|
<IconButton
|
|
988
1051
|
onClick={() => {
|
|
989
1052
|
handleAddStaticField(field)
|
|
990
1053
|
}}
|
|
991
|
-
size=
|
|
1054
|
+
size="small"
|
|
992
1055
|
sx={{
|
|
993
1056
|
color: 'text.secondary',
|
|
994
1057
|
'&:hover': {
|
|
995
1058
|
backgroundColor: 'transparent',
|
|
996
|
-
color: 'primary.main'
|
|
997
|
-
}
|
|
1059
|
+
color: 'primary.main',
|
|
1060
|
+
},
|
|
998
1061
|
}}
|
|
999
|
-
title=
|
|
1062
|
+
title="Add field to report"
|
|
1000
1063
|
>
|
|
1001
|
-
<AddBox fontSize=
|
|
1064
|
+
<AddBox fontSize="inherit"/>
|
|
1002
1065
|
</IconButton>
|
|
1003
1066
|
)}
|
|
1067
|
+
|
|
1004
1068
|
</Box>
|
|
1005
1069
|
</ListItem>
|
|
1006
|
-
)
|
|
1070
|
+
);
|
|
1007
1071
|
})}
|
|
1008
1072
|
</List>
|
|
1073
|
+
|
|
1009
1074
|
</Box>
|
|
1010
1075
|
|
|
1011
1076
|
{isLoading && (
|
|
1012
|
-
<Box sx={{
|
|
1013
|
-
<CircularProgress size={24}
|
|
1077
|
+
<Box sx={{display: 'flex', justifyContent: 'center', mt: 2}}>
|
|
1078
|
+
<CircularProgress size={24}/>
|
|
1014
1079
|
</Box>
|
|
1015
1080
|
)}
|
|
1016
1081
|
</Paper>
|
|
1017
1082
|
|
|
1018
|
-
<Box sx={{
|
|
1083
|
+
<Box sx={{flexGrow: 1, p: 3, overflowY: 'auto'}}>
|
|
1084
|
+
|
|
1019
1085
|
<Grid container spacing={2}>
|
|
1020
|
-
<Box sx={{
|
|
1021
|
-
<Box sx={{
|
|
1086
|
+
<Box sx={{flexGrow: 1, p: 3, overflowY: 'auto'}}>
|
|
1087
|
+
<Box sx={{mb: 3}}>
|
|
1022
1088
|
{/* Top row: Title (left) and Preview icon (right) */}
|
|
1023
1089
|
<Box
|
|
1024
1090
|
sx={{
|
|
1025
1091
|
display: 'flex',
|
|
1026
1092
|
justifyContent: 'space-between',
|
|
1027
1093
|
alignItems: 'center',
|
|
1028
|
-
mb: 2
|
|
1094
|
+
mb: 2,
|
|
1029
1095
|
}}
|
|
1030
1096
|
>
|
|
1031
|
-
<Box sx={{
|
|
1032
|
-
<Typography variant=
|
|
1097
|
+
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
|
1098
|
+
<Typography variant="h5" sx={{fontWeight: 600}}>
|
|
1033
1099
|
Builder
|
|
1034
1100
|
</Typography>
|
|
1035
1101
|
|
|
1036
1102
|
<FormControlLabel
|
|
1037
1103
|
control={
|
|
1038
1104
|
<Switch
|
|
1039
|
-
size=
|
|
1105
|
+
size="small"
|
|
1040
1106
|
checked={builderModel.isRaw}
|
|
1041
|
-
onChange={() =>
|
|
1042
|
-
|
|
1107
|
+
onChange={() =>
|
|
1108
|
+
setBuilderModel(prev => ({...prev, isRaw: !prev.isRaw}))
|
|
1109
|
+
}
|
|
1110
|
+
color="primary"
|
|
1043
1111
|
/>
|
|
1044
1112
|
}
|
|
1045
|
-
label=
|
|
1113
|
+
label="Raw"
|
|
1046
1114
|
/>
|
|
1047
1115
|
</Box>
|
|
1048
|
-
<Box sx={{
|
|
1116
|
+
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
|
1049
1117
|
<Tooltip title={'تصفية رئيسية'}>
|
|
1118
|
+
|
|
1050
1119
|
<IconButton
|
|
1051
1120
|
sx={{
|
|
1052
1121
|
color: isPreview ? 'primary.main' : 'text.secondary',
|
|
1053
1122
|
'&:hover': {
|
|
1054
1123
|
backgroundColor: 'transparent',
|
|
1055
|
-
color: 'primary.main'
|
|
1056
|
-
}
|
|
1124
|
+
color: 'primary.main',
|
|
1125
|
+
},
|
|
1057
1126
|
}}
|
|
1058
|
-
onClick={() => handleToggleDialogs('filter')
|
|
1127
|
+
onClick={() => handleToggleDialogs('filter')
|
|
1128
|
+
}
|
|
1059
1129
|
>
|
|
1060
|
-
<FilterAlt
|
|
1130
|
+
<FilterAlt/>
|
|
1061
1131
|
</IconButton>
|
|
1062
1132
|
</Tooltip>
|
|
1063
1133
|
<IconButton
|
|
@@ -1065,14 +1135,15 @@ const ReportBuilder = () => {
|
|
|
1065
1135
|
color: isPreview ? 'primary.main' : 'text.secondary',
|
|
1066
1136
|
'&:hover': {
|
|
1067
1137
|
backgroundColor: 'transparent',
|
|
1068
|
-
color: 'primary.main'
|
|
1069
|
-
}
|
|
1138
|
+
color: 'primary.main',
|
|
1139
|
+
},
|
|
1070
1140
|
}}
|
|
1071
1141
|
onClick={() => setIsPreview(prev => !prev)}
|
|
1072
1142
|
>
|
|
1073
|
-
<PreviewOutlined
|
|
1143
|
+
<PreviewOutlined/>
|
|
1074
1144
|
</IconButton>
|
|
1075
1145
|
</Box>
|
|
1146
|
+
|
|
1076
1147
|
</Box>
|
|
1077
1148
|
|
|
1078
1149
|
{/* Actions row: Just the Add Computed Field button aligned right */}
|
|
@@ -1085,22 +1156,22 @@ const ReportBuilder = () => {
|
|
|
1085
1156
|
borderRadius: '8px',
|
|
1086
1157
|
display: 'flex',
|
|
1087
1158
|
justifyContent: 'space-between',
|
|
1088
|
-
backgroundColor: 'background.paper'
|
|
1159
|
+
backgroundColor: 'background.paper',
|
|
1089
1160
|
}}
|
|
1090
1161
|
>
|
|
1091
1162
|
<Button
|
|
1092
|
-
variant=
|
|
1093
|
-
color=
|
|
1163
|
+
variant="outlined"
|
|
1164
|
+
color="primary"
|
|
1094
1165
|
onClick={() => {
|
|
1095
|
-
console.log(breadcrumbs)
|
|
1166
|
+
console.log(breadcrumbs);
|
|
1096
1167
|
handleToggleDialogs('expression')
|
|
1097
1168
|
}}
|
|
1098
1169
|
>
|
|
1099
1170
|
Add Computed Field
|
|
1100
1171
|
</Button>
|
|
1101
1172
|
<Button
|
|
1102
|
-
variant=
|
|
1103
|
-
color=
|
|
1173
|
+
variant="outlined"
|
|
1174
|
+
color="primary"
|
|
1104
1175
|
onClick={() => {
|
|
1105
1176
|
setSelectedRow(null) // reset edit
|
|
1106
1177
|
handleToggleDialogs('routing')
|
|
@@ -1109,38 +1180,56 @@ const ReportBuilder = () => {
|
|
|
1109
1180
|
Add Routing
|
|
1110
1181
|
</Button>
|
|
1111
1182
|
<Button
|
|
1112
|
-
variant=
|
|
1113
|
-
color=
|
|
1183
|
+
variant="outlined"
|
|
1184
|
+
color="primary"
|
|
1114
1185
|
onClick={() => {
|
|
1115
|
-
console.log(breadcrumbs)
|
|
1186
|
+
console.log(breadcrumbs);
|
|
1116
1187
|
handleToggleDialogs('params')
|
|
1117
1188
|
}}
|
|
1118
1189
|
>
|
|
1119
1190
|
Global Parameters
|
|
1120
1191
|
</Button>
|
|
1121
|
-
<Button
|
|
1192
|
+
<Button
|
|
1193
|
+
variant="outlined"
|
|
1194
|
+
color="secondary"
|
|
1195
|
+
onClick={() => handleToggleDialogs('fixedFilter')}
|
|
1196
|
+
>
|
|
1122
1197
|
Fixed Filter
|
|
1123
1198
|
</Button>
|
|
1199
|
+
<Button
|
|
1200
|
+
variant="outlined"
|
|
1201
|
+
color="info"
|
|
1202
|
+
onClick={() => handleToggleDialogs('columnConfig')}
|
|
1203
|
+
>
|
|
1204
|
+
Column Configurator
|
|
1205
|
+
</Button>
|
|
1206
|
+
<Button
|
|
1207
|
+
variant="outlined"
|
|
1208
|
+
color="warning"
|
|
1209
|
+
onClick={() => handleToggleDialogs('settings')}
|
|
1210
|
+
>
|
|
1211
|
+
Report Settings
|
|
1212
|
+
</Button>
|
|
1124
1213
|
{/* Hidden input for uploading builder model */}
|
|
1125
1214
|
<input
|
|
1126
|
-
type=
|
|
1127
|
-
accept=
|
|
1215
|
+
type="file"
|
|
1216
|
+
accept="application/json,.json"
|
|
1128
1217
|
ref={fileInputRef}
|
|
1129
|
-
style={{
|
|
1218
|
+
style={{display: 'none'}}
|
|
1130
1219
|
onChange={handleFileChange}
|
|
1131
1220
|
/>
|
|
1132
1221
|
<Button
|
|
1133
|
-
variant=
|
|
1134
|
-
color=
|
|
1135
|
-
startIcon={<CloudDownloadOutlined
|
|
1222
|
+
variant="outlined"
|
|
1223
|
+
color="secondary"
|
|
1224
|
+
startIcon={<CloudDownloadOutlined/>}
|
|
1136
1225
|
onClick={handleDownloadModel}
|
|
1137
1226
|
>
|
|
1138
1227
|
Download Model
|
|
1139
1228
|
</Button>
|
|
1140
1229
|
<Button
|
|
1141
|
-
variant=
|
|
1142
|
-
color=
|
|
1143
|
-
startIcon={<CloudUploadOutlined
|
|
1230
|
+
variant="outlined"
|
|
1231
|
+
color="secondary"
|
|
1232
|
+
startIcon={<CloudUploadOutlined/>}
|
|
1144
1233
|
onClick={handleUploadClick}
|
|
1145
1234
|
>
|
|
1146
1235
|
Upload Model
|
|
@@ -1148,35 +1237,31 @@ const ReportBuilder = () => {
|
|
|
1148
1237
|
</Box>
|
|
1149
1238
|
</Box>
|
|
1150
1239
|
|
|
1151
|
-
{!isPreview ?
|
|
1152
|
-
builderModel
|
|
1153
|
-
<Box>
|
|
1154
|
-
<SqlEditor builderModel={builderModel} setBuilderModel={setBuilderModel} />
|
|
1155
|
-
</Box>
|
|
1156
|
-
) : (
|
|
1157
|
-
<Box>
|
|
1158
|
-
<DragDropContext onDragEnd={handleDragEnd}>
|
|
1159
|
-
{renderSelectedFields(builderModel.selectedFields)}
|
|
1160
|
-
</DragDropContext>
|
|
1161
|
-
</Box>
|
|
1162
|
-
)
|
|
1163
|
-
) : (
|
|
1240
|
+
{!isPreview ? builderModel.isRaw ?
|
|
1241
|
+
<Box><SqlEditor builderModel={builderModel} setBuilderModel={setBuilderModel}/></Box> :
|
|
1164
1242
|
<Box>
|
|
1165
|
-
<
|
|
1166
|
-
|
|
1167
|
-
|
|
1243
|
+
<DragDropContext onDragEnd={handleDragEnd}>
|
|
1244
|
+
{renderSelectedFields(builderModel.selectedFields)}
|
|
1245
|
+
</DragDropContext>
|
|
1246
|
+
</Box> : <Box>
|
|
1247
|
+
<SGrid
|
|
1248
|
+
builderModel={builderModel}
|
|
1249
|
+
filter={builderModel.filter}
|
|
1250
|
+
extraCols={columnConfigResult?.extraCols ?? []}
|
|
1251
|
+
columnsConfig={columnConfigResult?.columnsConfig ?? []}
|
|
1252
|
+
updateRef={updateRef}
|
|
1253
|
+
/>
|
|
1254
|
+
</Box>}
|
|
1168
1255
|
</Box>
|
|
1169
1256
|
</Grid>
|
|
1170
|
-
{!isPreview &&
|
|
1171
|
-
<Box
|
|
1172
|
-
|
|
1173
|
-
>
|
|
1174
|
-
<Typography variant='h6' gutterBottom>
|
|
1257
|
+
{!isPreview &&
|
|
1258
|
+
<Box sx={{mt: 4, backgroundColor: '#fff', borderRadius: '12px', p: '10px', border: '1px solid lightgrey'}}>
|
|
1259
|
+
<Typography variant="h6" gutterBottom>
|
|
1175
1260
|
Routing Settings
|
|
1176
1261
|
</Typography>
|
|
1177
1262
|
|
|
1178
1263
|
{builderModel.routingSettings?.length === 0 && (
|
|
1179
|
-
<Typography variant=
|
|
1264
|
+
<Typography variant="body2" color="text.secondary">
|
|
1180
1265
|
No routing settings defined yet.
|
|
1181
1266
|
</Typography>
|
|
1182
1267
|
)}
|
|
@@ -1189,37 +1274,37 @@ const ReportBuilder = () => {
|
|
|
1189
1274
|
display: 'flex',
|
|
1190
1275
|
alignItems: 'center',
|
|
1191
1276
|
justifyContent: 'space-between',
|
|
1192
|
-
width: '100%'
|
|
1277
|
+
width: '100%',
|
|
1193
1278
|
}}
|
|
1194
1279
|
>
|
|
1195
1280
|
{/* Left side: routing info */}
|
|
1196
1281
|
<Box>
|
|
1197
|
-
<Typography variant=
|
|
1198
|
-
{route.fieldName} → {route.pattern}
|
|
1282
|
+
<Typography variant="body1">
|
|
1283
|
+
{route.name ? `${route.name} (${route.fieldName})` : route.fieldName} → {route.type === 'Filter' ? `Page: ${route.pageId}` : route.pattern}
|
|
1199
1284
|
</Typography>
|
|
1200
|
-
<Typography variant=
|
|
1201
|
-
Params: {route.params.join(', ')}
|
|
1285
|
+
<Typography variant="body2" color="text.secondary">
|
|
1286
|
+
Params: {(route.params || []).join(', ')}
|
|
1202
1287
|
</Typography>
|
|
1203
1288
|
</Box>
|
|
1204
1289
|
|
|
1205
1290
|
{/* Right side: action buttons */}
|
|
1206
|
-
<Box sx={{
|
|
1291
|
+
<Box sx={{display: 'flex', gap: 1}}>
|
|
1207
1292
|
<Button
|
|
1208
|
-
size=
|
|
1293
|
+
size="small"
|
|
1209
1294
|
onClick={() => {
|
|
1210
|
-
setSelectedRow({
|
|
1295
|
+
setSelectedRow({...route, index: idx})
|
|
1211
1296
|
handleToggleDialogs('routing')
|
|
1212
1297
|
}}
|
|
1213
1298
|
>
|
|
1214
1299
|
Update
|
|
1215
1300
|
</Button>
|
|
1216
1301
|
<Button
|
|
1217
|
-
size=
|
|
1218
|
-
color=
|
|
1302
|
+
size="small"
|
|
1303
|
+
color="error"
|
|
1219
1304
|
onClick={() =>
|
|
1220
|
-
setBuilderModel(prev => ({
|
|
1305
|
+
setBuilderModel((prev) => ({
|
|
1221
1306
|
...prev,
|
|
1222
|
-
routingSettings: prev.routingSettings.filter((_, i) => i !== idx)
|
|
1307
|
+
routingSettings: prev.routingSettings.filter((_, i) => i !== idx),
|
|
1223
1308
|
}))
|
|
1224
1309
|
}
|
|
1225
1310
|
>
|
|
@@ -1230,9 +1315,40 @@ const ReportBuilder = () => {
|
|
|
1230
1315
|
</ListItem>
|
|
1231
1316
|
))}
|
|
1232
1317
|
</List>
|
|
1233
|
-
|
|
1234
|
-
|
|
1318
|
+
|
|
1319
|
+
</Box>}
|
|
1320
|
+
|
|
1321
|
+
{!isPreview && columnConfigResult &&
|
|
1322
|
+
<Box sx={{mt: 4, backgroundColor: '#fff', borderRadius: '12px', p: '10px', border: '1px solid lightgrey'}}>
|
|
1323
|
+
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2}}>
|
|
1324
|
+
<Typography variant="h6">
|
|
1325
|
+
Column Configurator Result (Testing)
|
|
1326
|
+
</Typography>
|
|
1327
|
+
<Button
|
|
1328
|
+
size="small"
|
|
1329
|
+
color="error"
|
|
1330
|
+
onClick={() => setColumnConfigResult(null)}
|
|
1331
|
+
>
|
|
1332
|
+
Clear
|
|
1333
|
+
</Button>
|
|
1334
|
+
</Box>
|
|
1335
|
+
<Box
|
|
1336
|
+
component="pre"
|
|
1337
|
+
sx={{
|
|
1338
|
+
p: 2,
|
|
1339
|
+
bgcolor: 'grey.900',
|
|
1340
|
+
color: 'grey.100',
|
|
1341
|
+
borderRadius: 1,
|
|
1342
|
+
overflow: 'auto',
|
|
1343
|
+
maxHeight: 400,
|
|
1344
|
+
fontSize: '0.875rem'
|
|
1345
|
+
}}
|
|
1346
|
+
>
|
|
1347
|
+
{JSON.stringify(columnConfigResult, null, 2)}
|
|
1348
|
+
</Box>
|
|
1349
|
+
</Box>}
|
|
1235
1350
|
</Box>
|
|
1351
|
+
|
|
1236
1352
|
</Box>
|
|
1237
1353
|
|
|
1238
1354
|
<Dialog
|
|
@@ -1240,46 +1356,35 @@ const ReportBuilder = () => {
|
|
|
1240
1356
|
onClose={() => {
|
|
1241
1357
|
handleToggleDialogs('expression')
|
|
1242
1358
|
setSelectedRow(null)
|
|
1243
|
-
setSelectedCollection(null)
|
|
1244
|
-
}}
|
|
1245
|
-
maxWidth='lg'
|
|
1246
|
-
fullWidth
|
|
1247
|
-
>
|
|
1359
|
+
setSelectedCollection(null);
|
|
1360
|
+
}} maxWidth="lg" fullWidth>
|
|
1248
1361
|
<ExpressionEditor
|
|
1249
1362
|
oldValue={selectedRow}
|
|
1250
1363
|
addComputedField={handleAddComputedField}
|
|
1251
1364
|
handleUpdateComputedField={handleUpdateComputedField}
|
|
1252
1365
|
selectionParams={builderModel.selectionParams}
|
|
1253
|
-
setSelectionParams={params =>
|
|
1366
|
+
setSelectionParams={(params) =>
|
|
1367
|
+
setBuilderModel(prev => ({...prev, selectionParams: params}))
|
|
1368
|
+
}
|
|
1254
1369
|
/>
|
|
1255
1370
|
</Dialog>
|
|
1256
|
-
<Dialog
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
setSelectedRow(null)
|
|
1261
|
-
}}
|
|
1262
|
-
maxWidth='lg'
|
|
1263
|
-
fullWidth
|
|
1264
|
-
>
|
|
1371
|
+
<Dialog open={openDialogs.params} onClose={() => {
|
|
1372
|
+
handleToggleDialogs('params')
|
|
1373
|
+
setSelectedRow(null)
|
|
1374
|
+
}} maxWidth="lg" fullWidth>
|
|
1265
1375
|
<DynamicValueList
|
|
1266
1376
|
items={builderModel.selectionParams}
|
|
1267
1377
|
onChange={newParams => {
|
|
1268
1378
|
setBuilderModel(prev => ({
|
|
1269
1379
|
...prev,
|
|
1270
1380
|
selectionParams: newParams
|
|
1271
|
-
}))
|
|
1381
|
+
}));
|
|
1272
1382
|
}}
|
|
1273
1383
|
/>
|
|
1274
1384
|
</Dialog>
|
|
1275
|
-
<Dialog
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
handleToggleDialogs('save')
|
|
1279
|
-
}}
|
|
1280
|
-
maxWidth='lg'
|
|
1281
|
-
fullWidth
|
|
1282
|
-
>
|
|
1385
|
+
<Dialog open={openDialogs.save} onClose={() => {
|
|
1386
|
+
handleToggleDialogs('save')
|
|
1387
|
+
}} maxWidth="lg" fullWidth>
|
|
1283
1388
|
<ReportBuilderSaveForm
|
|
1284
1389
|
builderMetadata={builderMetadata}
|
|
1285
1390
|
setBuilderMetadata={setBuilderMetadata}
|
|
@@ -1296,15 +1401,18 @@ const ReportBuilder = () => {
|
|
|
1296
1401
|
scroll='body'
|
|
1297
1402
|
// onClose={() => handleToggleDialogs('CustomFilter')}
|
|
1298
1403
|
>
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1404
|
+
{openDialogs.filter && builderModel.reportSource && (
|
|
1405
|
+
<CustomFilterDialog
|
|
1406
|
+
handleToggleDialogs={handleToggleDialogs}
|
|
1407
|
+
Filter={builderModel.filter?.LocalTfilter || []}
|
|
1408
|
+
customFilterCode={builderModel.filter?.customFilterCode}
|
|
1409
|
+
handleFilterChange={handleFilterChange}
|
|
1410
|
+
className={builderModel?.reportSource?.fullName}
|
|
1411
|
+
LocalFilter={false}
|
|
1412
|
+
selectTFilter={[]}
|
|
1413
|
+
// selectParamsMeta={builderModel.selectionParams}
|
|
1414
|
+
/>
|
|
1415
|
+
)}
|
|
1308
1416
|
</Dialog>
|
|
1309
1417
|
<Dialog
|
|
1310
1418
|
fullWidth
|
|
@@ -1316,7 +1424,7 @@ const ReportBuilder = () => {
|
|
|
1316
1424
|
<FixedFilterDialog
|
|
1317
1425
|
className={builderModel?.reportSource?.fullName}
|
|
1318
1426
|
value={builderModel?.filter?.fixedTFilter || []}
|
|
1319
|
-
onSave={list => {
|
|
1427
|
+
onSave={(list) => {
|
|
1320
1428
|
setBuilderModel(prev => ({
|
|
1321
1429
|
...prev,
|
|
1322
1430
|
filter: {
|
|
@@ -1328,11 +1436,16 @@ const ReportBuilder = () => {
|
|
|
1328
1436
|
onClose={() => handleToggleDialogs('fixedFilter')}
|
|
1329
1437
|
/>
|
|
1330
1438
|
</Dialog>
|
|
1331
|
-
<Dialog
|
|
1439
|
+
<Dialog
|
|
1440
|
+
open={openDialogs.routing}
|
|
1441
|
+
onClose={() => handleToggleDialogs('routing')}
|
|
1442
|
+
maxWidth="sm"
|
|
1443
|
+
fullWidth
|
|
1444
|
+
>
|
|
1332
1445
|
<RoutingSettingDialog
|
|
1333
|
-
value={selectedRow || {
|
|
1334
|
-
onChange={newValue => {
|
|
1335
|
-
setBuilderModel(prev => {
|
|
1446
|
+
value={selectedRow || {fieldName: '', pattern: '', params: []}}
|
|
1447
|
+
onChange={(newValue) => {
|
|
1448
|
+
setBuilderModel((prev) => {
|
|
1336
1449
|
const clone = [...(prev.routingSettings || [])]
|
|
1337
1450
|
if (selectedRow?.index != null) {
|
|
1338
1451
|
// Update existing
|
|
@@ -1341,21 +1454,66 @@ const ReportBuilder = () => {
|
|
|
1341
1454
|
// Add new
|
|
1342
1455
|
clone.push(newValue)
|
|
1343
1456
|
}
|
|
1344
|
-
return {
|
|
1457
|
+
return {...prev, routingSettings: clone}
|
|
1345
1458
|
})
|
|
1346
1459
|
}}
|
|
1347
1460
|
onClose={() => handleToggleDialogs('routing')}
|
|
1348
1461
|
/>
|
|
1349
1462
|
</Dialog>
|
|
1463
|
+
<FormattingSettingsDialog
|
|
1464
|
+
open={openDialogs.formatting}
|
|
1465
|
+
onClose={() => setOpenDialogs(prev => ({...prev, formatting: false}))}
|
|
1466
|
+
field={selectedRow ? {
|
|
1467
|
+
...selectedRow,
|
|
1468
|
+
parentFields: builderModel.selectedFields.filter(f => f.path !== selectedRow.path && !f.children)
|
|
1469
|
+
} : null}
|
|
1470
|
+
onSave={(formatting) => {
|
|
1471
|
+
setBuilderModel(prev => {
|
|
1472
|
+
const updateNode = (node) => {
|
|
1473
|
+
if (node.propertyType === 'Collection') {
|
|
1474
|
+
return {...node, children: (node.children || []).map(updateNode)};
|
|
1475
|
+
}
|
|
1476
|
+
if (node.path === selectedRow.path) {
|
|
1477
|
+
return {...node, formatting};
|
|
1478
|
+
}
|
|
1479
|
+
return node;
|
|
1480
|
+
};
|
|
1481
|
+
return {
|
|
1482
|
+
...prev,
|
|
1483
|
+
selectedFields: prev.selectedFields.map(updateNode)
|
|
1484
|
+
};
|
|
1485
|
+
});
|
|
1486
|
+
}}
|
|
1487
|
+
/>
|
|
1488
|
+
<ColumnConfiguratorDialog
|
|
1489
|
+
open={openDialogs.columnConfig}
|
|
1490
|
+
onClose={() => handleToggleDialogs('columnConfig')}
|
|
1491
|
+
onResult={(result) => {
|
|
1492
|
+
console.log('Column Configuration Result:', result);
|
|
1493
|
+
setColumnConfigResult(result);
|
|
1494
|
+
handleToggleDialogs('columnConfig');
|
|
1495
|
+
}}
|
|
1496
|
+
/>
|
|
1497
|
+
<ReportSettingsDialog
|
|
1498
|
+
open={openDialogs.settings}
|
|
1499
|
+
onClose={() => handleToggleDialogs('settings')}
|
|
1500
|
+
settings={builderModel.settings}
|
|
1501
|
+
onSave={(newSettings) => {
|
|
1502
|
+
setBuilderModel(prev => ({
|
|
1503
|
+
...prev,
|
|
1504
|
+
settings: newSettings
|
|
1505
|
+
}));
|
|
1506
|
+
}}
|
|
1507
|
+
/>
|
|
1350
1508
|
</div>
|
|
1351
1509
|
)
|
|
1352
|
-
}
|
|
1510
|
+
};
|
|
1353
1511
|
|
|
1354
1512
|
ReportBuilder.acl = {
|
|
1355
1513
|
action: 'view',
|
|
1356
|
-
subject: PermissionsSubjects.
|
|
1357
|
-
}
|
|
1514
|
+
subject: PermissionsSubjects.ReportBuilder
|
|
1515
|
+
};
|
|
1358
1516
|
|
|
1359
|
-
ReportBuilder.getLayout = page => <BlankLayout>{page}</BlankLayout
|
|
1517
|
+
ReportBuilder.getLayout = (page) => <BlankLayout>{page}</BlankLayout>;
|
|
1360
1518
|
|
|
1361
|
-
export default ReportBuilder
|
|
1519
|
+
export default ReportBuilder;
|