@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,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
- }