@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.1

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 (77) hide show
  1. package/LICENSE +661 -0
  2. package/dist/assets/index-DD9fzkbz.js +232 -0
  3. package/dist/index.html +70 -0
  4. package/index.html +0 -1
  5. package/npm-shrinkwrap.json +6329 -0
  6. package/package.json +95 -51
  7. package/CHANGELOG.md +0 -120
  8. package/eslint.config.js +0 -23
  9. package/public/vite.svg +0 -1
  10. package/src/app/AppShell.tsx +0 -1733
  11. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  12. package/src/assets/react.svg +0 -1
  13. package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
  14. package/src/features/fetchxml/api/excelExport.ts +0 -318
  15. package/src/features/fetchxml/api/formattedValues.ts +0 -107
  16. package/src/features/fetchxml/api/pptbClient.ts +0 -2635
  17. package/src/features/fetchxml/model/displaySettings.ts +0 -18
  18. package/src/features/fetchxml/model/fetchxml.ts +0 -346
  19. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
  20. package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
  21. package/src/features/fetchxml/model/layoutxml.ts +0 -372
  22. package/src/features/fetchxml/model/nodes.ts +0 -255
  23. package/src/features/fetchxml/model/operators.ts +0 -624
  24. package/src/features/fetchxml/model/treeUtils.ts +0 -174
  25. package/src/features/fetchxml/state/builderStore.tsx +0 -1160
  26. package/src/features/fetchxml/state/cache.ts +0 -393
  27. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
  28. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
  29. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
  30. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
  31. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
  32. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
  37. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
  38. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
  39. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
  40. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
  41. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
  42. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
  43. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
  44. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
  45. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
  46. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
  47. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
  48. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
  49. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
  50. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
  51. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
  52. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
  53. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
  54. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
  55. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
  56. package/src/main.tsx +0 -11
  57. package/src/shared/components/AttributePicker.tsx +0 -138
  58. package/src/shared/components/BetweenInputPicker.tsx +0 -120
  59. package/src/shared/components/BooleanValuePicker.tsx +0 -165
  60. package/src/shared/components/DateTimeInputPicker.tsx +0 -170
  61. package/src/shared/components/DynamicValueList.tsx +0 -128
  62. package/src/shared/components/ExcelIcon.tsx +0 -30
  63. package/src/shared/components/MultiValuePicker.tsx +0 -178
  64. package/src/shared/components/NumericInputPicker.tsx +0 -156
  65. package/src/shared/components/OptionSetValuePicker.tsx +0 -150
  66. package/src/shared/components/RelationshipPicker.tsx +0 -268
  67. package/src/shared/contexts/ThemeContext.tsx +0 -19
  68. package/src/shared/hooks/useAccessMode.ts +0 -77
  69. package/src/shared/hooks/useLazyMetadata.ts +0 -132
  70. package/src/shared/hooks/usePptbContext.ts +0 -170
  71. package/src/shared/hooks/usePublisherFilter.ts +0 -214
  72. package/src/shared/hooks/useSolutionFilter.ts +0 -162
  73. package/src/shared/utils/debug.ts +0 -87
  74. package/tsconfig.app.json +0 -28
  75. package/tsconfig.json +0 -7
  76. package/tsconfig.node.json +0 -26
  77. package/vite.config.ts +0 -7
