@apvee/spfx-react-toolkit 1.3.0 → 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.js +8 -29
- package/lib/extensions/spFxReactToolkitTest/SpFxReactToolkitTestApplicationCustomizer.js.map +1 -1
- 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.js +27 -27
- 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.js +10 -10
- 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.js +25 -25
- package/lib/hooks/useSPFxMSGraphClient.js.map +1 -1
- package/lib/hooks/useSPFxOneDriveAppData.js +148 -209
- 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.js +19 -19
- 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 +277 -336
- 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 +1 -1
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// useSPFxTenantKeyValueStore.ts
|
|
2
|
+
// Hook to manage tenant-wide key-value pairs using a hidden SharePoint list in the tenant app catalog
|
|
3
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { useAppCatalogUrl } from './useAppCatalogUrl.internal';
|
|
5
|
+
import { SPHttpClient } from '@microsoft/sp-http';
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
// CONSTANTS
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
const LIST_TITLE = 'TenantKeyValueStore';
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// PURE FUNCTIONS (extracted for stability and testability)
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
/**
|
|
14
|
+
* Escape a string value for use in OData filter expressions.
|
|
15
|
+
* Single quotes must be doubled to prevent injection.
|
|
16
|
+
*
|
|
17
|
+
* @param value - Raw string value
|
|
18
|
+
* @returns Escaped string safe for OData $filter
|
|
19
|
+
*/
|
|
20
|
+
function escapeODataValue(value) {
|
|
21
|
+
return value.replace(/'/g, "''");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Serialize a value for storage.
|
|
25
|
+
* - Primitives (string, number, boolean, null, bigint) → String(value)
|
|
26
|
+
* - Date → ISO 8601 string
|
|
27
|
+
* - Objects/arrays → JSON.stringify()
|
|
28
|
+
*
|
|
29
|
+
* @param value - Value to serialize
|
|
30
|
+
* @returns Serialized string representation
|
|
31
|
+
*/
|
|
32
|
+
function serializeValue(value) {
|
|
33
|
+
if (value === null)
|
|
34
|
+
return String(value);
|
|
35
|
+
if (value instanceof Date)
|
|
36
|
+
return value.toISOString();
|
|
37
|
+
const type = typeof value;
|
|
38
|
+
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint') {
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Deserialize a stored string value back to a typed value.
|
|
45
|
+
* Attempts JSON.parse first; falls back to raw string.
|
|
46
|
+
*
|
|
47
|
+
* @param rawValue - Stored string value
|
|
48
|
+
* @returns Parsed value
|
|
49
|
+
*/
|
|
50
|
+
function deserializeValue(rawValue) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(rawValue);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return rawValue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the list REST API base URL.
|
|
60
|
+
*
|
|
61
|
+
* @param catalogUrl - Tenant app catalog absolute URL
|
|
62
|
+
* @returns REST API URL for the TenantKeyValueStore list
|
|
63
|
+
*/
|
|
64
|
+
function getListApiUrl(catalogUrl) {
|
|
65
|
+
return `${catalogUrl}/_api/web/lists/getByTitle('${LIST_TITLE}')`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a multiline text (Note) field on the list.
|
|
69
|
+
*
|
|
70
|
+
* @param client - SPHttpClient instance
|
|
71
|
+
* @param listApiUrl - REST API URL for the target list
|
|
72
|
+
* @param fieldTitle - Internal name / title for the new field
|
|
73
|
+
*/
|
|
74
|
+
async function createField(client, listApiUrl, fieldTitle) {
|
|
75
|
+
const response = await client.post(`${listApiUrl}/fields`, SPHttpClient.configurations.v1, {
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
FieldTypeKind: 3, // Note (multiline text)
|
|
78
|
+
Title: fieldTitle
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorText = await response.text();
|
|
83
|
+
throw new Error(`Failed to create ${fieldTitle} field: ${response.statusText}. ${errorText}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
87
|
+
// HOOK
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
/**
|
|
90
|
+
* Hook to manage tenant-wide key-value pairs backed by a hidden SharePoint list.
|
|
91
|
+
*
|
|
92
|
+
* The store uses a hidden list named `TenantKeyValueStore` in the tenant app catalog.
|
|
93
|
+
* The list is auto-provisioned on the first operation (get, list, save, or remove), with:
|
|
94
|
+
* - Title column: key (indexed, unique)
|
|
95
|
+
* - Value column: multiline text (Note)
|
|
96
|
+
* - Description column: multiline text (Note)
|
|
97
|
+
*
|
|
98
|
+
* This hook provides an alternative to tenant properties (StorageEntity) for scenarios
|
|
99
|
+
* requiring read/write access via REST, since Microsoft has blocked the
|
|
100
|
+
* SetStorageEntity and RemoveStorageEntity REST API endpoints.
|
|
101
|
+
*
|
|
102
|
+
* Features:
|
|
103
|
+
* - CRUD operations (get, list, save, remove) on tenant-scoped key-value data
|
|
104
|
+
* - Auto-provisioning of the hidden list on first operation (any CRUD call)
|
|
105
|
+
* - Smart provisioning: ref-based fast path (zero API calls after first check),
|
|
106
|
+
* field introspection only when list already exists, Title uniqueness only on creation
|
|
107
|
+
* - Smart serialization for primitives, Date, and complex objects
|
|
108
|
+
* - Permission checking (canWrite flag via IsSiteAdmin)
|
|
109
|
+
* - Unique key enforcement (Indexed + EnforceUniqueValues on Title)
|
|
110
|
+
* - Concurrent provisioning protection (mutex ref)
|
|
111
|
+
* - Memory leak safe with mounted state tracking
|
|
112
|
+
* - Idempotent remove (no-op if key or list doesn't exist)
|
|
113
|
+
*
|
|
114
|
+
* Requirements:
|
|
115
|
+
* - Tenant app catalog must be provisioned
|
|
116
|
+
* - Read: Any authenticated user
|
|
117
|
+
* - Write/Remove: Site Collection Administrator role on tenant app catalog site
|
|
118
|
+
*
|
|
119
|
+
* @returns Object with loading states, error states, permission flag, and CRUD functions
|
|
120
|
+
*
|
|
121
|
+
* @example Basic usage
|
|
122
|
+
* ```tsx
|
|
123
|
+
* function TenantConfigPanel() {
|
|
124
|
+
* const store = useSPFxTenantKeyValueStore();
|
|
125
|
+
*
|
|
126
|
+
* const [endpoint, setEndpoint] = React.useState<string>('');
|
|
127
|
+
*
|
|
128
|
+
* React.useEffect(() => {
|
|
129
|
+
* store.get<string>('apiEndpoint').then(item => {
|
|
130
|
+
* if (item) setEndpoint(item.value);
|
|
131
|
+
* });
|
|
132
|
+
* }, []);
|
|
133
|
+
*
|
|
134
|
+
* const handleSave = async () => {
|
|
135
|
+
* await store.save<string>('apiEndpoint', endpoint, 'Production API endpoint');
|
|
136
|
+
* };
|
|
137
|
+
*
|
|
138
|
+
* if (!store.isReady) return <Spinner />;
|
|
139
|
+
*
|
|
140
|
+
* return (
|
|
141
|
+
* <Stack tokens={{ childrenGap: 10 }}>
|
|
142
|
+
* <TextField value={endpoint} onChange={(_, v) => setEndpoint(v ?? '')} />
|
|
143
|
+
* <PrimaryButton onClick={handleSave} disabled={store.isWriting}>
|
|
144
|
+
* {store.isWriting ? 'Saving...' : 'Save'}
|
|
145
|
+
* </PrimaryButton>
|
|
146
|
+
* </Stack>
|
|
147
|
+
* );
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @example Heterogeneous value types
|
|
152
|
+
* ```tsx
|
|
153
|
+
* const store = useSPFxTenantKeyValueStore();
|
|
154
|
+
*
|
|
155
|
+
* // String
|
|
156
|
+
* await store.save<string>('appVersion', '2.1.0');
|
|
157
|
+
* const ver = await store.get<string>('appVersion');
|
|
158
|
+
*
|
|
159
|
+
* // Number
|
|
160
|
+
* await store.save<number>('maxUploadSize', 10485760);
|
|
161
|
+
* const size = await store.get<number>('maxUploadSize');
|
|
162
|
+
*
|
|
163
|
+
* // Complex object
|
|
164
|
+
* interface FeatureFlags { enableChat: boolean; maxUsers: number; }
|
|
165
|
+
* await store.save<FeatureFlags>('featureFlags', { enableChat: true, maxUsers: 500 });
|
|
166
|
+
* const flags = await store.get<FeatureFlags>('featureFlags');
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @example Dashboard listing all properties
|
|
170
|
+
* ```tsx
|
|
171
|
+
* function AllPropertiesView() {
|
|
172
|
+
* const store = useSPFxTenantKeyValueStore();
|
|
173
|
+
* const [items, setItems] = React.useState<SPFxTenantKeyValueStoreItem<unknown>[]>([]);
|
|
174
|
+
*
|
|
175
|
+
* React.useEffect(() => {
|
|
176
|
+
* store.list().then(setItems);
|
|
177
|
+
* }, []);
|
|
178
|
+
*
|
|
179
|
+
* return (
|
|
180
|
+
* <DetailsList
|
|
181
|
+
* items={items.map(i => ({
|
|
182
|
+
* key: i.key,
|
|
183
|
+
* value: typeof i.value === 'object' ? JSON.stringify(i.value) : String(i.value),
|
|
184
|
+
* description: i.description ?? '',
|
|
185
|
+
* }))}
|
|
186
|
+
* columns={[
|
|
187
|
+
* { key: 'key', name: 'Key', fieldName: 'key', minWidth: 150 },
|
|
188
|
+
* { key: 'value', name: 'Value', fieldName: 'value', minWidth: 200 },
|
|
189
|
+
* { key: 'desc', name: 'Description', fieldName: 'description', minWidth: 200 },
|
|
190
|
+
* ]}
|
|
191
|
+
* />
|
|
192
|
+
* );
|
|
193
|
+
* }
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @example Permission-aware UI
|
|
197
|
+
* ```tsx
|
|
198
|
+
* function TenantStoreManager() {
|
|
199
|
+
* const store = useSPFxTenantKeyValueStore();
|
|
200
|
+
*
|
|
201
|
+
* if (!store.canWrite) {
|
|
202
|
+
* return (
|
|
203
|
+
* <MessageBar messageBarType={MessageBarType.info}>
|
|
204
|
+
* Read-only access. Contact your SharePoint administrator for write permissions.
|
|
205
|
+
* </MessageBar>
|
|
206
|
+
* );
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* return <EditablePropertyPanel store={store} />;
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function useSPFxTenantKeyValueStore() {
|
|
214
|
+
const { spHttpClient, discoverAppCatalogUrl, checkWritePermission, isMountedRef } = useAppCatalogUrl();
|
|
215
|
+
// State management
|
|
216
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
217
|
+
const [error, setError] = useState(undefined);
|
|
218
|
+
const [isWriting, setIsWriting] = useState(false);
|
|
219
|
+
const [writeError, setWriteError] = useState(undefined);
|
|
220
|
+
const [canWrite, setCanWrite] = useState(false);
|
|
221
|
+
// Provisioning state
|
|
222
|
+
const listProvisionedRef = useRef(false);
|
|
223
|
+
const provisioningPromiseRef = useRef(undefined);
|
|
224
|
+
const permissionCheckedRef = useRef(false);
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
226
|
+
// Internal helpers
|
|
227
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Check and update write permission (executed once)
|
|
230
|
+
*/
|
|
231
|
+
const ensurePermissionChecked = useCallback((catalogUrl) => {
|
|
232
|
+
if (permissionCheckedRef.current)
|
|
233
|
+
return;
|
|
234
|
+
permissionCheckedRef.current = true;
|
|
235
|
+
checkWritePermission(catalogUrl)
|
|
236
|
+
.then(hasPermission => {
|
|
237
|
+
if (isMountedRef.current) {
|
|
238
|
+
setCanWrite(hasPermission);
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
.catch(() => {
|
|
242
|
+
if (isMountedRef.current) {
|
|
243
|
+
setCanWrite(false);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}, [checkWritePermission, isMountedRef]);
|
|
247
|
+
/**
|
|
248
|
+
* Ensure the hidden list exists and has all required fields.
|
|
249
|
+
* Uses a ref-based fast path (zero API calls after first successful check)
|
|
250
|
+
* and a mutex to prevent concurrent provisioning.
|
|
251
|
+
*
|
|
252
|
+
* Flow:
|
|
253
|
+
* 1. Fast path: `listProvisionedRef` is true → return immediately
|
|
254
|
+
* 2. Mutex: reuse in-flight promise if already provisioning
|
|
255
|
+
* 3. Lightweight existence check (1 API call): GET list?$select=Id
|
|
256
|
+
* - 200 (exists) → field introspection, create missing fields only
|
|
257
|
+
* - 404 (missing) → full provision: create list + fields + Title uniqueness
|
|
258
|
+
* 4. Mark ref true on success, cleanup mutex on success or failure
|
|
259
|
+
*/
|
|
260
|
+
const ensureListReady = useCallback((client, catalogUrl) => {
|
|
261
|
+
// Fast path: already verified — zero API calls
|
|
262
|
+
if (listProvisionedRef.current)
|
|
263
|
+
return Promise.resolve();
|
|
264
|
+
// Mutex: reuse in-flight provisioning promise
|
|
265
|
+
if (provisioningPromiseRef.current)
|
|
266
|
+
return provisioningPromiseRef.current;
|
|
267
|
+
const doProvision = async () => {
|
|
268
|
+
const listApiUrl = getListApiUrl(catalogUrl);
|
|
269
|
+
// ── Step 1: Lightweight existence check (single API call) ──────
|
|
270
|
+
const listResponse = await client.get(`${listApiUrl}?$select=Id`, SPHttpClient.configurations.v1);
|
|
271
|
+
const listExists = listResponse.status !== 404;
|
|
272
|
+
if (listExists && !listResponse.ok) {
|
|
273
|
+
throw new Error(`Failed to check list existence: ${listResponse.statusText}`);
|
|
274
|
+
}
|
|
275
|
+
if (listExists) {
|
|
276
|
+
// ── Step 2: List exists → field introspection ──────────────
|
|
277
|
+
const fieldsResponse = await client.get(`${listApiUrl}/fields?$filter=InternalName eq 'Value' or InternalName eq 'Description'&$select=InternalName`, SPHttpClient.configurations.v1);
|
|
278
|
+
if (!fieldsResponse.ok) {
|
|
279
|
+
throw new Error(`Failed to check list fields: ${fieldsResponse.statusText}`);
|
|
280
|
+
}
|
|
281
|
+
const fields = await fieldsResponse.json();
|
|
282
|
+
const existingFields = fields.value.map(f => f.InternalName);
|
|
283
|
+
const hasValue = existingFields.includes('Value');
|
|
284
|
+
const hasDescription = existingFields.includes('Description');
|
|
285
|
+
// All fields present → list is fully ready
|
|
286
|
+
if (hasValue && hasDescription) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// Create only the missing fields
|
|
290
|
+
if (!hasValue) {
|
|
291
|
+
await createField(client, listApiUrl, 'Value');
|
|
292
|
+
}
|
|
293
|
+
if (!hasDescription) {
|
|
294
|
+
await createField(client, listApiUrl, 'Description');
|
|
295
|
+
}
|
|
296
|
+
// Skip Title uniqueness — list already existed, constraint is either
|
|
297
|
+
// already set or intentionally not re-applied on field repair
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// ── Step 3: List does not exist → full provision ─────────────
|
|
301
|
+
// 3a. Create list
|
|
302
|
+
const createListResponse = await client.post(`${catalogUrl}/_api/web/lists`, SPHttpClient.configurations.v1, {
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
BaseTemplate: 100,
|
|
305
|
+
Title: LIST_TITLE,
|
|
306
|
+
Hidden: true,
|
|
307
|
+
NoCrawl: true
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
if (!createListResponse.ok) {
|
|
311
|
+
const errorText = await createListResponse.text();
|
|
312
|
+
throw new Error(`Failed to create list: ${createListResponse.statusText}. ${errorText}`);
|
|
313
|
+
}
|
|
314
|
+
// 3b. Create Value + Description fields (list is fresh — no need to check)
|
|
315
|
+
await createField(client, getListApiUrl(catalogUrl), 'Value');
|
|
316
|
+
await createField(client, getListApiUrl(catalogUrl), 'Description');
|
|
317
|
+
// 3c. Set Title field as Indexed + EnforceUniqueValues (only on creation)
|
|
318
|
+
const titleResp = await client.post(`${getListApiUrl(catalogUrl)}/fields/getByInternalNameOrTitle('Title')`, SPHttpClient.configurations.v1, {
|
|
319
|
+
headers: {
|
|
320
|
+
'X-HTTP-Method': 'MERGE',
|
|
321
|
+
'If-Match': '*'
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
Indexed: true,
|
|
325
|
+
EnforceUniqueValues: true
|
|
326
|
+
})
|
|
327
|
+
});
|
|
328
|
+
if (!titleResp.ok) {
|
|
329
|
+
// Non-fatal: unique constraint may already exist
|
|
330
|
+
console.warn('Failed to set Title uniqueness constraint. It may already be configured.');
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
// All ref mutations are synchronous — no await between read and write.
|
|
334
|
+
// doProvision() never touches refs; .then() callbacks are separate scopes.
|
|
335
|
+
const cleanupMutex = () => { provisioningPromiseRef.current = undefined; };
|
|
336
|
+
const promise = doProvision()
|
|
337
|
+
.then(() => { listProvisionedRef.current = true; cleanupMutex(); })
|
|
338
|
+
.catch((err) => { cleanupMutex(); throw err; });
|
|
339
|
+
provisioningPromiseRef.current = promise;
|
|
340
|
+
return promise;
|
|
341
|
+
}, []);
|
|
342
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
343
|
+
// Eager initialization: verify/provision list as soon as SPHttpClient is
|
|
344
|
+
// available, overlapping with component render. By the time the user
|
|
345
|
+
// triggers an operation, listProvisionedRef is already true → zero latency.
|
|
346
|
+
// The mutex in ensureListReady handles the race if an operation fires
|
|
347
|
+
// while this init is still in-flight.
|
|
348
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (!spHttpClient)
|
|
351
|
+
return;
|
|
352
|
+
discoverAppCatalogUrl()
|
|
353
|
+
.then(catalogUrl => {
|
|
354
|
+
ensurePermissionChecked(catalogUrl);
|
|
355
|
+
return ensureListReady(spHttpClient, catalogUrl);
|
|
356
|
+
})
|
|
357
|
+
.catch(() => {
|
|
358
|
+
// Silently fail — operations will retry on demand via their own ensureListReady call
|
|
359
|
+
});
|
|
360
|
+
}, [spHttpClient, discoverAppCatalogUrl, ensurePermissionChecked, ensureListReady]);
|
|
361
|
+
/**
|
|
362
|
+
* Find an item by key, returning the raw list item or undefined.
|
|
363
|
+
*/
|
|
364
|
+
const findItemByKey = useCallback(async (client, catalogUrl, key) => {
|
|
365
|
+
const safeKey = escapeODataValue(key);
|
|
366
|
+
const response = await client.get(`${getListApiUrl(catalogUrl)}/items?$filter=Title eq '${safeKey}'&$select=Id,Title,Value,Description&$top=1`, SPHttpClient.configurations.v1);
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`Failed to find item: ${response.statusText}`);
|
|
369
|
+
}
|
|
370
|
+
const data = await response.json();
|
|
371
|
+
return data.value.length > 0 ? data.value[0] : undefined;
|
|
372
|
+
}, []);
|
|
373
|
+
/**
|
|
374
|
+
* Map a raw SharePoint list item to an SPFxTenantKeyValueStoreItem.
|
|
375
|
+
*/
|
|
376
|
+
function mapItem(raw) {
|
|
377
|
+
return {
|
|
378
|
+
key: raw.Title,
|
|
379
|
+
value: deserializeValue(raw.Value),
|
|
380
|
+
description: raw.Description || undefined,
|
|
381
|
+
id: raw.Id,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
385
|
+
// Public operations
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
387
|
+
const get = useCallback(async (key) => {
|
|
388
|
+
if (!spHttpClient) {
|
|
389
|
+
throw new Error('SPHttpClient not available. Cannot get property.');
|
|
390
|
+
}
|
|
391
|
+
setIsLoading(true);
|
|
392
|
+
setError(undefined);
|
|
393
|
+
try {
|
|
394
|
+
const catalogUrl = await discoverAppCatalogUrl();
|
|
395
|
+
// Ensure list exists (auto-provision if needed; fallback-safe for read-only users)
|
|
396
|
+
try {
|
|
397
|
+
await ensureListReady(spHttpClient, catalogUrl);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Provisioning may fail for read-only users — treat as "list not yet created"
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const raw = await findItemByKey(spHttpClient, catalogUrl, key);
|
|
404
|
+
return raw ? mapItem(raw) : undefined;
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
if (isMountedRef.current) {
|
|
408
|
+
const capturedError = err instanceof Error ? err : new Error(String(err));
|
|
409
|
+
setError(capturedError);
|
|
410
|
+
console.error('Failed to get tenant key-value:', capturedError);
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
if (isMountedRef.current) {
|
|
416
|
+
setIsLoading(false);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}, [spHttpClient, discoverAppCatalogUrl, ensureListReady, findItemByKey, isMountedRef]);
|
|
420
|
+
const list = useCallback(async () => {
|
|
421
|
+
if (!spHttpClient) {
|
|
422
|
+
throw new Error('SPHttpClient not available. Cannot list properties.');
|
|
423
|
+
}
|
|
424
|
+
setIsLoading(true);
|
|
425
|
+
setError(undefined);
|
|
426
|
+
try {
|
|
427
|
+
const catalogUrl = await discoverAppCatalogUrl();
|
|
428
|
+
// Ensure list exists (auto-provision if needed; fallback-safe for read-only users)
|
|
429
|
+
try {
|
|
430
|
+
await ensureListReady(spHttpClient, catalogUrl);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// Provisioning may fail for read-only users — treat as "list not yet created"
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
const response = await spHttpClient.get(`${getListApiUrl(catalogUrl)}/items?$select=Id,Title,Value,Description&$orderby=Title`, SPHttpClient.configurations.v1);
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
throw new Error(`Failed to list items: ${response.statusText}`);
|
|
439
|
+
}
|
|
440
|
+
const data = await response.json();
|
|
441
|
+
return data.value.map(raw => mapItem(raw));
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
if (isMountedRef.current) {
|
|
445
|
+
const capturedError = err instanceof Error ? err : new Error(String(err));
|
|
446
|
+
setError(capturedError);
|
|
447
|
+
console.error('Failed to list tenant key-values:', capturedError);
|
|
448
|
+
}
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
if (isMountedRef.current) {
|
|
453
|
+
setIsLoading(false);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}, [spHttpClient, discoverAppCatalogUrl, ensureListReady, isMountedRef]);
|
|
457
|
+
const save = useCallback(async (key, value, description) => {
|
|
458
|
+
if (!spHttpClient) {
|
|
459
|
+
throw new Error('SPHttpClient not available. Cannot save property.');
|
|
460
|
+
}
|
|
461
|
+
setIsWriting(true);
|
|
462
|
+
setWriteError(undefined);
|
|
463
|
+
try {
|
|
464
|
+
const catalogUrl = await discoverAppCatalogUrl();
|
|
465
|
+
// Ensure list is ready (auto-provision on first operation)
|
|
466
|
+
await ensureListReady(spHttpClient, catalogUrl);
|
|
467
|
+
const serializedValue = serializeValue(value);
|
|
468
|
+
// Check if key already exists
|
|
469
|
+
const existing = await findItemByKey(spHttpClient, catalogUrl, key);
|
|
470
|
+
if (existing) {
|
|
471
|
+
// Update existing item
|
|
472
|
+
const updateResponse = await spHttpClient.post(`${getListApiUrl(catalogUrl)}/items(${existing.Id})`, SPHttpClient.configurations.v1, {
|
|
473
|
+
headers: {
|
|
474
|
+
'X-HTTP-Method': 'MERGE',
|
|
475
|
+
'If-Match': '*'
|
|
476
|
+
},
|
|
477
|
+
body: JSON.stringify({
|
|
478
|
+
Value: serializedValue,
|
|
479
|
+
Description: description ?? existing.Description ?? ''
|
|
480
|
+
})
|
|
481
|
+
});
|
|
482
|
+
if (!updateResponse.ok) {
|
|
483
|
+
const errorText = await updateResponse.text();
|
|
484
|
+
throw new Error(`Failed to update item: ${updateResponse.statusText}. ${errorText}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// Create new item
|
|
489
|
+
const createResponse = await spHttpClient.post(`${getListApiUrl(catalogUrl)}/items`, SPHttpClient.configurations.v1, {
|
|
490
|
+
body: JSON.stringify({
|
|
491
|
+
Title: key,
|
|
492
|
+
Value: serializedValue,
|
|
493
|
+
Description: description ?? ''
|
|
494
|
+
})
|
|
495
|
+
});
|
|
496
|
+
if (!createResponse.ok) {
|
|
497
|
+
const errorText = await createResponse.text();
|
|
498
|
+
throw new Error(`Failed to create item: ${createResponse.statusText}. ${errorText}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
if (isMountedRef.current) {
|
|
504
|
+
const capturedError = err instanceof Error ? err : new Error(String(err));
|
|
505
|
+
setWriteError(capturedError);
|
|
506
|
+
console.error('Failed to save tenant key-value:', capturedError);
|
|
507
|
+
}
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
if (isMountedRef.current) {
|
|
512
|
+
setIsWriting(false);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}, [spHttpClient, discoverAppCatalogUrl, ensureListReady, findItemByKey, isMountedRef]);
|
|
516
|
+
const remove = useCallback(async (key) => {
|
|
517
|
+
if (!spHttpClient) {
|
|
518
|
+
throw new Error('SPHttpClient not available. Cannot remove property.');
|
|
519
|
+
}
|
|
520
|
+
setIsWriting(true);
|
|
521
|
+
setWriteError(undefined);
|
|
522
|
+
try {
|
|
523
|
+
const catalogUrl = await discoverAppCatalogUrl();
|
|
524
|
+
// Ensure list is ready (auto-provision on first operation)
|
|
525
|
+
await ensureListReady(spHttpClient, catalogUrl);
|
|
526
|
+
// Find the item
|
|
527
|
+
const existing = await findItemByKey(spHttpClient, catalogUrl, key);
|
|
528
|
+
if (!existing) {
|
|
529
|
+
return; // Item doesn't exist — idempotent no-op
|
|
530
|
+
}
|
|
531
|
+
// Delete the item
|
|
532
|
+
const deleteResponse = await spHttpClient.post(`${getListApiUrl(catalogUrl)}/items(${existing.Id})`, SPHttpClient.configurations.v1, {
|
|
533
|
+
headers: {
|
|
534
|
+
'X-HTTP-Method': 'DELETE',
|
|
535
|
+
'If-Match': '*'
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
if (!deleteResponse.ok) {
|
|
539
|
+
const errorText = await deleteResponse.text();
|
|
540
|
+
throw new Error(`Failed to remove item: ${deleteResponse.statusText}. ${errorText}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
if (isMountedRef.current) {
|
|
545
|
+
const capturedError = err instanceof Error ? err : new Error(String(err));
|
|
546
|
+
setWriteError(capturedError);
|
|
547
|
+
console.error('Failed to remove tenant key-value:', capturedError);
|
|
548
|
+
}
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
if (isMountedRef.current) {
|
|
553
|
+
setIsWriting(false);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}, [spHttpClient, discoverAppCatalogUrl, ensureListReady, findItemByKey, isMountedRef]);
|
|
557
|
+
// Computed: ready when client is available
|
|
558
|
+
const isReady = spHttpClient !== undefined;
|
|
559
|
+
return {
|
|
560
|
+
isLoading,
|
|
561
|
+
error,
|
|
562
|
+
isWriting,
|
|
563
|
+
writeError,
|
|
564
|
+
canWrite,
|
|
565
|
+
isReady,
|
|
566
|
+
get,
|
|
567
|
+
list,
|
|
568
|
+
save,
|
|
569
|
+
remove,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
//# sourceMappingURL=useSPFxTenantKeyValueStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSPFxTenantKeyValueStore.js","sourceRoot":"","sources":["../../src/hooks/useSPFxTenantKeyValueStore.ts"],"names":[],"mappings":"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"}
|