@apvee/spfx-react-toolkit 1.2.1 → 2.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 (138) hide show
  1. package/lib/core/atoms.internal.js +1 -1
  2. package/lib/core/atoms.internal.js.map +1 -1
  3. package/lib/core/context.internal.js +2 -2
  4. package/lib/core/context.internal.js.map +1 -1
  5. package/lib/core/provider-base.internal.js +29 -29
  6. package/lib/core/provider-base.internal.js.map +1 -1
  7. package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.d.ts +14 -0
  8. package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.d.ts.map +1 -0
  9. package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.js +20 -0
  10. package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.js.map +1 -0
  11. package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.manifest.json +17 -0
  12. package/lib/extensions/spFxReactToolkitTest/loc/en-us.js +5 -0
  13. package/lib/hooks/index.d.ts +1 -0
  14. package/lib/hooks/index.d.ts.map +1 -1
  15. package/lib/hooks/index.js +1 -0
  16. package/lib/hooks/index.js.map +1 -1
  17. package/lib/hooks/useAppCatalogUrl.internal.d.ts +26 -0
  18. package/lib/hooks/useAppCatalogUrl.internal.d.ts.map +1 -0
  19. package/lib/hooks/useAppCatalogUrl.internal.js +72 -0
  20. package/lib/hooks/useAppCatalogUrl.internal.js.map +1 -0
  21. package/lib/hooks/useAsyncInvoke.internal.js +27 -75
  22. package/lib/hooks/useAsyncInvoke.internal.js.map +1 -1
  23. package/lib/hooks/useSPFxAadHttpClient.d.ts +46 -0
  24. package/lib/hooks/useSPFxAadHttpClient.d.ts.map +1 -1
  25. package/lib/hooks/useSPFxAadHttpClient.js +65 -20
  26. package/lib/hooks/useSPFxAadHttpClient.js.map +1 -1
  27. package/lib/hooks/useSPFxContainerInfo.js +5 -5
  28. package/lib/hooks/useSPFxContainerInfo.js.map +1 -1
  29. package/lib/hooks/useSPFxContainerSize.js +9 -10
  30. package/lib/hooks/useSPFxContainerSize.js.map +1 -1
  31. package/lib/hooks/useSPFxCorrelationInfo.js +6 -7
  32. package/lib/hooks/useSPFxCorrelationInfo.js.map +1 -1
  33. package/lib/hooks/useSPFxCrossSitePermissions.js +48 -58
  34. package/lib/hooks/useSPFxCrossSitePermissions.js.map +1 -1
  35. package/lib/hooks/useSPFxDisplayMode.js +8 -8
  36. package/lib/hooks/useSPFxDisplayMode.js.map +1 -1
  37. package/lib/hooks/useSPFxEnvironmentInfo.js +17 -18
  38. package/lib/hooks/useSPFxEnvironmentInfo.js.map +1 -1
  39. package/lib/hooks/useSPFxFluent9ThemeInfo.js +4 -4
  40. package/lib/hooks/useSPFxFluent9ThemeInfo.js.map +1 -1
  41. package/lib/hooks/useSPFxHttpClient.d.ts +18 -2
  42. package/lib/hooks/useSPFxHttpClient.d.ts.map +1 -1
  43. package/lib/hooks/useSPFxHttpClient.js +19 -9
  44. package/lib/hooks/useSPFxHttpClient.js.map +1 -1
  45. package/lib/hooks/useSPFxHubSiteInfo.js +21 -24
  46. package/lib/hooks/useSPFxHubSiteInfo.js.map +1 -1
  47. package/lib/hooks/useSPFxInstanceInfo.js +2 -2
  48. package/lib/hooks/useSPFxInstanceInfo.js.map +1 -1
  49. package/lib/hooks/useSPFxListInfo.js +8 -9
  50. package/lib/hooks/useSPFxListInfo.js.map +1 -1
  51. package/lib/hooks/useSPFxLocaleInfo.js +10 -10
  52. package/lib/hooks/useSPFxLocaleInfo.js.map +1 -1
  53. package/lib/hooks/useSPFxLogger.js +26 -26
  54. package/lib/hooks/useSPFxLogger.js.map +1 -1
  55. package/lib/hooks/useSPFxMSGraphClient.d.ts +50 -3
  56. package/lib/hooks/useSPFxMSGraphClient.d.ts.map +1 -1
  57. package/lib/hooks/useSPFxMSGraphClient.js +68 -15
  58. package/lib/hooks/useSPFxMSGraphClient.js.map +1 -1
  59. package/lib/hooks/useSPFxOneDriveAppData.d.ts +0 -1
  60. package/lib/hooks/useSPFxOneDriveAppData.d.ts.map +1 -1
  61. package/lib/hooks/useSPFxOneDriveAppData.js +420 -230
  62. package/lib/hooks/useSPFxOneDriveAppData.js.map +1 -1
  63. package/lib/hooks/useSPFxPageContext.js +2 -2
  64. package/lib/hooks/useSPFxPageContext.js.map +1 -1
  65. package/lib/hooks/useSPFxPageType.js +19 -20
  66. package/lib/hooks/useSPFxPageType.js.map +1 -1
  67. package/lib/hooks/useSPFxPerformance.js +33 -87
  68. package/lib/hooks/useSPFxPerformance.js.map +1 -1
  69. package/lib/hooks/useSPFxPermissions.js +14 -15
  70. package/lib/hooks/useSPFxPermissions.js.map +1 -1
  71. package/lib/hooks/useSPFxPnP.js +62 -119
  72. package/lib/hooks/useSPFxPnP.js.map +1 -1
  73. package/lib/hooks/useSPFxPnPContext.js +22 -25
  74. package/lib/hooks/useSPFxPnPContext.js.map +1 -1
  75. package/lib/hooks/useSPFxPnPList.js +307 -451
  76. package/lib/hooks/useSPFxPnPList.js.map +1 -1
  77. package/lib/hooks/useSPFxPnPSearch.js +262 -353
  78. package/lib/hooks/useSPFxPnPSearch.js.map +1 -1
  79. package/lib/hooks/useSPFxProperties.js +12 -20
  80. package/lib/hooks/useSPFxProperties.js.map +1 -1
  81. package/lib/hooks/useSPFxSPHttpClient.d.ts +18 -2
  82. package/lib/hooks/useSPFxSPHttpClient.d.ts.map +1 -1
  83. package/lib/hooks/useSPFxSPHttpClient.js +28 -18
  84. package/lib/hooks/useSPFxSPHttpClient.js.map +1 -1
  85. package/lib/hooks/useSPFxServiceScope.js +6 -6
  86. package/lib/hooks/useSPFxServiceScope.js.map +1 -1
  87. package/lib/hooks/useSPFxSiteInfo.js +7 -8
  88. package/lib/hooks/useSPFxSiteInfo.js.map +1 -1
  89. package/lib/hooks/useSPFxStorage.js +22 -22
  90. package/lib/hooks/useSPFxStorage.js.map +1 -1
  91. package/lib/hooks/useSPFxTeams.js +37 -92
  92. package/lib/hooks/useSPFxTeams.js.map +1 -1
  93. package/lib/hooks/useSPFxTenantKeyValueStore.d.ts +252 -0
  94. package/lib/hooks/useSPFxTenantKeyValueStore.d.ts.map +1 -0
  95. package/lib/hooks/useSPFxTenantKeyValueStore.js +572 -0
  96. package/lib/hooks/useSPFxTenantKeyValueStore.js.map +1 -0
  97. package/lib/hooks/useSPFxTenantProperty.d.ts +23 -244
  98. package/lib/hooks/useSPFxTenantProperty.d.ts.map +1 -1
  99. package/lib/hooks/useSPFxTenantProperty.js +85 -559
  100. package/lib/hooks/useSPFxTenantProperty.js.map +1 -1
  101. package/lib/hooks/useSPFxUserInfo.js +3 -4
  102. package/lib/hooks/useSPFxUserInfo.js.map +1 -1
  103. package/lib/hooks/useSPFxUserPhoto.js +76 -123
  104. package/lib/hooks/useSPFxUserPhoto.js.map +1 -1
  105. package/lib/utils/resize-observer.internal.js +6 -7
  106. package/lib/utils/resize-observer.internal.js.map +1 -1
  107. package/lib/utils/theme-subscription.internal.js +8 -8
  108. package/lib/utils/theme-subscription.internal.js.map +1 -1
  109. package/lib/utils/type-guards.internal.js +6 -6
  110. package/lib/utils/type-guards.internal.js.map +1 -1
  111. package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.js +12 -37
  112. package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.js.map +1 -1
  113. package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.d.ts.map +1 -1
  114. package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.js +279 -342
  115. package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.js.map +1 -1
  116. package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.js +1 -1
  117. package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.js.map +1 -1
  118. package/lib/webparts/spFxReactToolkitTest/components/demos/HttpClientDemo.js +26 -86
  119. package/lib/webparts/spFxReactToolkitTest/components/demos/HttpClientDemo.js.map +1 -1
  120. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPContextDemo.js +53 -113
  121. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPContextDemo.js.map +1 -1
  122. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPListDemo.js +49 -121
  123. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPListDemo.js.map +1 -1
  124. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPOperationsDemo.js +44 -103
  125. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPOperationsDemo.js.map +1 -1
  126. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchAdvancedDemo.js +15 -15
  127. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchAdvancedDemo.js.map +1 -1
  128. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchBasicDemo.js +18 -66
  129. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchBasicDemo.js.map +1 -1
  130. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchRefinersDemo.js +9 -9
  131. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchRefinersDemo.js.map +1 -1
  132. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchSuggestionsDemo.js +37 -86
  133. package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchSuggestionsDemo.js.map +1 -1
  134. package/lib/webparts/spFxReactToolkitTest/components/shared/InfoRow.js +6 -9
  135. package/lib/webparts/spFxReactToolkitTest/components/shared/InfoRow.js.map +1 -1
  136. package/lib/webparts/spFxReactToolkitTest/components/shared/StatusBadge.js +3 -6
  137. package/lib/webparts/spFxReactToolkitTest/components/shared/StatusBadge.js.map +1 -1
  138. package/package.json +8 -6
