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