@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.
- package/lib/core/atoms.internal.js +1 -1
- package/lib/core/atoms.internal.js.map +1 -1
- package/lib/core/context.internal.js +2 -2
- package/lib/core/context.internal.js.map +1 -1
- package/lib/core/provider-base.internal.js +29 -29
- package/lib/core/provider-base.internal.js.map +1 -1
- package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.d.ts +14 -0
- package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.d.ts.map +1 -0
- package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.js +20 -0
- package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.js.map +1 -0
- package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.manifest.json +17 -0
- package/lib/extensions/spFxReactToolkitTest/loc/en-us.js +5 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.d.ts.map +1 -1
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/index.js.map +1 -1
- package/lib/hooks/useAppCatalogUrl.internal.d.ts +26 -0
- package/lib/hooks/useAppCatalogUrl.internal.d.ts.map +1 -0
- package/lib/hooks/useAppCatalogUrl.internal.js +72 -0
- package/lib/hooks/useAppCatalogUrl.internal.js.map +1 -0
- package/lib/hooks/useAsyncInvoke.internal.js +27 -75
- package/lib/hooks/useAsyncInvoke.internal.js.map +1 -1
- package/lib/hooks/useSPFxAadHttpClient.d.ts +46 -0
- package/lib/hooks/useSPFxAadHttpClient.d.ts.map +1 -1
- package/lib/hooks/useSPFxAadHttpClient.js +65 -20
- package/lib/hooks/useSPFxAadHttpClient.js.map +1 -1
- package/lib/hooks/useSPFxContainerInfo.js +5 -5
- package/lib/hooks/useSPFxContainerInfo.js.map +1 -1
- package/lib/hooks/useSPFxContainerSize.js +9 -10
- package/lib/hooks/useSPFxContainerSize.js.map +1 -1
- package/lib/hooks/useSPFxCorrelationInfo.js +6 -7
- package/lib/hooks/useSPFxCorrelationInfo.js.map +1 -1
- package/lib/hooks/useSPFxCrossSitePermissions.js +48 -58
- package/lib/hooks/useSPFxCrossSitePermissions.js.map +1 -1
- package/lib/hooks/useSPFxDisplayMode.js +8 -8
- package/lib/hooks/useSPFxDisplayMode.js.map +1 -1
- package/lib/hooks/useSPFxEnvironmentInfo.js +17 -18
- package/lib/hooks/useSPFxEnvironmentInfo.js.map +1 -1
- package/lib/hooks/useSPFxFluent9ThemeInfo.js +4 -4
- package/lib/hooks/useSPFxFluent9ThemeInfo.js.map +1 -1
- package/lib/hooks/useSPFxHttpClient.d.ts +18 -2
- package/lib/hooks/useSPFxHttpClient.d.ts.map +1 -1
- package/lib/hooks/useSPFxHttpClient.js +19 -9
- package/lib/hooks/useSPFxHttpClient.js.map +1 -1
- package/lib/hooks/useSPFxHubSiteInfo.js +21 -24
- package/lib/hooks/useSPFxHubSiteInfo.js.map +1 -1
- package/lib/hooks/useSPFxInstanceInfo.js +2 -2
- package/lib/hooks/useSPFxInstanceInfo.js.map +1 -1
- package/lib/hooks/useSPFxListInfo.js +8 -9
- package/lib/hooks/useSPFxListInfo.js.map +1 -1
- package/lib/hooks/useSPFxLocaleInfo.js +10 -10
- package/lib/hooks/useSPFxLocaleInfo.js.map +1 -1
- package/lib/hooks/useSPFxLogger.js +26 -26
- package/lib/hooks/useSPFxLogger.js.map +1 -1
- package/lib/hooks/useSPFxMSGraphClient.d.ts +50 -3
- package/lib/hooks/useSPFxMSGraphClient.d.ts.map +1 -1
- package/lib/hooks/useSPFxMSGraphClient.js +68 -15
- package/lib/hooks/useSPFxMSGraphClient.js.map +1 -1
- package/lib/hooks/useSPFxOneDriveAppData.d.ts +0 -1
- package/lib/hooks/useSPFxOneDriveAppData.d.ts.map +1 -1
- package/lib/hooks/useSPFxOneDriveAppData.js +420 -230
- package/lib/hooks/useSPFxOneDriveAppData.js.map +1 -1
- package/lib/hooks/useSPFxPageContext.js +2 -2
- package/lib/hooks/useSPFxPageContext.js.map +1 -1
- package/lib/hooks/useSPFxPageType.js +19 -20
- package/lib/hooks/useSPFxPageType.js.map +1 -1
- package/lib/hooks/useSPFxPerformance.js +33 -87
- package/lib/hooks/useSPFxPerformance.js.map +1 -1
- package/lib/hooks/useSPFxPermissions.js +14 -15
- package/lib/hooks/useSPFxPermissions.js.map +1 -1
- package/lib/hooks/useSPFxPnP.js +62 -119
- package/lib/hooks/useSPFxPnP.js.map +1 -1
- package/lib/hooks/useSPFxPnPContext.js +22 -25
- package/lib/hooks/useSPFxPnPContext.js.map +1 -1
- package/lib/hooks/useSPFxPnPList.js +307 -451
- package/lib/hooks/useSPFxPnPList.js.map +1 -1
- package/lib/hooks/useSPFxPnPSearch.js +262 -353
- package/lib/hooks/useSPFxPnPSearch.js.map +1 -1
- package/lib/hooks/useSPFxProperties.js +12 -20
- package/lib/hooks/useSPFxProperties.js.map +1 -1
- package/lib/hooks/useSPFxSPHttpClient.d.ts +18 -2
- package/lib/hooks/useSPFxSPHttpClient.d.ts.map +1 -1
- package/lib/hooks/useSPFxSPHttpClient.js +28 -18
- package/lib/hooks/useSPFxSPHttpClient.js.map +1 -1
- package/lib/hooks/useSPFxServiceScope.js +6 -6
- package/lib/hooks/useSPFxServiceScope.js.map +1 -1
- package/lib/hooks/useSPFxSiteInfo.js +7 -8
- package/lib/hooks/useSPFxSiteInfo.js.map +1 -1
- package/lib/hooks/useSPFxStorage.js +22 -22
- package/lib/hooks/useSPFxStorage.js.map +1 -1
- package/lib/hooks/useSPFxTeams.js +37 -92
- package/lib/hooks/useSPFxTeams.js.map +1 -1
- package/lib/hooks/useSPFxTenantKeyValueStore.d.ts +252 -0
- package/lib/hooks/useSPFxTenantKeyValueStore.d.ts.map +1 -0
- package/lib/hooks/useSPFxTenantKeyValueStore.js +572 -0
- package/lib/hooks/useSPFxTenantKeyValueStore.js.map +1 -0
- package/lib/hooks/useSPFxTenantProperty.d.ts +23 -244
- package/lib/hooks/useSPFxTenantProperty.d.ts.map +1 -1
- package/lib/hooks/useSPFxTenantProperty.js +85 -559
- package/lib/hooks/useSPFxTenantProperty.js.map +1 -1
- package/lib/hooks/useSPFxUserInfo.js +3 -4
- package/lib/hooks/useSPFxUserInfo.js.map +1 -1
- package/lib/hooks/useSPFxUserPhoto.js +76 -123
- package/lib/hooks/useSPFxUserPhoto.js.map +1 -1
- package/lib/utils/resize-observer.internal.js +6 -7
- package/lib/utils/resize-observer.internal.js.map +1 -1
- package/lib/utils/theme-subscription.internal.js +8 -8
- package/lib/utils/theme-subscription.internal.js.map +1 -1
- package/lib/utils/type-guards.internal.js +6 -6
- package/lib/utils/type-guards.internal.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.js +12 -37
- package/lib/webparts/spFxReactToolkitTest/SpFxReactToolkitTestWebPart.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.d.ts.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.js +279 -342
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.js +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/SpFxReactToolkitTest.module.scss.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/HttpClientDemo.js +26 -86
- package/lib/webparts/spFxReactToolkitTest/components/demos/HttpClientDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPContextDemo.js +53 -113
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPContextDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPListDemo.js +49 -121
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPListDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPOperationsDemo.js +44 -103
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPOperationsDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchAdvancedDemo.js +15 -15
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchAdvancedDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchBasicDemo.js +18 -66
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchBasicDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchRefinersDemo.js +9 -9
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchRefinersDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchSuggestionsDemo.js +37 -86
- package/lib/webparts/spFxReactToolkitTest/components/demos/PnPSearchSuggestionsDemo.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/shared/InfoRow.js +6 -9
- package/lib/webparts/spFxReactToolkitTest/components/shared/InfoRow.js.map +1 -1
- package/lib/webparts/spFxReactToolkitTest/components/shared/StatusBadge.js +3 -6
- package/lib/webparts/spFxReactToolkitTest/components/shared/StatusBadge.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
useEffect(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
isMounted.current = false;
|
|
259
|
+
const isMountedRef = useRef(true);
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
return () => {
|
|
262
|
+
isMountedRef.current = false;
|
|
66
263
|
};
|
|
67
264
|
}, []);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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,
|
|
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
|
-
|
|
437
|
+
const isReady = !isLoading && !error && data !== undefined;
|
|
248
438
|
return {
|
|
249
|
-
data
|
|
250
|
-
isLoading
|
|
251
|
-
error
|
|
252
|
-
isWriting
|
|
253
|
-
writeError
|
|
254
|
-
isNotFound
|
|
255
|
-
load
|
|
256
|
-
write
|
|
257
|
-
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
|