@mohsinonxrm/pptb-fetchxml-studio 1.0.0

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