@@ -1,800 +0,0 @@
1
- /**
2
- * Add Columns Panel (Drawer)
3
- * Shows available attributes with support for related entities (lookups)
4
- * Matches Power Apps Model-Driven Apps UX
5
- */
6
-
7
- import { useState, useMemo, useCallback, useEffect } from "react";
8
- import {
9
- DrawerBody,
10
- DrawerHeader,
11
- DrawerHeaderTitle,
12
- OverlayDrawer,
13
- Button,
14
- makeStyles,
15
- tokens,
16
- Text,
17
- Input,
18
- Checkbox,
19
- Tree,
20
- TreeItem,
21
- TreeItemLayout,
22
- Spinner,
23
- TabList,
24
- Tab,
25
- Combobox,
26
- Option,
27
- Badge,
28
- Accordion,
29
- AccordionItem,
30
- AccordionHeader,
31
- AccordionPanel,
32
- type SelectTabData,
33
- type SelectTabEvent,
34
- } from "@fluentui/react-components";
35
- import { Dismiss24Regular, Search20Regular } from "@fluentui/react-icons";
36
- import type { AttributeMetadata, RelationshipMetadata } from "../../api/pptbClient";
37
-
38
- const useStyles = makeStyles({
39
- drawer: {
40
- width: "420px",
41
- },
42
- searchContainer: {
43
- display: "flex",
44
- gap: tokens.spacingHorizontalS,
45
- marginBottom: tokens.spacingVerticalM,
46
- },
47
- searchInput: {
48
- flex: 1,
49
- },
50
- filterCombo: {
51
- minWidth: "120px",
52
- },
53
- tabContent: {
54
- marginTop: tokens.spacingVerticalM,
55
- },
56
- attributeList: {
57
- display: "flex",
58
- flexDirection: "column",
59
- gap: tokens.spacingVerticalXXS,
60
- maxHeight: "calc(100vh - 300px)",
61
- overflowY: "auto",
62
- },
63
- attributeItem: {
64
- display: "flex",
65
- alignItems: "center",
66
- gap: tokens.spacingHorizontalS,
67
- padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
68
- borderRadius: tokens.borderRadiusSmall,
69
- cursor: "pointer",
70
- "&:hover": {
71
- backgroundColor: tokens.colorNeutralBackground1Hover,
72
- },
73
- },
74
- attributeItemSelected: {
75
- backgroundColor: tokens.colorNeutralBackground1Selected,
76
- },
77
- relationshipTree: {
78
- maxHeight: "calc(100vh - 300px)",
79
- overflowY: "auto",
80
- },
81
- relationshipItem: {
82
- display: "flex",
83
- alignItems: "center",
84
- gap: tokens.spacingHorizontalS,
85
- },
86
- relationshipLabel: {
87
- display: "flex",
88
- flexDirection: "column",
89
- },
90
- relationshipName: {
91
- fontWeight: tokens.fontWeightSemibold,
92
- },
93
- relationshipEntity: {
94
- fontSize: tokens.fontSizeBase200,
95
- color: tokens.colorNeutralForeground3,
96
- },
97
- relationshipType: {
98
- marginLeft: tokens.spacingHorizontalS,
99
- },
100
- relationshipHeader: {
101
- display: "flex",
102
- alignItems: "center",
103
- justifyContent: "space-between",
104
- width: "100%",
105
- },
106
- accordionHeader: {
107
- display: "flex",
108
- alignItems: "center",
109
- gap: tokens.spacingHorizontalS,
110
- },
111
- warningBadge: {
112
- marginLeft: tokens.spacingHorizontalS,
113
- },
114
- loadingContainer: {
115
- display: "flex",
116
- alignItems: "center",
117
- justifyContent: "center",
118
- padding: tokens.spacingVerticalXXL,
119
- },
120
- footer: {
121
- display: "flex",
122
- justifyContent: "flex-end",
123
- gap: tokens.spacingHorizontalS,
124
- padding: tokens.spacingVerticalM,
125
- borderTop: `1px solid ${tokens.colorNeutralStroke1}`,
126
- marginTop: "auto",
127
- },
128
- emptyState: {
129
- display: "flex",
130
- flexDirection: "column",
131
- alignItems: "center",
132
- justifyContent: "center",
133
- padding: tokens.spacingVerticalXXL,
134
- color: tokens.colorNeutralForeground3,
135
- textAlign: "center",
136
- },
137
- selectedCount: {
138
- marginRight: "auto",
139
- color: tokens.colorNeutralForeground2,
140
- display: "flex",
141
- alignItems: "center",
142
- },
143
- });
144
-
145
- type FilterType = "all" | "standard" | "custom";
146
-
147
- export interface RelatedEntityColumn {
148
- /** The relationship used to access this entity */
149
- relationship: RelationshipMetadata;
150
- /** The attribute on the related entity */
151
- attribute: AttributeMetadata;
152
- /** Display name including relationship context, e.g. "First Name (Primary Contact)" */
153
- displayName: string;
154
- /** The lookup attribute name on the root entity (for N:1) */
155
- lookupAttribute: string;
156
- /** The relationship type: N1 (lookup) or 1N (one-to-many) */
157
- relationshipType: "N1" | "1N";
158
- }
159
-
160
- export interface AddColumnSelection {
161
- /** Root entity attributes to add */
162
- rootAttributes: string[];
163
- /** Related entity columns to add (will create link-entity if needed) */
164
- relatedColumns: RelatedEntityColumn[];
165
- }
166
-
167
- export interface AddColumnsPanelProps {
168
- /** Whether the panel is open */
169
- open: boolean;
170
- /** Entity display name */
171
- entityDisplayName?: string;
172
- /** Entity logical name */
173
- entityLogicalName?: string;
174
- /** Available attributes for the root entity */
175
- availableAttributes?: AttributeMetadata[];
176
- /** Currently selected attribute names (already in the query) */
177
- selectedAttributes?: string[];
178
- /** Lookup relationships (many-to-one) for related entity columns */
179
- lookupRelationships?: RelationshipMetadata[];
180
- /** One-to-many relationships for 1-N related entity columns */
181
- oneToManyRelationships?: RelationshipMetadata[];
182
- /** Whether relationship data is loading */
183
- isLoadingRelationships?: boolean;
184
- /** Callback to load attributes for a related entity */
185
- onLoadRelatedAttributes?: (entityLogicalName: string) => Promise<AttributeMetadata[]>;
186
- /** Called when panel is closed */
187
- onClose: () => void;
188
- /** Called when columns are selected and Apply is clicked */
189
- onApply: (selection: AddColumnSelection) => void;
190
- }
191
-
192
- export function AddColumnsPanel({
193
- open,
194
- entityDisplayName,
195
- entityLogicalName,
196
- availableAttributes = [],
197
- selectedAttributes = [],
198
- lookupRelationships = [],
199
- oneToManyRelationships = [],
200
- isLoadingRelationships,
201
- onLoadRelatedAttributes,
202
- onClose,
203
- onApply,
204
- }: AddColumnsPanelProps) {
205
- const styles = useStyles();
206
- const [activeTab, setActiveTab] = useState<"entity" | "related">("entity");
207
- const [searchText, setSearchText] = useState("");
208
- const [filterType, setFilterType] = useState<FilterType>("all");
209
- const [selectedRootAttrs, setSelectedRootAttrs] = useState<Set<string>>(new Set());
210
- const [selectedRelatedCols, setSelectedRelatedCols] = useState<RelatedEntityColumn[]>([]);
211
- const [expandedRelationships, setExpandedRelationships] = useState<Set<string>>(new Set());
212
- const [relatedAttributesCache, setRelatedAttributesCache] = useState<
213
- Map<string, AttributeMetadata[]>
214
- >(new Map());
215
- const [loadingEntities, setLoadingEntities] = useState<Set<string>>(new Set());
216
-
217
- // Reset state when panel opens
218
- useEffect(() => {
219
- if (open) {
220
- setSelectedRootAttrs(new Set());
221
- setSelectedRelatedCols([]);
222
- setSearchText("");
223
- setFilterType("all");
224
- setActiveTab("entity");
225
- }
226
- }, [open]);
227
-
228
- // Filter attributes based on search and filter type
229
- const filteredAttributes = useMemo(() => {
230
- let attrs = availableAttributes.filter(
231
- (attr) => !selectedAttributes.includes(attr.LogicalName)
232
- );
233
-
234
- // Apply filter type
235
- if (filterType === "standard") {
236
- attrs = attrs.filter(
237
- (attr) => !attr.LogicalName.startsWith("new_") && !attr.SchemaName.includes("_")
238
- );
239
- } else if (filterType === "custom") {
240
- attrs = attrs.filter(
241
- (attr) => attr.LogicalName.startsWith("new_") || attr.SchemaName.includes("_")
242
- );
243
- }
244
-
245
- // Apply search
246
- if (searchText) {
247
- const search = searchText.toLowerCase();
248
- attrs = attrs.filter(
249
- (attr) =>
250
- attr.LogicalName.toLowerCase().includes(search) ||
251
- (attr.DisplayName?.UserLocalizedLabel?.Label || "").toLowerCase().includes(search)
252
- );
253
- }
254
-
255
- // Sort by display name
256
- return attrs.sort((a, b) => {
257
- const aName = a.DisplayName?.UserLocalizedLabel?.Label || a.LogicalName;
258
- const bName = b.DisplayName?.UserLocalizedLabel?.Label || b.LogicalName;
259
- return aName.localeCompare(bName);
260
- });
261
- }, [availableAttributes, selectedAttributes, searchText, filterType]);
262
-
263
- // Combined relationship type for display
264
- type RelationshipWithType = {
265
- relationship: RelationshipMetadata;
266
- type: "N1" | "1N";
267
- displayName: string;
268
- relatedEntity: string;
269
- };
270
-
271
- // Filter and combine N:1 (lookup) and 1:N relationships
272
- const { filteredN1Relationships, filtered1NRelationships } = useMemo(() => {
273
- const search = searchText.toLowerCase();
274
-
275
- // Process N:1 (lookup/many-to-one) relationships
276
- let n1Rels: RelationshipWithType[] = lookupRelationships.map((rel) => {
277
- const lookupAttr = availableAttributes.find(
278
- (a) => a.LogicalName === rel.ReferencingAttribute
279
- );
280
- const displayName =
281
- lookupAttr?.DisplayName?.UserLocalizedLabel?.Label || rel.ReferencingAttribute;
282
- return {
283
- relationship: rel,
284
- type: "N1" as const,
285
- displayName,
286
- relatedEntity: rel.ReferencedEntity,
287
- };
288
- });
289
-
290
- // Process 1:N (one-to-many) relationships
291
- let oneNRels: RelationshipWithType[] = oneToManyRelationships.map((rel) => {
292
- // For 1:N, ReferencingEntity is the related entity (the "many" side)
293
- return {
294
- relationship: rel,
295
- type: "1N" as const,
296
- displayName: rel.ReferencingEntity, // Show the related entity name
297
- relatedEntity: rel.ReferencingEntity,
298
- };
299
- });
300
-
301
- // Apply search filter
302
- if (searchText) {
303
- n1Rels = n1Rels.filter(
304
- (r) =>
305
- r.relatedEntity.toLowerCase().includes(search) ||
306
- r.displayName.toLowerCase().includes(search) ||
307
- r.relationship.SchemaName.toLowerCase().includes(search)
308
- );
309
- oneNRels = oneNRels.filter(
310
- (r) =>
311
- r.relatedEntity.toLowerCase().includes(search) ||
312
- r.displayName.toLowerCase().includes(search) ||
313
- r.relationship.SchemaName.toLowerCase().includes(search)
314
- );
315
- }
316
-
317
- // Sort by display name
318
- n1Rels.sort((a, b) => a.displayName.localeCompare(b.displayName));
319
- oneNRels.sort((a, b) => a.displayName.localeCompare(b.displayName));
320
-
321
- return {
322
- filteredN1Relationships: n1Rels,
323
- filtered1NRelationships: oneNRels,
324
- };
325
- }, [lookupRelationships, oneToManyRelationships, availableAttributes, searchText]);
326
-
327
- const handleTabSelect = useCallback((_e: SelectTabEvent, data: SelectTabData) => {
328
- setActiveTab(data.value as "entity" | "related");
329
- }, []);
330
-
331
- const handleRootAttributeToggle = useCallback((logicalName: string) => {
332
- setSelectedRootAttrs((prev) => {
333
- const next = new Set(prev);
334
- if (next.has(logicalName)) {
335
- next.delete(logicalName);
336
- } else {
337
- next.add(logicalName);
338
- }
339
- return next;
340
- });
341
- }, []);
342
-
343
- const handleExpandRelationship = useCallback(
344
- async (relationship: RelationshipMetadata, relatedEntity: string) => {
345
- const relKey = relationship.SchemaName;
346
-
347
- setExpandedRelationships((prev) => {
348
- const next = new Set(prev);
349
- if (next.has(relKey)) {
350
- next.delete(relKey);
351
- } else {
352
- next.add(relKey);
353
- }
354
- return next;
355
- });
356
-
357
- // Load related entity attributes if not already loaded
358
- if (!relatedAttributesCache.has(relatedEntity) && onLoadRelatedAttributes) {
359
- setLoadingEntities((prev) => new Set(prev).add(relatedEntity));
360
- try {
361
- const attrs = await onLoadRelatedAttributes(relatedEntity);
362
- setRelatedAttributesCache((prev) => new Map(prev).set(relatedEntity, attrs));
363
- } finally {
364
- setLoadingEntities((prev) => {
365
- const next = new Set(prev);
366
- next.delete(relatedEntity);
367
- return next;
368
- });
369
- }
370
- }
371
- },
372
- [relatedAttributesCache, onLoadRelatedAttributes]
373
- );
374
-
375
- const handleRelatedAttributeToggle = useCallback(
376
- (
377
- relationship: RelationshipMetadata,
378
- attribute: AttributeMetadata,
379
- relType: "N1" | "1N",
380
- relatedEntity: string
381
- ) => {
382
- const attrDisplayName =
383
- attribute.DisplayName?.UserLocalizedLabel?.Label || attribute.LogicalName;
384
-
385
- let lookupAttr: string;
386
- let displaySuffix: string;
387
-
388
- if (relType === "N1") {
389
- // For N:1, the lookup attribute is on the root entity
390
- lookupAttr = relationship.ReferencingAttribute;
391
- const lookupDisplayName =
392
- availableAttributes.find((a) => a.LogicalName === lookupAttr)?.DisplayName
393
- ?.UserLocalizedLabel?.Label || lookupAttr;
394
- displaySuffix = lookupDisplayName;
395
- } else {
396
- // For 1:N, there's no lookup on root - use the related entity name
397
- lookupAttr = relationship.ReferencingAttribute; // FK on the related entity
398
- displaySuffix = relatedEntity;
399
- }
400
-
401
- const relatedCol: RelatedEntityColumn = {
402
- relationship,
403
- attribute,
404
- displayName: `${attrDisplayName} (${displaySuffix})`,
405
- lookupAttribute: lookupAttr,
406
- relationshipType: relType,
407
- };
408
-
409
- setSelectedRelatedCols((prev) => {
410
- // Check if already selected
411
- const existingIndex = prev.findIndex(
412
- (col) =>
413
- col.relationship.SchemaName === relationship.SchemaName &&
414
- col.attribute.LogicalName === attribute.LogicalName
415
- );
416
-
417
- if (existingIndex >= 0) {
418
- // Remove it
419
- return prev.filter((_, i) => i !== existingIndex);
420
- } else {
421
- // Add it
422
- return [...prev, relatedCol];
423
- }
424
- });
425
- },
426
- [availableAttributes]
427
- );
428
-
429
- const isRelatedAttributeSelected = useCallback(
430
- (relationship: RelationshipMetadata, attribute: AttributeMetadata) => {
431
- return selectedRelatedCols.some(
432
- (col) =>
433
- col.relationship.SchemaName === relationship.SchemaName &&
434
- col.attribute.LogicalName === attribute.LogicalName
435
- );
436
- },
437
- [selectedRelatedCols]
438
- );
439
-
440
- const handleApply = useCallback(() => {
441
- onApply({
442
- rootAttributes: Array.from(selectedRootAttrs),
443
- relatedColumns: selectedRelatedCols,
444
- });
445
- }, [selectedRootAttrs, selectedRelatedCols, onApply]);
446
-
447
- const totalSelected = selectedRootAttrs.size + selectedRelatedCols.length;
448
-
449
- return (
450
- <OverlayDrawer
451
- open={open}
452
- onOpenChange={(_e, data) => !data.open && onClose()}
453
- position="end"
454
- size="medium"
455
- className={styles.drawer}
456
- >
457
- <DrawerHeader>
458
- <DrawerHeaderTitle
459
- action={
460
- <Button
461
- appearance="subtle"
462
- aria-label="Close"
463
- icon={<Dismiss24Regular />}
464
- onClick={onClose}
465
- />
466
- }
467
- >
468
- Add columns
469
- </DrawerHeaderTitle>
470
- </DrawerHeader>
471
- <DrawerBody>
472
- <TabList selectedValue={activeTab} onTabSelect={handleTabSelect}>
473
- <Tab value="entity">{entityDisplayName || entityLogicalName || "Entity"}</Tab>
474
- <Tab value="related">Related</Tab>
475
- </TabList>
476
-
477
- <div className={styles.tabContent}>
478
- <div className={styles.searchContainer}>
479
- <Input
480
- className={styles.searchInput}
481
- contentBefore={<Search20Regular />}
482
- placeholder="Search columns..."
483
- value={searchText}
484
- onChange={(_e, data) => setSearchText(data.value)}
485
- />
486
- <Combobox
487
- className={styles.filterCombo}
488
- value={
489
- filterType === "all" ? "All" : filterType === "standard" ? "Standard" : "Custom"
490
- }
491
- onOptionSelect={(_e, data) =>
492
- setFilterType((data.optionValue as FilterType) || "all")
493
- }
494
- >
495
- <Option value="all">All</Option>
496
- <Option value="standard">Standard</Option>
497
- <Option value="custom">Custom</Option>
498
- </Combobox>
499
- </div>
500
-
501
- {activeTab === "entity" && (
502
- <div className={styles.attributeList}>
503
- {filteredAttributes.length === 0 ? (
504
- <div className={styles.emptyState}>
505
- <Text>
506
- {searchText ? "No matching attributes found" : "All attributes already added"}
507
- </Text>
508
- </div>
509
- ) : (
510
- filteredAttributes.map((attr) => (
511
- <div
512
- key={attr.LogicalName}
513
- className={`${styles.attributeItem} ${
514
- selectedRootAttrs.has(attr.LogicalName) ? styles.attributeItemSelected : ""
515
- }`}
516
- onClick={() => handleRootAttributeToggle(attr.LogicalName)}
517
- role="checkbox"
518
- aria-checked={selectedRootAttrs.has(attr.LogicalName)}
519
- tabIndex={0}
520
- onKeyDown={(e) => {
521
- if (e.key === "Enter" || e.key === " ") {
522
- e.preventDefault();
523
- handleRootAttributeToggle(attr.LogicalName);
524
- }
525
- }}
526
- >
527
- <Checkbox
528
- checked={selectedRootAttrs.has(attr.LogicalName)}
529
- onChange={(e) => {
530
- e.stopPropagation();
531
- handleRootAttributeToggle(attr.LogicalName);
532
- }}
533
- />
534
- <Text>{attr.DisplayName?.UserLocalizedLabel?.Label || attr.LogicalName}</Text>
535
- </div>
536
- ))
537
- )}
538
- </div>
539
- )}
540
-
541
- {activeTab === "related" && (
542
- <div className={styles.relationshipTree}>
543
- {isLoadingRelationships ? (
544
- <div className={styles.loadingContainer}>
545
- <Spinner size="small" label="Loading relationships..." />
546
- </div>
547
- ) : filteredN1Relationships.length === 0 && filtered1NRelationships.length === 0 ? (
548
- <div className={styles.emptyState}>
549
- <Text>No relationships found</Text>
550
- </div>
551
- ) : (
552
- <Accordion multiple collapsible defaultOpenItems={["n1"]}>
553
- {/* N:1 (Lookup) Relationships */}
554
- {filteredN1Relationships.length > 0 && (
555
- <AccordionItem value="n1">
556
- <AccordionHeader size="small">
557
- <div className={styles.accordionHeader}>
558
- <Text weight="semibold">Lookups (N:1)</Text>
559
- <Badge appearance="filled" color="informative" size="small">
560
- {filteredN1Relationships.length}
561
- </Badge>
562
- </div>
563
- </AccordionHeader>
564
- <AccordionPanel>
565
- <Tree aria-label="Lookup relationships">
566
- {filteredN1Relationships.map((relItem) => {
567
- const rel = relItem.relationship;
568
- const isExpanded = expandedRelationships.has(rel.SchemaName);
569
- const relatedAttrs = relatedAttributesCache.get(relItem.relatedEntity);
570
- const isLoading = loadingEntities.has(relItem.relatedEntity);
571
-
572
- return (
573
- <TreeItem key={rel.SchemaName} itemType="branch" open={isExpanded}>
574
- <TreeItemLayout
575
- onClick={() =>
576
- handleExpandRelationship(rel, relItem.relatedEntity)
577
- }
578
- >
579
- <div className={styles.relationshipHeader}>
580
- <div className={styles.relationshipLabel}>
581
- <span className={styles.relationshipName}>
582
- {relItem.displayName}
583
- </span>
584
- <span className={styles.relationshipEntity}>
585
- {relItem.relatedEntity}
586
- </span>
587
- </div>
588
- </div>
589
- </TreeItemLayout>
590
- {isExpanded && (
591
- <Tree>
592
- {isLoading ? (
593
- <TreeItem itemType="leaf">
594
- <TreeItemLayout>
595
- <Spinner size="tiny" label="Loading..." />
596
- </TreeItemLayout>
597
- </TreeItem>
598
- ) : relatedAttrs && relatedAttrs.length > 0 ? (
599
- relatedAttrs
600
- .filter((attr) => attr.AttributeType !== "Virtual")
601
- .sort((a, b) => {
602
- const aName =
603
- a.DisplayName?.UserLocalizedLabel?.Label ||
604
- a.LogicalName;
605
- const bName =
606
- b.DisplayName?.UserLocalizedLabel?.Label ||
607
- b.LogicalName;
608
- return aName.localeCompare(bName);
609
- })
610
- .map((attr) => (
611
- <TreeItem key={attr.LogicalName} itemType="leaf">
612
- <TreeItemLayout
613
- onClick={() =>
614
- handleRelatedAttributeToggle(
615
- rel,
616
- attr,
617
- "N1",
618
- relItem.relatedEntity
619
- )
620
- }
621
- >
622
- <div className={styles.attributeItem}>
623
- <Checkbox
624
- checked={isRelatedAttributeSelected(rel, attr)}
625
- onChange={(e) => {
626
- e.stopPropagation();
627
- handleRelatedAttributeToggle(
628
- rel,
629
- attr,
630
- "N1",
631
- relItem.relatedEntity
632
- );
633
- }}
634
- />
635
- <Text>
636
- {attr.DisplayName?.UserLocalizedLabel?.Label ||
637
- attr.LogicalName}
638
- </Text>
639
- </div>
640
- </TreeItemLayout>
641
- </TreeItem>
642
- ))
643
- ) : (
644
- <TreeItem itemType="leaf">
645
- <TreeItemLayout>
646
- <Text style={{ color: tokens.colorNeutralForeground3 }}>
647
- No attributes available
648
- </Text>
649
- </TreeItemLayout>
650
- </TreeItem>
651
- )}
652
- </Tree>
653
- )}
654
- </TreeItem>
655
- );
656
- })}
657
- </Tree>
658
- </AccordionPanel>
659
- </AccordionItem>
660
- )}
661
-
662
- {/* 1:N (One-to-Many) Relationships */}
663
- {filtered1NRelationships.length > 0 && (
664
- <AccordionItem value="1n">
665
- <AccordionHeader size="small">
666
- <div className={styles.accordionHeader}>
667
- <Text weight="semibold">Related (1:N)</Text>
668
- <Badge appearance="filled" color="informative" size="small">
669
- {filtered1NRelationships.length}
670
- </Badge>
671
- <Badge
672
- appearance="tint"
673
- color="warning"
674
- size="small"
675
- className={styles.warningBadge}
676
- >
677
- May duplicate rows
678
- </Badge>
679
- </div>
680
- </AccordionHeader>
681
- <AccordionPanel>
682
- <Tree aria-label="One-to-many relationships">
683
- {filtered1NRelationships.map((relItem) => {
684
- const rel = relItem.relationship;
685
- const isExpanded = expandedRelationships.has(rel.SchemaName);
686
- const relatedAttrs = relatedAttributesCache.get(relItem.relatedEntity);
687
- const isLoading = loadingEntities.has(relItem.relatedEntity);
688
-
689
- return (
690
- <TreeItem key={rel.SchemaName} itemType="branch" open={isExpanded}>
691
- <TreeItemLayout
692
- onClick={() =>
693
- handleExpandRelationship(rel, relItem.relatedEntity)
694
- }
695
- >
696
- <div className={styles.relationshipHeader}>
697
- <div className={styles.relationshipLabel}>
698
- <span className={styles.relationshipName}>
699
- {relItem.displayName}
700
- </span>
701
- <span className={styles.relationshipEntity}>
702
- {rel.SchemaName}
703
- </span>
704
- </div>
705
- </div>
706
- </TreeItemLayout>
707
- {isExpanded && (
708
- <Tree>
709
- {isLoading ? (
710
- <TreeItem itemType="leaf">
711
- <TreeItemLayout>
712
- <Spinner size="tiny" label="Loading..." />
713
- </TreeItemLayout>
714
- </TreeItem>
715
- ) : relatedAttrs && relatedAttrs.length > 0 ? (
716
- relatedAttrs
717
- .filter((attr) => attr.AttributeType !== "Virtual")
718
- .sort((a, b) => {
719
- const aName =
720
- a.DisplayName?.UserLocalizedLabel?.Label ||
721
- a.LogicalName;
722
- const bName =
723
- b.DisplayName?.UserLocalizedLabel?.Label ||
724
- b.LogicalName;
725
- return aName.localeCompare(bName);
726
- })
727
- .map((attr) => (
728
- <TreeItem key={attr.LogicalName} itemType="leaf">
729
- <TreeItemLayout
730
- onClick={() =>
731
- handleRelatedAttributeToggle(
732
- rel,
733
- attr,
734
- "1N",
735
- relItem.relatedEntity
736
- )
737
- }
738
- >
739
- <div className={styles.attributeItem}>
740
- <Checkbox
741
- checked={isRelatedAttributeSelected(rel, attr)}
742
- onChange={(e) => {
743
- e.stopPropagation();
744
- handleRelatedAttributeToggle(
745
- rel,
746
- attr,
747
- "1N",
748
- relItem.relatedEntity
749
- );
750
- }}
751
- />
752
- <Text>
753
- {attr.DisplayName?.UserLocalizedLabel?.Label ||
754
- attr.LogicalName}
755
- </Text>
756
- </div>
757
- </TreeItemLayout>
758
- </TreeItem>
759
- ))
760
- ) : (
761
- <TreeItem itemType="leaf">
762
- <TreeItemLayout>
763
- <Text style={{ color: tokens.colorNeutralForeground3 }}>
764
- No attributes available
765
- </Text>
766
- </TreeItemLayout>
767
- </TreeItem>
768
- )}
769
- </Tree>
770
- )}
771
- </TreeItem>
772
- );
773
- })}
774
- </Tree>
775
- </AccordionPanel>
776
- </AccordionItem>
777
- )}
778
- </Accordion>
779
- )}
780
- </div>
781
- )}
782
- </div>
783
-
784
- <div className={styles.footer}>
785
- {totalSelected > 0 && (
786
- <Text className={styles.selectedCount}>
787
- {totalSelected} column{totalSelected !== 1 ? "s" : ""} selected
788
- </Text>
789
- )}
790
- <Button appearance="secondary" onClick={onClose}>
791
- Close
792
- </Button>
793
- <Button appearance="primary" onClick={handleApply} disabled={totalSelected === 0}>
794
- Add
795
- </Button>
796
- </div>
797
- </DrawerBody>
798
- </OverlayDrawer>
799
- );
800
- }