@mohsinonxrm/pptb-fetchxml-studio 1.0.0
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/CHANGELOG.md +120 -0
- package/README.md +250 -0
- package/eslint.config.js +23 -0
- package/index.html +71 -0
- package/package.json +53 -0
- package/public/vite.svg +1 -0
- package/src/app/AppShell.tsx +1733 -0
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/features/fetchxml/api/dataverseMetadata.ts +192 -0
- package/src/features/fetchxml/api/excelExport.ts +318 -0
- package/src/features/fetchxml/api/formattedValues.ts +107 -0
- package/src/features/fetchxml/api/pptbClient.ts +2635 -0
- package/src/features/fetchxml/model/displaySettings.ts +18 -0
- package/src/features/fetchxml/model/fetchxml.ts +346 -0
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +583 -0
- package/src/features/fetchxml/model/fetchxmlParser.ts +708 -0
- package/src/features/fetchxml/model/layoutxml.ts +372 -0
- package/src/features/fetchxml/model/nodes.ts +255 -0
- package/src/features/fetchxml/model/operators.ts +624 -0
- package/src/features/fetchxml/model/treeUtils.ts +174 -0
- package/src/features/fetchxml/state/builderStore.tsx +1160 -0
- package/src/features/fetchxml/state/cache.ts +393 -0
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +352 -0
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +396 -0
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +612 -0
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +114 -0
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +437 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +214 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +533 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +296 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +106 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +433 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +161 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +194 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +225 -0
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +604 -0
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +800 -0
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +170 -0
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +356 -0
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +372 -0
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +399 -0
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +189 -0
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +171 -0
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +471 -0
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +430 -0
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +983 -0
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +65 -0
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +175 -0
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +788 -0
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +274 -0
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +174 -0
- package/src/main.tsx +11 -0
- package/src/shared/components/AttributePicker.tsx +138 -0
- package/src/shared/components/BetweenInputPicker.tsx +120 -0
- package/src/shared/components/BooleanValuePicker.tsx +165 -0
- package/src/shared/components/DateTimeInputPicker.tsx +170 -0
- package/src/shared/components/DynamicValueList.tsx +128 -0
- package/src/shared/components/ExcelIcon.tsx +30 -0
- package/src/shared/components/MultiValuePicker.tsx +178 -0
- package/src/shared/components/NumericInputPicker.tsx +156 -0
- package/src/shared/components/OptionSetValuePicker.tsx +150 -0
- package/src/shared/components/RelationshipPicker.tsx +268 -0
- package/src/shared/contexts/ThemeContext.tsx +19 -0
- package/src/shared/hooks/useAccessMode.ts +77 -0
- package/src/shared/hooks/useLazyMetadata.ts +132 -0
- package/src/shared/hooks/usePptbContext.ts +170 -0
- package/src/shared/hooks/usePublisherFilter.ts +214 -0
- package/src/shared/hooks/useSolutionFilter.ts +162 -0
- package/src/shared/utils/debug.ts +87 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SaveViewDialog - Dialog for saving views to Dataverse
|
|
3
|
+
* Handles validation, name/description input, and view type selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogSurface,
|
|
10
|
+
DialogBody,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogActions,
|
|
14
|
+
Button,
|
|
15
|
+
Input,
|
|
16
|
+
Textarea,
|
|
17
|
+
Field,
|
|
18
|
+
Radio,
|
|
19
|
+
RadioGroup,
|
|
20
|
+
MessageBar,
|
|
21
|
+
MessageBarTitle,
|
|
22
|
+
MessageBarBody,
|
|
23
|
+
Spinner,
|
|
24
|
+
makeStyles,
|
|
25
|
+
tokens,
|
|
26
|
+
Divider,
|
|
27
|
+
Text,
|
|
28
|
+
} from "@fluentui/react-components";
|
|
29
|
+
import {
|
|
30
|
+
DismissRegular,
|
|
31
|
+
InfoRegular,
|
|
32
|
+
WarningRegular,
|
|
33
|
+
ErrorCircleRegular,
|
|
34
|
+
CheckmarkCircleRegular,
|
|
35
|
+
} from "@fluentui/react-icons";
|
|
36
|
+
import {
|
|
37
|
+
validateFetchXmlExpression,
|
|
38
|
+
checkSavedQueryPrivileges,
|
|
39
|
+
checkUserQueryPrivileges,
|
|
40
|
+
createSavedQuery,
|
|
41
|
+
updateSavedQuery,
|
|
42
|
+
createUserQuery,
|
|
43
|
+
updateUserQuery,
|
|
44
|
+
publishSavedQuery,
|
|
45
|
+
addSolutionComponent,
|
|
46
|
+
getUnmanagedSolutions,
|
|
47
|
+
COMPONENT_TYPE_SAVEDQUERY,
|
|
48
|
+
type ValidatorIssue,
|
|
49
|
+
type Solution,
|
|
50
|
+
} from "../../api/pptbClient";
|
|
51
|
+
import { SolutionPicker } from "./SolutionPicker";
|
|
52
|
+
|
|
53
|
+
const useStyles = makeStyles({
|
|
54
|
+
surface: {
|
|
55
|
+
maxWidth: "500px",
|
|
56
|
+
width: "100%",
|
|
57
|
+
},
|
|
58
|
+
content: {
|
|
59
|
+
display: "flex",
|
|
60
|
+
flexDirection: "column",
|
|
61
|
+
gap: "16px",
|
|
62
|
+
},
|
|
63
|
+
fieldGroup: {
|
|
64
|
+
display: "flex",
|
|
65
|
+
flexDirection: "column",
|
|
66
|
+
gap: "12px",
|
|
67
|
+
},
|
|
68
|
+
validationSection: {
|
|
69
|
+
display: "flex",
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
gap: "8px",
|
|
72
|
+
},
|
|
73
|
+
validationHeader: {
|
|
74
|
+
display: "flex",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
gap: "8px",
|
|
77
|
+
},
|
|
78
|
+
validationList: {
|
|
79
|
+
display: "flex",
|
|
80
|
+
flexDirection: "column",
|
|
81
|
+
gap: "6px",
|
|
82
|
+
maxHeight: "150px",
|
|
83
|
+
overflowY: "auto",
|
|
84
|
+
paddingLeft: "4px",
|
|
85
|
+
},
|
|
86
|
+
validationItem: {
|
|
87
|
+
display: "flex",
|
|
88
|
+
alignItems: "flex-start",
|
|
89
|
+
gap: "8px",
|
|
90
|
+
fontSize: "13px",
|
|
91
|
+
},
|
|
92
|
+
severityIcon: {
|
|
93
|
+
flexShrink: 0,
|
|
94
|
+
marginTop: "2px",
|
|
95
|
+
},
|
|
96
|
+
severityLow: {
|
|
97
|
+
color: tokens.colorPaletteBlueBackground2,
|
|
98
|
+
},
|
|
99
|
+
severityMedium: {
|
|
100
|
+
color: tokens.colorPaletteYellowForeground1,
|
|
101
|
+
},
|
|
102
|
+
severityHigh: {
|
|
103
|
+
color: tokens.colorPaletteRedForeground1,
|
|
104
|
+
},
|
|
105
|
+
severityCritical: {
|
|
106
|
+
color: tokens.colorPaletteRedForeground1,
|
|
107
|
+
},
|
|
108
|
+
solutionInfo: {
|
|
109
|
+
display: "flex",
|
|
110
|
+
flexDirection: "column",
|
|
111
|
+
gap: "4px",
|
|
112
|
+
padding: "8px 12px",
|
|
113
|
+
backgroundColor: tokens.colorNeutralBackground3,
|
|
114
|
+
borderRadius: "4px",
|
|
115
|
+
marginTop: "8px",
|
|
116
|
+
},
|
|
117
|
+
solutionLabel: {
|
|
118
|
+
fontSize: "12px",
|
|
119
|
+
color: tokens.colorNeutralForeground3,
|
|
120
|
+
},
|
|
121
|
+
solutionValue: {
|
|
122
|
+
fontSize: "13px",
|
|
123
|
+
fontWeight: 500,
|
|
124
|
+
},
|
|
125
|
+
overwriteWarning: {
|
|
126
|
+
marginTop: "4px",
|
|
127
|
+
},
|
|
128
|
+
radioDescription: {
|
|
129
|
+
fontSize: "12px",
|
|
130
|
+
color: tokens.colorNeutralForeground3,
|
|
131
|
+
marginLeft: "28px",
|
|
132
|
+
marginTop: "-4px",
|
|
133
|
+
},
|
|
134
|
+
publishNote: {
|
|
135
|
+
fontSize: "12px",
|
|
136
|
+
color: tokens.colorNeutralForeground3,
|
|
137
|
+
fontStyle: "italic",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export type SaveViewDialogMode = "save" | "saveAs";
|
|
142
|
+
|
|
143
|
+
interface SaveViewDialogProps {
|
|
144
|
+
/** Whether the dialog is open */
|
|
145
|
+
open: boolean;
|
|
146
|
+
/** Close callback */
|
|
147
|
+
onClose: () => void;
|
|
148
|
+
/** Save complete callback */
|
|
149
|
+
onSaveComplete: (viewId: string, viewType: "system" | "personal", viewName: string) => void;
|
|
150
|
+
/** Dialog mode: save (overwrite) or saveAs (new) */
|
|
151
|
+
mode: SaveViewDialogMode;
|
|
152
|
+
/** Whether to publish after save (system views only) */
|
|
153
|
+
shouldPublish: boolean;
|
|
154
|
+
/** FetchXML to save */
|
|
155
|
+
fetchXml: string;
|
|
156
|
+
/** LayoutXML configuration */
|
|
157
|
+
layoutXml: string;
|
|
158
|
+
/** Entity logical name */
|
|
159
|
+
entityLogicalName: string;
|
|
160
|
+
/** Entity object type code */
|
|
161
|
+
objectTypeCode: number;
|
|
162
|
+
/** Primary ID attribute */
|
|
163
|
+
primaryIdAttribute: string;
|
|
164
|
+
/** Currently loaded view (for Save mode) */
|
|
165
|
+
loadedView: {
|
|
166
|
+
id: string;
|
|
167
|
+
type: "system" | "personal";
|
|
168
|
+
name: string;
|
|
169
|
+
} | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface ValidationState {
|
|
173
|
+
status: "idle" | "validating" | "valid" | "warning" | "error";
|
|
174
|
+
messages: ValidatorIssue[];
|
|
175
|
+
errorMessage?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function SaveViewDialog({
|
|
179
|
+
open,
|
|
180
|
+
onClose,
|
|
181
|
+
onSaveComplete,
|
|
182
|
+
mode,
|
|
183
|
+
shouldPublish,
|
|
184
|
+
fetchXml,
|
|
185
|
+
layoutXml,
|
|
186
|
+
entityLogicalName,
|
|
187
|
+
objectTypeCode: _objectTypeCode, // Reserved for future use
|
|
188
|
+
primaryIdAttribute: _primaryIdAttribute, // Reserved for future use
|
|
189
|
+
loadedView,
|
|
190
|
+
}: SaveViewDialogProps) {
|
|
191
|
+
const styles = useStyles();
|
|
192
|
+
|
|
193
|
+
// Form state
|
|
194
|
+
const [name, setName] = useState("");
|
|
195
|
+
const [description, setDescription] = useState("");
|
|
196
|
+
const [viewType, setViewType] = useState<"system" | "personal">("personal");
|
|
197
|
+
|
|
198
|
+
// Solution state (for system views)
|
|
199
|
+
const [selectedSolution, setSelectedSolution] = useState<Solution | null>(null);
|
|
200
|
+
const [unmanagedSolutions, setUnmanagedSolutions] = useState<Solution[]>([]);
|
|
201
|
+
const [solutionsLoading, setSolutionsLoading] = useState(false);
|
|
202
|
+
|
|
203
|
+
// Privileges state - systemView privileges from checkSavedQueryPrivileges
|
|
204
|
+
const [systemPrivileges, setSystemPrivileges] = useState<{
|
|
205
|
+
canWrite: boolean;
|
|
206
|
+
canPublish: boolean;
|
|
207
|
+
} | null>(null);
|
|
208
|
+
// Personal view privileges from checkUserQueryPrivileges
|
|
209
|
+
const [personalPrivileges, setPersonalPrivileges] = useState<{
|
|
210
|
+
canWrite: boolean;
|
|
211
|
+
} | null>(null);
|
|
212
|
+
|
|
213
|
+
// Validation state
|
|
214
|
+
const [validation, setValidation] = useState<ValidationState>({
|
|
215
|
+
status: "idle",
|
|
216
|
+
messages: [],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Save state
|
|
220
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
221
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
222
|
+
|
|
223
|
+
// Check if this is a "Save" to existing view
|
|
224
|
+
const isOverwriteMode = mode === "save" && loadedView !== null;
|
|
225
|
+
|
|
226
|
+
// Initialize form when dialog opens
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (open) {
|
|
229
|
+
if (isOverwriteMode && loadedView) {
|
|
230
|
+
setName(loadedView.name);
|
|
231
|
+
setViewType(loadedView.type);
|
|
232
|
+
} else {
|
|
233
|
+
setName("");
|
|
234
|
+
setViewType("personal");
|
|
235
|
+
}
|
|
236
|
+
setDescription("");
|
|
237
|
+
setSaveError(null);
|
|
238
|
+
setValidation({ status: "idle", messages: [] });
|
|
239
|
+
}
|
|
240
|
+
}, [open, isOverwriteMode, loadedView]);
|
|
241
|
+
|
|
242
|
+
// Check privileges when dialog opens
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (open) {
|
|
245
|
+
// Check both system and personal view privileges
|
|
246
|
+
checkSavedQueryPrivileges().then(setSystemPrivileges);
|
|
247
|
+
checkUserQueryPrivileges().then(setPersonalPrivileges);
|
|
248
|
+
}
|
|
249
|
+
}, [open]);
|
|
250
|
+
|
|
251
|
+
// Load unmanaged solutions when system view is selected
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (!open || viewType !== "system") {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function loadSolutionInfo() {
|
|
258
|
+
setSolutionsLoading(true);
|
|
259
|
+
try {
|
|
260
|
+
// Always load unmanaged solutions for the picker
|
|
261
|
+
const solutions = await getUnmanagedSolutions();
|
|
262
|
+
setUnmanagedSolutions(solutions);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error("Failed to load solution info:", error);
|
|
265
|
+
setUnmanagedSolutions([]);
|
|
266
|
+
} finally {
|
|
267
|
+
setSolutionsLoading(false);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
loadSolutionInfo();
|
|
272
|
+
}, [open, viewType]);
|
|
273
|
+
|
|
274
|
+
// Auto-validate when dialog opens
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (!open || !fetchXml) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runValidation() {
|
|
281
|
+
setValidation({ status: "validating", messages: [] });
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const result = await validateFetchXmlExpression(fetchXml);
|
|
285
|
+
const messages = result.ValidationResults?.Messages || [];
|
|
286
|
+
|
|
287
|
+
if (messages.length === 0) {
|
|
288
|
+
setValidation({ status: "valid", messages: [] });
|
|
289
|
+
} else {
|
|
290
|
+
// Check for critical/high severity
|
|
291
|
+
const hasCritical = messages.some((m) => m.Severity >= 2);
|
|
292
|
+
setValidation({
|
|
293
|
+
status: hasCritical ? "warning" : "warning",
|
|
294
|
+
messages,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// Validation API call failed - this doesn't mean the FetchXML is invalid
|
|
299
|
+
// Just skip validation and allow the user to proceed
|
|
300
|
+
console.warn("FetchXML validation unavailable:", error);
|
|
301
|
+
setValidation({
|
|
302
|
+
status: "warning",
|
|
303
|
+
messages: [],
|
|
304
|
+
errorMessage: "Validation unavailable - FetchXML will be validated when saved",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
runValidation();
|
|
310
|
+
}, [open, fetchXml]);
|
|
311
|
+
|
|
312
|
+
// Get severity icon
|
|
313
|
+
const getSeverityIcon = useCallback(
|
|
314
|
+
(severity: number) => {
|
|
315
|
+
switch (severity) {
|
|
316
|
+
case 0:
|
|
317
|
+
return <InfoRegular className={`${styles.severityIcon} ${styles.severityLow}`} />;
|
|
318
|
+
case 1:
|
|
319
|
+
return <WarningRegular className={`${styles.severityIcon} ${styles.severityMedium}`} />;
|
|
320
|
+
case 2:
|
|
321
|
+
case 3:
|
|
322
|
+
return <ErrorCircleRegular className={`${styles.severityIcon} ${styles.severityHigh}`} />;
|
|
323
|
+
default:
|
|
324
|
+
return <InfoRegular className={`${styles.severityIcon} ${styles.severityLow}`} />;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
[styles]
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Can save check
|
|
331
|
+
const canSave = useMemo(() => {
|
|
332
|
+
if (!name.trim()) return false;
|
|
333
|
+
if (isSaving) return false;
|
|
334
|
+
if (viewType === "system") {
|
|
335
|
+
// System views require canWrite privilege (covers both create and update)
|
|
336
|
+
if (!systemPrivileges?.canWrite) return false;
|
|
337
|
+
} else {
|
|
338
|
+
// Personal views require canWrite privilege
|
|
339
|
+
if (!personalPrivileges?.canWrite) return false;
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
}, [name, isSaving, viewType, systemPrivileges, personalPrivileges]);
|
|
343
|
+
|
|
344
|
+
// Handle save
|
|
345
|
+
const handleSave = useCallback(async () => {
|
|
346
|
+
if (!canSave) return;
|
|
347
|
+
|
|
348
|
+
setIsSaving(true);
|
|
349
|
+
setSaveError(null);
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
let viewId: string;
|
|
353
|
+
|
|
354
|
+
if (viewType === "personal") {
|
|
355
|
+
// Personal view
|
|
356
|
+
if (isOverwriteMode && loadedView) {
|
|
357
|
+
await updateUserQuery(loadedView.id, {
|
|
358
|
+
name: name.trim(),
|
|
359
|
+
fetchxml: fetchXml,
|
|
360
|
+
layoutxml: layoutXml,
|
|
361
|
+
description: description.trim() || undefined,
|
|
362
|
+
});
|
|
363
|
+
viewId = loadedView.id;
|
|
364
|
+
} else {
|
|
365
|
+
viewId = await createUserQuery({
|
|
366
|
+
name: name.trim(),
|
|
367
|
+
fetchxml: fetchXml,
|
|
368
|
+
layoutxml: layoutXml,
|
|
369
|
+
returnedtypecode: entityLogicalName,
|
|
370
|
+
description: description.trim() || undefined,
|
|
371
|
+
querytype: 0,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
// System view
|
|
376
|
+
if (isOverwriteMode && loadedView) {
|
|
377
|
+
await updateSavedQuery(loadedView.id, {
|
|
378
|
+
name: name.trim(),
|
|
379
|
+
fetchxml: fetchXml,
|
|
380
|
+
layoutxml: layoutXml,
|
|
381
|
+
description: description.trim() || undefined,
|
|
382
|
+
});
|
|
383
|
+
viewId = loadedView.id;
|
|
384
|
+
} else {
|
|
385
|
+
viewId = await createSavedQuery({
|
|
386
|
+
name: name.trim(),
|
|
387
|
+
fetchxml: fetchXml,
|
|
388
|
+
layoutxml: layoutXml,
|
|
389
|
+
returnedtypecode: entityLogicalName,
|
|
390
|
+
description: description.trim() || undefined,
|
|
391
|
+
querytype: 0,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Add to solution if selected
|
|
395
|
+
if (selectedSolution) {
|
|
396
|
+
await addSolutionComponent(
|
|
397
|
+
viewId,
|
|
398
|
+
COMPONENT_TYPE_SAVEDQUERY,
|
|
399
|
+
selectedSolution.uniquename,
|
|
400
|
+
false
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Publish if requested
|
|
406
|
+
if (shouldPublish) {
|
|
407
|
+
await publishSavedQuery(viewId);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
onSaveComplete(viewId, viewType, name.trim());
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error("Failed to save view:", error);
|
|
414
|
+
setSaveError(error instanceof Error ? error.message : "Failed to save view");
|
|
415
|
+
} finally {
|
|
416
|
+
setIsSaving(false);
|
|
417
|
+
}
|
|
418
|
+
}, [
|
|
419
|
+
canSave,
|
|
420
|
+
viewType,
|
|
421
|
+
isOverwriteMode,
|
|
422
|
+
loadedView,
|
|
423
|
+
name,
|
|
424
|
+
fetchXml,
|
|
425
|
+
layoutXml,
|
|
426
|
+
description,
|
|
427
|
+
entityLogicalName,
|
|
428
|
+
selectedSolution,
|
|
429
|
+
shouldPublish,
|
|
430
|
+
onSaveComplete,
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
// Dialog title
|
|
434
|
+
const dialogTitle = useMemo(() => {
|
|
435
|
+
if (isOverwriteMode) {
|
|
436
|
+
return shouldPublish ? "Save and Publish View" : "Save View";
|
|
437
|
+
}
|
|
438
|
+
return "Save As New View";
|
|
439
|
+
}, [isOverwriteMode, shouldPublish]);
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<Dialog open={open} onOpenChange={(_, data) => !data.open && onClose()}>
|
|
443
|
+
<DialogSurface className={styles.surface}>
|
|
444
|
+
<DialogBody>
|
|
445
|
+
<DialogTitle
|
|
446
|
+
action={
|
|
447
|
+
<Button
|
|
448
|
+
appearance="subtle"
|
|
449
|
+
aria-label="Close"
|
|
450
|
+
icon={<DismissRegular />}
|
|
451
|
+
onClick={onClose}
|
|
452
|
+
/>
|
|
453
|
+
}
|
|
454
|
+
>
|
|
455
|
+
{dialogTitle}
|
|
456
|
+
</DialogTitle>
|
|
457
|
+
<DialogContent className={styles.content}>
|
|
458
|
+
{/* Validation Section */}
|
|
459
|
+
<div className={styles.validationSection}>
|
|
460
|
+
<div className={styles.validationHeader}>
|
|
461
|
+
{validation.status === "validating" && (
|
|
462
|
+
<>
|
|
463
|
+
<Spinner size="tiny" />
|
|
464
|
+
<Text>Validating FetchXML...</Text>
|
|
465
|
+
</>
|
|
466
|
+
)}
|
|
467
|
+
{validation.status === "valid" && (
|
|
468
|
+
<>
|
|
469
|
+
<CheckmarkCircleRegular
|
|
470
|
+
style={{ color: tokens.colorPaletteGreenForeground1 }}
|
|
471
|
+
/>
|
|
472
|
+
<Text>FetchXML is valid</Text>
|
|
473
|
+
</>
|
|
474
|
+
)}
|
|
475
|
+
{validation.status === "warning" && (
|
|
476
|
+
<>
|
|
477
|
+
<WarningRegular style={{ color: tokens.colorPaletteYellowForeground1 }} />
|
|
478
|
+
<Text>
|
|
479
|
+
{validation.messages.length} validation{" "}
|
|
480
|
+
{validation.messages.length === 1 ? "message" : "messages"}
|
|
481
|
+
</Text>
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
{validation.status === "error" && (
|
|
485
|
+
<>
|
|
486
|
+
<ErrorCircleRegular style={{ color: tokens.colorPaletteRedForeground1 }} />
|
|
487
|
+
<Text>{validation.errorMessage}</Text>
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
{validation.messages.length > 0 && (
|
|
493
|
+
<div className={styles.validationList}>
|
|
494
|
+
{validation.messages.map((msg, idx) => (
|
|
495
|
+
<div key={idx} className={styles.validationItem}>
|
|
496
|
+
{getSeverityIcon(msg.Severity)}
|
|
497
|
+
<span>{msg.LocalizedMessageText}</span>
|
|
498
|
+
</div>
|
|
499
|
+
))}
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
<Divider />
|
|
505
|
+
|
|
506
|
+
{/* Form Fields */}
|
|
507
|
+
<div className={styles.fieldGroup}>
|
|
508
|
+
<Field label="Name" required>
|
|
509
|
+
<Input
|
|
510
|
+
value={name}
|
|
511
|
+
onChange={(_, data) => setName(data.value)}
|
|
512
|
+
placeholder="Enter view name"
|
|
513
|
+
disabled={isSaving}
|
|
514
|
+
/>
|
|
515
|
+
</Field>
|
|
516
|
+
<Field label="Description">
|
|
517
|
+
<Textarea
|
|
518
|
+
value={description}
|
|
519
|
+
onChange={(_, data) => setDescription(data.value)}
|
|
520
|
+
placeholder="Optional description"
|
|
521
|
+
disabled={isSaving}
|
|
522
|
+
rows={2}
|
|
523
|
+
/>
|
|
524
|
+
</Field>
|
|
525
|
+
{/* View Type Selection - only for Save As */}
|
|
526
|
+
{!isOverwriteMode && (
|
|
527
|
+
<Field label="View Type">
|
|
528
|
+
<RadioGroup
|
|
529
|
+
value={viewType}
|
|
530
|
+
onChange={(_, data) => setViewType(data.value as "system" | "personal")}
|
|
531
|
+
disabled={isSaving}
|
|
532
|
+
>
|
|
533
|
+
<Radio
|
|
534
|
+
value="personal"
|
|
535
|
+
label="Personal View"
|
|
536
|
+
disabled={!personalPrivileges?.canWrite}
|
|
537
|
+
/>
|
|
538
|
+
<div className={styles.radioDescription}>
|
|
539
|
+
{personalPrivileges?.canWrite
|
|
540
|
+
? "Only visible to you"
|
|
541
|
+
: "You don't have permission to create personal views"}
|
|
542
|
+
</div>
|
|
543
|
+
<Radio
|
|
544
|
+
value="system"
|
|
545
|
+
label="System View"
|
|
546
|
+
disabled={!systemPrivileges?.canWrite}
|
|
547
|
+
/>
|
|
548
|
+
<div className={styles.radioDescription}>
|
|
549
|
+
{systemPrivileges?.canWrite
|
|
550
|
+
? "Visible to all users with access to this entity"
|
|
551
|
+
: "You don't have permission to create system views"}
|
|
552
|
+
</div>
|
|
553
|
+
</RadioGroup>
|
|
554
|
+
</Field>
|
|
555
|
+
)}
|
|
556
|
+
{/* Solution Picker for System Views */}
|
|
557
|
+
{viewType === "system" && !isOverwriteMode && (
|
|
558
|
+
<>
|
|
559
|
+
{solutionsLoading ? (
|
|
560
|
+
<div className={styles.solutionInfo}>
|
|
561
|
+
<Spinner size="tiny" label="Loading solutions..." />
|
|
562
|
+
</div>
|
|
563
|
+
) : (
|
|
564
|
+
<SolutionPicker
|
|
565
|
+
solutions={unmanagedSolutions}
|
|
566
|
+
selectedSolution={selectedSolution}
|
|
567
|
+
onSolutionChange={setSelectedSolution}
|
|
568
|
+
disabled={isSaving}
|
|
569
|
+
/>
|
|
570
|
+
)}
|
|
571
|
+
</>
|
|
572
|
+
)}{" "}
|
|
573
|
+
{/* Overwrite Warning */}
|
|
574
|
+
{isOverwriteMode && loadedView && (
|
|
575
|
+
<MessageBar intent="warning" className={styles.overwriteWarning}>
|
|
576
|
+
<MessageBarBody>
|
|
577
|
+
<MessageBarTitle>Overwrite Warning</MessageBarTitle>
|
|
578
|
+
This will overwrite the existing view "{loadedView.name}".
|
|
579
|
+
</MessageBarBody>
|
|
580
|
+
</MessageBar>
|
|
581
|
+
)}
|
|
582
|
+
{/* Publish Note */}
|
|
583
|
+
{shouldPublish && viewType === "system" && (
|
|
584
|
+
<Text className={styles.publishNote}>
|
|
585
|
+
The view will be published after saving, making it immediately available to users.
|
|
586
|
+
</Text>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* Save Error */}
|
|
591
|
+
{saveError && (
|
|
592
|
+
<MessageBar intent="error">
|
|
593
|
+
<MessageBarBody>
|
|
594
|
+
<MessageBarTitle>Save Failed</MessageBarTitle>
|
|
595
|
+
{saveError}
|
|
596
|
+
</MessageBarBody>
|
|
597
|
+
</MessageBar>
|
|
598
|
+
)}
|
|
599
|
+
</DialogContent>
|
|
600
|
+
<DialogActions>
|
|
601
|
+
<Button appearance="secondary" onClick={onClose} disabled={isSaving}>
|
|
602
|
+
Cancel
|
|
603
|
+
</Button>
|
|
604
|
+
<Button appearance="primary" onClick={handleSave} disabled={!canSave}>
|
|
605
|
+
{isSaving ? <Spinner size="tiny" /> : shouldPublish ? "Save and Publish" : "Save"}
|
|
606
|
+
</Button>
|
|
607
|
+
</DialogActions>
|
|
608
|
+
</DialogBody>
|
|
609
|
+
</DialogSurface>
|
|
610
|
+
</Dialog>
|
|
611
|
+
);
|
|
612
|
+
}
|