@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.
Files changed (78) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +1 -1
  3. package/dist/assets/index-DD9fzkbz.js +232 -0
  4. package/dist/index.html +70 -0
  5. package/index.html +0 -1
  6. package/npm-shrinkwrap.json +6329 -0
  7. package/package.json +95 -51
  8. package/CHANGELOG.md +0 -120
  9. package/eslint.config.js +0 -23
  10. package/public/vite.svg +0 -1
  11. package/src/app/AppShell.tsx +0 -1733
  12. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  13. package/src/assets/react.svg +0 -1
  14. package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
  15. package/src/features/fetchxml/api/excelExport.ts +0 -318
  16. package/src/features/fetchxml/api/formattedValues.ts +0 -107
  17. package/src/features/fetchxml/api/pptbClient.ts +0 -2635
  18. package/src/features/fetchxml/model/displaySettings.ts +0 -18
  19. package/src/features/fetchxml/model/fetchxml.ts +0 -346
  20. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
  21. package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
  22. package/src/features/fetchxml/model/layoutxml.ts +0 -372
  23. package/src/features/fetchxml/model/nodes.ts +0 -255
  24. package/src/features/fetchxml/model/operators.ts +0 -624
  25. package/src/features/fetchxml/model/treeUtils.ts +0 -174
  26. package/src/features/fetchxml/state/builderStore.tsx +0 -1160
  27. package/src/features/fetchxml/state/cache.ts +0 -393
  28. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
  29. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
  30. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
  31. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
  32. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
  37. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
  38. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
  39. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
  40. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
  41. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
  42. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
  43. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
  44. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
  45. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
  46. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
  47. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
  48. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
  49. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
  50. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
  51. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
  52. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
  53. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
  54. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
  55. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
  56. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
  57. package/src/main.tsx +0 -11
  58. package/src/shared/components/AttributePicker.tsx +0 -138
  59. package/src/shared/components/BetweenInputPicker.tsx +0 -120
  60. package/src/shared/components/BooleanValuePicker.tsx +0 -165
  61. package/src/shared/components/DateTimeInputPicker.tsx +0 -170
  62. package/src/shared/components/DynamicValueList.tsx +0 -128
  63. package/src/shared/components/ExcelIcon.tsx +0 -30
  64. package/src/shared/components/MultiValuePicker.tsx +0 -178
  65. package/src/shared/components/NumericInputPicker.tsx +0 -156
  66. package/src/shared/components/OptionSetValuePicker.tsx +0 -150
  67. package/src/shared/components/RelationshipPicker.tsx +0 -268
  68. package/src/shared/contexts/ThemeContext.tsx +0 -19
  69. package/src/shared/hooks/useAccessMode.ts +0 -77
  70. package/src/shared/hooks/useLazyMetadata.ts +0 -132
  71. package/src/shared/hooks/usePptbContext.ts +0 -170
  72. package/src/shared/hooks/usePublisherFilter.ts +0 -214
  73. package/src/shared/hooks/useSolutionFilter.ts +0 -162
  74. package/src/shared/utils/debug.ts +0 -87
  75. package/tsconfig.app.json +0 -28
  76. package/tsconfig.json +0 -7
  77. package/tsconfig.node.json +0 -26
  78. 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
- }