@powerportalspro/react 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2263 @@
1
+ import { createContext, useContext, useCallback, useMemo, useEffect, useSyncExternalStore, useState, useRef, useReducer, useId } from 'react';
2
+ import { Transport, AuthClient, PowerPortalsProClient, LoginResult, SwitchIdentityResult } from '@powerportalspro/core';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/context.ts
6
+ var AuthStatus = {
7
+ Loading: "loading",
8
+ Anonymous: "anonymous",
9
+ Authenticated: "authenticated"
10
+ };
11
+ var PowerPortalsProContext = createContext(null);
12
+
13
+ // src/caches/in-memory-cache.ts
14
+ var InMemoryCache = class {
15
+ constructor(fetcher) {
16
+ this.fetcher = fetcher;
17
+ }
18
+ fetcher;
19
+ resolved = /* @__PURE__ */ new Map();
20
+ inFlight = /* @__PURE__ */ new Map();
21
+ async getAsync(key) {
22
+ const cached = this.resolved.get(key);
23
+ if (cached !== void 0) return cached;
24
+ const pending = this.inFlight.get(key);
25
+ if (pending) return pending;
26
+ const promise = (async () => {
27
+ try {
28
+ const result = await this.fetcher(key);
29
+ if (result !== null) this.resolved.set(key, result);
30
+ return result;
31
+ } finally {
32
+ this.inFlight.delete(key);
33
+ }
34
+ })();
35
+ this.inFlight.set(key, promise);
36
+ return promise;
37
+ }
38
+ invalidate(key) {
39
+ this.resolved.delete(key);
40
+ }
41
+ clear() {
42
+ this.resolved.clear();
43
+ }
44
+ };
45
+ var InMemoryTableMetadataCache = class {
46
+ cache;
47
+ constructor(ppp) {
48
+ this.cache = new InMemoryCache(
49
+ (tableLogicalName) => ppp.retrieveTableMetadataAsync(tableLogicalName)
50
+ );
51
+ }
52
+ getAsync(tableLogicalName) {
53
+ return this.cache.getAsync(tableLogicalName);
54
+ }
55
+ invalidate(tableLogicalName) {
56
+ this.cache.invalidate(tableLogicalName);
57
+ }
58
+ clear() {
59
+ this.cache.clear();
60
+ }
61
+ };
62
+ var InMemoryViewMetadataCache = class {
63
+ cache;
64
+ constructor(ppp) {
65
+ this.cache = new InMemoryCache(
66
+ (viewId) => ppp.retrieveViewMetadataAsync(viewId)
67
+ );
68
+ }
69
+ getAsync(viewId) {
70
+ return this.cache.getAsync(viewId);
71
+ }
72
+ invalidate(viewId) {
73
+ this.cache.invalidate(viewId);
74
+ }
75
+ clear() {
76
+ this.cache.clear();
77
+ }
78
+ };
79
+ var InMemoryEnvironmentFileSettingsCache = class _InMemoryEnvironmentFileSettingsCache {
80
+ static KEY = "environment-file-settings";
81
+ cache;
82
+ constructor(ppp) {
83
+ this.cache = new InMemoryCache(
84
+ () => ppp.getEnvironmentFileSettingsAsync()
85
+ );
86
+ }
87
+ getAsync() {
88
+ return this.cache.getAsync(_InMemoryEnvironmentFileSettingsCache.KEY);
89
+ }
90
+ invalidate() {
91
+ this.cache.invalidate(_InMemoryEnvironmentFileSettingsCache.KEY);
92
+ }
93
+ };
94
+ var InMemoryViewsForTableCache = class {
95
+ cache;
96
+ constructor(ppp) {
97
+ this.cache = new InMemoryCache(async (tableLogicalName) => {
98
+ const result = await ppp.retrieveViewsForTableAsync(tableLogicalName);
99
+ return result ?? [];
100
+ });
101
+ }
102
+ async getAsync(tableLogicalName) {
103
+ const result = await this.cache.getAsync(tableLogicalName);
104
+ return result ?? [];
105
+ }
106
+ invalidate(tableLogicalName) {
107
+ this.cache.invalidate(tableLogicalName);
108
+ }
109
+ clear() {
110
+ this.cache.clear();
111
+ }
112
+ };
113
+ function usePowerPortalsPro() {
114
+ const ctx = useContext(PowerPortalsProContext);
115
+ if (!ctx) {
116
+ throw new Error(
117
+ "usePowerPortalsPro must be used inside <PowerPortalsProProvider>. Wrap your app at or near the root."
118
+ );
119
+ }
120
+ return ctx;
121
+ }
122
+ var LocalizerContext = createContext(null);
123
+ LocalizerContext.displayName = "Localizer";
124
+ function useLocalizer() {
125
+ const ctx = useContext(LocalizerContext);
126
+ if (!ctx) {
127
+ throw new Error(
128
+ "useLocalizer must be used inside a <LocalizerProvider>. PowerPortalsProProvider auto-mounts a default localizer; ensure your tree is wrapped."
129
+ );
130
+ }
131
+ return ctx;
132
+ }
133
+ function useT() {
134
+ return useLocalizer().t;
135
+ }
136
+ function usePrefixedT(prefix) {
137
+ const t = useT();
138
+ return useCallback(
139
+ (key, args) => t(`${prefix}.${key}`, args),
140
+ [t, prefix]
141
+ );
142
+ }
143
+ var LocalizationPrefetcherContext = createContext(null);
144
+ LocalizationPrefetcherContext.displayName = "LocalizationPrefetcher";
145
+ function useLocalizationPrefetcher() {
146
+ return useContext(LocalizationPrefetcherContext);
147
+ }
148
+ var NOOP_UNSUBSCRIBE = () => void 0;
149
+ function useLocalization(prefixes) {
150
+ const prefetcher = useLocalizationPrefetcher();
151
+ const prefixesKey = useMemo(() => {
152
+ if (prefixes.length === 0) return "";
153
+ const unique = Array.from(new Set(prefixes));
154
+ unique.sort();
155
+ return unique.join("\n");
156
+ }, [prefixes]);
157
+ useEffect(() => {
158
+ if (!prefetcher) return;
159
+ if (prefixesKey === "") return;
160
+ const tokens = prefixesKey.split("\n");
161
+ prefetcher.prefetch(tokens);
162
+ return prefetcher.subscribe(() => {
163
+ prefetcher.prefetch(tokens);
164
+ });
165
+ }, [prefetcher, prefixesKey]);
166
+ }
167
+ function useLocalizationLoaded(prefixes) {
168
+ const prefetcher = useLocalizationPrefetcher();
169
+ const subscribe = useCallback(
170
+ (listener) => prefetcher?.subscribe(listener) ?? NOOP_UNSUBSCRIBE,
171
+ [prefetcher]
172
+ );
173
+ const getSnapshot = useCallback(
174
+ () => prefetcher ? prefetcher.isLoaded(prefixes) : true,
175
+ [prefetcher, prefixes]
176
+ );
177
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshotAlwaysLoaded);
178
+ }
179
+ var getServerSnapshotAlwaysLoaded = () => true;
180
+ function LocalizerProvider({
181
+ value,
182
+ children
183
+ }) {
184
+ return /* @__PURE__ */ jsx(LocalizerContext.Provider, { value, children });
185
+ }
186
+ function flattenStrings(obj, prefix = "") {
187
+ const out = {};
188
+ for (const [k, v] of Object.entries(obj)) {
189
+ const key = prefix ? `${prefix}.${k}` : k;
190
+ if (typeof v === "string") {
191
+ out[key] = v;
192
+ } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
193
+ Object.assign(out, flattenStrings(v, key));
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+ function interpolate(template, args) {
199
+ if (!args || args.length === 0) return template;
200
+ return template.replace(/\{(\d+)(?::[^}]*)?\}/g, (match, indexStr) => {
201
+ const idx = Number(indexStr);
202
+ if (idx < 0 || idx >= args.length) return match;
203
+ return String(args[idx]);
204
+ });
205
+ }
206
+ function createLocalizer(strings, options) {
207
+ const warnedKeys = /* @__PURE__ */ new Set();
208
+ return {
209
+ t(key, args) {
210
+ const template = strings[key];
211
+ if (template === void 0) {
212
+ if (options?.onMissingKey && !warnedKeys.has(key)) {
213
+ warnedKeys.add(key);
214
+ options.onMissingKey(key);
215
+ }
216
+ return key;
217
+ }
218
+ return interpolate(template, args);
219
+ }
220
+ };
221
+ }
222
+ async function fetchLocalizationJson(url) {
223
+ const response = await fetch(url);
224
+ if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${url}`);
225
+ return await response.json();
226
+ }
227
+ function resolveLocalizationUrl(template, locale) {
228
+ return template.replace(/\{locale\}/g, locale);
229
+ }
230
+ var ASP_NET_CULTURE_COOKIE = ".AspNetCore.Culture";
231
+ function getAspNetCultureCookie() {
232
+ if (typeof document === "undefined" || !document.cookie) return null;
233
+ for (const part of document.cookie.split("; ")) {
234
+ const eq = part.indexOf("=");
235
+ if (eq < 0) continue;
236
+ const name = part.substring(0, eq);
237
+ if (name !== ASP_NET_CULTURE_COOKIE) continue;
238
+ const value = decodeURIComponent(part.substring(eq + 1));
239
+ const cMatch = value.match(/c=([^|]+)/);
240
+ if (cMatch?.[1]) return cMatch[1];
241
+ return value || null;
242
+ }
243
+ return null;
244
+ }
245
+ function setAspNetCultureCookie(culture, options) {
246
+ if (typeof document === "undefined") return;
247
+ const path = options?.path ?? "/";
248
+ const maxAge = options?.maxAgeSeconds ?? 31536e3;
249
+ const value = encodeURIComponent(`c=${culture}|uic=${culture}`);
250
+ document.cookie = `${ASP_NET_CULTURE_COOKIE}=${value}; path=${path}; max-age=${maxAge}; samesite=lax`;
251
+ }
252
+ var DEFAULT_URL_LOCALE_PATTERN = /^\/([a-z]{2}(?:-[a-zA-Z]{2,4})?)(?:\/|$)/;
253
+ function getLocaleFromUrlPath(pattern = DEFAULT_URL_LOCALE_PATTERN) {
254
+ if (typeof window === "undefined") return null;
255
+ const match = window.location.pathname.match(pattern);
256
+ return match?.[1] ?? null;
257
+ }
258
+ function detectLocale(options) {
259
+ const {
260
+ matchUrl = true,
261
+ urlPattern = DEFAULT_URL_LOCALE_PATTERN,
262
+ matchBrowser = true,
263
+ fallback = "en"
264
+ } = options ?? {};
265
+ if (matchUrl) {
266
+ const fromUrl = getLocaleFromUrlPath(urlPattern);
267
+ if (fromUrl) return fromUrl;
268
+ }
269
+ const fromCookie = getAspNetCultureCookie();
270
+ if (fromCookie) return fromCookie;
271
+ if (matchBrowser && typeof navigator !== "undefined" && navigator.language) {
272
+ const primary = navigator.language.split("-")[0];
273
+ if (primary) return primary;
274
+ }
275
+ return fallback;
276
+ }
277
+ var MISSING_KEY_FLUSH_DELAY_MS = 75;
278
+ var DEFAULT_BUNDLE_SENTINEL = "defaultBundle";
279
+ function coerceKeyToToken(key) {
280
+ if (!key) return null;
281
+ const lower = key.toLowerCase();
282
+ if (!lower.startsWith("tables.")) return null;
283
+ const afterTables = lower.substring("tables.".length);
284
+ const firstDot = afterTables.indexOf(".");
285
+ if (firstDot < 0) return `tables.${afterTables}`;
286
+ const tableName = afterTables.substring(0, firstDot);
287
+ const afterTable = afterTables.substring(firstDot + 1);
288
+ if (afterTable.startsWith("views.")) {
289
+ const afterViews = afterTable.substring("views.".length);
290
+ const nextDot = afterViews.indexOf(".");
291
+ const viewId = nextDot < 0 ? afterViews : afterViews.substring(0, nextDot);
292
+ if (!viewId) return null;
293
+ return `views.${viewId}`;
294
+ }
295
+ return `tables.${tableName}`;
296
+ }
297
+ function coercePrefixToToken(prefix) {
298
+ if (!prefix) return null;
299
+ const lower = prefix.toLowerCase();
300
+ if (lower.startsWith("views.")) {
301
+ const afterViews = lower.substring("views.".length);
302
+ const nextDot = afterViews.indexOf(".");
303
+ const viewId = nextDot < 0 ? afterViews : afterViews.substring(0, nextDot);
304
+ return viewId ? `views.${viewId}` : null;
305
+ }
306
+ return coerceKeyToToken(prefix);
307
+ }
308
+ var LocaleContext = createContext(null);
309
+ LocaleContext.displayName = "Locale";
310
+ var LocalizationManifestContext = createContext(null);
311
+ LocalizationManifestContext.displayName = "LocalizationManifest";
312
+ function useLocale() {
313
+ const ctx = useContext(LocaleContext);
314
+ return ctx ?? { locale: "en", setLocale: noopSetLocale };
315
+ }
316
+ var noopSetLocale = (_) => void 0;
317
+ function LocaleProvider({
318
+ initialLocale = "en",
319
+ autoDetect = true,
320
+ urlPattern,
321
+ persistToCookie = true,
322
+ locale: controlledLocale,
323
+ onLocaleChange,
324
+ children
325
+ }) {
326
+ const [internalLocale, setInternalLocale] = useState(() => {
327
+ if (!autoDetect) return initialLocale;
328
+ return detectLocale({
329
+ matchUrl: urlPattern !== null,
330
+ ...urlPattern instanceof RegExp && { urlPattern },
331
+ fallback: initialLocale
332
+ });
333
+ });
334
+ const isControlled = controlledLocale !== void 0;
335
+ const locale = isControlled ? controlledLocale : internalLocale;
336
+ const setLocale = useCallback(
337
+ (next) => {
338
+ if (!isControlled) setInternalLocale(next);
339
+ if (persistToCookie) setAspNetCultureCookie(next);
340
+ onLocaleChange?.(next);
341
+ },
342
+ [isControlled, persistToCookie, onLocaleChange]
343
+ );
344
+ const value = useMemo(() => ({ locale, setLocale }), [locale, setLocale]);
345
+ return /* @__PURE__ */ jsx(LocaleContext.Provider, { value, children });
346
+ }
347
+ function defaultMissingKeyWarning(key) {
348
+ console.warn(`PowerPortalsPro: missing localization key "${key}"`);
349
+ }
350
+ function DefaultLocalizerProvider({
351
+ urls,
352
+ url,
353
+ onMissingKey = defaultMissingKeyWarning,
354
+ prefixes,
355
+ children
356
+ }) {
357
+ const [cache, setCache] = useState({});
358
+ const [supportedLocales, setSupportedLocales] = useState(void 0);
359
+ const { locale } = useLocale();
360
+ const { ppp } = usePowerPortalsPro();
361
+ const localeRef = useRef(locale);
362
+ localeRef.current = locale;
363
+ const pppRef = useRef(ppp);
364
+ pppRef.current = ppp;
365
+ const resolvedLocaleRef = useRef(null);
366
+ const thumbprintsRef = useRef(null);
367
+ const pendingTokensRef = useRef(/* @__PURE__ */ new Set());
368
+ const triedTokensRef = useRef(/* @__PURE__ */ new Set());
369
+ const pendingTimerRef = useRef(null);
370
+ const settledTokensRef = useRef(/* @__PURE__ */ new Set());
371
+ const listenersRef = useRef(/* @__PURE__ */ new Set());
372
+ const baselinePrefixesRef = useRef([]);
373
+ const baselineTokensRef = useRef([]);
374
+ const baselinePrefixesKey = useMemo(() => {
375
+ const input = prefixes ?? [];
376
+ if (input.length === 0) {
377
+ baselinePrefixesRef.current = [];
378
+ baselineTokensRef.current = [];
379
+ return "";
380
+ }
381
+ const unique = Array.from(new Set(input));
382
+ unique.sort();
383
+ baselinePrefixesRef.current = unique;
384
+ const tokens = [];
385
+ for (const p of unique) {
386
+ const t = coercePrefixToToken(p);
387
+ if (t !== null) tokens.push(t);
388
+ }
389
+ baselineTokensRef.current = tokens;
390
+ return unique.join("\n");
391
+ }, [prefixes]);
392
+ const notifyListeners = useCallback(() => {
393
+ const snapshot = Array.from(listenersRef.current);
394
+ for (const listener of snapshot) listener();
395
+ }, []);
396
+ const previousRunRef = useRef(null);
397
+ const flushPendingRef = useRef(() => void 0);
398
+ const overlayTemplates = urls ?? (url !== void 0 ? [url] : []);
399
+ const resolvedOverlayUrls = overlayTemplates.map((t) => resolveLocalizationUrl(t, locale));
400
+ const overlayUrlsKey = resolvedOverlayUrls.join("\n");
401
+ useEffect(() => {
402
+ let cancelled = false;
403
+ const previous = previousRunRef.current;
404
+ const isLocaleOrUrlChange = previous !== null && (previous.locale !== locale || previous.overlayKey !== overlayUrlsKey);
405
+ previousRunRef.current = { locale, overlayKey: overlayUrlsKey };
406
+ if (isLocaleOrUrlChange) {
407
+ setCache({});
408
+ setSupportedLocales(void 0);
409
+ pendingTokensRef.current = /* @__PURE__ */ new Set();
410
+ triedTokensRef.current = /* @__PURE__ */ new Set();
411
+ if (pendingTimerRef.current !== null) {
412
+ clearTimeout(pendingTimerRef.current);
413
+ pendingTimerRef.current = null;
414
+ }
415
+ if (settledTokensRef.current.size > 0) {
416
+ settledTokensRef.current = /* @__PURE__ */ new Set();
417
+ notifyListeners();
418
+ }
419
+ }
420
+ resolvedLocaleRef.current = null;
421
+ thumbprintsRef.current = null;
422
+ const overlayList = overlayUrlsKey.split("\n").filter((u) => u.length > 0);
423
+ (async () => {
424
+ try {
425
+ const manifest = await pppRef.current.retrieveLocalizationBundleManifestAsync();
426
+ if (cancelled) return;
427
+ setSupportedLocales(manifest.supportedLocales);
428
+ const resolvedLocale = manifest.supportedLocales.includes(locale) ? locale : manifest.supportedLocales[0] ?? locale;
429
+ const thumbs = await pppRef.current.retrieveLocalizationThumbprintsAsync(resolvedLocale);
430
+ if (cancelled) return;
431
+ resolvedLocaleRef.current = resolvedLocale;
432
+ thumbprintsRef.current = thumbs;
433
+ const [bundleData, ...overlayDatas] = await Promise.all([
434
+ pppRef.current.retrieveLocalizationBundleAsync(resolvedLocale, thumbs.bundle),
435
+ ...overlayList.map(fetchLocalizationJson)
436
+ ]);
437
+ if (cancelled) return;
438
+ const merged = {};
439
+ Object.assign(merged, flattenStrings(bundleData));
440
+ for (const data of overlayDatas) {
441
+ Object.assign(merged, flattenStrings(data));
442
+ }
443
+ setCache(merged);
444
+ } catch (e) {
445
+ if (cancelled) return;
446
+ console.error(
447
+ "PowerPortalsPro: failed to load localization bundle; staying on key-fallback localizer.",
448
+ e
449
+ );
450
+ }
451
+ if (cancelled) return;
452
+ settledTokensRef.current.add(DEFAULT_BUNDLE_SENTINEL);
453
+ for (const token of baselineTokensRef.current) {
454
+ if (triedTokensRef.current.has(token)) continue;
455
+ if (pendingTokensRef.current.has(token)) continue;
456
+ pendingTokensRef.current.add(token);
457
+ }
458
+ if (pendingTokensRef.current.size > 0 && pendingTimerRef.current === null) {
459
+ pendingTimerRef.current = setTimeout(
460
+ () => flushPendingRef.current(),
461
+ MISSING_KEY_FLUSH_DELAY_MS
462
+ );
463
+ }
464
+ notifyListeners();
465
+ })();
466
+ return () => {
467
+ cancelled = true;
468
+ if (pendingTimerRef.current !== null) {
469
+ clearTimeout(pendingTimerRef.current);
470
+ pendingTimerRef.current = null;
471
+ }
472
+ };
473
+ }, [locale, overlayUrlsKey]);
474
+ const flushPending = useCallback(async () => {
475
+ pendingTimerRef.current = null;
476
+ const tokensToFetch = Array.from(pendingTokensRef.current);
477
+ if (tokensToFetch.length === 0) return;
478
+ pendingTokensRef.current = /* @__PURE__ */ new Set();
479
+ for (const tok of tokensToFetch) {
480
+ triedTokensRef.current.add(tok);
481
+ }
482
+ const thumbs = thumbprintsRef.current;
483
+ const requestLocale = resolvedLocaleRef.current;
484
+ if (!thumbs || !requestLocale) {
485
+ for (const tok of tokensToFetch) {
486
+ triedTokensRef.current.delete(tok);
487
+ pendingTokensRef.current.add(tok);
488
+ }
489
+ return;
490
+ }
491
+ const fetches = tokensToFetch.map(async (token) => {
492
+ try {
493
+ if (token.startsWith("tables.")) {
494
+ const tableName = token.substring("tables.".length);
495
+ const thumb = thumbs.tables[tableName];
496
+ if (!thumb) return null;
497
+ return await pppRef.current.retrieveLocalizationTableBundleAsync(
498
+ tableName,
499
+ requestLocale,
500
+ thumb
501
+ );
502
+ }
503
+ if (token.startsWith("views.")) {
504
+ const viewId = token.substring("views.".length);
505
+ const thumb = thumbs.views[viewId];
506
+ if (!thumb) return null;
507
+ return await pppRef.current.retrieveLocalizationViewBundleAsync(
508
+ viewId,
509
+ requestLocale,
510
+ thumb
511
+ );
512
+ }
513
+ return null;
514
+ } catch (e) {
515
+ console.warn(`PowerPortalsPro: localization fetch failed for token ${token}`, e);
516
+ triedTokensRef.current.delete(token);
517
+ return null;
518
+ }
519
+ });
520
+ const responses = await Promise.all(fetches);
521
+ if (requestLocale !== resolvedLocaleRef.current) return;
522
+ let settledChanged = false;
523
+ for (const tok of tokensToFetch) {
524
+ if (!settledTokensRef.current.has(tok)) {
525
+ settledTokensRef.current.add(tok);
526
+ settledChanged = true;
527
+ }
528
+ }
529
+ const merged = {};
530
+ for (const data of responses) {
531
+ if (data === null) continue;
532
+ Object.assign(merged, flattenStrings(data));
533
+ }
534
+ if (Object.keys(merged).length > 0) {
535
+ setCache((prev) => ({ ...prev, ...merged }));
536
+ }
537
+ if (settledChanged) {
538
+ notifyListeners();
539
+ }
540
+ }, [notifyListeners]);
541
+ flushPendingRef.current = flushPending;
542
+ useEffect(() => {
543
+ if (!settledTokensRef.current.has(DEFAULT_BUNDLE_SENTINEL)) return;
544
+ let added = false;
545
+ for (const token of baselineTokensRef.current) {
546
+ if (triedTokensRef.current.has(token)) continue;
547
+ if (pendingTokensRef.current.has(token)) continue;
548
+ pendingTokensRef.current.add(token);
549
+ added = true;
550
+ }
551
+ if (pendingTokensRef.current.size > 0 && pendingTimerRef.current === null) {
552
+ pendingTimerRef.current = setTimeout(flushPending, MISSING_KEY_FLUSH_DELAY_MS);
553
+ }
554
+ if (added) notifyListeners();
555
+ }, [baselinePrefixesKey, flushPending, notifyListeners]);
556
+ const queueMissingKey = useCallback(
557
+ (key) => {
558
+ const token = coerceKeyToToken(key);
559
+ if (token === null) return;
560
+ if (triedTokensRef.current.has(token)) return;
561
+ if (pendingTokensRef.current.has(token)) return;
562
+ pendingTokensRef.current.add(token);
563
+ if (pendingTimerRef.current === null) {
564
+ pendingTimerRef.current = setTimeout(flushPending, MISSING_KEY_FLUSH_DELAY_MS);
565
+ }
566
+ },
567
+ [flushPending]
568
+ );
569
+ const onMissingKeyCombined = useCallback(
570
+ (key) => {
571
+ onMissingKey(key);
572
+ queueMissingKey(key);
573
+ },
574
+ [onMissingKey, queueMissingKey]
575
+ );
576
+ const localizer = useMemo(
577
+ () => createLocalizer(cache, { onMissingKey: onMissingKeyCombined }),
578
+ [cache, onMissingKeyCombined]
579
+ );
580
+ const prefetcher = useMemo(
581
+ () => ({
582
+ prefetch(prefixes2) {
583
+ for (const prefix of prefixes2) {
584
+ const token = coercePrefixToToken(prefix);
585
+ if (token === null) continue;
586
+ if (triedTokensRef.current.has(token)) continue;
587
+ if (pendingTokensRef.current.has(token)) continue;
588
+ pendingTokensRef.current.add(token);
589
+ }
590
+ if (pendingTokensRef.current.size > 0 && pendingTimerRef.current === null) {
591
+ pendingTimerRef.current = setTimeout(flushPending, MISSING_KEY_FLUSH_DELAY_MS);
592
+ }
593
+ },
594
+ isLoaded(prefixes2) {
595
+ if (!settledTokensRef.current.has(DEFAULT_BUNDLE_SENTINEL)) return false;
596
+ for (const token of baselineTokensRef.current) {
597
+ if (!settledTokensRef.current.has(token)) return false;
598
+ }
599
+ for (const prefix of prefixes2) {
600
+ const token = coercePrefixToToken(prefix);
601
+ if (token === null) continue;
602
+ if (!settledTokensRef.current.has(token)) return false;
603
+ }
604
+ return true;
605
+ },
606
+ subscribe(listener) {
607
+ listenersRef.current.add(listener);
608
+ return () => {
609
+ listenersRef.current.delete(listener);
610
+ };
611
+ }
612
+ }),
613
+ [flushPending]
614
+ );
615
+ const manifestContextValue = useMemo(
616
+ () => ({ supportedLocales }),
617
+ [supportedLocales]
618
+ );
619
+ return /* @__PURE__ */ jsx(LocalizationManifestContext.Provider, { value: manifestContextValue, children: /* @__PURE__ */ jsx(LocalizationPrefetcherContext.Provider, { value: prefetcher, children: /* @__PURE__ */ jsx(LocalizerProvider, { value: localizer, children }) }) });
620
+ }
621
+ var UnsavedChangesRegistryContext = createContext(null);
622
+ UnsavedChangesRegistryContext.displayName = "UnsavedChangesRegistry";
623
+ function UnsavedChangesRegistryProvider({ children }) {
624
+ const reportersRef = useRef(/* @__PURE__ */ new Set());
625
+ const listenersRef = useRef(/* @__PURE__ */ new Set());
626
+ const notify = useCallback(() => {
627
+ for (const listener of listenersRef.current) listener();
628
+ }, []);
629
+ const register = useCallback(
630
+ (reporter) => {
631
+ reportersRef.current.add(reporter);
632
+ notify();
633
+ return () => {
634
+ reportersRef.current.delete(reporter);
635
+ notify();
636
+ };
637
+ },
638
+ [notify]
639
+ );
640
+ const subscribe = useCallback((listener) => {
641
+ listenersRef.current.add(listener);
642
+ return () => {
643
+ listenersRef.current.delete(listener);
644
+ };
645
+ }, []);
646
+ const isAnyDirty = useCallback(() => {
647
+ for (const reporter of reportersRef.current) {
648
+ if (reporter.isDirty()) return true;
649
+ }
650
+ return false;
651
+ }, []);
652
+ const registry = useMemo(
653
+ () => ({ register, subscribe, isAnyDirty, notifyDirtyChanged: notify }),
654
+ [register, subscribe, isAnyDirty, notify]
655
+ );
656
+ return /* @__PURE__ */ jsx(UnsavedChangesRegistryContext.Provider, { value: registry, children });
657
+ }
658
+ function useUnsavedChangesRegistry() {
659
+ return useContext(UnsavedChangesRegistryContext);
660
+ }
661
+ function useIsAnyDirty() {
662
+ const registry = useUnsavedChangesRegistry();
663
+ const subscribe = useCallback(
664
+ (listener) => {
665
+ if (!registry) return () => {
666
+ };
667
+ return registry.subscribe(listener);
668
+ },
669
+ [registry]
670
+ );
671
+ const getSnapshot = useCallback(() => {
672
+ if (!registry) return false;
673
+ return registry.isAnyDirty();
674
+ }, [registry]);
675
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
676
+ }
677
+ function PowerPortalsProProvider({
678
+ children,
679
+ transport,
680
+ localizationUrl,
681
+ localizationUrls,
682
+ localizationPrefixes,
683
+ tableMetadataCache,
684
+ viewMetadataCache,
685
+ viewsForTableCache,
686
+ environmentFileSettingsCache
687
+ }) {
688
+ const transportRef = useRef(null);
689
+ if (transportRef.current === null) {
690
+ transportRef.current = transport ?? new Transport();
691
+ }
692
+ const transportInstance = transportRef.current;
693
+ const auth = useMemo(() => new AuthClient(transportInstance), [transportInstance]);
694
+ const ppp = useMemo(() => new PowerPortalsProClient(transportInstance), [transportInstance]);
695
+ const resolvedTableMetadataCache = useMemo(
696
+ () => tableMetadataCache ?? new InMemoryTableMetadataCache(ppp),
697
+ [ppp, tableMetadataCache]
698
+ );
699
+ const resolvedViewMetadataCache = useMemo(
700
+ () => viewMetadataCache ?? new InMemoryViewMetadataCache(ppp),
701
+ [ppp, viewMetadataCache]
702
+ );
703
+ const resolvedViewsForTableCache = useMemo(
704
+ () => viewsForTableCache ?? new InMemoryViewsForTableCache(ppp),
705
+ [ppp, viewsForTableCache]
706
+ );
707
+ const resolvedEnvironmentFileSettingsCache = useMemo(
708
+ () => environmentFileSettingsCache ?? new InMemoryEnvironmentFileSettingsCache(ppp),
709
+ [ppp, environmentFileSettingsCache]
710
+ );
711
+ const [authState, setAuthState] = useState({ status: AuthStatus.Loading });
712
+ const refreshAuth = useCallback(async () => {
713
+ try {
714
+ const me = await auth.getCurrentUserAsync();
715
+ if (me.isAuthenticated) {
716
+ setAuthState({ status: AuthStatus.Authenticated, user: me });
717
+ } else {
718
+ setAuthState({ status: AuthStatus.Anonymous });
719
+ }
720
+ } catch {
721
+ setAuthState({ status: AuthStatus.Anonymous });
722
+ }
723
+ }, [auth]);
724
+ const login = useCallback(
725
+ async (request) => {
726
+ const response = await auth.loginAsync(request);
727
+ if (response.result === LoginResult.Success) {
728
+ await refreshAuth();
729
+ }
730
+ return response;
731
+ },
732
+ [auth, refreshAuth]
733
+ );
734
+ const logout = useCallback(async () => {
735
+ await auth.logoutAsync();
736
+ setAuthState({ status: AuthStatus.Anonymous });
737
+ }, [auth]);
738
+ const switchIdentity = useCallback(async () => {
739
+ const response = await auth.switchIdentityAsync();
740
+ if (response.result === SwitchIdentityResult.Switched) {
741
+ await refreshAuth();
742
+ }
743
+ return response;
744
+ }, [auth, refreshAuth]);
745
+ useEffect(() => {
746
+ refreshAuth();
747
+ }, [refreshAuth]);
748
+ const value = useMemo(
749
+ () => ({
750
+ transport: transportInstance,
751
+ auth,
752
+ ppp,
753
+ tableMetadataCache: resolvedTableMetadataCache,
754
+ viewMetadataCache: resolvedViewMetadataCache,
755
+ viewsForTableCache: resolvedViewsForTableCache,
756
+ environmentFileSettingsCache: resolvedEnvironmentFileSettingsCache,
757
+ authState,
758
+ refreshAuth,
759
+ login,
760
+ logout,
761
+ switchIdentity
762
+ }),
763
+ [
764
+ transportInstance,
765
+ auth,
766
+ ppp,
767
+ resolvedTableMetadataCache,
768
+ resolvedViewMetadataCache,
769
+ resolvedViewsForTableCache,
770
+ resolvedEnvironmentFileSettingsCache,
771
+ authState,
772
+ refreshAuth,
773
+ login,
774
+ logout,
775
+ switchIdentity
776
+ ]
777
+ );
778
+ let localizationContent;
779
+ if (localizationUrl === false) {
780
+ localizationContent = children;
781
+ } else {
782
+ const overlayUrls = [];
783
+ if (typeof localizationUrl === "string") overlayUrls.push(localizationUrl);
784
+ if (localizationUrls) overlayUrls.push(...localizationUrls);
785
+ localizationContent = /* @__PURE__ */ jsxs(LocaleProvider, { children: [
786
+ /* @__PURE__ */ jsx(CacheLocaleSync, {}),
787
+ /* @__PURE__ */ jsx(
788
+ DefaultLocalizerProvider,
789
+ {
790
+ ...overlayUrls.length > 0 && { urls: overlayUrls },
791
+ ...localizationPrefixes !== void 0 && { prefixes: localizationPrefixes },
792
+ children
793
+ }
794
+ )
795
+ ] });
796
+ }
797
+ return /* @__PURE__ */ jsx(PowerPortalsProContext.Provider, { value, children: /* @__PURE__ */ jsx(UnsavedChangesRegistryProvider, { children: localizationContent }) });
798
+ }
799
+ function CacheLocaleSync() {
800
+ const { locale } = useLocale();
801
+ const {
802
+ tableMetadataCache,
803
+ viewMetadataCache,
804
+ viewsForTableCache,
805
+ environmentFileSettingsCache
806
+ } = usePowerPortalsPro();
807
+ const isFirstRun = useRef(true);
808
+ useEffect(() => {
809
+ if (isFirstRun.current) {
810
+ isFirstRun.current = false;
811
+ return;
812
+ }
813
+ tableMetadataCache.clear();
814
+ viewMetadataCache.clear();
815
+ viewsForTableCache.clear();
816
+ environmentFileSettingsCache.invalidate();
817
+ }, [
818
+ locale,
819
+ tableMetadataCache,
820
+ viewMetadataCache,
821
+ viewsForTableCache,
822
+ environmentFileSettingsCache
823
+ ]);
824
+ return null;
825
+ }
826
+
827
+ // src/use-auth.ts
828
+ function useAuth() {
829
+ const { authState, refreshAuth, login, logout, switchIdentity } = usePowerPortalsPro();
830
+ return {
831
+ ...authState,
832
+ refresh: refreshAuth,
833
+ login,
834
+ logout,
835
+ switchIdentity
836
+ };
837
+ }
838
+ function AuthorizedView({ children, roles, fallback = null }) {
839
+ const auth = useAuth();
840
+ if (auth.status !== AuthStatus.Authenticated) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
841
+ if (roles !== void 0) {
842
+ const required = typeof roles === "string" ? [roles] : roles;
843
+ if (required.length > 0) {
844
+ const userRoles = auth.user.roles ?? [];
845
+ const hasAny = required.some((r) => userRoles.includes(r));
846
+ if (!hasAny) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
847
+ }
848
+ }
849
+ return /* @__PURE__ */ jsx(Fragment, { children });
850
+ }
851
+ var QueryStatus = {
852
+ Loading: "loading",
853
+ Success: "success",
854
+ Error: "error"
855
+ };
856
+ function useAsyncResource(fetcher, deps, enabled = true, keepPreviousData = false) {
857
+ const [state, setState] = useState({
858
+ status: enabled ? QueryStatus.Loading : QueryStatus.Success,
859
+ data: void 0,
860
+ error: void 0,
861
+ isFetching: enabled
862
+ });
863
+ const [refetchTick, setRefetchTick] = useState(0);
864
+ const pendingResolversRef = useRef([]);
865
+ useEffect(() => {
866
+ if (!enabled) {
867
+ setState({
868
+ status: QueryStatus.Success,
869
+ data: void 0,
870
+ error: void 0,
871
+ isFetching: false
872
+ });
873
+ const resolvers = pendingResolversRef.current;
874
+ pendingResolversRef.current = [];
875
+ for (const r of resolvers) r.resolve();
876
+ return;
877
+ }
878
+ const controller = new AbortController();
879
+ setState((prev) => {
880
+ if (keepPreviousData && prev.status === QueryStatus.Success && prev.data !== void 0) {
881
+ if (prev.isFetching && prev.error === void 0) return prev;
882
+ return { ...prev, error: void 0, isFetching: true };
883
+ }
884
+ if (prev.status === QueryStatus.Loading && prev.data === void 0 && prev.error === void 0 && prev.isFetching) {
885
+ return prev;
886
+ }
887
+ return {
888
+ status: QueryStatus.Loading,
889
+ data: void 0,
890
+ error: void 0,
891
+ isFetching: true
892
+ };
893
+ });
894
+ fetcher(controller.signal).then(
895
+ (data) => {
896
+ if (!controller.signal.aborted) {
897
+ setState({
898
+ status: QueryStatus.Success,
899
+ data,
900
+ error: void 0,
901
+ isFetching: false
902
+ });
903
+ }
904
+ const resolvers = pendingResolversRef.current;
905
+ pendingResolversRef.current = [];
906
+ for (const r of resolvers) r.resolve();
907
+ },
908
+ (err) => {
909
+ if (controller.signal.aborted || err instanceof Error && err.name === "AbortError") {
910
+ const resolvers2 = pendingResolversRef.current;
911
+ pendingResolversRef.current = [];
912
+ for (const r of resolvers2) r.resolve();
913
+ return;
914
+ }
915
+ setState({
916
+ status: QueryStatus.Error,
917
+ data: void 0,
918
+ error: err,
919
+ isFetching: false
920
+ });
921
+ const resolvers = pendingResolversRef.current;
922
+ pendingResolversRef.current = [];
923
+ for (const r of resolvers) r.reject(err);
924
+ }
925
+ );
926
+ return () => {
927
+ controller.abort();
928
+ const resolvers = pendingResolversRef.current;
929
+ pendingResolversRef.current = [];
930
+ for (const r of resolvers) r.resolve();
931
+ };
932
+ }, [...deps, enabled, refetchTick]);
933
+ const refetch = useCallback(() => {
934
+ return new Promise((resolve, reject) => {
935
+ pendingResolversRef.current.push({ resolve, reject });
936
+ setRefetchTick((n) => n + 1);
937
+ });
938
+ }, []);
939
+ return { ...state, refetch };
940
+ }
941
+
942
+ // src/use-record.ts
943
+ function useRecord(tableLogicalName, recordId, options) {
944
+ const { ppp } = usePowerPortalsPro();
945
+ const columns = options?.columns;
946
+ const enabled = options?.enabled ?? true;
947
+ return useAsyncResource(
948
+ (signal) => ppp.retrieveRecordAsync(tableLogicalName, recordId, columns, signal),
949
+ // Joining the columns array is the cheapest way to get a stable dep without
950
+ // forcing the consumer to memoize. Column lists are short (typically <20).
951
+ [ppp, tableLogicalName, recordId, columns?.join(",") ?? ""],
952
+ enabled
953
+ );
954
+ }
955
+
956
+ // src/use-fetch-records.ts
957
+ function useFetchRecords(fetchXml, options) {
958
+ const { ppp } = usePowerPortalsPro();
959
+ const enabled = options?.enabled ?? true;
960
+ return useAsyncResource(
961
+ (signal) => ppp.retrieveRecordsAsync(fetchXml, signal),
962
+ [ppp, fetchXml],
963
+ enabled
964
+ );
965
+ }
966
+
967
+ // src/use-grid-data.ts
968
+ function useGridData(request, options) {
969
+ const { ppp } = usePowerPortalsPro();
970
+ const enabled = options?.enabled ?? true;
971
+ const keepPreviousData = options?.keepPreviousData ?? false;
972
+ const requestKey = JSON.stringify(request);
973
+ return useAsyncResource(
974
+ (signal) => ppp.loadGridAsync(request, signal),
975
+ [ppp, requestKey],
976
+ enabled,
977
+ keepPreviousData
978
+ );
979
+ }
980
+ function reduce(state, action) {
981
+ switch (action.type) {
982
+ // Filter / sort / search / pageSize changes invalidate the current
983
+ // page — the new result set has different rows, the user expects to
984
+ // land on page 1. Mirrors what `<MainGrid>` does for standalone
985
+ // mode. Highlight changes DON'T reset the page (purely client-side).
986
+ case "setFilter":
987
+ return { ...state, filter: action.filter, page: 1 };
988
+ case "setSort":
989
+ return { ...state, sort: action.sort, page: 1 };
990
+ case "setSearch":
991
+ return { ...state, search: action.search, page: 1 };
992
+ case "setPage":
993
+ return { ...state, page: action.page };
994
+ case "setPageSize":
995
+ return { ...state, pageSize: action.pageSize, page: 1 };
996
+ case "setHighlight":
997
+ return { ...state, highlight: action.highlight };
998
+ }
999
+ }
1000
+ function useViewDataSource(options) {
1001
+ const debounceMs = options.debounceMs ?? 300;
1002
+ const enabled = options.enabled ?? true;
1003
+ const keepPreviousData = options.keepPreviousData ?? true;
1004
+ const [state, dispatch] = useReducer(reduce, void 0, () => ({
1005
+ filter: options.initialFilter ?? EMPTY_FILTERS,
1006
+ sort: options.initialSort ?? EMPTY_SORTS,
1007
+ search: options.initialSearch ?? "",
1008
+ page: options.initialPage ?? 1,
1009
+ pageSize: options.initialPageSize ?? 50,
1010
+ highlight: options.initialHighlight
1011
+ }));
1012
+ const aggregationFingerprint = useMemo(() => {
1013
+ if (!options.aggregations || options.aggregations.length === 0) return null;
1014
+ return JSON.stringify({
1015
+ viewId: options.viewId,
1016
+ fetchXml: options.fetchXml,
1017
+ staticFilters: options.staticFilters,
1018
+ filter: state.filter,
1019
+ search: state.search,
1020
+ aggregations: options.aggregations
1021
+ });
1022
+ }, [
1023
+ options.viewId,
1024
+ options.fetchXml,
1025
+ options.staticFilters,
1026
+ options.aggregations,
1027
+ state.filter,
1028
+ state.search
1029
+ ]);
1030
+ const lastFetchedAggregationFingerprintRef = useRef(null);
1031
+ const cachedAggregationResultsRef = useRef(EMPTY_AGGREGATIONS);
1032
+ const request = useMemo(() => {
1033
+ const filters = [
1034
+ ...options.staticFilters ?? [],
1035
+ ...state.filter
1036
+ ];
1037
+ const includeAggregations = aggregationFingerprint !== null && aggregationFingerprint !== lastFetchedAggregationFingerprintRef.current;
1038
+ return {
1039
+ ...options.viewId !== void 0 && { viewId: options.viewId },
1040
+ ...options.fetchXml !== void 0 && { fetchXml: options.fetchXml },
1041
+ sorts: state.sort,
1042
+ pageNumber: state.page,
1043
+ pageSize: state.pageSize,
1044
+ ...state.search !== "" && { searchText: state.search },
1045
+ ...filters.length > 0 && { filters },
1046
+ ...includeAggregations && options.aggregations && {
1047
+ aggregations: options.aggregations
1048
+ }
1049
+ };
1050
+ }, [
1051
+ options.viewId,
1052
+ options.fetchXml,
1053
+ options.staticFilters,
1054
+ options.aggregations,
1055
+ state.filter,
1056
+ state.sort,
1057
+ state.search,
1058
+ state.page,
1059
+ state.pageSize,
1060
+ aggregationFingerprint
1061
+ ]);
1062
+ const debouncedRequest = useDebouncedValue(request, debounceMs);
1063
+ const query = useGridData(debouncedRequest, { enabled, keepPreviousData });
1064
+ const setFilter = useCallback((filter) => {
1065
+ dispatch({ type: "setFilter", filter });
1066
+ }, []);
1067
+ const setSort = useCallback((sort) => {
1068
+ dispatch({ type: "setSort", sort });
1069
+ }, []);
1070
+ const setSearch = useCallback((search) => {
1071
+ dispatch({ type: "setSearch", search });
1072
+ }, []);
1073
+ const setPage = useCallback((page) => {
1074
+ dispatch({ type: "setPage", page });
1075
+ }, []);
1076
+ const setPageSize = useCallback((pageSize) => {
1077
+ dispatch({ type: "setPageSize", pageSize });
1078
+ }, []);
1079
+ const setHighlight = useCallback((highlight) => {
1080
+ dispatch({ type: "setHighlight", highlight });
1081
+ }, []);
1082
+ const refresh = useCallback(async () => {
1083
+ await query.refetch();
1084
+ }, [query.refetch]);
1085
+ const data = query.data;
1086
+ const rows = data?.tableRecords ?? EMPTY_RECORDS;
1087
+ const columns = data?.columns ?? EMPTY_COLUMNS;
1088
+ const totalCount = data?.pagingInfo?.totalRecordCount;
1089
+ const tablePermissions = data?.tablePermissions;
1090
+ const aggregations = options.aggregations ?? EMPTY_AGGREGATION_SPEC;
1091
+ const isFetchingAggregations = query.isFetching && debouncedRequest.aggregations !== void 0 && debouncedRequest.aggregations !== null && debouncedRequest.aggregations.length > 0;
1092
+ const lastSeenDataRef = useRef(void 0);
1093
+ useEffect(() => {
1094
+ if (data === lastSeenDataRef.current) return;
1095
+ lastSeenDataRef.current = data;
1096
+ if (data?.aggregations) {
1097
+ cachedAggregationResultsRef.current = data.aggregations;
1098
+ lastFetchedAggregationFingerprintRef.current = aggregationFingerprint;
1099
+ }
1100
+ }, [data, aggregationFingerprint]);
1101
+ const aggregationResults = (options.aggregations?.length ?? 0) === 0 ? EMPTY_AGGREGATIONS : data?.aggregations ?? cachedAggregationResultsRef.current;
1102
+ return useMemo(
1103
+ () => ({
1104
+ filter: state.filter,
1105
+ sort: state.sort,
1106
+ search: state.search,
1107
+ page: state.page,
1108
+ pageSize: state.pageSize,
1109
+ highlight: state.highlight,
1110
+ aggregations,
1111
+ status: query.status,
1112
+ rows,
1113
+ totalCount,
1114
+ columns,
1115
+ tablePermissions,
1116
+ aggregationResults,
1117
+ error: query.error,
1118
+ isFetching: query.isFetching,
1119
+ isFetchingAggregations,
1120
+ setFilter,
1121
+ setSort,
1122
+ setSearch,
1123
+ setPage,
1124
+ setPageSize,
1125
+ setHighlight,
1126
+ refresh
1127
+ }),
1128
+ [
1129
+ state.filter,
1130
+ state.sort,
1131
+ state.search,
1132
+ state.page,
1133
+ state.pageSize,
1134
+ state.highlight,
1135
+ aggregations,
1136
+ query.status,
1137
+ rows,
1138
+ totalCount,
1139
+ columns,
1140
+ tablePermissions,
1141
+ aggregationResults,
1142
+ query.error,
1143
+ query.isFetching,
1144
+ isFetchingAggregations,
1145
+ setFilter,
1146
+ setSort,
1147
+ setSearch,
1148
+ setPage,
1149
+ setPageSize,
1150
+ setHighlight,
1151
+ refresh
1152
+ ]
1153
+ );
1154
+ }
1155
+ var EMPTY_FILTERS = [];
1156
+ var EMPTY_SORTS = [];
1157
+ var EMPTY_RECORDS = [];
1158
+ var EMPTY_COLUMNS = [];
1159
+ var EMPTY_AGGREGATIONS = [];
1160
+ var EMPTY_AGGREGATION_SPEC = [];
1161
+ function useDebouncedValue(value, delayMs) {
1162
+ const [debounced, setDebounced] = useState(value);
1163
+ const valueKey = JSON.stringify(value);
1164
+ const previousKeyRef = useRef(valueKey);
1165
+ useEffect(() => {
1166
+ if (delayMs === 0) {
1167
+ setDebounced(value);
1168
+ previousKeyRef.current = valueKey;
1169
+ return;
1170
+ }
1171
+ if (previousKeyRef.current === valueKey) return;
1172
+ const timer = setTimeout(() => {
1173
+ setDebounced(value);
1174
+ previousKeyRef.current = valueKey;
1175
+ }, delayMs);
1176
+ return () => clearTimeout(timer);
1177
+ }, [valueKey, delayMs]);
1178
+ return debounced;
1179
+ }
1180
+
1181
+ // src/use-table-metadata.ts
1182
+ function useTableMetadata(tableLogicalName, options) {
1183
+ const { tableMetadataCache } = usePowerPortalsPro();
1184
+ const enabled = options?.enabled ?? true;
1185
+ return useAsyncResource(
1186
+ async () => {
1187
+ const result = await tableMetadataCache.getAsync(tableLogicalName);
1188
+ return result ?? void 0;
1189
+ },
1190
+ [tableMetadataCache, tableLogicalName],
1191
+ enabled
1192
+ );
1193
+ }
1194
+
1195
+ // src/use-table-permissions.ts
1196
+ function useTablePermissions(tableLogicalName, options) {
1197
+ const { ppp } = usePowerPortalsPro();
1198
+ const enabled = (options?.enabled ?? true) && tableLogicalName.length > 0;
1199
+ return useAsyncResource(
1200
+ async () => ppp.retrieveTablePermissionsAsync(tableLogicalName),
1201
+ [ppp, tableLogicalName],
1202
+ enabled
1203
+ );
1204
+ }
1205
+
1206
+ // src/use-view-metadata.ts
1207
+ function useViewMetadata(viewId, options) {
1208
+ const { viewMetadataCache } = usePowerPortalsPro();
1209
+ const enabled = options?.enabled ?? true;
1210
+ return useAsyncResource(
1211
+ async () => {
1212
+ const result = await viewMetadataCache.getAsync(viewId);
1213
+ return result ?? void 0;
1214
+ },
1215
+ [viewMetadataCache, viewId],
1216
+ enabled
1217
+ );
1218
+ }
1219
+
1220
+ // src/use-views-for-table.ts
1221
+ function useViewsForTable(tableLogicalName, options) {
1222
+ const { viewsForTableCache } = usePowerPortalsPro();
1223
+ const enabled = options?.enabled ?? true;
1224
+ return useAsyncResource(
1225
+ () => viewsForTableCache.getAsync(tableLogicalName),
1226
+ [viewsForTableCache, tableLogicalName],
1227
+ enabled
1228
+ );
1229
+ }
1230
+
1231
+ // src/use-environment-file-settings.ts
1232
+ function useEnvironmentFileSettings(options) {
1233
+ const { environmentFileSettingsCache } = usePowerPortalsPro();
1234
+ const enabled = options?.enabled ?? true;
1235
+ return useAsyncResource(
1236
+ async () => {
1237
+ const result = await environmentFileSettingsCache.getAsync();
1238
+ return result ?? void 0;
1239
+ },
1240
+ [environmentFileSettingsCache],
1241
+ enabled
1242
+ );
1243
+ }
1244
+ function useSupportedLocales(options) {
1245
+ const { ppp } = usePowerPortalsPro();
1246
+ const enabled = options?.enabled ?? true;
1247
+ const manifestState = useContext(LocalizationManifestContext);
1248
+ const providerScoped = manifestState !== null;
1249
+ const fallback = useAsyncResource(
1250
+ async () => {
1251
+ const manifest = await ppp.retrieveLocalizationBundleManifestAsync();
1252
+ return manifest.supportedLocales;
1253
+ },
1254
+ [ppp],
1255
+ enabled && !providerScoped
1256
+ );
1257
+ if (providerScoped) {
1258
+ if (manifestState.supportedLocales === void 0) {
1259
+ return {
1260
+ status: enabled ? QueryStatus.Loading : QueryStatus.Success,
1261
+ data: void 0,
1262
+ error: void 0,
1263
+ isFetching: enabled,
1264
+ refetch: fallback.refetch
1265
+ };
1266
+ }
1267
+ return {
1268
+ status: QueryStatus.Success,
1269
+ data: manifestState.supportedLocales,
1270
+ error: void 0,
1271
+ isFetching: false,
1272
+ refetch: fallback.refetch
1273
+ };
1274
+ }
1275
+ return fallback;
1276
+ }
1277
+ function useQueryParam(name) {
1278
+ const [value, setValue] = useState(() => readParam(name));
1279
+ useEffect(() => {
1280
+ if (typeof window === "undefined") return void 0;
1281
+ installHistoryPatch();
1282
+ setValue(readParam(name));
1283
+ const handler = () => setValue(readParam(name));
1284
+ window.addEventListener("popstate", handler);
1285
+ window.addEventListener(PUSH_EVENT, handler);
1286
+ window.addEventListener(REPLACE_EVENT, handler);
1287
+ return () => {
1288
+ window.removeEventListener("popstate", handler);
1289
+ window.removeEventListener(PUSH_EVENT, handler);
1290
+ window.removeEventListener(REPLACE_EVENT, handler);
1291
+ };
1292
+ }, [name]);
1293
+ return value;
1294
+ }
1295
+ var PUSH_EVENT = "ppp:pushstate";
1296
+ var REPLACE_EVENT = "ppp:replacestate";
1297
+ var historyPatched = false;
1298
+ function installHistoryPatch() {
1299
+ if (historyPatched) return;
1300
+ if (typeof window === "undefined" || typeof window.history === "undefined") return;
1301
+ historyPatched = true;
1302
+ const origPush = window.history.pushState.bind(window.history);
1303
+ const origReplace = window.history.replaceState.bind(window.history);
1304
+ window.history.pushState = function(...args) {
1305
+ origPush(...args);
1306
+ window.dispatchEvent(new Event(PUSH_EVENT));
1307
+ };
1308
+ window.history.replaceState = function(...args) {
1309
+ origReplace(...args);
1310
+ window.dispatchEvent(new Event(REPLACE_EVENT));
1311
+ };
1312
+ }
1313
+ function readParam(name) {
1314
+ if (typeof window === "undefined") return null;
1315
+ try {
1316
+ const params = new URLSearchParams(window.location.search);
1317
+ return params.get(name);
1318
+ } catch {
1319
+ return null;
1320
+ }
1321
+ }
1322
+ function useBeforeUnloadWhen(condition) {
1323
+ useEffect(() => {
1324
+ if (!condition) return;
1325
+ const handler = (e) => {
1326
+ e.preventDefault();
1327
+ e.returnValue = "";
1328
+ };
1329
+ window.addEventListener("beforeunload", handler);
1330
+ return () => window.removeEventListener("beforeunload", handler);
1331
+ }, [condition]);
1332
+ }
1333
+ var MainContextContext = createContext(null);
1334
+ MainContextContext.displayName = "MainContext";
1335
+ function MainContext({
1336
+ children,
1337
+ onBeforeSave,
1338
+ warnOnUnsavedChanges,
1339
+ forceSuccessfulValidationBeforeSave
1340
+ }) {
1341
+ const parentMainContext = useContext(MainContextContext);
1342
+ const isTopMost = parentMainContext === null;
1343
+ const resolvedWarnOnUnsavedChanges = warnOnUnsavedChanges ?? parentMainContext?.warnOnUnsavedChanges ?? true;
1344
+ const resolvedForceSuccessfulValidationBeforeSave = forceSuccessfulValidationBeforeSave ?? parentMainContext?.forceSuccessfulValidationBeforeSave ?? true;
1345
+ const descendantsRef = useRef(/* @__PURE__ */ new Set());
1346
+ const [version, setVersion] = useState(0);
1347
+ const { ppp } = usePowerPortalsPro();
1348
+ const refreshAll = useCallback(async () => {
1349
+ for (const descendant of descendantsRef.current) {
1350
+ await descendant.refresh();
1351
+ }
1352
+ }, []);
1353
+ const resetState = useCallback(() => {
1354
+ for (const descendant of descendantsRef.current) {
1355
+ descendant.resetState();
1356
+ }
1357
+ }, []);
1358
+ const validate = useCallback(async () => {
1359
+ for (const descendant of descendantsRef.current) {
1360
+ const ok = await descendant.validate();
1361
+ if (!ok) return false;
1362
+ }
1363
+ return true;
1364
+ }, []);
1365
+ const saveAll = useCallback(async () => {
1366
+ if (onBeforeSave) await onBeforeSave();
1367
+ if (resolvedForceSuccessfulValidationBeforeSave) {
1368
+ const valid = await validate();
1369
+ if (!valid) return;
1370
+ }
1371
+ const requests = [];
1372
+ for (const descendant of descendantsRef.current) {
1373
+ requests.push(...descendant.getRequests());
1374
+ }
1375
+ if (requests.length === 0) return;
1376
+ await ppp.executeMultipleAsync(requests, { returnResponses: false });
1377
+ await refreshAll();
1378
+ }, [onBeforeSave, validate, ppp, refreshAll, resolvedForceSuccessfulValidationBeforeSave]);
1379
+ const registerDescendant = useCallback((descendant) => {
1380
+ descendantsRef.current.add(descendant);
1381
+ setVersion((v) => v + 1);
1382
+ return () => {
1383
+ descendantsRef.current.delete(descendant);
1384
+ setVersion((v) => v + 1);
1385
+ };
1386
+ }, []);
1387
+ const notifyDescendantDirtyChanged = useCallback(() => {
1388
+ setVersion((v) => v + 1);
1389
+ }, []);
1390
+ const isDirty = useMemo(() => {
1391
+ for (const descendant of descendantsRef.current) {
1392
+ if (descendant.isDirty()) return true;
1393
+ }
1394
+ return false;
1395
+ }, [version]);
1396
+ const getRequests = useCallback(() => {
1397
+ const all = [];
1398
+ for (const descendant of descendantsRef.current) {
1399
+ all.push(...descendant.getRequests());
1400
+ }
1401
+ return all;
1402
+ }, []);
1403
+ const value = useMemo(
1404
+ () => ({
1405
+ saveAll,
1406
+ refreshAll,
1407
+ resetState,
1408
+ validate,
1409
+ getRequests,
1410
+ registerDescendant,
1411
+ notifyDescendantDirtyChanged,
1412
+ isDirty,
1413
+ warnOnUnsavedChanges: resolvedWarnOnUnsavedChanges,
1414
+ forceSuccessfulValidationBeforeSave: resolvedForceSuccessfulValidationBeforeSave
1415
+ }),
1416
+ [
1417
+ saveAll,
1418
+ refreshAll,
1419
+ resetState,
1420
+ validate,
1421
+ getRequests,
1422
+ registerDescendant,
1423
+ notifyDescendantDirtyChanged,
1424
+ isDirty,
1425
+ resolvedWarnOnUnsavedChanges,
1426
+ resolvedForceSuccessfulValidationBeforeSave
1427
+ ]
1428
+ );
1429
+ useEffect(() => {
1430
+ if (!parentMainContext) return;
1431
+ return parentMainContext.registerDescendant({
1432
+ getRequests,
1433
+ validate,
1434
+ refresh: refreshAll,
1435
+ resetState,
1436
+ isDirty: () => isDirtyRef.current
1437
+ });
1438
+ }, [parentMainContext?.registerDescendant, getRequests, validate, refreshAll, resetState]);
1439
+ const parentMainContextRef = useRef(parentMainContext);
1440
+ parentMainContextRef.current = parentMainContext;
1441
+ useEffect(() => {
1442
+ parentMainContextRef.current?.notifyDescendantDirtyChanged();
1443
+ }, [isDirty]);
1444
+ useBeforeUnloadWhen(isTopMost && resolvedWarnOnUnsavedChanges && isDirty);
1445
+ const unsavedChangesRegistry = useUnsavedChangesRegistry();
1446
+ const isDirtyRef = useRef(isDirty);
1447
+ isDirtyRef.current = isDirty;
1448
+ const resolvedWarnRef = useRef(resolvedWarnOnUnsavedChanges);
1449
+ resolvedWarnRef.current = resolvedWarnOnUnsavedChanges;
1450
+ useEffect(() => {
1451
+ if (!isTopMost) return;
1452
+ if (!unsavedChangesRegistry) return;
1453
+ return unsavedChangesRegistry.register({
1454
+ isDirty: () => resolvedWarnRef.current && isDirtyRef.current
1455
+ });
1456
+ }, [isTopMost, unsavedChangesRegistry]);
1457
+ useEffect(() => {
1458
+ if (!isTopMost) return;
1459
+ if (!unsavedChangesRegistry) return;
1460
+ unsavedChangesRegistry.notifyDirtyChanged();
1461
+ }, [isTopMost, unsavedChangesRegistry, isDirty, resolvedWarnOnUnsavedChanges]);
1462
+ return /* @__PURE__ */ jsx(MainContextContext.Provider, { value, children });
1463
+ }
1464
+ function useMainContext() {
1465
+ return useContext(MainContextContext);
1466
+ }
1467
+ function MainContextProvider({
1468
+ value,
1469
+ children
1470
+ }) {
1471
+ return /* @__PURE__ */ jsx(MainContextContext.Provider, { value, children });
1472
+ }
1473
+ var OverlayContext = createContext(null);
1474
+ OverlayContext.displayName = "OverlayService";
1475
+ function useOverlayService() {
1476
+ return useContext(OverlayContext)?.service ?? null;
1477
+ }
1478
+ function useOverlayForTarget(targetId) {
1479
+ const ctx = useContext(OverlayContext);
1480
+ if (!ctx) return null;
1481
+ const stack = ctx.overlaysByTarget.get(targetId ?? "") ?? [];
1482
+ return stack[stack.length - 1] ?? null;
1483
+ }
1484
+ function OverlayServiceProvider({
1485
+ value,
1486
+ children
1487
+ }) {
1488
+ return /* @__PURE__ */ jsx(OverlayContext.Provider, { value, children });
1489
+ }
1490
+ function useOverlayServiceState() {
1491
+ const [overlaysByTarget, setOverlaysByTarget] = useState(() => /* @__PURE__ */ new Map());
1492
+ const nextIdRef = useRef(1);
1493
+ const service = useMemo(
1494
+ () => ({
1495
+ show(options) {
1496
+ const id = `overlay-${nextIdRef.current++}`;
1497
+ const instance = { id, ...options };
1498
+ setOverlaysByTarget((prev) => {
1499
+ const key = options?.targetId ?? "";
1500
+ const next = new Map(prev);
1501
+ next.set(key, [...next.get(key) ?? [], instance]);
1502
+ return next;
1503
+ });
1504
+ return instance;
1505
+ },
1506
+ hide(overlay) {
1507
+ setOverlaysByTarget((prev) => {
1508
+ const key = overlay.targetId ?? "";
1509
+ const stack = prev.get(key) ?? [];
1510
+ const idx = stack.findIndex((o) => o.id === overlay.id);
1511
+ if (idx < 0) return prev;
1512
+ const next = new Map(prev);
1513
+ if (stack.length === 1) {
1514
+ next.delete(key);
1515
+ } else {
1516
+ next.set(key, [...stack.slice(0, idx), ...stack.slice(idx + 1)]);
1517
+ }
1518
+ return next;
1519
+ });
1520
+ },
1521
+ hideAll() {
1522
+ setOverlaysByTarget(/* @__PURE__ */ new Map());
1523
+ }
1524
+ }),
1525
+ []
1526
+ );
1527
+ return useMemo(() => ({ service, overlaysByTarget }), [service, overlaysByTarget]);
1528
+ }
1529
+ async function runWithOverlayAsync(service, options, operation) {
1530
+ if (!service) return operation();
1531
+ const handle = service.show(options);
1532
+ try {
1533
+ return await operation();
1534
+ } finally {
1535
+ service.hide(handle);
1536
+ }
1537
+ }
1538
+ function useRunWithOverlay() {
1539
+ const service = useOverlayService();
1540
+ return useCallback(
1541
+ (options, operation) => runWithOverlayAsync(service, options, operation),
1542
+ [service]
1543
+ );
1544
+ }
1545
+ var ValidationContextContext = createContext(null);
1546
+ ValidationContextContext.displayName = "ValidationContext";
1547
+ function useValidationContext() {
1548
+ return useContext(ValidationContextContext);
1549
+ }
1550
+ function useColumnValidationErrors(columnName) {
1551
+ const ctx = useValidationContext();
1552
+ if (!ctx) return void 0;
1553
+ return ctx.errors.get(columnName);
1554
+ }
1555
+ function ValidationProvider({ children }) {
1556
+ const validatorsRef = useRef(/* @__PURE__ */ new Map());
1557
+ const [errors, setErrors] = useState(
1558
+ () => /* @__PURE__ */ new Map()
1559
+ );
1560
+ const register = useCallback(
1561
+ (columnName, validator) => {
1562
+ validatorsRef.current.set(columnName, validator);
1563
+ return () => {
1564
+ validatorsRef.current.delete(columnName);
1565
+ setErrors((prev) => {
1566
+ if (!prev.has(columnName)) return prev;
1567
+ const next = new Map(prev);
1568
+ next.delete(columnName);
1569
+ return next;
1570
+ });
1571
+ };
1572
+ },
1573
+ []
1574
+ );
1575
+ const validateColumn = useCallback(async (columnName) => {
1576
+ const validator = validatorsRef.current.get(columnName);
1577
+ if (!validator) return;
1578
+ const fieldErrors = await validator();
1579
+ setErrors((prev) => {
1580
+ const existing = prev.get(columnName) ?? [];
1581
+ if (existing.length === fieldErrors.length && existing.every((e, i) => e === fieldErrors[i])) {
1582
+ return prev;
1583
+ }
1584
+ const next = new Map(prev);
1585
+ if (fieldErrors.length === 0) {
1586
+ next.delete(columnName);
1587
+ } else {
1588
+ next.set(columnName, fieldErrors);
1589
+ }
1590
+ return next;
1591
+ });
1592
+ }, []);
1593
+ const validateAll = useCallback(async () => {
1594
+ const next = /* @__PURE__ */ new Map();
1595
+ for (const [col, validator] of validatorsRef.current) {
1596
+ const fieldErrors = await validator();
1597
+ if (fieldErrors.length > 0) {
1598
+ next.set(col, fieldErrors);
1599
+ }
1600
+ }
1601
+ setErrors(next);
1602
+ return next.size === 0;
1603
+ }, []);
1604
+ const clearErrors = useCallback(() => {
1605
+ setErrors(/* @__PURE__ */ new Map());
1606
+ }, []);
1607
+ const isValid = errors.size === 0;
1608
+ const value = useMemo(
1609
+ () => ({ register, validateColumn, validateAll, clearErrors, isValid, errors }),
1610
+ [register, validateColumn, validateAll, clearErrors, isValid, errors]
1611
+ );
1612
+ return /* @__PURE__ */ jsx(ValidationContextContext.Provider, { value, children });
1613
+ }
1614
+ function valuesEqual(a, b) {
1615
+ const aEmpty = a === null || a === void 0;
1616
+ const bEmpty = b === null || b === void 0;
1617
+ if (aEmpty && bEmpty) return true;
1618
+ if (aEmpty !== bEmpty) return false;
1619
+ return JSON.stringify(a) === JSON.stringify(b);
1620
+ }
1621
+ var GUID_PATTERN = /^\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
1622
+ function parseQueryDefault(metadata, rawValue) {
1623
+ switch (metadata.$type) {
1624
+ // StringMetadata + MemoMetadata both ride on StringValue. In Blazor
1625
+ // MemoMetadata derives from StringMetadata so a single `StringMetadata`
1626
+ // pattern catches both; in TS the discriminators are distinct so we
1627
+ // list both arms.
1628
+ case 14:
1629
+ case 7:
1630
+ return { $type: 14, value: rawValue };
1631
+ case 5: {
1632
+ const n = Number(rawValue);
1633
+ return Number.isFinite(n) && Number.isInteger(n) ? { $type: 5, value: n } : null;
1634
+ }
1635
+ case 18: {
1636
+ const n = Number(rawValue);
1637
+ return Number.isFinite(n) && Number.isInteger(n) ? { $type: 18, value: n } : null;
1638
+ }
1639
+ case 3: {
1640
+ const n = Number(rawValue);
1641
+ return Number.isFinite(n) ? { $type: 3, value: n } : null;
1642
+ }
1643
+ case 4: {
1644
+ const n = Number(rawValue);
1645
+ return Number.isFinite(n) ? { $type: 4, value: n } : null;
1646
+ }
1647
+ case 8: {
1648
+ const n = Number(rawValue);
1649
+ return Number.isFinite(n) ? { $type: 8, value: n } : null;
1650
+ }
1651
+ case 0: {
1652
+ const lower = rawValue.trim().toLowerCase();
1653
+ if (lower === "true") return { $type: 0, value: true };
1654
+ if (lower === "false") return { $type: 0, value: false };
1655
+ return null;
1656
+ }
1657
+ case 2: {
1658
+ const ts = Date.parse(rawValue);
1659
+ if (Number.isNaN(ts)) return null;
1660
+ return { $type: 2, value: new Date(ts).toISOString() };
1661
+ }
1662
+ case 11: {
1663
+ const n = Number(rawValue);
1664
+ return Number.isFinite(n) && Number.isInteger(n) ? { $type: 11, value: n } : null;
1665
+ }
1666
+ case 40: {
1667
+ const parts = rawValue.split(",").map((s) => s.trim()).filter(Boolean);
1668
+ const values = [];
1669
+ for (const p of parts) {
1670
+ const n = Number(p);
1671
+ if (Number.isFinite(n) && Number.isInteger(n)) values.push({ value: n });
1672
+ }
1673
+ return values.length > 0 ? { $type: 40, value: values } : null;
1674
+ }
1675
+ case 6: {
1676
+ try {
1677
+ const parsed = JSON.parse(rawValue);
1678
+ if (!parsed || typeof parsed !== "object") return null;
1679
+ const findKey = (target) => Object.keys(parsed).find(
1680
+ (k) => k.toLowerCase() === target.toLowerCase()
1681
+ );
1682
+ const obj = parsed;
1683
+ const tableNameKey = findKey("tableName");
1684
+ const valueKey = findKey("value");
1685
+ const nameKey = findKey("name");
1686
+ const tableName = tableNameKey ? obj[tableNameKey] : void 0;
1687
+ const value = valueKey ? obj[valueKey] : void 0;
1688
+ const name = nameKey ? obj[nameKey] : null;
1689
+ if (typeof tableName !== "string" || tableName.length === 0) return null;
1690
+ if (typeof value !== "string" || value.length === 0) return null;
1691
+ return {
1692
+ $type: 6,
1693
+ tableName,
1694
+ value,
1695
+ name: typeof name === "string" ? name : null
1696
+ };
1697
+ } catch {
1698
+ return null;
1699
+ }
1700
+ }
1701
+ case 15: {
1702
+ if (!GUID_PATTERN.test(rawValue)) return null;
1703
+ return { $type: 15, value: rawValue.replace(/[{}]/g, "") };
1704
+ }
1705
+ default:
1706
+ return null;
1707
+ }
1708
+ }
1709
+ function parseQueryDefaultsFromUrl(search, tableMetadata) {
1710
+ const out = {};
1711
+ if (!search) return out;
1712
+ const params = new URLSearchParams(search);
1713
+ for (const [columnName, rawValue] of params) {
1714
+ if (!columnName) continue;
1715
+ const meta = tableMetadata.columns.find(
1716
+ (c) => c.columnName === columnName
1717
+ );
1718
+ if (!meta) continue;
1719
+ try {
1720
+ const parsed = parseQueryDefault(meta, rawValue);
1721
+ if (parsed) out[columnName] = parsed;
1722
+ } catch {
1723
+ }
1724
+ }
1725
+ return out;
1726
+ }
1727
+ var RecordContextContext = createContext(null);
1728
+ RecordContextContext.displayName = "RecordContext";
1729
+ function RecordContext(props) {
1730
+ return /* @__PURE__ */ jsx(ValidationProvider, { children: /* @__PURE__ */ jsx(RecordContextInner, { ...props }) });
1731
+ }
1732
+ function RecordContextInner({
1733
+ children,
1734
+ table,
1735
+ id: idProp,
1736
+ queryParameterName,
1737
+ initialRecord,
1738
+ loadedRecord: loadedRecordProp,
1739
+ initialPendingChanges,
1740
+ columns,
1741
+ onRecordLoaded,
1742
+ onBeforeSave,
1743
+ onBeforeDelete,
1744
+ warnOnUnsavedChanges,
1745
+ forceSuccessfulValidationBeforeSave
1746
+ }) {
1747
+ const queryId = useQueryParam(queryParameterName ?? "");
1748
+ const resolvedId = queryParameterName && queryId !== null && queryId.length > 0 ? queryId : idProp;
1749
+ const propIsEmptyId = !resolvedId || resolvedId === "00000000-0000-0000-0000-000000000000";
1750
+ const [effectiveId, setEffectiveId] = useState(
1751
+ propIsEmptyId ? void 0 : resolvedId
1752
+ );
1753
+ useEffect(() => {
1754
+ setEffectiveId(propIsEmptyId ? void 0 : resolvedId);
1755
+ }, [resolvedId, propIsEmptyId]);
1756
+ const isCreateMode = effectiveId === void 0;
1757
+ const isSkipFetchMode = !isCreateMode && loadedRecordProp !== void 0;
1758
+ const { ppp } = usePowerPortalsPro();
1759
+ const mainContext = useMainContext();
1760
+ const validation = useValidationContext();
1761
+ const isTopMost = mainContext === null;
1762
+ const resolvedWarnOnUnsavedChanges = warnOnUnsavedChanges ?? mainContext?.warnOnUnsavedChanges ?? true;
1763
+ const resolvedForceSuccessfulValidationBeforeSave = forceSuccessfulValidationBeforeSave ?? mainContext?.forceSuccessfulValidationBeforeSave ?? true;
1764
+ const overlayService = useOverlayService();
1765
+ const localizer = useLocalizer();
1766
+ const overlayTargetId = useId();
1767
+ const query = useRecord(table, effectiveId ?? "", {
1768
+ ...columns ? { columns } : {},
1769
+ enabled: !isCreateMode && !isSkipFetchMode
1770
+ });
1771
+ const metadataQuery = useTableMetadata(table);
1772
+ const [loadedRecord, setLoadedRecord] = useState(
1773
+ () => isSkipFetchMode ? loadedRecordProp : void 0
1774
+ );
1775
+ const [pendingChanges, setPendingChanges] = useState(
1776
+ () => /* @__PURE__ */ new Map()
1777
+ );
1778
+ const onRecordLoadedRef = useRef(onRecordLoaded);
1779
+ onRecordLoadedRef.current = onRecordLoaded;
1780
+ const onBeforeSaveRef = useRef(onBeforeSave);
1781
+ onBeforeSaveRef.current = onBeforeSave;
1782
+ const refetchRef = useRef(query.refetch);
1783
+ refetchRef.current = query.refetch;
1784
+ const recordRef = useRef(void 0);
1785
+ const loadedRecordRef = useRef(void 0);
1786
+ const ownDirtyRef = useRef(false);
1787
+ const pendingChangesRef = useRef(/* @__PURE__ */ new Map());
1788
+ pendingChangesRef.current = pendingChanges;
1789
+ const validationRef = useRef(validation);
1790
+ validationRef.current = validation;
1791
+ const isCreateModeRef = useRef(isCreateMode);
1792
+ isCreateModeRef.current = isCreateMode;
1793
+ const forceSuccessfulValidationBeforeSaveRef = useRef(
1794
+ resolvedForceSuccessfulValidationBeforeSave
1795
+ );
1796
+ forceSuccessfulValidationBeforeSaveRef.current = resolvedForceSuccessfulValidationBeforeSave;
1797
+ const queryParameterNameRef = useRef(queryParameterName);
1798
+ queryParameterNameRef.current = queryParameterName;
1799
+ const isTopMostRef = useRef(mainContext === null);
1800
+ isTopMostRef.current = mainContext === null;
1801
+ useEffect(() => {
1802
+ if (!isCreateMode) return;
1803
+ const baseSeed = initialRecord ?? { tableName: table, properties: {} };
1804
+ const seeded = baseSeed.id !== void 0 ? baseSeed : { ...baseSeed, _idForCreate: crypto.randomUUID() };
1805
+ setLoadedRecord(seeded);
1806
+ setPendingChanges(/* @__PURE__ */ new Map());
1807
+ }, [isCreateMode, initialRecord, table]);
1808
+ useEffect(() => {
1809
+ if (!isCreateMode) return;
1810
+ if (!queryParameterName) return;
1811
+ if (!metadataQuery.data) return;
1812
+ if (!loadedRecord) return;
1813
+ if (pendingChangesRef.current.size > 0) return;
1814
+ const search = typeof window !== "undefined" ? window.location.search : "";
1815
+ const defaults = parseQueryDefaultsFromUrl(search, metadataQuery.data);
1816
+ const cols = Object.keys(defaults);
1817
+ if (cols.length === 0) return;
1818
+ let changed = false;
1819
+ const nextProperties = {
1820
+ ...loadedRecord.properties ?? {}
1821
+ };
1822
+ for (const col of cols) {
1823
+ const next = defaults[col];
1824
+ if (!valuesEqual(nextProperties[col], next)) {
1825
+ nextProperties[col] = next;
1826
+ changed = true;
1827
+ }
1828
+ }
1829
+ if (!changed) return;
1830
+ setLoadedRecord(
1831
+ (current) => current ? { ...current, properties: nextProperties } : current
1832
+ );
1833
+ }, [isCreateMode, queryParameterName, metadataQuery.data, loadedRecord]);
1834
+ useEffect(() => {
1835
+ if (!isSkipFetchMode || !loadedRecordProp) return;
1836
+ setLoadedRecord(loadedRecordProp);
1837
+ setPendingChanges(
1838
+ initialPendingChanges && initialPendingChanges.size > 0 ? new Map(initialPendingChanges) : /* @__PURE__ */ new Map()
1839
+ );
1840
+ validationRef.current?.clearErrors();
1841
+ onRecordLoadedRef.current?.(loadedRecordProp);
1842
+ }, [isSkipFetchMode, loadedRecordProp, initialPendingChanges]);
1843
+ useEffect(() => {
1844
+ if (query.status === "success" && query.data) {
1845
+ setLoadedRecord(query.data);
1846
+ setPendingChanges(
1847
+ initialPendingChanges && initialPendingChanges.size > 0 ? new Map(initialPendingChanges) : /* @__PURE__ */ new Map()
1848
+ );
1849
+ validationRef.current?.clearErrors();
1850
+ onRecordLoadedRef.current?.(query.data);
1851
+ }
1852
+ }, [query.status, query.data, initialPendingChanges]);
1853
+ loadedRecordRef.current = loadedRecord;
1854
+ const record = useMemo(() => {
1855
+ if (!loadedRecord) return void 0;
1856
+ if (pendingChanges.size === 0) return loadedRecord;
1857
+ const properties = { ...loadedRecord.properties ?? {} };
1858
+ for (const [col, val] of pendingChanges) {
1859
+ if (val === null) {
1860
+ delete properties[col];
1861
+ } else {
1862
+ properties[col] = val;
1863
+ }
1864
+ }
1865
+ return { ...loadedRecord, properties, isDirty: true };
1866
+ }, [loadedRecord, pendingChanges]);
1867
+ const dirtyColumns = useMemo(
1868
+ () => new Set(pendingChanges.keys()),
1869
+ [pendingChanges]
1870
+ );
1871
+ const ownDirty = pendingChanges.size > 0;
1872
+ recordRef.current = record;
1873
+ ownDirtyRef.current = ownDirty;
1874
+ const nestedRef = useRef(/* @__PURE__ */ new Set());
1875
+ const [nestedVersion, setNestedVersion] = useState(0);
1876
+ const nestedDirty = useMemo(() => {
1877
+ for (const d of nestedRef.current) {
1878
+ if (d.isDirty()) return true;
1879
+ }
1880
+ return false;
1881
+ }, [nestedVersion]);
1882
+ const isDirty = ownDirty || nestedDirty;
1883
+ const registerNested = useCallback((descendant) => {
1884
+ nestedRef.current.add(descendant);
1885
+ setNestedVersion((v) => v + 1);
1886
+ return () => {
1887
+ nestedRef.current.delete(descendant);
1888
+ setNestedVersion((v) => v + 1);
1889
+ };
1890
+ }, []);
1891
+ const notifyNestedDirtyChanged = useCallback(() => {
1892
+ setNestedVersion((v) => v + 1);
1893
+ }, []);
1894
+ const buildOwnSaveRequest = useCallback(() => {
1895
+ if (!recordRef.current || !ownDirtyRef.current) return null;
1896
+ const current = recordRef.current;
1897
+ if (isCreateModeRef.current) {
1898
+ const allProperties = {
1899
+ ...current.properties
1900
+ };
1901
+ return {
1902
+ $type: "CreateRequest",
1903
+ record: {
1904
+ tableName: current.tableName,
1905
+ // Include the client-pre-generated GUID. The seed effect writes
1906
+ // it into `_idForCreate` (Blazor parity); the server's in-batch
1907
+ // resolver matches AssociateRequest / child-FK references
1908
+ // against either `id` or `_idForCreate`, so a Create + Associate
1909
+ // pair within one `ExecuteMultiple` resolves to the same record.
1910
+ // `id` is also forwarded when the consumer supplied an explicit
1911
+ // one via `initialRecord` (rare — pre-assigned ids on data
1912
+ // import).
1913
+ ...current.id !== void 0 && { id: current.id },
1914
+ ...current._idForCreate !== void 0 && current._idForCreate !== null && {
1915
+ _idForCreate: current._idForCreate
1916
+ },
1917
+ properties: allProperties
1918
+ }
1919
+ };
1920
+ }
1921
+ const partialProperties = {};
1922
+ for (const [col, val] of pendingChangesRef.current) {
1923
+ partialProperties[col] = val;
1924
+ }
1925
+ const partialRecord = {
1926
+ tableName: current.tableName,
1927
+ properties: partialProperties,
1928
+ ...current.id !== void 0 && { id: current.id }
1929
+ };
1930
+ return { $type: "UpdateRequest", record: partialRecord };
1931
+ }, []);
1932
+ const getRequests = useCallback(() => {
1933
+ const all = [];
1934
+ const own = buildOwnSaveRequest();
1935
+ if (own) all.push(own);
1936
+ for (const descendant of nestedRef.current) {
1937
+ all.push(...descendant.getRequests());
1938
+ }
1939
+ return all;
1940
+ }, [buildOwnSaveRequest]);
1941
+ const validate = useCallback(async () => {
1942
+ for (const descendant of nestedRef.current) {
1943
+ if (!await descendant.validate()) return false;
1944
+ }
1945
+ return true;
1946
+ }, []);
1947
+ const saveAll = useCallback(async () => {
1948
+ if (!loadedRecordRef.current) {
1949
+ throw new Error("RecordContext.save called before the record loaded.");
1950
+ }
1951
+ await onBeforeSaveRef.current?.();
1952
+ if (forceSuccessfulValidationBeforeSaveRef.current) {
1953
+ const valid = await validate();
1954
+ if (!valid) return;
1955
+ }
1956
+ const ownReq = buildOwnSaveRequest();
1957
+ const descendantReqs = [];
1958
+ for (const descendant of nestedRef.current) {
1959
+ descendantReqs.push(...descendant.getRequests());
1960
+ }
1961
+ const requests = ownReq ? [ownReq, ...descendantReqs] : descendantReqs;
1962
+ if (requests.length > 0) {
1963
+ const responses = await ppp.executeMultipleAsync(requests, { returnResponses: true });
1964
+ if (ownReq?.$type === "CreateRequest") {
1965
+ const ownResp = responses[0];
1966
+ if (ownResp?.$type === "CreateResponse" && ownResp.id) {
1967
+ setEffectiveId(ownResp.id);
1968
+ const qpn = queryParameterNameRef.current;
1969
+ if (qpn && isTopMostRef.current && typeof window !== "undefined") {
1970
+ const existing = new URLSearchParams(window.location.search).get(
1971
+ qpn
1972
+ );
1973
+ if (!existing) {
1974
+ const newUrl = new URL(window.location.href);
1975
+ newUrl.searchParams.set(qpn, ownResp.id);
1976
+ window.history.replaceState(
1977
+ window.history.state,
1978
+ "",
1979
+ newUrl.toString()
1980
+ );
1981
+ }
1982
+ }
1983
+ }
1984
+ }
1985
+ setPendingChanges(/* @__PURE__ */ new Map());
1986
+ }
1987
+ if (!isCreateModeRef.current) {
1988
+ void refetchRef.current();
1989
+ }
1990
+ for (const descendant of nestedRef.current) {
1991
+ await descendant.refresh();
1992
+ }
1993
+ }, [ppp, validate, buildOwnSaveRequest]);
1994
+ const reload = useCallback(async () => {
1995
+ refetchRef.current();
1996
+ for (const descendant of nestedRef.current) {
1997
+ await descendant.refresh();
1998
+ }
1999
+ }, []);
2000
+ const resetState = useCallback(() => {
2001
+ setPendingChanges(/* @__PURE__ */ new Map());
2002
+ validationRef.current?.clearErrors();
2003
+ for (const descendant of nestedRef.current) {
2004
+ descendant.resetState();
2005
+ }
2006
+ }, []);
2007
+ const setColumnValue = useCallback(
2008
+ (columnName, value2) => {
2009
+ setPendingChanges((prev) => {
2010
+ if (!loadedRecord) return prev;
2011
+ const original = loadedRecord.properties?.[columnName];
2012
+ const newValue = value2 ?? null;
2013
+ if (valuesEqual(original, newValue)) {
2014
+ if (!prev.has(columnName)) return prev;
2015
+ const next2 = new Map(prev);
2016
+ next2.delete(columnName);
2017
+ return next2;
2018
+ }
2019
+ const existing = prev.has(columnName) ? prev.get(columnName) ?? null : void 0;
2020
+ if (existing !== void 0 && valuesEqual(existing, newValue)) return prev;
2021
+ const next = new Map(prev);
2022
+ next.set(columnName, newValue);
2023
+ return next;
2024
+ });
2025
+ },
2026
+ [loadedRecord]
2027
+ );
2028
+ const hydrateColumnValue = useCallback(
2029
+ (columnName, value2) => {
2030
+ setLoadedRecord((current) => {
2031
+ if (!current) return current;
2032
+ if (pendingChanges.has(columnName)) return current;
2033
+ const previous = current.properties?.[columnName];
2034
+ if (value2 === null) {
2035
+ if (previous === void 0) return current;
2036
+ const nextProperties = { ...current.properties ?? {} };
2037
+ delete nextProperties[columnName];
2038
+ return { ...current, properties: nextProperties };
2039
+ }
2040
+ if (valuesEqual(previous, value2)) return current;
2041
+ return {
2042
+ ...current,
2043
+ properties: { ...current.properties ?? {}, [columnName]: value2 }
2044
+ };
2045
+ });
2046
+ },
2047
+ [pendingChanges]
2048
+ );
2049
+ const loadingOverlayRef = useRef(null);
2050
+ useEffect(() => {
2051
+ if (!overlayService) return;
2052
+ if (query.status === "loading") {
2053
+ if (!loadingOverlayRef.current) {
2054
+ loadingOverlayRef.current = overlayService.show({
2055
+ targetId: overlayTargetId,
2056
+ description: localizer.t("app.messages.record-loading")
2057
+ });
2058
+ }
2059
+ } else if (loadingOverlayRef.current) {
2060
+ overlayService.hide(loadingOverlayRef.current);
2061
+ loadingOverlayRef.current = null;
2062
+ }
2063
+ return () => {
2064
+ if (loadingOverlayRef.current) {
2065
+ overlayService.hide(loadingOverlayRef.current);
2066
+ loadingOverlayRef.current = null;
2067
+ }
2068
+ };
2069
+ }, [overlayService, query.status, overlayTargetId, localizer]);
2070
+ const isDirtyRef = useRef(isDirty);
2071
+ isDirtyRef.current = isDirty;
2072
+ const mainContextRef = useRef(mainContext);
2073
+ mainContextRef.current = mainContext;
2074
+ useEffect(() => {
2075
+ if (!mainContext) return;
2076
+ return mainContext.registerDescendant({
2077
+ getRequests,
2078
+ validate,
2079
+ refresh: reload,
2080
+ resetState,
2081
+ isDirty: () => isDirtyRef.current
2082
+ });
2083
+ }, [mainContext?.registerDescendant, getRequests, validate, reload, resetState]);
2084
+ useEffect(() => {
2085
+ mainContextRef.current?.notifyDescendantDirtyChanged();
2086
+ }, [isDirty]);
2087
+ useBeforeUnloadWhen(isTopMost && resolvedWarnOnUnsavedChanges && isDirty);
2088
+ const unsavedChangesRegistry = useUnsavedChangesRegistry();
2089
+ const resolvedWarnRef = useRef(resolvedWarnOnUnsavedChanges);
2090
+ resolvedWarnRef.current = resolvedWarnOnUnsavedChanges;
2091
+ useEffect(() => {
2092
+ if (!isTopMost) return;
2093
+ if (!unsavedChangesRegistry) return;
2094
+ return unsavedChangesRegistry.register({
2095
+ isDirty: () => resolvedWarnRef.current && isDirtyRef.current
2096
+ });
2097
+ }, [isTopMost, unsavedChangesRegistry]);
2098
+ useEffect(() => {
2099
+ if (!isTopMost) return;
2100
+ if (!unsavedChangesRegistry) return;
2101
+ unsavedChangesRegistry.notifyDirtyChanged();
2102
+ }, [isTopMost, unsavedChangesRegistry, isDirty, resolvedWarnOnUnsavedChanges]);
2103
+ const innerMainContext = useMemo(
2104
+ () => ({
2105
+ saveAll,
2106
+ refreshAll: reload,
2107
+ resetState,
2108
+ validate,
2109
+ getRequests,
2110
+ registerDescendant: registerNested,
2111
+ notifyDescendantDirtyChanged: notifyNestedDirtyChanged,
2112
+ isDirty,
2113
+ warnOnUnsavedChanges: resolvedWarnOnUnsavedChanges,
2114
+ forceSuccessfulValidationBeforeSave: resolvedForceSuccessfulValidationBeforeSave
2115
+ }),
2116
+ [
2117
+ saveAll,
2118
+ reload,
2119
+ resetState,
2120
+ validate,
2121
+ getRequests,
2122
+ registerNested,
2123
+ notifyNestedDirtyChanged,
2124
+ isDirty,
2125
+ resolvedWarnOnUnsavedChanges,
2126
+ resolvedForceSuccessfulValidationBeforeSave
2127
+ ]
2128
+ );
2129
+ const onBeforeDeleteRef = useRef(onBeforeDelete);
2130
+ onBeforeDeleteRef.current = onBeforeDelete;
2131
+ const runBeforeDelete = useCallback(async () => {
2132
+ const handler = onBeforeDeleteRef.current;
2133
+ if (handler) await handler();
2134
+ }, []);
2135
+ const value = useMemo(
2136
+ () => ({
2137
+ status: query.status,
2138
+ record,
2139
+ error: query.error,
2140
+ reload,
2141
+ save: saveAll,
2142
+ getRequests,
2143
+ resetState,
2144
+ setColumnValue,
2145
+ hydrateColumnValue,
2146
+ tableMetadata: metadataQuery.data,
2147
+ isDirty,
2148
+ dirtyColumns,
2149
+ overlayTargetId,
2150
+ runBeforeDelete
2151
+ }),
2152
+ [
2153
+ query.status,
2154
+ query.error,
2155
+ record,
2156
+ reload,
2157
+ saveAll,
2158
+ getRequests,
2159
+ resetState,
2160
+ setColumnValue,
2161
+ hydrateColumnValue,
2162
+ metadataQuery.data,
2163
+ isDirty,
2164
+ dirtyColumns,
2165
+ overlayTargetId,
2166
+ runBeforeDelete
2167
+ ]
2168
+ );
2169
+ return /* @__PURE__ */ jsx(MainContextProvider, { value: innerMainContext, children: /* @__PURE__ */ jsx(RecordContextContext.Provider, { value, children }) });
2170
+ }
2171
+ function useRecordContext() {
2172
+ const ctx = useContext(RecordContextContext);
2173
+ if (!ctx) {
2174
+ throw new Error(
2175
+ "useRecordContext must be used inside <RecordContext>. Wrap the component tree where you need the record."
2176
+ );
2177
+ }
2178
+ return ctx;
2179
+ }
2180
+ function useRecordContextOptional() {
2181
+ return useContext(RecordContextContext);
2182
+ }
2183
+ var EMPTY_GUID = "00000000-0000-0000-0000-000000000000";
2184
+ function LookupRecordContext({
2185
+ columnName,
2186
+ onBeforeSave,
2187
+ children,
2188
+ isVisible = true
2189
+ }) {
2190
+ const parent = useRecordContext();
2191
+ if (!isVisible) return null;
2192
+ const raw = parent.record?.properties?.[columnName];
2193
+ if (!raw?.value || raw.value === EMPTY_GUID || !raw.tableName) {
2194
+ return null;
2195
+ }
2196
+ return /* @__PURE__ */ jsx(
2197
+ RecordContext,
2198
+ {
2199
+ table: raw.tableName,
2200
+ id: raw.value,
2201
+ ...onBeforeSave && { onBeforeSave },
2202
+ children
2203
+ }
2204
+ );
2205
+ }
2206
+
2207
+ // src/use-column-metadata.ts
2208
+ function useColumnMetadata(columnName) {
2209
+ const { tableMetadata } = useRecordContext();
2210
+ return tableMetadata?.columns.find((c) => c.columnName === columnName);
2211
+ }
2212
+ var DialogServiceContext = createContext(null);
2213
+ DialogServiceContext.displayName = "DialogService";
2214
+ function useDialogService() {
2215
+ const ctx = useContext(DialogServiceContext);
2216
+ if (!ctx) {
2217
+ throw new Error(
2218
+ "useDialogService must be used inside a <DialogServiceProvider>. Wrap your app in <FluentDialogProvider> from @powerportalspro/react-fluent (or your own implementation) so framework dialogs have somewhere to render."
2219
+ );
2220
+ }
2221
+ return ctx;
2222
+ }
2223
+ function DialogServiceProvider({
2224
+ value,
2225
+ children
2226
+ }) {
2227
+ return /* @__PURE__ */ jsx(DialogServiceContext.Provider, { value, children });
2228
+ }
2229
+ async function warnAboutUnsavedChangesAsync(dialog, localizer) {
2230
+ const result = await dialog.showConfirmationAsync(localizer.t("app.messages.unsaved-changes"), {
2231
+ title: localizer.t("app.messages.unsaved-changes-title"),
2232
+ primaryText: localizer.t("app.buttons.yes.label"),
2233
+ secondaryText: localizer.t("app.buttons.no.label")
2234
+ });
2235
+ return !result.cancelled;
2236
+ }
2237
+ function useWarnAboutUnsavedChanges() {
2238
+ const dialog = useDialogService();
2239
+ const localizer = useLocalizer();
2240
+ return useCallback(
2241
+ () => warnAboutUnsavedChangesAsync(dialog, localizer),
2242
+ [dialog, localizer]
2243
+ );
2244
+ }
2245
+
2246
+ // src/unsaved-changes-warning.ts
2247
+ function useUnsavedChangesWarning() {
2248
+ const ctx = useMainContext();
2249
+ const isDirty = ctx?.isDirty ?? false;
2250
+ useBeforeUnloadWhen(isDirty);
2251
+ }
2252
+ function useUnsavedChangesGuard() {
2253
+ const isAnyDirty = useIsAnyDirty();
2254
+ const warn = useWarnAboutUnsavedChanges();
2255
+ return { isAnyDirty, warn };
2256
+ }
2257
+
2258
+ // src/index.ts
2259
+ var VERSION = "0.1.0";
2260
+
2261
+ export { ASP_NET_CULTURE_COOKIE, AuthStatus, AuthorizedView, DEFAULT_URL_LOCALE_PATTERN, DefaultLocalizerProvider, DialogServiceProvider, InMemoryEnvironmentFileSettingsCache, InMemoryTableMetadataCache, InMemoryViewMetadataCache, InMemoryViewsForTableCache, LocaleProvider, LocalizerProvider, LookupRecordContext, MainContext, MainContextProvider, OverlayServiceProvider, PowerPortalsProContext, PowerPortalsProProvider, QueryStatus, RecordContext, UnsavedChangesRegistryProvider, VERSION, ValidationProvider, createLocalizer, detectLocale, flattenStrings, getAspNetCultureCookie, getLocaleFromUrlPath, interpolate, resolveLocalizationUrl, runWithOverlayAsync, setAspNetCultureCookie, useAuth, useColumnMetadata, useColumnValidationErrors, useDialogService, useEnvironmentFileSettings, useFetchRecords, useGridData, useIsAnyDirty, useLocale, useLocalization, useLocalizationLoaded, useLocalizationPrefetcher, useLocalizer, useMainContext, useOverlayForTarget, useOverlayService, useOverlayServiceState, usePowerPortalsPro, usePrefixedT, useQueryParam, useRecord, useRecordContext, useRecordContextOptional, useRunWithOverlay, useSupportedLocales, useT, useTableMetadata, useTablePermissions, useUnsavedChangesGuard, useUnsavedChangesRegistry, useUnsavedChangesWarning, useValidationContext, useViewDataSource, useViewMetadata, useViewsForTable, useWarnAboutUnsavedChanges, warnAboutUnsavedChangesAsync };
2262
+ //# sourceMappingURL=index.js.map
2263
+ //# sourceMappingURL=index.js.map