@@ -1,260 +1,450 @@
1
1
  // useSPFxOneDriveAppData.ts
2
2
  // Hook to manage JSON files in OneDrive appRoot folder with state management
3
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
- return new (P || (P = Promise))(function (resolve, reject) {
6
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
- step((generator = generator.apply(thisArg, _arguments || [])).next());
10
- });
11
- };
12
- var __generator = (this && this.__generator) || function (thisArg, body) {
13
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
14
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
15
- function verb(n) { return function (v) { return step([n, v]); }; }
16
- function step(op) {
17
- if (f) throw new TypeError("Generator is already executing.");
18
- while (g && (g = 0, op[0] && (_ = 0)), _) try {
19
- if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
20
- if (y = 0, t) op = [op[0] & 2, t.value];
21
- switch (op[0]) {
22
- case 0: case 1: t = op; break;
23
- case 4: _.label++; return { value: op[1], done: false };
24
- case 5: _.label++; y = op[1]; op = [0]; continue;
25
- case 7: op = _.ops.pop(); _.trys.pop(); continue;
26
- default:
27
- if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
28
- if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
29
- if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
30
- if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
31
- if (t[2]) _.ops.pop();
32
- _.trys.pop(); continue;
33
- }
34
- op = body.call(thisArg, _);
35
- } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
36
- if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
37
- }
38
- };
39
- import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
40
4
  import { useSPFxMSGraphClient } from './useSPFxMSGraphClient';
