@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.
- package/LICENSE +661 -0
- 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,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Relationship Picker component using Fluent UI v9 Combobox
|
|
3
|
-
* Loads relationships from metadata and displays categorized by type (1:N, N:1, N:N)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect } from "react";
|
|
7
|
-
import {
|
|
8
|
-
Combobox,
|
|
9
|
-
Option,
|
|
10
|
-
OptionGroup,
|
|
11
|
-
useId,
|
|
12
|
-
Spinner,
|
|
13
|
-
type ComboboxProps,
|
|
14
|
-
} from "@fluentui/react-components";
|
|
15
|
-
import { debugLog } from "../utils/debug";
|
|
16
|
-
import { loadEntityRelationships } from "../../features/fetchxml/api/dataverseMetadata";
|
|
17
|
-
import type { RelationshipMetadata } from "../../features/fetchxml/api/pptbClient";
|
|
18
|
-
|
|
19
|
-
interface RelationshipOption {
|
|
20
|
-
schemaName: string;
|
|
21
|
-
displayText: string;
|
|
22
|
-
referencedEntity: string;
|
|
23
|
-
referencingEntity: string;
|
|
24
|
-
referencedAttribute: string;
|
|
25
|
-
referencingAttribute: string;
|
|
26
|
-
type: "1N" | "N1" | "NN";
|
|
27
|
-
category: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface RelationshipPickerProps {
|
|
31
|
-
entityLogicalName: string;
|
|
32
|
-
value?: string; // SchemaName of selected relationship
|
|
33
|
-
onChange: (relationship: RelationshipOption) => void;
|
|
34
|
-
placeholder?: string;
|
|
35
|
-
disabled?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function RelationshipPicker({
|
|
39
|
-
entityLogicalName,
|
|
40
|
-
value,
|
|
41
|
-
onChange,
|
|
42
|
-
placeholder = "Select a relationship",
|
|
43
|
-
disabled = false,
|
|
44
|
-
}: RelationshipPickerProps) {
|
|
45
|
-
const comboId = useId("relationship-combobox");
|
|
46
|
-
const [relationships, setRelationships] = useState<RelationshipOption[]>([]);
|
|
47
|
-
const [loading, setLoading] = useState(false);
|
|
48
|
-
const [error, setError] = useState<string | null>(null);
|
|
49
|
-
const [query, setQuery] = useState<string>("");
|
|
50
|
-
|
|
51
|
-
// Load relationships when entity changes
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (!entityLogicalName) {
|
|
54
|
-
debugLog("relationshipPicker", "No entity name provided, clearing relationships");
|
|
55
|
-
setRelationships([]);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
debugLog("relationshipPicker", "Loading relationships for entity:", entityLogicalName);
|
|
60
|
-
setLoading(true);
|
|
61
|
-
setError(null);
|
|
62
|
-
|
|
63
|
-
loadEntityRelationships(entityLogicalName)
|
|
64
|
-
.then((data) => {
|
|
65
|
-
debugLog("relationshipPicker", "Received relationship data:", {
|
|
66
|
-
entityLogicalName,
|
|
67
|
-
oneToManyCount: data.oneToMany.length,
|
|
68
|
-
manyToOneCount: data.manyToOne.length,
|
|
69
|
-
manyToManyCount: data.manyToMany.length,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const options: RelationshipOption[] = [];
|
|
73
|
-
|
|
74
|
-
// Process 1:N relationships (current entity is Referenced/parent)
|
|
75
|
-
data.oneToMany.forEach((rel: RelationshipMetadata) => {
|
|
76
|
-
// Show: childEntity โ parentEntity via lookupField (SchemaName)
|
|
77
|
-
const lookupField = rel.ReferencingAttribute || "";
|
|
78
|
-
const displayText = lookupField
|
|
79
|
-
? `${rel.ReferencingEntity} โ ${rel.ReferencedEntity} via ${lookupField} (${rel.SchemaName})`
|
|
80
|
-
: `${rel.ReferencingEntity} โ ${rel.ReferencedEntity} (${rel.SchemaName})`;
|
|
81
|
-
|
|
82
|
-
options.push({
|
|
83
|
-
schemaName: rel.SchemaName,
|
|
84
|
-
displayText,
|
|
85
|
-
referencedEntity: rel.ReferencedEntity,
|
|
86
|
-
referencingEntity: rel.ReferencingEntity,
|
|
87
|
-
referencedAttribute: rel.ReferencedAttribute,
|
|
88
|
-
referencingAttribute: rel.ReferencingAttribute,
|
|
89
|
-
type: "1N",
|
|
90
|
-
category: "One-to-Many (1:N)",
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Process N:1 relationships (current entity is Referencing/child)
|
|
95
|
-
data.manyToOne.forEach((rel: RelationshipMetadata) => {
|
|
96
|
-
// Show: childEntity โ parentEntity via lookupField (SchemaName)
|
|
97
|
-
const lookupField = rel.ReferencingAttribute || "";
|
|
98
|
-
const displayText = lookupField
|
|
99
|
-
? `${rel.ReferencingEntity} โ ${rel.ReferencedEntity} via ${lookupField} (${rel.SchemaName})`
|
|
100
|
-
: `${rel.ReferencingEntity} โ ${rel.ReferencedEntity} (${rel.SchemaName})`;
|
|
101
|
-
|
|
102
|
-
options.push({
|
|
103
|
-
schemaName: rel.SchemaName,
|
|
104
|
-
displayText,
|
|
105
|
-
referencedEntity: rel.ReferencedEntity,
|
|
106
|
-
referencingEntity: rel.ReferencingEntity,
|
|
107
|
-
referencedAttribute: rel.ReferencedAttribute,
|
|
108
|
-
referencingAttribute: rel.ReferencingAttribute,
|
|
109
|
-
type: "N1",
|
|
110
|
-
category: "Many-to-One (N:1)",
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Process N:N relationships
|
|
115
|
-
data.manyToMany.forEach((rel: RelationshipMetadata) => {
|
|
116
|
-
// N:N relationships have Entity1/Entity2, not Referencing/Referenced
|
|
117
|
-
const entity1 = rel.Entity1LogicalName || "";
|
|
118
|
-
const entity2 = rel.Entity2LogicalName || "";
|
|
119
|
-
const attr1 = rel.Entity1IntersectAttribute || "";
|
|
120
|
-
const attr2 = rel.Entity2IntersectAttribute || "";
|
|
121
|
-
|
|
122
|
-
options.push({
|
|
123
|
-
schemaName: rel.SchemaName,
|
|
124
|
-
displayText: `${entity1} โ ${entity2} (${rel.SchemaName})`,
|
|
125
|
-
referencedEntity: entity2, // Store entity2 as "referenced" for compatibility
|
|
126
|
-
referencingEntity: entity1, // Store entity1 as "referencing" for compatibility
|
|
127
|
-
referencedAttribute: attr2,
|
|
128
|
-
referencingAttribute: attr1,
|
|
129
|
-
type: "NN",
|
|
130
|
-
category: "Many-to-Many (N:N)",
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
setRelationships(options);
|
|
135
|
-
setLoading(false);
|
|
136
|
-
debugLog("relationshipPicker", "Relationships loaded successfully:", {
|
|
137
|
-
entityLogicalName,
|
|
138
|
-
totalRelationships: options.length,
|
|
139
|
-
});
|
|
140
|
-
})
|
|
141
|
-
.catch((err: unknown) => {
|
|
142
|
-
console.error("[RelationshipPicker] Failed to load relationships:", {
|
|
143
|
-
entityLogicalName,
|
|
144
|
-
error: err,
|
|
145
|
-
});
|
|
146
|
-
setError("Failed to load relationships");
|
|
147
|
-
setLoading(false);
|
|
148
|
-
});
|
|
149
|
-
}, [entityLogicalName]);
|
|
150
|
-
|
|
151
|
-
// Sync query with selected value
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
if (value) {
|
|
154
|
-
const rel = relationships.find((r) => r.schemaName === value);
|
|
155
|
-
if (rel) {
|
|
156
|
-
setQuery(rel.displayText);
|
|
157
|
-
} else {
|
|
158
|
-
setQuery(value);
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
setQuery("");
|
|
162
|
-
}
|
|
163
|
-
}, [value, relationships]);
|
|
164
|
-
|
|
165
|
-
// Manual filtering that preserves grouping
|
|
166
|
-
const filteredRelationships = query.trim()
|
|
167
|
-
? relationships.filter(
|
|
168
|
-
(rel) =>
|
|
169
|
-
rel.displayText.toLowerCase().includes(query.toLowerCase()) ||
|
|
170
|
-
rel.schemaName.toLowerCase().includes(query.toLowerCase()) ||
|
|
171
|
-
rel.referencingAttribute.toLowerCase().includes(query.toLowerCase())
|
|
172
|
-
)
|
|
173
|
-
: relationships;
|
|
174
|
-
|
|
175
|
-
// Group by category
|
|
176
|
-
const categories = {
|
|
177
|
-
"One-to-Many (1:N)": filteredRelationships.filter((r) => r.category === "One-to-Many (1:N)"),
|
|
178
|
-
"Many-to-One (N:1)": filteredRelationships.filter((r) => r.category === "Many-to-One (N:1)"),
|
|
179
|
-
"Many-to-Many (N:N)": filteredRelationships.filter((r) => r.category === "Many-to-Many (N:N)"),
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const onOptionSelect: ComboboxProps["onOptionSelect"] = (_e, data) => {
|
|
183
|
-
const schemaName = data.optionValue;
|
|
184
|
-
debugLog("relationshipPicker", "Relationship selected:", {
|
|
185
|
-
schemaName,
|
|
186
|
-
optionText: data.optionText,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (schemaName) {
|
|
190
|
-
const rel = relationships.find((r) => r.schemaName === schemaName);
|
|
191
|
-
if (rel) {
|
|
192
|
-
debugLog("relationshipPicker", "Found relationship details:", rel);
|
|
193
|
-
setQuery(data.optionText ?? "");
|
|
194
|
-
onChange(rel);
|
|
195
|
-
}
|
|
196
|
-
} else {
|
|
197
|
-
// Handle clear
|
|
198
|
-
setQuery("");
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
if (loading) {
|
|
203
|
-
return <Spinner size="tiny" label="Loading relationships..." />;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (error) {
|
|
207
|
-
return (
|
|
208
|
-
<Combobox
|
|
209
|
-
aria-labelledby={comboId}
|
|
210
|
-
value=""
|
|
211
|
-
placeholder="Error loading relationships"
|
|
212
|
-
disabled={true}
|
|
213
|
-
/>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<Combobox
|
|
219
|
-
aria-labelledby={comboId}
|
|
220
|
-
value={query}
|
|
221
|
-
onOptionSelect={onOptionSelect}
|
|
222
|
-
onChange={(ev) => setQuery(ev.target.value)}
|
|
223
|
-
placeholder={
|
|
224
|
-
loading
|
|
225
|
-
? "Loading relationships..."
|
|
226
|
-
: relationships.length === 0
|
|
227
|
-
? "No relationships found"
|
|
228
|
-
: placeholder
|
|
229
|
-
}
|
|
230
|
-
disabled={disabled || loading || relationships.length === 0}
|
|
231
|
-
clearable
|
|
232
|
-
>
|
|
233
|
-
{filteredRelationships.length === 0 && query.trim() ? (
|
|
234
|
-
<Option disabled>No relationships match your search</Option>
|
|
235
|
-
) : (
|
|
236
|
-
<>
|
|
237
|
-
{categories["One-to-Many (1:N)"].length > 0 && (
|
|
238
|
-
<OptionGroup label="One-to-Many (1:N)">
|
|
239
|
-
{categories["One-to-Many (1:N)"].map((rel) => (
|
|
240
|
-
<Option key={rel.schemaName} value={rel.schemaName}>
|
|
241
|
-
{rel.displayText}
|
|
242
|
-
</Option>
|
|
243
|
-
))}
|
|
244
|
-
</OptionGroup>
|
|
245
|
-
)}
|
|
246
|
-
{categories["Many-to-One (N:1)"].length > 0 && (
|
|
247
|
-
<OptionGroup label="Many-to-One (N:1)">
|
|
248
|
-
{categories["Many-to-One (N:1)"].map((rel) => (
|
|
249
|
-
<Option key={rel.schemaName} value={rel.schemaName}>
|
|
250
|
-
{rel.displayText}
|
|
251
|
-
</Option>
|
|
252
|
-
))}
|
|
253
|
-
</OptionGroup>
|
|
254
|
-
)}
|
|
255
|
-
{categories["Many-to-Many (N:N)"].length > 0 && (
|
|
256
|
-
<OptionGroup label="Many-to-Many (N:N)">
|
|
257
|
-
{categories["Many-to-Many (N:N)"].map((rel) => (
|
|
258
|
-
<Option key={rel.schemaName} value={rel.schemaName}>
|
|
259
|
-
{rel.displayText}
|
|
260
|
-
</Option>
|
|
261
|
-
))}
|
|
262
|
-
</OptionGroup>
|
|
263
|
-
)}
|
|
264
|
-
</>
|
|
265
|
-
)}
|
|
266
|
-
</Combobox>
|
|
267
|
-
);
|
|
268
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme context for accessing dark/light mode throughout the app
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
interface ThemeContextValue {
|
|
8
|
-
isDark: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const ThemeContext = createContext<ThemeContextValue>({ isDark: false });
|
|
12
|
-
|
|
13
|
-
export function ThemeProvider({ isDark, children }: { isDark: boolean; children: ReactNode }) {
|
|
14
|
-
return <ThemeContext.Provider value={{ isDark }}>{children}</ThemeContext.Provider>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function useTheme(): ThemeContextValue {
|
|
18
|
-
return useContext(ThemeContext);
|
|
19
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook to determine user's access mode based on privileges
|
|
3
|
-
* Checks WhoAmI and privilege checks on mount
|
|
4
|
-
* Preloads all entity metadata if user has customization access
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useEffect } from "react";
|
|
8
|
-
import {
|
|
9
|
-
getAccessSummary,
|
|
10
|
-
getAllAdvancedFindEntities,
|
|
11
|
-
type AccessSummary,
|
|
12
|
-
} from "../../features/fetchxml/api/pptbClient";
|
|
13
|
-
|
|
14
|
-
export function useAccessMode() {
|
|
15
|
-
const [accessSummary, setAccessSummary] = useState<AccessSummary | null>(null);
|
|
16
|
-
const [loading, setLoading] = useState(true);
|
|
17
|
-
const [error, setError] = useState<string | null>(null);
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
let mounted = true;
|
|
21
|
-
|
|
22
|
-
async function checkAccess() {
|
|
23
|
-
try {
|
|
24
|
-
setLoading(true);
|
|
25
|
-
setError(null);
|
|
26
|
-
|
|
27
|
-
const summary = await getAccessSummary();
|
|
28
|
-
|
|
29
|
-
if (mounted) {
|
|
30
|
-
setAccessSummary(summary);
|
|
31
|
-
|
|
32
|
-
// Preload all entity metadata if user has customization access
|
|
33
|
-
if (summary && !summary.noAccessMode) {
|
|
34
|
-
console.log("[useAccessMode] Preloading all entity metadata...");
|
|
35
|
-
getAllAdvancedFindEntities()
|
|
36
|
-
.then((entities) => {
|
|
37
|
-
console.log(
|
|
38
|
-
"[useAccessMode] Entity metadata preloaded:",
|
|
39
|
-
entities.length,
|
|
40
|
-
"entities"
|
|
41
|
-
);
|
|
42
|
-
})
|
|
43
|
-
.catch((err) => {
|
|
44
|
-
console.error("[useAccessMode] Failed to preload entity metadata:", err);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
if (mounted) {
|
|
50
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
51
|
-
}
|
|
52
|
-
} finally {
|
|
53
|
-
if (mounted) {
|
|
54
|
-
setLoading(false);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
checkAccess();
|
|
60
|
-
|
|
61
|
-
return () => {
|
|
62
|
-
mounted = false;
|
|
63
|
-
};
|
|
64
|
-
}, []);
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
accessSummary,
|
|
68
|
-
loading,
|
|
69
|
-
error,
|
|
70
|
-
// Convenience flags
|
|
71
|
-
fullFilterMode: accessSummary?.fullFilterMode ?? false,
|
|
72
|
-
solutionsOnlyMode: accessSummary?.solutionsOnlyMode ?? false,
|
|
73
|
-
publishersOnlyMode: accessSummary?.publishersOnlyMode ?? false,
|
|
74
|
-
metadataOnlyMode: accessSummary?.metadataOnlyMode ?? false,
|
|
75
|
-
noAccessMode: accessSummary?.noAccessMode ?? false,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React hook for lazy metadata loading with loading states
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useState, useCallback } from "react";
|
|
6
|
-
import type {
|
|
7
|
-
EntityMetadata,
|
|
8
|
-
AttributeMetadata,
|
|
9
|
-
RelationshipMetadata,
|
|
10
|
-
} from "../../features/fetchxml/api/pptbClient";
|
|
11
|
-
import * as metadataLoader from "../../features/fetchxml/api/dataverseMetadata";
|
|
12
|
-
import { metadataCache } from "../../features/fetchxml/state/cache";
|
|
13
|
-
|
|
14
|
-
interface UseLazyMetadataResult {
|
|
15
|
-
// Loaders
|
|
16
|
-
loadEntities: (advancedFindOnly?: boolean) => Promise<EntityMetadata[]>;
|
|
17
|
-
loadEntityMetadata: (logicalName: string) => Promise<EntityMetadata>;
|
|
18
|
-
loadAttributes: (logicalName: string) => Promise<AttributeMetadata[]>;
|
|
19
|
-
loadRelationships: (logicalName: string) => Promise<{
|
|
20
|
-
oneToMany: RelationshipMetadata[];
|
|
21
|
-
manyToOne: RelationshipMetadata[];
|
|
22
|
-
manyToMany: RelationshipMetadata[];
|
|
23
|
-
}>;
|
|
24
|
-
|
|
25
|
-
// State
|
|
26
|
-
isLoading: boolean;
|
|
27
|
-
error: Error | null;
|
|
28
|
-
|
|
29
|
-
// Utilities
|
|
30
|
-
clearCache: () => void;
|
|
31
|
-
clearEntityCache: (logicalName: string) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function useLazyMetadata(): UseLazyMetadataResult {
|
|
35
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
36
|
-
const [error, setError] = useState<Error | null>(null);
|
|
37
|
-
|
|
38
|
-
const loadEntities = useCallback(async (advancedFindOnly: boolean = true) => {
|
|
39
|
-
setIsLoading(true);
|
|
40
|
-
setError(null);
|
|
41
|
-
try {
|
|
42
|
-
// Check global cache first (preloaded by useAccessMode)
|
|
43
|
-
if (advancedFindOnly) {
|
|
44
|
-
const cached = metadataCache.getAllEntityMetadata();
|
|
45
|
-
if (cached) {
|
|
46
|
-
console.log(
|
|
47
|
-
"[useLazyMetadata] Using preloaded entity metadata:",
|
|
48
|
-
cached.length,
|
|
49
|
-
"entities"
|
|
50
|
-
);
|
|
51
|
-
return cached;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const entities = await metadataLoader.loadAllEntities(advancedFindOnly);
|
|
56
|
-
return entities;
|
|
57
|
-
} catch (err) {
|
|
58
|
-
const error = err instanceof Error ? err : new Error("Failed to load entities");
|
|
59
|
-
setError(error);
|
|
60
|
-
throw error;
|
|
61
|
-
} finally {
|
|
62
|
-
setIsLoading(false);
|
|
63
|
-
}
|
|
64
|
-
}, []);
|
|
65
|
-
|
|
66
|
-
const loadEntityMetadata = useCallback(async (logicalName: string) => {
|
|
67
|
-
setIsLoading(true);
|
|
68
|
-
setError(null);
|
|
69
|
-
try {
|
|
70
|
-
const metadata = await metadataLoader.loadEntityMetadata(logicalName);
|
|
71
|
-
return metadata;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
const error =
|
|
74
|
-
err instanceof Error ? err : new Error(`Failed to load metadata for ${logicalName}`);
|
|
75
|
-
setError(error);
|
|
76
|
-
throw error;
|
|
77
|
-
} finally {
|
|
78
|
-
setIsLoading(false);
|
|
79
|
-
}
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
const loadAttributes = useCallback(async (logicalName: string) => {
|
|
83
|
-
setIsLoading(true);
|
|
84
|
-
setError(null);
|
|
85
|
-
try {
|
|
86
|
-
const attributes = await metadataLoader.loadEntityAttributes(logicalName);
|
|
87
|
-
return attributes;
|
|
88
|
-
} catch (err) {
|
|
89
|
-
const error =
|
|
90
|
-
err instanceof Error ? err : new Error(`Failed to load attributes for ${logicalName}`);
|
|
91
|
-
setError(error);
|
|
92
|
-
throw error;
|
|
93
|
-
} finally {
|
|
94
|
-
setIsLoading(false);
|
|
95
|
-
}
|
|
96
|
-
}, []);
|
|
97
|
-
|
|
98
|
-
const loadRelationships = useCallback(async (logicalName: string) => {
|
|
99
|
-
setIsLoading(true);
|
|
100
|
-
setError(null);
|
|
101
|
-
try {
|
|
102
|
-
const relationships = await metadataLoader.loadEntityRelationships(logicalName);
|
|
103
|
-
return relationships;
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const error =
|
|
106
|
-
err instanceof Error ? err : new Error(`Failed to load relationships for ${logicalName}`);
|
|
107
|
-
setError(error);
|
|
108
|
-
throw error;
|
|
109
|
-
} finally {
|
|
110
|
-
setIsLoading(false);
|
|
111
|
-
}
|
|
112
|
-
}, []);
|
|
113
|
-
|
|
114
|
-
const clearCache = useCallback(() => {
|
|
115
|
-
metadataLoader.clearMetadataCache();
|
|
116
|
-
}, []);
|
|
117
|
-
|
|
118
|
-
const clearEntityCache = useCallback((logicalName: string) => {
|
|
119
|
-
metadataLoader.clearEntityCache(logicalName);
|
|
120
|
-
}, []);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
loadEntities,
|
|
124
|
-
loadEntityMetadata,
|
|
125
|
-
loadAttributes,
|
|
126
|
-
loadRelationships,
|
|
127
|
-
isLoading,
|
|
128
|
-
error,
|
|
129
|
-
clearCache,
|
|
130
|
-
clearEntityCache,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook to access PPTB host context (connection, theme, etc.)
|
|
3
|
-
* Subscribes to PPTB events for dynamic updates
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect } from "react";
|
|
7
|
-
|
|
8
|
-
export interface PptbContext {
|
|
9
|
-
theme: "light" | "dark";
|
|
10
|
-
connected: boolean;
|
|
11
|
-
environmentUrl?: string;
|
|
12
|
-
organizationId?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// PPTB Event payload structure
|
|
16
|
-
interface ToolBoxEventPayload {
|
|
17
|
-
event: string;
|
|
18
|
-
data: unknown;
|
|
19
|
-
timestamp?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Extend window interface for PPTB API
|
|
23
|
-
declare global {
|
|
24
|
-
interface Window {
|
|
25
|
-
toolboxAPI?: {
|
|
26
|
-
utils?: {
|
|
27
|
-
getCurrentTheme?: () => Promise<"light" | "dark">;
|
|
28
|
-
};
|
|
29
|
-
connections?: {
|
|
30
|
-
getActiveConnection?: () => Promise<{
|
|
31
|
-
url?: string;
|
|
32
|
-
organizationId?: string;
|
|
33
|
-
name?: string;
|
|
34
|
-
environment?: string;
|
|
35
|
-
} | null>;
|
|
36
|
-
};
|
|
37
|
-
events?: {
|
|
38
|
-
// PPTB uses a single callback that receives all events
|
|
39
|
-
on?: (callback: (event: unknown, payload: ToolBoxEventPayload) => void) => void;
|
|
40
|
-
off?: (callback: (event: unknown, payload: ToolBoxEventPayload) => void) => void;
|
|
41
|
-
getHistory?: (count: number) => Promise<ToolBoxEventPayload[]>;
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get tool context from PPTB host
|
|
49
|
-
* Returns theme and connection status
|
|
50
|
-
* Subscribes to PPTB events for dynamic updates (theme, connection changes)
|
|
51
|
-
*/
|
|
52
|
-
export function usePptbContext(): PptbContext {
|
|
53
|
-
const [context, setContext] = useState<PptbContext>({
|
|
54
|
-
theme: "light", // Default to light theme
|
|
55
|
-
connected: false,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
console.log("๐ usePptbContext: Initializing...");
|
|
60
|
-
|
|
61
|
-
// Check if running in PPTB host
|
|
62
|
-
if (typeof window === "undefined" || !window.toolboxAPI) {
|
|
63
|
-
console.log("๐ก Running standalone - using default light theme");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const toolboxAPI = window.toolboxAPI;
|
|
68
|
-
|
|
69
|
-
// ============ THEME ============
|
|
70
|
-
const updateTheme = async () => {
|
|
71
|
-
try {
|
|
72
|
-
const hostTheme = await toolboxAPI.utils?.getCurrentTheme?.();
|
|
73
|
-
console.log("๐จ getCurrentTheme() returned:", hostTheme);
|
|
74
|
-
const normalizedTheme =
|
|
75
|
-
hostTheme && String(hostTheme).toLowerCase() === "dark" ? "dark" : "light";
|
|
76
|
-
setContext((prev) => ({
|
|
77
|
-
...prev,
|
|
78
|
-
theme: normalizedTheme,
|
|
79
|
-
}));
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.warn("โ Failed to get theme from host:", err);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// ============ CONNECTION ============
|
|
86
|
-
const updateConnection = async () => {
|
|
87
|
-
try {
|
|
88
|
-
const connection = await toolboxAPI.connections?.getActiveConnection?.();
|
|
89
|
-
console.log("๐ getActiveConnection() returned:", connection);
|
|
90
|
-
setContext((prev) => ({
|
|
91
|
-
...prev,
|
|
92
|
-
connected: !!connection,
|
|
93
|
-
environmentUrl: connection?.url,
|
|
94
|
-
organizationId: connection?.organizationId,
|
|
95
|
-
}));
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.warn("โ Failed to get active connection:", err);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// ============ EVENT HANDLER ============
|
|
102
|
-
// PPTB calls this with (event, payload) where payload has { event, data, timestamp }
|
|
103
|
-
const handlePptbEvent = (_event: unknown, payload: ToolBoxEventPayload) => {
|
|
104
|
-
console.log("๐ PPTB Event received:", payload.event, payload.data);
|
|
105
|
-
|
|
106
|
-
switch (payload.event) {
|
|
107
|
-
case "settings:updated":
|
|
108
|
-
// Settings changed - check if theme was updated
|
|
109
|
-
if (payload.data && typeof payload.data === "object" && "theme" in payload.data) {
|
|
110
|
-
const themeValue = (payload.data as { theme: string }).theme;
|
|
111
|
-
console.log("๐จ Theme changed via settings:updated:", themeValue);
|
|
112
|
-
const normalizedTheme = String(themeValue).toLowerCase() === "dark" ? "dark" : "light";
|
|
113
|
-
setContext((prev) => ({
|
|
114
|
-
...prev,
|
|
115
|
-
theme: normalizedTheme,
|
|
116
|
-
}));
|
|
117
|
-
} else {
|
|
118
|
-
// Theme might have changed, refetch it
|
|
119
|
-
updateTheme();
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
|
|
123
|
-
case "connection:updated":
|
|
124
|
-
case "connection:created":
|
|
125
|
-
// Connection changed - refetch connection info
|
|
126
|
-
console.log("๐ Connection changed, refreshing...");
|
|
127
|
-
updateConnection();
|
|
128
|
-
break;
|
|
129
|
-
|
|
130
|
-
case "connection:deleted":
|
|
131
|
-
// Connection was deleted
|
|
132
|
-
console.log("๐ Connection deleted");
|
|
133
|
-
setContext((prev) => ({
|
|
134
|
-
...prev,
|
|
135
|
-
connected: false,
|
|
136
|
-
environmentUrl: undefined,
|
|
137
|
-
organizationId: undefined,
|
|
138
|
-
}));
|
|
139
|
-
break;
|
|
140
|
-
|
|
141
|
-
default:
|
|
142
|
-
// Log other events for debugging
|
|
143
|
-
console.log("๐ข Unhandled PPTB event:", payload.event);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// ============ INITIALIZATION ============
|
|
148
|
-
// Get initial theme and connection
|
|
149
|
-
updateTheme();
|
|
150
|
-
updateConnection();
|
|
151
|
-
|
|
152
|
-
// Subscribe to events
|
|
153
|
-
if (toolboxAPI.events?.on) {
|
|
154
|
-
console.log("โ
Subscribing to PPTB events...");
|
|
155
|
-
toolboxAPI.events.on(handlePptbEvent);
|
|
156
|
-
} else {
|
|
157
|
-
console.warn("โ ๏ธ toolboxAPI.events.on not available");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ============ CLEANUP ============
|
|
161
|
-
return () => {
|
|
162
|
-
if (toolboxAPI.events?.off) {
|
|
163
|
-
console.log("๐งน Unsubscribing from PPTB events...");
|
|
164
|
-
toolboxAPI.events.off(handlePptbEvent);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}, []);
|
|
168
|
-
|
|
169
|
-
return context;
|
|
170
|
-
}
|