@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.2
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/LICENSE +661 -0
- package/README.md +1 -1
- package/dist/assets/index-DD9fzkbz.js +232 -0
- package/dist/index.html +70 -0
- package/index.html +0 -1
- package/npm-shrinkwrap.json +6329 -0
- package/package.json +95 -51
- package/CHANGELOG.md +0 -120
- package/eslint.config.js +0 -23
- package/public/vite.svg +0 -1
- package/src/app/AppShell.tsx +0 -1733
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
- package/src/features/fetchxml/api/excelExport.ts +0 -318
- package/src/features/fetchxml/api/formattedValues.ts +0 -107
- package/src/features/fetchxml/api/pptbClient.ts +0 -2635
- package/src/features/fetchxml/model/displaySettings.ts +0 -18
- package/src/features/fetchxml/model/fetchxml.ts +0 -346
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
- package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
- package/src/features/fetchxml/model/layoutxml.ts +0 -372
- package/src/features/fetchxml/model/nodes.ts +0 -255
- package/src/features/fetchxml/model/operators.ts +0 -624
- package/src/features/fetchxml/model/treeUtils.ts +0 -174
- package/src/features/fetchxml/state/builderStore.tsx +0 -1160
- package/src/features/fetchxml/state/cache.ts +0 -393
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
- package/src/main.tsx +0 -11
- package/src/shared/components/AttributePicker.tsx +0 -138
- package/src/shared/components/BetweenInputPicker.tsx +0 -120
- package/src/shared/components/BooleanValuePicker.tsx +0 -165
- package/src/shared/components/DateTimeInputPicker.tsx +0 -170
- package/src/shared/components/DynamicValueList.tsx +0 -128
- package/src/shared/components/ExcelIcon.tsx +0 -30
- package/src/shared/components/MultiValuePicker.tsx +0 -178
- package/src/shared/components/NumericInputPicker.tsx +0 -156
- package/src/shared/components/OptionSetValuePicker.tsx +0 -150
- package/src/shared/components/RelationshipPicker.tsx +0 -268
- package/src/shared/contexts/ThemeContext.tsx +0 -19
- package/src/shared/hooks/useAccessMode.ts +0 -77
- package/src/shared/hooks/useLazyMetadata.ts +0 -132
- package/src/shared/hooks/usePptbContext.ts +0 -170
- package/src/shared/hooks/usePublisherFilter.ts +0 -214
- package/src/shared/hooks/useSolutionFilter.ts +0 -162
- package/src/shared/utils/debug.ts +0 -87
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -7
|
@@ -1,788 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity selector with Publisher/Solution filtering support
|
|
3
|
-
* Adapts UI based on user privileges (Full Filter / Solutions-Only / Publishers-Only / Metadata-Only / No Access modes)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
7
|
-
import {
|
|
8
|
-
Combobox,
|
|
9
|
-
Option,
|
|
10
|
-
OptionGroup,
|
|
11
|
-
makeStyles,
|
|
12
|
-
Button,
|
|
13
|
-
Spinner,
|
|
14
|
-
useId,
|
|
15
|
-
tokens,
|
|
16
|
-
Tooltip,
|
|
17
|
-
useComboboxFilter,
|
|
18
|
-
Dialog,
|
|
19
|
-
DialogSurface,
|
|
20
|
-
DialogBody,
|
|
21
|
-
DialogTitle,
|
|
22
|
-
DialogContent,
|
|
23
|
-
DialogActions,
|
|
24
|
-
type ComboboxProps,
|
|
25
|
-
} from "@fluentui/react-components";
|
|
26
|
-
import { Add20Regular, LockClosed20Regular } from "@fluentui/react-icons";
|
|
27
|
-
import { useAccessMode } from "../../../../shared/hooks/useAccessMode";
|
|
28
|
-
import { usePublisherFilter } from "../../../../shared/hooks/usePublisherFilter";
|
|
29
|
-
import { useSolutionFilter } from "../../../../shared/hooks/useSolutionFilter";
|
|
30
|
-
import { useLazyMetadata } from "../../../../shared/hooks/useLazyMetadata";
|
|
31
|
-
import { LoadViewPicker } from "./LoadViewPicker";
|
|
32
|
-
import type { EntityMetadata, LoadedViewInfo } from "../../api/pptbClient";
|
|
33
|
-
|
|
34
|
-
const useStyles = makeStyles({
|
|
35
|
-
container: {
|
|
36
|
-
display: "flex",
|
|
37
|
-
flexDirection: "column",
|
|
38
|
-
gap: "8px",
|
|
39
|
-
padding: "8px 12px",
|
|
40
|
-
borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
41
|
-
containerType: "inline-size",
|
|
42
|
-
},
|
|
43
|
-
filtersRow: {
|
|
44
|
-
display: "flex",
|
|
45
|
-
gap: "12px",
|
|
46
|
-
alignItems: "flex-end",
|
|
47
|
-
flexWrap: "wrap",
|
|
48
|
-
// Stack vertically when container is narrow (< 520px)
|
|
49
|
-
"@container (max-width: 519px)": {
|
|
50
|
-
flexDirection: "column",
|
|
51
|
-
alignItems: "stretch",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
entityRow: {
|
|
55
|
-
display: "flex",
|
|
56
|
-
alignItems: "flex-end",
|
|
57
|
-
gap: "12px",
|
|
58
|
-
},
|
|
59
|
-
field: {
|
|
60
|
-
display: "flex",
|
|
61
|
-
flexDirection: "column",
|
|
62
|
-
gap: "4px",
|
|
63
|
-
flex: 1,
|
|
64
|
-
minWidth: 0,
|
|
65
|
-
},
|
|
66
|
-
label: {
|
|
67
|
-
fontSize: "14px",
|
|
68
|
-
fontWeight: 600,
|
|
69
|
-
color: tokens.colorNeutralForeground2,
|
|
70
|
-
display: "flex",
|
|
71
|
-
alignItems: "center",
|
|
72
|
-
gap: "6px",
|
|
73
|
-
},
|
|
74
|
-
disabledLabel: {
|
|
75
|
-
color: tokens.colorNeutralForegroundDisabled,
|
|
76
|
-
},
|
|
77
|
-
loadingContainer: {
|
|
78
|
-
display: "flex",
|
|
79
|
-
alignItems: "center",
|
|
80
|
-
justifyContent: "center",
|
|
81
|
-
padding: "24px",
|
|
82
|
-
},
|
|
83
|
-
errorText: {
|
|
84
|
-
color: tokens.colorPaletteRedForeground1,
|
|
85
|
-
fontSize: "12px",
|
|
86
|
-
marginTop: "4px",
|
|
87
|
-
},
|
|
88
|
-
noAccessMessage: {
|
|
89
|
-
padding: "16px",
|
|
90
|
-
color: tokens.colorNeutralForeground3,
|
|
91
|
-
fontSize: "14px",
|
|
92
|
-
textAlign: "center",
|
|
93
|
-
},
|
|
94
|
-
viewPickerRow: {
|
|
95
|
-
display: "flex",
|
|
96
|
-
gap: "12px",
|
|
97
|
-
alignItems: "flex-end",
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
interface EntitySelectorProps {
|
|
102
|
-
selectedEntity: string | null;
|
|
103
|
-
onEntityChange: (entityLogicalName: string) => void;
|
|
104
|
-
onNewQuery: () => void;
|
|
105
|
-
/** Callback when a saved view should be loaded - provides full view info for execution optimization */
|
|
106
|
-
onViewLoad?: (viewInfo: LoadedViewInfo) => void;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function EntitySelector({
|
|
110
|
-
selectedEntity,
|
|
111
|
-
onEntityChange,
|
|
112
|
-
onNewQuery,
|
|
113
|
-
onViewLoad,
|
|
114
|
-
}: EntitySelectorProps) {
|
|
115
|
-
const styles = useStyles();
|
|
116
|
-
const publisherComboId = useId("publisher-combobox");
|
|
117
|
-
const solutionComboId = useId("solution-combobox");
|
|
118
|
-
const entityComboId = useId("entity-combobox");
|
|
119
|
-
|
|
120
|
-
// Access mode detection
|
|
121
|
-
const {
|
|
122
|
-
loading: accessLoading,
|
|
123
|
-
fullFilterMode,
|
|
124
|
-
solutionsOnlyMode,
|
|
125
|
-
publishersOnlyMode,
|
|
126
|
-
metadataOnlyMode,
|
|
127
|
-
noAccessMode,
|
|
128
|
-
} = useAccessMode();
|
|
129
|
-
|
|
130
|
-
// Full filter mode (Publisher → Solution → Entity)
|
|
131
|
-
const publisherFilter = usePublisherFilter();
|
|
132
|
-
|
|
133
|
-
// Solutions-only mode (Solution → Entity)
|
|
134
|
-
const solutionFilter = useSolutionFilter();
|
|
135
|
-
|
|
136
|
-
// Metadata-only mode (all AF-valid entities) or Publishers-only mode
|
|
137
|
-
const { loadEntities } = useLazyMetadata();
|
|
138
|
-
const [allEntities, setAllEntities] = useState<EntityMetadata[]>([]);
|
|
139
|
-
const [allEntitiesLoading, setAllEntitiesLoading] = useState(false);
|
|
140
|
-
|
|
141
|
-
// Load all entities for metadata-only or publishers-only mode
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
if (!metadataOnlyMode && !publishersOnlyMode) return;
|
|
144
|
-
|
|
145
|
-
setAllEntitiesLoading(true);
|
|
146
|
-
loadEntities(true)
|
|
147
|
-
.then((entities) => setAllEntities(entities))
|
|
148
|
-
.catch((err) => console.error("Failed to load entities:", err))
|
|
149
|
-
.finally(() => setAllEntitiesLoading(false));
|
|
150
|
-
}, [metadataOnlyMode, publishersOnlyMode, loadEntities]);
|
|
151
|
-
|
|
152
|
-
// Determine available entities based on mode
|
|
153
|
-
const availableEntities = useMemo(() => {
|
|
154
|
-
let entities: EntityMetadata[] = [];
|
|
155
|
-
if (fullFilterMode) {
|
|
156
|
-
entities = publisherFilter.entities;
|
|
157
|
-
} else if (solutionsOnlyMode) {
|
|
158
|
-
entities = solutionFilter.entities;
|
|
159
|
-
} else if (metadataOnlyMode || publishersOnlyMode) {
|
|
160
|
-
entities = allEntities;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
console.log("[EntitySelector] Available Entities Updated:", {
|
|
164
|
-
mode: fullFilterMode ? "Full Filter" : solutionsOnlyMode ? "Solutions Only" : "Other",
|
|
165
|
-
selectedSolutionIds: fullFilterMode
|
|
166
|
-
? publisherFilter.selectedSolutionIds
|
|
167
|
-
: solutionFilter.selectedSolutionIds,
|
|
168
|
-
entityCount: entities.length,
|
|
169
|
-
entityNames: entities.map((e) => e.LogicalName).slice(0, 10),
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return entities;
|
|
173
|
-
}, [
|
|
174
|
-
fullFilterMode,
|
|
175
|
-
solutionsOnlyMode,
|
|
176
|
-
metadataOnlyMode,
|
|
177
|
-
publishersOnlyMode,
|
|
178
|
-
publisherFilter.entities,
|
|
179
|
-
solutionFilter.entities,
|
|
180
|
-
allEntities,
|
|
181
|
-
publisherFilter.selectedSolutionIds,
|
|
182
|
-
solutionFilter.selectedSolutionIds,
|
|
183
|
-
]);
|
|
184
|
-
|
|
185
|
-
// Get full entity metadata for the selected entity (needed for LoadViewPicker)
|
|
186
|
-
const selectedEntityMetadata = useMemo(() => {
|
|
187
|
-
if (!selectedEntity) return null;
|
|
188
|
-
return availableEntities.find((e) => e.LogicalName === selectedEntity) || null;
|
|
189
|
-
}, [selectedEntity, availableEntities]);
|
|
190
|
-
|
|
191
|
-
// Handle view selection from LoadViewPicker
|
|
192
|
-
const handleViewSelect = useCallback(
|
|
193
|
-
(viewInfo: LoadedViewInfo) => {
|
|
194
|
-
if (onViewLoad) {
|
|
195
|
-
onViewLoad(viewInfo);
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
[onViewLoad]
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// Handle view clear from LoadViewPicker - resets tree to default entity state
|
|
202
|
-
const handleViewClear = useCallback(() => {
|
|
203
|
-
// Re-set the same entity to reset tree to fresh state while keeping entity selected
|
|
204
|
-
if (selectedEntity) {
|
|
205
|
-
onEntityChange(selectedEntity);
|
|
206
|
-
}
|
|
207
|
-
}, [selectedEntity, onEntityChange]);
|
|
208
|
-
|
|
209
|
-
// Publisher multiselect with filtering
|
|
210
|
-
const [publisherQuery, setPublisherQuery] = useState("");
|
|
211
|
-
|
|
212
|
-
const publisherOptions = useMemo(() => {
|
|
213
|
-
return publisherFilter.publishers.map((pub) => ({
|
|
214
|
-
value: pub.publisherid,
|
|
215
|
-
children: pub.friendlyname,
|
|
216
|
-
}));
|
|
217
|
-
}, [publisherFilter.publishers]);
|
|
218
|
-
|
|
219
|
-
// Manual filtering for publishers
|
|
220
|
-
const filteredPublisherOptions = useMemo(() => {
|
|
221
|
-
if (!publisherQuery) return publisherOptions;
|
|
222
|
-
const lowerQuery = publisherQuery.toLowerCase();
|
|
223
|
-
return publisherOptions.filter(
|
|
224
|
-
(opt) => typeof opt.children === "string" && opt.children.toLowerCase().includes(lowerQuery)
|
|
225
|
-
);
|
|
226
|
-
}, [publisherQuery, publisherOptions]);
|
|
227
|
-
|
|
228
|
-
// Debug logging for publisher filtering
|
|
229
|
-
useEffect(() => {
|
|
230
|
-
console.log("[EntitySelector] Publisher Filter State:", {
|
|
231
|
-
publisherQuery,
|
|
232
|
-
totalPublishers: publisherOptions.length,
|
|
233
|
-
filteredPublishers: filteredPublisherOptions.length,
|
|
234
|
-
selectedPublisherIds: publisherFilter.selectedPublisherIds,
|
|
235
|
-
});
|
|
236
|
-
}, [
|
|
237
|
-
publisherQuery,
|
|
238
|
-
publisherOptions,
|
|
239
|
-
filteredPublisherOptions,
|
|
240
|
-
publisherFilter.selectedPublisherIds,
|
|
241
|
-
]);
|
|
242
|
-
|
|
243
|
-
const onPublisherSelect: ComboboxProps["onOptionSelect"] = async (_e, data) => {
|
|
244
|
-
const newPublisherIds = data.selectedOptions;
|
|
245
|
-
|
|
246
|
-
// Check if this change would invalidate the current entity
|
|
247
|
-
if (selectedEntity && publisherFilter.selectedPublisherIds.length > 0) {
|
|
248
|
-
// If deselecting publishers, check if entity would still be available
|
|
249
|
-
const isDeselecting = newPublisherIds.length < publisherFilter.selectedPublisherIds.length;
|
|
250
|
-
|
|
251
|
-
if (isDeselecting) {
|
|
252
|
-
// Find which publishers are being removed
|
|
253
|
-
const removedPublisherIds = publisherFilter.selectedPublisherIds.filter(
|
|
254
|
-
(id) => !newPublisherIds.includes(id)
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Find solutions under removed publishers
|
|
258
|
-
const removedPublisherSolutionIds = publisherFilter.solutions
|
|
259
|
-
.filter(
|
|
260
|
-
(sol) => sol._publisherid_value && removedPublisherIds.includes(sol._publisherid_value)
|
|
261
|
-
)
|
|
262
|
-
.map((sol) => sol.solutionid);
|
|
263
|
-
|
|
264
|
-
// Check if any of the currently selected solutions are from removed publishers
|
|
265
|
-
const wouldLoseSolutions = publisherFilter.selectedSolutionIds.some((solId) =>
|
|
266
|
-
removedPublisherSolutionIds.includes(solId)
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// If we'd lose solutions that might contain the entity, show confirmation
|
|
270
|
-
if (wouldLoseSolutions) {
|
|
271
|
-
setPendingPublisherIds(newPublisherIds);
|
|
272
|
-
setConfirmDialogType("publisher");
|
|
273
|
-
setShowConfirmDialog(true);
|
|
274
|
-
setPublisherQuery("");
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// No validation needed, proceed with update
|
|
281
|
-
publisherFilter.updateSelectedPublishers(newPublisherIds);
|
|
282
|
-
setPublisherQuery("");
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
// Compute publisher placeholder dynamically
|
|
286
|
-
const publisherPlaceholder = useMemo(() => {
|
|
287
|
-
if (publisherFilter.selectedPublisherIds.length === 0) {
|
|
288
|
-
return "Select publishers...";
|
|
289
|
-
}
|
|
290
|
-
const selectedNames = publisherFilter.publishers
|
|
291
|
-
.filter((pub) => publisherFilter.selectedPublisherIds.includes(pub.publisherid))
|
|
292
|
-
.map((pub) => pub.friendlyname);
|
|
293
|
-
return `${selectedNames.length} selected`;
|
|
294
|
-
}, [publisherFilter.selectedPublisherIds, publisherFilter.publishers]);
|
|
295
|
-
|
|
296
|
-
// Solution multiselect with filtering and grouping
|
|
297
|
-
const [solutionQuery, setSolutionQuery] = useState("");
|
|
298
|
-
|
|
299
|
-
// Get current solutions based on mode
|
|
300
|
-
const currentSolutions = fullFilterMode ? publisherFilter.solutions : solutionFilter.solutions;
|
|
301
|
-
|
|
302
|
-
const currentSelectedSolutionIds = fullFilterMode
|
|
303
|
-
? publisherFilter.selectedSolutionIds
|
|
304
|
-
: solutionFilter.selectedSolutionIds;
|
|
305
|
-
|
|
306
|
-
// Group solutions by managed status
|
|
307
|
-
const { unmanagedSolutions, managedSolutions } = useMemo(() => {
|
|
308
|
-
const unmanaged = currentSolutions.filter((sol) => !sol.ismanaged);
|
|
309
|
-
const managed = currentSolutions.filter((sol) => sol.ismanaged);
|
|
310
|
-
return { unmanagedSolutions: unmanaged, managedSolutions: managed };
|
|
311
|
-
}, [currentSolutions]);
|
|
312
|
-
|
|
313
|
-
const onSolutionSelect: ComboboxProps["onOptionSelect"] = async (_e, data) => {
|
|
314
|
-
const newSolutionIds = data.selectedOptions;
|
|
315
|
-
|
|
316
|
-
// Check if this change would invalidate the current entity
|
|
317
|
-
if (selectedEntity && currentSelectedSolutionIds.length > 0) {
|
|
318
|
-
// If deselecting solutions, check if entity would still be available
|
|
319
|
-
const isDeselecting = newSolutionIds.length < currentSelectedSolutionIds.length;
|
|
320
|
-
|
|
321
|
-
if (isDeselecting) {
|
|
322
|
-
// Find which solutions are being removed
|
|
323
|
-
const removedSolutionIds = currentSelectedSolutionIds.filter(
|
|
324
|
-
(id) => !newSolutionIds.includes(id)
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
// Check if current entity exists in the remaining solutions
|
|
328
|
-
const remainingEntityExists = availableEntities.some(
|
|
329
|
-
(entity) => entity.LogicalName === selectedEntity
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// Only show confirmation if entity would be removed
|
|
333
|
-
if (remainingEntityExists && removedSolutionIds.length > 0) {
|
|
334
|
-
// We need to check if entity exists in remaining solutions after removal
|
|
335
|
-
// For now, show confirmation dialog and let validation happen after
|
|
336
|
-
setPendingSolutionIds(newSolutionIds);
|
|
337
|
-
setConfirmDialogType("solution");
|
|
338
|
-
setShowConfirmDialog(true);
|
|
339
|
-
setSolutionQuery("");
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// No validation needed, proceed with update
|
|
346
|
-
if (fullFilterMode) {
|
|
347
|
-
publisherFilter.updateSelectedSolutions(newSolutionIds);
|
|
348
|
-
} else if (solutionsOnlyMode) {
|
|
349
|
-
solutionFilter.updateSelectedSolutions(newSolutionIds);
|
|
350
|
-
}
|
|
351
|
-
setSolutionQuery("");
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
const handleConfirmPublisherChange = () => {
|
|
355
|
-
// User confirmed - proceed with publisher change, cascades to solutions and entity
|
|
356
|
-
publisherFilter.updateSelectedPublishers(pendingPublisherIds);
|
|
357
|
-
setShowConfirmDialog(false);
|
|
358
|
-
setPendingPublisherIds([]);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const handleConfirmSolutionChange = () => {
|
|
362
|
-
// User confirmed - proceed with solution change, entity will auto-clear via useEffect
|
|
363
|
-
if (fullFilterMode) {
|
|
364
|
-
publisherFilter.updateSelectedSolutions(pendingSolutionIds);
|
|
365
|
-
} else if (solutionsOnlyMode) {
|
|
366
|
-
solutionFilter.updateSelectedSolutions(pendingSolutionIds);
|
|
367
|
-
}
|
|
368
|
-
setShowConfirmDialog(false);
|
|
369
|
-
setPendingSolutionIds([]);
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
const handleCancelChange = () => {
|
|
373
|
-
// User cancelled - just close dialog, keep current selection
|
|
374
|
-
setShowConfirmDialog(false);
|
|
375
|
-
setPendingPublisherIds([]);
|
|
376
|
-
setPendingSolutionIds([]);
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
// Compute solution placeholder dynamically
|
|
380
|
-
const solutionPlaceholder = useMemo(() => {
|
|
381
|
-
if (currentSelectedSolutionIds.length === 0) {
|
|
382
|
-
return "Select solutions...";
|
|
383
|
-
}
|
|
384
|
-
const selectedNames = currentSolutions
|
|
385
|
-
.filter((sol) => currentSelectedSolutionIds.includes(sol.solutionid))
|
|
386
|
-
.map((sol) => sol.friendlyname);
|
|
387
|
-
return `${selectedNames.length} selected`;
|
|
388
|
-
}, [currentSelectedSolutionIds, currentSolutions]);
|
|
389
|
-
|
|
390
|
-
// Validate entity when available entities change (solution selection changes)
|
|
391
|
-
useEffect(() => {
|
|
392
|
-
// Only validate if we have a selected entity
|
|
393
|
-
if (!selectedEntity) return;
|
|
394
|
-
|
|
395
|
-
// If available entities is empty, clear entity and tree
|
|
396
|
-
if (availableEntities.length === 0) {
|
|
397
|
-
onEntityChange(""); // Clear entity selection
|
|
398
|
-
onNewQuery(); // Reset fetch tree
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Check if selected entity is still available
|
|
403
|
-
const isEntityStillAvailable = availableEntities.some(
|
|
404
|
-
(entity) => entity.LogicalName === selectedEntity
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
// If entity is no longer available, reset it and the fetch tree
|
|
408
|
-
if (!isEntityStillAvailable) {
|
|
409
|
-
onEntityChange(""); // Clear entity selection
|
|
410
|
-
onNewQuery(); // Reset fetch tree
|
|
411
|
-
}
|
|
412
|
-
}, [availableEntities, selectedEntity, onEntityChange, onNewQuery]);
|
|
413
|
-
|
|
414
|
-
// Entity search query with filteringhanges that would invalidate entity
|
|
415
|
-
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
416
|
-
const [confirmDialogType, setConfirmDialogType] = useState<"publisher" | "solution">("solution");
|
|
417
|
-
const [pendingPublisherIds, setPendingPublisherIds] = useState<string[]>([]);
|
|
418
|
-
const [pendingSolutionIds, setPendingSolutionIds] = useState<string[]>([]);
|
|
419
|
-
|
|
420
|
-
// Entity search query with filtering
|
|
421
|
-
const [entityQuery, setEntityQuery] = useState<string>("");
|
|
422
|
-
|
|
423
|
-
const entityOptions = useMemo(() => {
|
|
424
|
-
return availableEntities.map((entity) => ({
|
|
425
|
-
value: entity.LogicalName,
|
|
426
|
-
children: entity.DisplayName?.UserLocalizedLabel?.Label || entity.LogicalName,
|
|
427
|
-
}));
|
|
428
|
-
}, [availableEntities]);
|
|
429
|
-
|
|
430
|
-
const filteredEntityOptions = useComboboxFilter(entityQuery, entityOptions, {
|
|
431
|
-
noOptionsMessage: "No entities match your search.",
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const onEntitySelect: ComboboxProps["onOptionSelect"] = (_e, data) => {
|
|
435
|
-
if (data.optionValue) {
|
|
436
|
-
onEntityChange(data.optionValue);
|
|
437
|
-
setEntityQuery(data.optionText ?? "");
|
|
438
|
-
} else {
|
|
439
|
-
setEntityQuery("");
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
// Loading state during initial access check
|
|
444
|
-
if (accessLoading) {
|
|
445
|
-
return (
|
|
446
|
-
<div className={styles.container}>
|
|
447
|
-
<div className={styles.loadingContainer}>
|
|
448
|
-
<Spinner size="small" label="Checking access permissions..." />
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// No access mode
|
|
455
|
-
if (noAccessMode) {
|
|
456
|
-
return (
|
|
457
|
-
<div className={styles.container}>
|
|
458
|
-
<div className={styles.noAccessMessage}>
|
|
459
|
-
<LockClosed20Regular style={{ marginBottom: "8px" }} />
|
|
460
|
-
<div>You don't have permission to read metadata (prvReadCustomization).</div>
|
|
461
|
-
<div style={{ fontSize: "12px", marginTop: "4px" }}>
|
|
462
|
-
Contact your system administrator to request access.
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
465
|
-
</div>
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Entity loading state
|
|
470
|
-
const entityLoading = fullFilterMode
|
|
471
|
-
? publisherFilter.entitiesLoading
|
|
472
|
-
: solutionsOnlyMode
|
|
473
|
-
? solutionFilter.entitiesLoading
|
|
474
|
-
: allEntitiesLoading;
|
|
475
|
-
|
|
476
|
-
const entityError = fullFilterMode
|
|
477
|
-
? publisherFilter.entitiesError
|
|
478
|
-
: solutionsOnlyMode
|
|
479
|
-
? solutionFilter.entitiesError
|
|
480
|
-
: null;
|
|
481
|
-
|
|
482
|
-
return (
|
|
483
|
-
<div className={styles.container}>
|
|
484
|
-
{/* Full Filter Mode: Publisher + Solution filters */}
|
|
485
|
-
{fullFilterMode && (
|
|
486
|
-
<div className={styles.filtersRow}>
|
|
487
|
-
{/* Publishers */}
|
|
488
|
-
<div className={styles.field}>
|
|
489
|
-
<label id={publisherComboId} className={styles.label}>
|
|
490
|
-
Publishers
|
|
491
|
-
</label>
|
|
492
|
-
<Combobox
|
|
493
|
-
aria-labelledby={publisherComboId}
|
|
494
|
-
placeholder={publisherPlaceholder}
|
|
495
|
-
multiselect
|
|
496
|
-
value={publisherQuery}
|
|
497
|
-
selectedOptions={publisherFilter.selectedPublisherIds}
|
|
498
|
-
onOptionSelect={onPublisherSelect}
|
|
499
|
-
onChange={(ev) => setPublisherQuery(ev.target.value)}
|
|
500
|
-
disabled={publisherFilter.publishersLoading}
|
|
501
|
-
>
|
|
502
|
-
{filteredPublisherOptions.length === 0 ? (
|
|
503
|
-
<Option>No publishers match your search</Option>
|
|
504
|
-
) : (
|
|
505
|
-
filteredPublisherOptions.map((opt) => (
|
|
506
|
-
<Option key={opt.value} value={opt.value}>
|
|
507
|
-
{opt.children}
|
|
508
|
-
</Option>
|
|
509
|
-
))
|
|
510
|
-
)}
|
|
511
|
-
</Combobox>
|
|
512
|
-
{publisherFilter.publishersError && (
|
|
513
|
-
<div className={styles.errorText}>{publisherFilter.publishersError}</div>
|
|
514
|
-
)}
|
|
515
|
-
</div>
|
|
516
|
-
|
|
517
|
-
{/* Solutions */}
|
|
518
|
-
<div className={styles.field}>
|
|
519
|
-
<label id={solutionComboId} className={styles.label}>
|
|
520
|
-
Solutions
|
|
521
|
-
</label>
|
|
522
|
-
<Combobox
|
|
523
|
-
aria-labelledby={solutionComboId}
|
|
524
|
-
placeholder={solutionPlaceholder}
|
|
525
|
-
multiselect
|
|
526
|
-
value={solutionQuery}
|
|
527
|
-
selectedOptions={currentSelectedSolutionIds}
|
|
528
|
-
onOptionSelect={onSolutionSelect}
|
|
529
|
-
onChange={(ev) => setSolutionQuery(ev.target.value)}
|
|
530
|
-
disabled={publisherFilter.selectedPublisherIds.length === 0}
|
|
531
|
-
>
|
|
532
|
-
{publisherFilter.selectedPublisherIds.length === 0 ? (
|
|
533
|
-
<Option>Select publishers first</Option>
|
|
534
|
-
) : unmanagedSolutions.length === 0 && managedSolutions.length === 0 ? (
|
|
535
|
-
<Option>No solutions found</Option>
|
|
536
|
-
) : (
|
|
537
|
-
<>
|
|
538
|
-
{unmanagedSolutions.length > 0 && (
|
|
539
|
-
<OptionGroup label="Unmanaged">
|
|
540
|
-
{unmanagedSolutions
|
|
541
|
-
.filter((sol) =>
|
|
542
|
-
sol.friendlyname.toLowerCase().includes(solutionQuery.toLowerCase())
|
|
543
|
-
)
|
|
544
|
-
.map((sol) => (
|
|
545
|
-
<Option
|
|
546
|
-
key={sol.solutionid}
|
|
547
|
-
value={sol.solutionid}
|
|
548
|
-
text={sol.friendlyname}
|
|
549
|
-
>
|
|
550
|
-
{sol.friendlyname}
|
|
551
|
-
</Option>
|
|
552
|
-
))}
|
|
553
|
-
</OptionGroup>
|
|
554
|
-
)}
|
|
555
|
-
{managedSolutions.length > 0 && (
|
|
556
|
-
<OptionGroup label="Managed">
|
|
557
|
-
{managedSolutions
|
|
558
|
-
.filter((sol) =>
|
|
559
|
-
sol.friendlyname.toLowerCase().includes(solutionQuery.toLowerCase())
|
|
560
|
-
)
|
|
561
|
-
.map((sol) => (
|
|
562
|
-
<Option
|
|
563
|
-
key={sol.solutionid}
|
|
564
|
-
value={sol.solutionid}
|
|
565
|
-
text={sol.friendlyname}
|
|
566
|
-
>
|
|
567
|
-
{sol.friendlyname}
|
|
568
|
-
</Option>
|
|
569
|
-
))}
|
|
570
|
-
</OptionGroup>
|
|
571
|
-
)}
|
|
572
|
-
</>
|
|
573
|
-
)}
|
|
574
|
-
</Combobox>
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
)}
|
|
578
|
-
|
|
579
|
-
{/* Solutions-Only Mode: Solution filter with disabled Publisher */}
|
|
580
|
-
{solutionsOnlyMode && (
|
|
581
|
-
<div className={styles.filtersRow}>
|
|
582
|
-
{/* Publisher - disabled with tooltip */}
|
|
583
|
-
<div className={styles.field}>
|
|
584
|
-
<Tooltip
|
|
585
|
-
content="Publisher filtering requires prvReadPublisher privilege"
|
|
586
|
-
relationship="description"
|
|
587
|
-
>
|
|
588
|
-
<label id={publisherComboId} className={`${styles.label} ${styles.disabledLabel}`}>
|
|
589
|
-
Publishers <LockClosed20Regular fontSize={14} />
|
|
590
|
-
</label>
|
|
591
|
-
</Tooltip>
|
|
592
|
-
<Combobox
|
|
593
|
-
aria-labelledby={publisherComboId}
|
|
594
|
-
placeholder="No access to publishers"
|
|
595
|
-
disabled
|
|
596
|
-
>
|
|
597
|
-
<Option>Requires prvReadPublisher privilege</Option>
|
|
598
|
-
</Combobox>
|
|
599
|
-
</div>
|
|
600
|
-
|
|
601
|
-
{/* Solutions */}
|
|
602
|
-
<div className={styles.field}>
|
|
603
|
-
<label id={solutionComboId} className={styles.label}>
|
|
604
|
-
Solutions
|
|
605
|
-
</label>
|
|
606
|
-
<Combobox
|
|
607
|
-
aria-labelledby={solutionComboId}
|
|
608
|
-
placeholder={solutionPlaceholder}
|
|
609
|
-
multiselect
|
|
610
|
-
value={solutionQuery}
|
|
611
|
-
selectedOptions={currentSelectedSolutionIds}
|
|
612
|
-
onOptionSelect={onSolutionSelect}
|
|
613
|
-
onChange={(ev) => setSolutionQuery(ev.target.value)}
|
|
614
|
-
disabled={solutionFilter.solutionsLoading}
|
|
615
|
-
>
|
|
616
|
-
{solutionFilter.solutionsLoading ? (
|
|
617
|
-
<Option>Loading...</Option>
|
|
618
|
-
) : unmanagedSolutions.length === 0 && managedSolutions.length === 0 ? (
|
|
619
|
-
<Option>No solutions found</Option>
|
|
620
|
-
) : (
|
|
621
|
-
<>
|
|
622
|
-
{unmanagedSolutions.length > 0 && (
|
|
623
|
-
<OptionGroup label="Unmanaged">
|
|
624
|
-
{unmanagedSolutions
|
|
625
|
-
.filter((sol) =>
|
|
626
|
-
sol.friendlyname.toLowerCase().includes(solutionQuery.toLowerCase())
|
|
627
|
-
)
|
|
628
|
-
.map((sol) => (
|
|
629
|
-
<Option
|
|
630
|
-
key={sol.solutionid}
|
|
631
|
-
value={sol.solutionid}
|
|
632
|
-
text={sol.friendlyname}
|
|
633
|
-
>
|
|
634
|
-
{sol.friendlyname}
|
|
635
|
-
</Option>
|
|
636
|
-
))}
|
|
637
|
-
</OptionGroup>
|
|
638
|
-
)}
|
|
639
|
-
{managedSolutions.length > 0 && (
|
|
640
|
-
<OptionGroup label="Managed">
|
|
641
|
-
{managedSolutions
|
|
642
|
-
.filter((sol) =>
|
|
643
|
-
sol.friendlyname.toLowerCase().includes(solutionQuery.toLowerCase())
|
|
644
|
-
)
|
|
645
|
-
.map((sol) => (
|
|
646
|
-
<Option
|
|
647
|
-
key={sol.solutionid}
|
|
648
|
-
value={sol.solutionid}
|
|
649
|
-
text={sol.friendlyname}
|
|
650
|
-
>
|
|
651
|
-
{sol.friendlyname}
|
|
652
|
-
</Option>
|
|
653
|
-
))}
|
|
654
|
-
</OptionGroup>
|
|
655
|
-
)}
|
|
656
|
-
</>
|
|
657
|
-
)}
|
|
658
|
-
</Combobox>
|
|
659
|
-
{solutionFilter.solutionsError && (
|
|
660
|
-
<div className={styles.errorText}>{solutionFilter.solutionsError}</div>
|
|
661
|
-
)}
|
|
662
|
-
</div>
|
|
663
|
-
</div>
|
|
664
|
-
)}
|
|
665
|
-
|
|
666
|
-
{/* Publishers-only mode: Disabled Publisher with tooltip + direct entity loading */}
|
|
667
|
-
{publishersOnlyMode && (
|
|
668
|
-
<div className={styles.filtersRow}>
|
|
669
|
-
{/* Publisher - disabled with tooltip */}
|
|
670
|
-
<div className={styles.field}>
|
|
671
|
-
<Tooltip
|
|
672
|
-
content="Solution filtering requires prvReadSolution privilege"
|
|
673
|
-
relationship="description"
|
|
674
|
-
>
|
|
675
|
-
<label id={publisherComboId} className={`${styles.label} ${styles.disabledLabel}`}>
|
|
676
|
-
Publishers <LockClosed20Regular fontSize={14} />
|
|
677
|
-
</label>
|
|
678
|
-
</Tooltip>
|
|
679
|
-
<Combobox
|
|
680
|
-
aria-labelledby={publisherComboId}
|
|
681
|
-
placeholder="No access to solutions"
|
|
682
|
-
disabled
|
|
683
|
-
>
|
|
684
|
-
<Option>Requires prvReadSolution privilege</Option>
|
|
685
|
-
</Combobox>
|
|
686
|
-
</div>
|
|
687
|
-
</div>
|
|
688
|
-
)}
|
|
689
|
-
|
|
690
|
-
{/* Entity selector (all modes) */}
|
|
691
|
-
<div className={styles.entityRow}>
|
|
692
|
-
<div className={styles.field}>
|
|
693
|
-
<label id={entityComboId} className={styles.label}>
|
|
694
|
-
Entity
|
|
695
|
-
</label>
|
|
696
|
-
<Combobox
|
|
697
|
-
aria-labelledby={entityComboId}
|
|
698
|
-
placeholder={entityLoading ? "Loading entities..." : "Select an entity..."}
|
|
699
|
-
value={entityQuery}
|
|
700
|
-
onOptionSelect={onEntitySelect}
|
|
701
|
-
onChange={(ev) => setEntityQuery(ev.target.value)}
|
|
702
|
-
clearable
|
|
703
|
-
disabled={entityLoading || availableEntities.length === 0}
|
|
704
|
-
>
|
|
705
|
-
{entityLoading ? (
|
|
706
|
-
<Option>Loading...</Option>
|
|
707
|
-
) : availableEntities.length === 0 ? (
|
|
708
|
-
<Option>
|
|
709
|
-
{fullFilterMode || solutionsOnlyMode
|
|
710
|
-
? "Select solutions to see entities"
|
|
711
|
-
: publishersOnlyMode
|
|
712
|
-
? "Loading all entities..."
|
|
713
|
-
: "No entities available"}
|
|
714
|
-
</Option>
|
|
715
|
-
) : (
|
|
716
|
-
filteredEntityOptions
|
|
717
|
-
)}
|
|
718
|
-
</Combobox>
|
|
719
|
-
{entityError && <div className={styles.errorText}>{entityError}</div>}
|
|
720
|
-
</div>
|
|
721
|
-
<Button
|
|
722
|
-
appearance="primary"
|
|
723
|
-
icon={<Add20Regular />}
|
|
724
|
-
onClick={onNewQuery}
|
|
725
|
-
title="Create a new query"
|
|
726
|
-
>
|
|
727
|
-
New
|
|
728
|
-
</Button>
|
|
729
|
-
</div>
|
|
730
|
-
|
|
731
|
-
{/* Load View Picker - only show when entity is selected */}
|
|
732
|
-
{selectedEntityMetadata && onViewLoad && (
|
|
733
|
-
<div className={styles.viewPickerRow}>
|
|
734
|
-
<LoadViewPicker
|
|
735
|
-
selectedEntityMetadata={selectedEntityMetadata}
|
|
736
|
-
onViewSelect={handleViewSelect}
|
|
737
|
-
onViewClear={handleViewClear}
|
|
738
|
-
/>
|
|
739
|
-
</div>
|
|
740
|
-
)}
|
|
741
|
-
|
|
742
|
-
{/* Confirmation Dialog for Publisher/Solution Change */}
|
|
743
|
-
<Dialog
|
|
744
|
-
open={showConfirmDialog}
|
|
745
|
-
onOpenChange={(_, data) => !data.open && handleCancelChange()}
|
|
746
|
-
>
|
|
747
|
-
<DialogSurface>
|
|
748
|
-
<DialogBody>
|
|
749
|
-
<DialogTitle>
|
|
750
|
-
{confirmDialogType === "publisher"
|
|
751
|
-
? "Confirm Publisher Change"
|
|
752
|
-
: "Confirm Solution Change"}
|
|
753
|
-
</DialogTitle>
|
|
754
|
-
<DialogContent>
|
|
755
|
-
<p>
|
|
756
|
-
{confirmDialogType === "publisher"
|
|
757
|
-
? "Removing publishers will also remove their associated solutions, which may reset the FetchXML tree and entity selection as"
|
|
758
|
-
: "Removing solutions will reset the FetchXML tree and entity selection as"}
|
|
759
|
-
<strong> {selectedEntity}</strong> may no longer be available
|
|
760
|
-
{confirmDialogType === "publisher"
|
|
761
|
-
? " in the remaining publishers."
|
|
762
|
-
: " in the remaining selected solutions."}
|
|
763
|
-
</p>
|
|
764
|
-
<p>This action cannot be undone. Do you want to proceed?</p>
|
|
765
|
-
</DialogContent>
|
|
766
|
-
<DialogActions>
|
|
767
|
-
<Button
|
|
768
|
-
appearance="primary"
|
|
769
|
-
onClick={
|
|
770
|
-
confirmDialogType === "publisher"
|
|
771
|
-
? handleConfirmPublisherChange
|
|
772
|
-
: handleConfirmSolutionChange
|
|
773
|
-
}
|
|
774
|
-
>
|
|
775
|
-
{confirmDialogType === "publisher"
|
|
776
|
-
? "Yes, Update Publishers"
|
|
777
|
-
: "Yes, Update Solutions"}
|
|
778
|
-
</Button>
|
|
779
|
-
<Button appearance="secondary" onClick={handleCancelChange}>
|
|
780
|
-
Cancel
|
|
781
|
-
</Button>
|
|
782
|
-
</DialogActions>
|
|
783
|
-
</DialogBody>
|
|
784
|
-
</DialogSurface>
|
|
785
|
-
</Dialog>
|
|
786
|
-
</div>
|
|
787
|
-
);
|
|
788
|
-
}
|