41
- export function useSPFxOneDriveAppData(fileName, folderOrOptions, autoFetch) {
42
- var _this = this;
43
- var _a, _b;
44
- if (autoFetch === void 0) { autoFetch = true; }
45
- var client = useSPFxMSGraphClient().client;
46
- var options = typeof folderOrOptions === 'object' && folderOrOptions !== null
47
- ? folderOrOptions
48
- : { folder: folderOrOptions, autoFetch: autoFetch };
49
- var folder = options.folder;
50
- var shouldAutoFetch = (_a = options.autoFetch) !== null && _a !== void 0 ? _a : true;
51
- var defaultValue = options.defaultValue;
52
- var createIfMissing = (_b = options.createIfMissing) !== null && _b !== void 0 ? _b : false;
53
- // State management
54
- var _c = useState(defaultValue), data = _c[0], setData = _c[1];
55
- var _d = useState(false), isLoading = _d[0], setIsLoading = _d[1];
56
- var _e = useState(undefined), error = _e[0], setError = _e[1];
57
- var _f = useState(false), isWriting = _f[0], setIsWriting = _f[1];
58
- var _g = useState(undefined), writeError = _g[0], setWriteError = _g[1];
59
- var _h = useState(false), isNotFound = _h[0], setIsNotFound = _h[1];
5
+ // ═══════════════════════════════════════════════════════════════════════════
6
+ // PURE FUNCTIONS (extracted for stability and testability)
7
+ // ═══════════════════════════════════════════════════════════════════════════
8
+ /**
9
+ * Build Graph API path with optional folder namespace.
10
+ * Sanitizes folder name to prevent path traversal attacks.
11
+ *
12
+ * @param fileName - Name of the file
13
+ * @param folderName - Optional folder namespace
14
+ * @returns Full Graph API path for file content
15
+ */
16
+ function buildApiPath(fileName, folderName) {
17
+ const basePath = '/me/drive/special/appRoot:';
18
+ if (folderName) {
19
+ // Sanitize folder name: only allow alphanumeric, hyphens, underscores
20
+ // This prevents path traversal (../) and other injection attacks
21
+ const safeFolderName = folderName.replace(/[^a-zA-Z0-9-_]/g, '-');
22
+ return `${basePath}/${safeFolderName}/${fileName}:/content`;
23
+ }
24
+ return `${basePath}/${fileName}:/content`;
25
+ }
26
+ /**
27
+ * Check if an error indicates a 404 / itemNotFound response from Graph API.
28
+ *
29
+ * @param err - The error to check
30
+ * @returns True if the error indicates file not found
31
+ */
32
+ function isNotFoundError(err) {
33
+ const anyErr = err;
34
+ // Check status codes
35
+ if (anyErr?.statusCode === 404 || anyErr?.status === 404)
36
+ return true;
37
+ // Check error codes
38
+ const code = anyErr?.code ?? anyErr?.body?.error?.code;
39
+ if (code && /itemnotfound/i.test(code))
40
+ return true;
41
+ // Check error messages as fallback
42
+ const message = anyErr?.message ?? anyErr?.body?.error?.message;
43
+ if (message && /(\b404\b|not found|itemnotfound)/i.test(message))
44
+ return true;
45
+ return false;
46
+ }
47
+ /**
48
+ * Hook to manage JSON files in user's OneDrive appRoot folder
49
+ *
50
+ * Provides unified read/write operations for JSON data stored in OneDrive's special
51
+ * appRoot folder (accessible per-app, user-scoped storage).
52
+ *
53
+ * Features:
54
+ * - Automatic JSON serialization/deserialization
55
+ * - Separate loading states for read/write operations
56
+ * - Optional auto-fetch on mount
57
+ * - Folder/namespace support for file organization
58
+ * - Type-safe with TypeScript generics
59
+ * - Memory leak safe with mounted state tracking
60
+ * - Error handling for both read and write operations
61
+ *
62
+ * Requirements:
63
+ * - Microsoft Graph permissions: Files.ReadWrite or Files.ReadWrite.AppFolder
64
+ * - User must be authenticated
65
+ *
66
+ * @param fileName - Name of the JSON file (e.g., 'config.json', 'settings.json')
67
+ * @param folder - Optional folder/namespace identifier for file organization.
68
+ * Will be sanitized to prevent path traversal.
69
+ * Examples: 'my-app', instanceId (GUID), 'config-v2'
70
+ * @param autoFetch - Whether to automatically load file on mount. Default: true
71
+ *
72
+ * @returns Object with data, loading states, error states, and read/write functions
73
+ *
74
+ * @example Basic usage - auto-fetch from root
75
+ * ```tsx
76
+ * import type { MyConfig } from './types';
77
+ *
78
+ * function ConfigPanel() {
79
+ * const { data, isLoading, error, write, isWriting } =
80
+ * useSPFxOneDriveAppData<MyConfig>('config.json');
81
+ *
82
+ * if (isLoading) return <Spinner label="Loading configuration..." />;
83
+ * if (error) return <MessageBar messageBarType={MessageBarType.error}>
84
+ * Failed to load: {error.message}
85
+ * </MessageBar>;
86
+ *
87
+ * const handleSave = async (newConfig: MyConfig) => {
88
+ * try {
89
+ * await write(newConfig);
90
+ * console.log('Saved successfully!');
91
+ * } catch (err) {
92
+ * console.error('Save failed:', err);
93
+ * }
94
+ * };
95
+ *
96
+ * return (
97
+ * <div>
98
+ * <TextField
99
+ * value={data?.title}
100
+ * onChange={(_, val) => handleSave({ ...data, title: val })}
101
+ * disabled={isWriting}
102
+ * />
103
+ * {isWriting && <Spinner label="Saving..." />}
104
+ * </div>
105
+ * );
106
+ * }
107
+ * ```
108
+ *
109
+ * @example With folder namespace
110
+ * ```tsx
111
+ * // Store files in a dedicated folder
112
+ * const { data, write } = useSPFxOneDriveAppData<State>(
113
+ * 'state.json',
114
+ * 'my-app-v2' // Files stored in appRoot:/my-app-v2/state.json
115
+ * );
116
+ * ```
117
+ *
118
+ * @example Per-instance storage (multi-instance support)
119
+ * ```tsx
120
+ * // Each WebPart instance has its own data
121
+ * const { id } = useSPFxInstanceInfo();
122
+ * const { data, write } = useSPFxOneDriveAppData<Settings>(
123
+ * 'settings.json',
124
+ * id // Files stored in appRoot:/abc-123-guid/settings.json
125
+ * );
126
+ * ```
127
+ *
128
+ * @example Lazy loading (manual load)
129
+ * ```tsx
130
+ * const { data, load, isLoading, write } = useSPFxOneDriveAppData<Cache>(
131
+ * 'cache.json',
132
+ * 'my-app',
133
+ * false // Don't auto-fetch
134
+ * );
135
+ *
136
+ * return (
137
+ * <div>
138
+ * <button onClick={load} disabled={isLoading}>
139
+ * {isLoading ? 'Loading...' : 'Load Cache'}
140
+ * </button>
141
+ * {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
142
+ * </div>
143
+ * );
144
+ * ```
145
+ *
146
+ * @example Multiple files in same namespace
147
+ * ```tsx
148
+ * function MyApp() {
149
+ * const config = useSPFxOneDriveAppData<Config>('config.json', 'myapp');
150
+ * const state = useSPFxOneDriveAppData<State>('state.json', 'myapp');
151
+ * const cache = useSPFxOneDriveAppData<Cache>('cache.json', 'myapp');
152
+ *
153
+ * // All files stored in appRoot:/myapp/
154
+ * // Easy to manage and clean up as a group
155
+ * }
156
+ * ```
157
+ *
158
+ * @example Error handling and retry
159
+ * ```tsx
160
+ * function DataManager() {
161
+ * const { data, error, load, writeError, write, isReady } =
162
+ * useSPFxOneDriveAppData<MyData>('data.json');
163
+ *
164
+ * if (error) {
165
+ * return (
166
+ * <MessageBar
167
+ * messageBarType={MessageBarType.error}
168
+ * actions={<button onClick={load}>Retry</button>}
169
+ * >
170
+ * Load failed: {error.message}
171
+ * </MessageBar>
172
+ * );
173
+ * }
174
+ *
175
+ * if (writeError) {
176
+ * return (
177
+ * <MessageBar messageBarType={MessageBarType.warning}>
178
+ * Save failed: {writeError.message}
179
+ * </MessageBar>
180
+ * );
181
+ * }
182
+ *
183
+ * if (!isReady) return <Spinner />;
184
+ *
185
+ * return <DataDisplay data={data} onSave={write} />;
186
+ * }
187
+ * ```
188
+ *
189
+ * @example CRUD-like operations
190
+ * ```tsx
191
+ * interface TodoList {
192
+ * items: Array<{ id: string; text: string; done: boolean }>;
193
+ * }
194
+ *
195
+ * function TodoApp() {
196
+ * const { data, write, isLoading, isWriting } =
197
+ * useSPFxOneDriveAppData<TodoList>('todos.json', 'todo-app');
198
+ *
199
+ * const addTodo = async (text: string) => {
200
+ * const newItem = { id: crypto.randomUUID(), text, done: false };
201
+ * await write({
202
+ * items: [...(data?.items ?? []), newItem]
203
+ * });
204
+ * };
205
+ *
206
+ * const toggleTodo = async (id: string) => {
207
+ * await write({
208
+ * items: data?.items.map(item =>
209
+ * item.id === id ? { ...item, done: !item.done } : item
210
+ * ) ?? []
211
+ * });
212
+ * };
213
+ *
214
+ * const deleteTodo = async (id: string) => {
215
+ * await write({
216
+ * items: data?.items.filter(item => item.id !== id) ?? []
217
+ * });
218
+ * };
219
+ *
220
+ * if (isLoading) return <Spinner />;
221
+ *
222
+ * return (
223
+ * <div>
224
+ * <TodoList
225
+ * items={data?.items ?? []}
226
+ * onToggle={toggleTodo}
227
+ * onDelete={deleteTodo}
228
+ * />
229
+ * <AddTodoForm onAdd={addTodo} disabled={isWriting} />
230
+ * </div>
231
+ * );
232
+ * }
233
+ * ```
234
+ */
235
+ export function useSPFxOneDriveAppData(fileName, options) {
236
+ const { client, isReady: isClientReady, isInitializing: isClientInitializing, initError: clientInitError } = useSPFxMSGraphClient();
237
+ // ═══════════════════════════════════════════════════════════════════════════
238
+ // OPTIONS (stabilized with useMemo to prevent unnecessary re-renders)
239
+ // ═══════════════════════════════════════════════════════════════════════════
240
+ const resolvedOptions = useMemo(() => options ?? {}, [options]);
241
+ // Extract stable primitive values for dependency arrays
242
+ const folder = resolvedOptions.folder;
243
+ const shouldAutoFetch = resolvedOptions.autoFetch ?? true;
244
+ const defaultValue = resolvedOptions.defaultValue;
245
+ const createIfMissing = resolvedOptions.createIfMissing ?? false;
246
+ // ═══════════════════════════════════════════════════════════════════════════
247
+ // STATE MANAGEMENT
248
+ // ═══════════════════════════════════════════════════════════════════════════
249
+ const [data, setData] = useState(defaultValue);
250
+ const [isLoading, setIsLoading] = useState(false);
251
+ const [error, setError] = useState(undefined);
252
+ const [isWriting, setIsWriting] = useState(false);
253
+ const [writeError, setWriteError] = useState(undefined);
254
+ const [isNotFound, setIsNotFound] = useState(false);
255
+ // ═══════════════════════════════════════════════════════════════════════════
256
+ // REFS (for cleanup and stable references)
257
+ // ═══════════════════════════════════════════════════════════════════════════
60
258
  // Track component mounted state to prevent memory leaks
61
- var isMounted = useRef(true);
62
- useEffect(function () {
63
- isMounted.current = true;
64
- return function () {
65
- isMounted.current = false;
259
+ const isMountedRef = useRef(true);
260
+ useEffect(() => {
261
+ return () => {
262
+ isMountedRef.current = false;
66
263
  };
67
264
  }, []);
68
- /**
69
- * Build Graph API path with optional folder namespace
70
- * Sanitizes folder name to prevent path traversal attacks
71
- */
72
- var buildApiPath = useCallback(function (file, folderName) {
73
- var basePath = '/me/drive/special/appRoot:';
74
- if (folderName) {
75
- // Sanitize folder name: only allow alphanumeric, hyphens, underscores
76
- // This prevents path traversal (../) and other injection attacks
77
- var safeFolderName = folderName.replace(/[^a-zA-Z0-9-_]/g, '-');
78
- return "".concat(basePath, "/").concat(safeFolderName, "/").concat(file, ":/content");
79
- }
80
- return "".concat(basePath, "/").concat(file, ":/content");
81
- }, []);
82
- var isNotFoundError = useCallback(function (err) {
83
- var _a, _b, _c, _d, _e, _f;
84
- var anyErr = err;
85
- if ((anyErr === null || anyErr === void 0 ? void 0 : anyErr.statusCode) === 404 || (anyErr === null || anyErr === void 0 ? void 0 : anyErr.status) === 404)
86
- return true;
87
- var code = (_a = anyErr === null || anyErr === void 0 ? void 0 : anyErr.code) !== null && _a !== void 0 ? _a : (_c = (_b = anyErr === null || anyErr === void 0 ? void 0 : anyErr.body) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.code;
88
- if (code && /itemnotfound/i.test(code))
89
- return true;
90
- var message = (_d = anyErr === null || anyErr === void 0 ? void 0 : anyErr.message) !== null && _d !== void 0 ? _d : (_f = (_e = anyErr === null || anyErr === void 0 ? void 0 : anyErr.body) === null || _e === void 0 ? void 0 : _e.error) === null || _f === void 0 ? void 0 : _f.message;
91
- if (message && /(\b404\b|not found|itemnotfound)/i.test(message))
92
- return true;
93
- return false;
94
- }, []);
265
+ // Track if createIfMissing write has been attempted (to prevent multiple writes)
266
+ const createAttemptedRef = useRef(false);
267
+ // ═══════════════════════════════════════════════════════════════════════════
268
+ // WRITE CALLBACK (defined first, no dependency on load)
269
+ // ═══════════════════════════════════════════════════════════════════════════
95
270
  /**
96
271
  * Write data to OneDrive file
97
272
  * Creates file if it doesn't exist, updates if it does (upsert)
98
273
  * Updates isWriting and writeError states
99
274
  */
100
- var write = useCallback(function (content) { return __awaiter(_this, void 0, void 0, function () {
101
- var apiPath, jsonContent, err_1, error_1;
102
- return __generator(this, function (_a) {
103
- switch (_a.label) {
104
- case 0:
105
- if (!client) {
106
- throw new Error('Graph client not available. Cannot write file.');
107
- }
108
- if (!fileName) {
109
- throw new Error('fileName is required. Cannot write file.');
110
- }
111
- setIsWriting(true);
112
- setWriteError(undefined);
113
- _a.label = 1;
114
- case 1:
115
- _a.trys.push([1, 3, 4, 5]);
116
- apiPath = buildApiPath(fileName, folder);
117
- jsonContent = JSON.stringify(content);
118
- return [4 /*yield*/, client
119
- .api(apiPath)
120
- .header('Content-Type', 'application/json')
121
- .put(jsonContent)];
122
- case 2:
123
- _a.sent();
124
- if (isMounted.current) {
125
- // Update local data to reflect successful write
126
- setData(content);
127
- setIsNotFound(false);
128
- // Clear read error if write succeeds (fresh state)
129
- setError(undefined);
130
- }
131
- return [3 /*break*/, 5];
132
- case 3:
133
- err_1 = _a.sent();
134
- if (isMounted.current) {
135
- error_1 = err_1 instanceof Error ? err_1 : new Error(String(err_1));
136
- setWriteError(error_1);
137
- console.error('Failed to write file to OneDrive:', error_1);
138
- }
139
- // Re-throw to allow caller to handle
140
- throw err_1;
141
- case 4:
142
- if (isMounted.current) {
143
- setIsWriting(false);
144
- }
145
- return [7 /*endfinally*/];
146
- case 5: return [2 /*return*/];
275
+ const write = useCallback(async (content) => {
276
+ if (!client) {
277
+ if (isClientInitializing) {
278
+ throw new Error('Graph client is still initializing. Please wait and try again.');
279
+ }
280
+ if (clientInitError) {
281
+ throw new Error(`Graph client initialization failed: ${clientInitError.message}`);
282
+ }
283
+ throw new Error('Graph client not available. Cannot write file.');
284
+ }
285
+ if (!fileName) {
286
+ throw new Error('fileName is required. Cannot write file.');
287
+ }
288
+ setIsWriting(true);
289
+ setWriteError(undefined);
290
+ try {
291
+ const apiPath = buildApiPath(fileName, folder);
292
+ // Always stringify to ensure valid JSON
293
+ const jsonContent = JSON.stringify(content);
294
+ await client
295
+ .api(apiPath)
296
+ .header('Content-Type', 'application/json')
297
+ .put(jsonContent);
298
+ if (isMountedRef.current) {
299
+ // Update local data to reflect successful write
300
+ setData(content);
301
+ setIsNotFound(false);
302
+ // Clear read error if write succeeds (fresh state)
303
+ setError(undefined);
147
304
  }
148
- });
149
- }); }, [client, fileName, folder, buildApiPath]);
305
+ }
306
+ catch (err) {
307
+ if (isMountedRef.current) {
308
+ const writeErr = err instanceof Error ? err : new Error(String(err));
309
+ setWriteError(writeErr);
310
+ console.error('Failed to write file to OneDrive:', writeErr);
311
+ }
312
+ // Re-throw to allow caller to handle
313
+ throw err;
314
+ }
315
+ finally {
316
+ if (isMountedRef.current) {
317
+ setIsWriting(false);
318
+ }
319
+ }
320
+ }, [client, fileName, folder, isClientInitializing, clientInitError]);
321
+ // ═══════════════════════════════════════════════════════════════════════════
322
+ // LOAD CALLBACK (NO dependency on write - uses effect for createIfMissing)
323
+ // ═══════════════════════════════════════════════════════════════════════════
150
324
  /**
151
325
  * Load file from OneDrive
152
326
  * Updates data, isLoading, and error states
327
+ * Does NOT call write directly - createIfMissing is handled by separate effect
153
328
  */
154
- var load = useCallback(function () { return __awaiter(_this, void 0, void 0, function () {
155
- var apiPath, fileContent, err_2, notFound, writeErr_1, error_2;
156
- return __generator(this, function (_a) {
157
- switch (_a.label) {
158
- case 0:
159
- if (!client) {
160
- console.warn('Graph client not available yet. Skipping load.');
161
- return [2 /*return*/];
329
+ const load = useCallback(async () => {
330
+ if (!client) {
331
+ if (isClientInitializing) {
332
+ console.info('Graph client is still initializing. Skipping load - will auto-retry when ready.');
333
+ return;
334
+ }
335
+ if (clientInitError) {
336
+ console.error('Graph client initialization failed:', clientInitError.message);
337
+ return;
338
+ }
339
+ console.warn('Graph client not available. Skipping load.');
340
+ return;
341
+ }
342
+ if (!fileName) {
343
+ console.warn('fileName is required. Skipping load.');
344
+ return;
345
+ }
346
+ // Reset createAttempted flag when load is called (fresh attempt)
347
+ createAttemptedRef.current = false;
348
+ setIsLoading(true);
349
+ setError(undefined);
350
+ setIsNotFound(false);
351
+ try {
352
+ const apiPath = buildApiPath(fileName, folder);
353
+ const fileContent = await client.api(apiPath).get();
354
+ if (isMountedRef.current) {
355
+ // Parse JSON if response is string, otherwise use as-is
356
+ if (typeof fileContent === 'string') {
357
+ try {
358
+ setData(JSON.parse(fileContent));
162
359
  }
163
- if (!fileName) {
164
- console.warn('fileName is required. Skipping load.');
165
- return [2 /*return*/];
360
+ catch (parseError) {
361
+ throw new Error(`Failed to parse JSON: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`);
166
362
  }
167
- setIsLoading(true);
168
- setError(undefined);
169
- setIsNotFound(false);
170
- _a.label = 1;
171
- case 1:
172
- _a.trys.push([1, 3, 12, 13]);
173
- apiPath = buildApiPath(fileName, folder);
174
- return [4 /*yield*/, client.api(apiPath).get()];
175
- case 2:
176
- fileContent = _a.sent();
177
- if (isMounted.current) {
178
- // Parse JSON if response is string, otherwise use as-is
179
- if (typeof fileContent === 'string') {
180
- try {
181
- setData(JSON.parse(fileContent));
182
- }
183
- catch (parseError) {
184
- throw new Error("Failed to parse JSON: ".concat(parseError instanceof Error ? parseError.message : 'Unknown error'));
185
- }
186
- }
187
- else {
188
- setData(fileContent);
189
- }
190
- setIsNotFound(false);
363
+ }
364
+ else {
365
+ setData(fileContent);
366
+ }
367
+ setIsNotFound(false);
368
+ }
369
+ }
370
+ catch (err) {
371
+ if (isMountedRef.current) {
372
+ const notFound = isNotFoundError(err);
373
+ setIsNotFound(notFound);
374
+ if (notFound) {
375
+ // Missing file is treated as a non-error.
376
+ // Set data to defaultValue if provided, otherwise undefined
377
+ if (defaultValue !== undefined) {
378
+ setData(defaultValue);
379
+ }
380
+ else {
381
+ setData(undefined);
191
382
  }
192
- return [3 /*break*/, 13];
193
- case 3:
194
- err_2 = _a.sent();
195
- if (!isMounted.current) return [3 /*break*/, 11];
196
- notFound = isNotFoundError(err_2);
197
- setIsNotFound(notFound);
198
- if (!notFound) return [3 /*break*/, 10];
199
- if (!(defaultValue !== undefined)) return [3 /*break*/, 8];
200
- setData(defaultValue);
201
- if (!createIfMissing) return [3 /*break*/, 7];
202
- _a.label = 4;
203
- case 4:
204
- _a.trys.push([4, 6, , 7]);
205
- return [4 /*yield*/, write(defaultValue)];
206
- case 5:
207
- _a.sent();
208
- return [3 /*break*/, 7];
209
- case 6:
210
- writeErr_1 = _a.sent();
211
- // write() already updates writeError state
212
- console.error('Failed to create missing file in OneDrive:', writeErr_1);
213
- return [3 /*break*/, 7];
214
- case 7: return [3 /*break*/, 9];
215
- case 8:
216
- setData(undefined);
217
- _a.label = 9;
218
- case 9:
219
383
  setError(undefined);
220
384
  console.info('OneDrive file not found. isNotFound=true');
221
- return [2 /*return*/];
222
- case 10:
223
- error_2 = err_2 instanceof Error ? err_2 : new Error(String(err_2));
224
- setError(error_2);
225
- // Don't throw - allow component to handle error via state
226
- console.error('Failed to load file from OneDrive:', error_2);
227
- _a.label = 11;
228
- case 11: return [3 /*break*/, 13];
229
- case 12:
230
- if (isMounted.current) {
231
- setIsLoading(false);
232
- }
233
- return [7 /*endfinally*/];
234
- case 13: return [2 /*return*/];
385
+ // NOTE: createIfMissing is handled by separate useEffect
386
+ return;
387
+ }
388
+ const loadError = err instanceof Error ? err : new Error(String(err));
389
+ setError(loadError);
390
+ console.error('Failed to load file from OneDrive:', loadError);
235
391
  }
236
- });
237
- }); }, [client, fileName, folder, buildApiPath, defaultValue, createIfMissing, isNotFoundError, write]);
238
- // Auto-fetch on mount if enabled
239
- useEffect(function () {
240
- if (shouldAutoFetch && client && fileName) {
241
- load().catch(function () {
392
+ }
393
+ finally {
394
+ if (isMountedRef.current) {
395
+ setIsLoading(false);
396
+ }
397
+ }
398
+ }, [client, fileName, folder, defaultValue, isClientInitializing, clientInitError]); // ← NO write, NO createIfMissing
399
+ // ═══════════════════════════════════════════════════════════════════════════
400
+ // EFFECTS
401
+ // ═══════════════════════════════════════════════════════════════════════════
402
+ // Auto-fetch on mount if enabled (wait for client to be ready)
403
+ useEffect(() => {
404
+ if (shouldAutoFetch && isClientReady && fileName) {
405
+ load().catch(() => {
242
406
  // Error already handled in load() function
243
407
  });
244
408
  }
245
- }, [shouldAutoFetch, client, fileName, load]);
409
+ }, [shouldAutoFetch, isClientReady, fileName, load]);
410
+ // Separate effect for createIfMissing - reacts to isNotFound state
411
+ // This breaks the circular dependency: load → write
412
+ useEffect(() => {
413
+ // Guard conditions:
414
+ // 1. File must be not found
415
+ // 2. createIfMissing must be enabled
416
+ // 3. defaultValue must be provided
417
+ // 4. Must not be currently writing (prevent double-write)
418
+ // 5. Must not have already attempted create (prevent infinite loop)
419
+ // 6. Must not be currently loading (wait for load to complete)
420
+ if (isNotFound &&
421
+ createIfMissing &&
422
+ defaultValue !== undefined &&
423
+ !isWriting &&
424
+ !createAttemptedRef.current &&
425
+ !isLoading) {
426
+ createAttemptedRef.current = true;
427
+ write(defaultValue).catch((writeErr) => {
428
+ // write() already updates writeError state
429
+ console.error('Failed to create missing file in OneDrive:', writeErr);
430
+ });
431
+ }
432
+ }, [isNotFound, createIfMissing, defaultValue, isWriting, isLoading, write]);
433
+ // ═══════════════════════════════════════════════════════════════════════════
434
+ // COMPUTED STATE & RETURN
435
+ // ═══════════════════════════════════════════════════════════════════════════
246
436
  // Computed state: ready when data loaded successfully
247
- var isReady = !isLoading && !error && data !== undefined;
437
+ const isReady = !isLoading && !error && data !== undefined;
248
438
  return {
249
- data: data,
250
- isLoading: isLoading,
251
- error: error,
252
- isWriting: isWriting,
253
- writeError: writeError,
254
- isNotFound: isNotFound,
255
- load: load,
256
- write: write,
257
- isReady: isReady,
439
+ data,
440
+ isLoading,
441
+ error,
442
+ isWriting,
443
+ writeError,
444
+ isNotFound,
445
+ load,
446
+ write,
447
+ isReady,
258
448
  };
259
449
  }
260
450
  //# sourceMappingURL=useSPFxOneDriveAppData.js.map