@jsenv/navi 0.19.0 → 0.20.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/jsenv_navi.js +1314 -362
- package/dist/jsenv_navi.js.map +132 -77
- package/package.json +3 -3
package/dist/jsenv_navi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
|
|
2
|
-
import { isValidElement, createContext, toChildArray, render, createRef, cloneElement } from "preact";
|
|
2
|
+
import { isValidElement, h, createContext, toChildArray, render, createRef, cloneElement } from "preact";
|
|
3
3
|
import { useErrorBoundary, useLayoutEffect, useEffect, useMemo, useRef, useState, useCallback, useContext, useImperativeHandle, useId } from "preact/hooks";
|
|
4
4
|
import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
|
|
5
5
|
import { signal, effect, computed, batch, useSignal } from "@preact/signals";
|
|
@@ -7,7 +7,7 @@ import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mer
|
|
|
7
7
|
export { contrastColor } from "@jsenv/dom";
|
|
8
8
|
import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
|
|
9
9
|
import { createValidity } from "@jsenv/validity";
|
|
10
|
-
import { createPortal, forwardRef } from "preact/compat";
|
|
10
|
+
import { Suspense, createPortal, forwardRef } from "preact/compat";
|
|
11
11
|
|
|
12
12
|
const actionPrivatePropertiesWeakMap = new WeakMap();
|
|
13
13
|
const getActionPrivateProperties = (action) => {
|
|
@@ -1113,7 +1113,7 @@ const updateActions = ({
|
|
|
1113
1113
|
const { runningSet, settledSet } = getActivationInfo();
|
|
1114
1114
|
|
|
1115
1115
|
if (DEBUG$3) {
|
|
1116
|
-
let argSource = `reason:
|
|
1116
|
+
let argSource = `reason: ${JSON.stringify(reason)}`;
|
|
1117
1117
|
if (isReplace) {
|
|
1118
1118
|
argSource += `, isReplace: true`;
|
|
1119
1119
|
}
|
|
@@ -1868,13 +1868,17 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
1868
1868
|
});
|
|
1869
1869
|
|
|
1870
1870
|
if (ui.hasRenderers || onError) {
|
|
1871
|
+
// When inside suspense this console.error is redundant with the error thrown by preact debug at
|
|
1872
|
+
// https://github.com/preactjs/preact/blob/21dd6d04c1a9a43e5b60976bb5eb7d856253195b/debug/src/debug.js#L109
|
|
1871
1873
|
console.error(e);
|
|
1874
|
+
|
|
1872
1875
|
// For UI-bound actions: error is properly handled by logging + UI display
|
|
1873
1876
|
// Return error instead of throwing to signal it's handled and prevent:
|
|
1874
1877
|
// - jsenv error overlay from appearing
|
|
1875
1878
|
// - error being treated as unhandled by runtime
|
|
1876
1879
|
return e;
|
|
1877
1880
|
}
|
|
1881
|
+
e.action = action;
|
|
1878
1882
|
throw e;
|
|
1879
1883
|
};
|
|
1880
1884
|
|
|
@@ -2143,6 +2147,7 @@ const createActionProxyFromSignal = (
|
|
|
2143
2147
|
};
|
|
2144
2148
|
|
|
2145
2149
|
Object.assign(actionProxy, {
|
|
2150
|
+
isAction: true,
|
|
2146
2151
|
isProxy: true,
|
|
2147
2152
|
callback: undefined,
|
|
2148
2153
|
params: undefined,
|
|
@@ -2435,14 +2440,6 @@ const actionRunEffect = (
|
|
|
2435
2440
|
return actionRunnedByThisEffect;
|
|
2436
2441
|
};
|
|
2437
2442
|
|
|
2438
|
-
const useActionData = (action) => {
|
|
2439
|
-
if (!action) {
|
|
2440
|
-
return undefined;
|
|
2441
|
-
}
|
|
2442
|
-
const data = action.dataSignal.value;
|
|
2443
|
-
return data;
|
|
2444
|
-
};
|
|
2445
|
-
|
|
2446
2443
|
const useRunOnMount = (action, Component) => {
|
|
2447
2444
|
useEffect(() => {
|
|
2448
2445
|
action.run({
|
|
@@ -2725,8 +2722,9 @@ const arraySignalStore = (
|
|
|
2725
2722
|
});
|
|
2726
2723
|
|
|
2727
2724
|
const itemPropertiesObserverSet = new Set();
|
|
2728
|
-
const observeItemProperties = (itemSignal, callback) => {
|
|
2729
|
-
const
|
|
2725
|
+
const observeItemProperties = (itemSignal, callback, { properties } = {}) => {
|
|
2726
|
+
const propertiesSet = properties ? new Set(properties) : null;
|
|
2727
|
+
const observer = { itemSignal, callback, propertiesSet };
|
|
2730
2728
|
itemPropertiesObserverSet.add(observer);
|
|
2731
2729
|
return () => {
|
|
2732
2730
|
itemPropertiesObserverSet.delete(observer);
|
|
@@ -2734,10 +2732,12 @@ const arraySignalStore = (
|
|
|
2734
2732
|
};
|
|
2735
2733
|
|
|
2736
2734
|
const propertiesObserverSet = new Set();
|
|
2737
|
-
const observeProperties = (callback) => {
|
|
2738
|
-
|
|
2735
|
+
const observeProperties = (callback, { properties } = {}) => {
|
|
2736
|
+
const propertiesSet = properties ? new Set(properties) : null;
|
|
2737
|
+
const observer = { callback, propertiesSet };
|
|
2738
|
+
propertiesObserverSet.add(observer);
|
|
2739
2739
|
return () => {
|
|
2740
|
-
propertiesObserverSet.delete(
|
|
2740
|
+
propertiesObserverSet.delete(observer);
|
|
2741
2741
|
};
|
|
2742
2742
|
};
|
|
2743
2743
|
|
|
@@ -2887,24 +2887,50 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
2887
2887
|
const upsert = (...args) => {
|
|
2888
2888
|
const mutationsMap = new Map(); // Map<itemId, propertyMutations>
|
|
2889
2889
|
const triggerPropertyMutations = () => {
|
|
2890
|
-
// we call at the end so that itemWithProps and arraySignal.value was set too
|
|
2891
2890
|
for (const itemPropertiesObserver of itemPropertiesObserverSet) {
|
|
2892
|
-
const { itemSignal, callback } = itemPropertiesObserver;
|
|
2891
|
+
const { itemSignal, callback, propertiesSet } = itemPropertiesObserver;
|
|
2893
2892
|
const watchedItem = itemSignal.peek();
|
|
2894
2893
|
if (!watchedItem) {
|
|
2895
2894
|
continue;
|
|
2896
2895
|
}
|
|
2897
|
-
|
|
2898
|
-
// Check if this item has mutations
|
|
2899
2896
|
const itemMutations = mutationsMap.get(watchedItem[idKey]);
|
|
2900
2897
|
if (itemMutations) {
|
|
2901
|
-
|
|
2898
|
+
if (propertiesSet) {
|
|
2899
|
+
let hasRelevantMutation = false;
|
|
2900
|
+
for (const p of propertiesSet) {
|
|
2901
|
+
if (Object.hasOwn(itemMutations, p)) {
|
|
2902
|
+
hasRelevantMutation = true;
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if (hasRelevantMutation) {
|
|
2907
|
+
callback(itemMutations);
|
|
2908
|
+
}
|
|
2909
|
+
} else {
|
|
2910
|
+
callback(itemMutations);
|
|
2911
|
+
}
|
|
2902
2912
|
}
|
|
2903
2913
|
}
|
|
2904
2914
|
if (propertiesObserverSet.size) {
|
|
2905
|
-
const
|
|
2915
|
+
const allMutations = Array.from(mutationsMap.values());
|
|
2906
2916
|
for (const propertiesObserver of propertiesObserverSet) {
|
|
2907
|
-
propertiesObserver
|
|
2917
|
+
const { callback, propertiesSet } = propertiesObserver;
|
|
2918
|
+
if (propertiesSet) {
|
|
2919
|
+
const filteredMutations = [];
|
|
2920
|
+
for (const propertyMutations of allMutations) {
|
|
2921
|
+
for (const p of propertiesSet) {
|
|
2922
|
+
if (Object.hasOwn(propertyMutations, p)) {
|
|
2923
|
+
filteredMutations.push(propertyMutations);
|
|
2924
|
+
break;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
if (filteredMutations.length > 0) {
|
|
2929
|
+
callback(filteredMutations);
|
|
2930
|
+
}
|
|
2931
|
+
} else {
|
|
2932
|
+
callback(allMutations);
|
|
2933
|
+
}
|
|
2908
2934
|
}
|
|
2909
2935
|
}
|
|
2910
2936
|
};
|
|
@@ -2948,7 +2974,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
2948
2974
|
return item;
|
|
2949
2975
|
}
|
|
2950
2976
|
|
|
2951
|
-
// Store mutations
|
|
2977
|
+
// Store mutations keyed by old id
|
|
2952
2978
|
mutationsMap.set(item[idKey], propertyMutations);
|
|
2953
2979
|
return itemWithProps;
|
|
2954
2980
|
};
|
|
@@ -3163,7 +3189,18 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
3163
3189
|
return null;
|
|
3164
3190
|
};
|
|
3165
3191
|
|
|
3166
|
-
const
|
|
3192
|
+
const signalForKey = (key, keyValueSignal) => {
|
|
3193
|
+
if (key === idKey) {
|
|
3194
|
+
return _signalForIdKey(keyValueSignal);
|
|
3195
|
+
}
|
|
3196
|
+
if (uniqueKeys.includes(key)) {
|
|
3197
|
+
return _signalForUniqueKey(key, keyValueSignal);
|
|
3198
|
+
}
|
|
3199
|
+
throw new Error(
|
|
3200
|
+
`signalForKey: "${key}" is not the idKey or a uniqueKey of this store (idKey: ${idKey}, uniqueKeys: ${uniqueKeys.join(", ")})`,
|
|
3201
|
+
);
|
|
3202
|
+
};
|
|
3203
|
+
const _signalForUniqueKey = (uniqueKey, uniqueKeyValueSignal) => {
|
|
3167
3204
|
const itemIdSignal = signal(null);
|
|
3168
3205
|
const check = (value) => {
|
|
3169
3206
|
const item = select(uniqueKey, value);
|
|
@@ -3173,7 +3210,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
3173
3210
|
itemIdSignal.value = item[idKey];
|
|
3174
3211
|
return true;
|
|
3175
3212
|
};
|
|
3176
|
-
if (!check()) {
|
|
3213
|
+
if (!check(uniqueKeyValueSignal.peek())) {
|
|
3177
3214
|
effect(function () {
|
|
3178
3215
|
const uniqueKeyValue = uniqueKeyValueSignal.value;
|
|
3179
3216
|
if (check(uniqueKeyValue)) {
|
|
@@ -3181,7 +3218,44 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
3181
3218
|
}
|
|
3182
3219
|
});
|
|
3183
3220
|
}
|
|
3221
|
+
return computed(() => {
|
|
3222
|
+
return select(itemIdSignal.value);
|
|
3223
|
+
});
|
|
3224
|
+
};
|
|
3184
3225
|
|
|
3226
|
+
const _signalForIdKey = (idValueSignal) => {
|
|
3227
|
+
const itemIdSignal = signal(null);
|
|
3228
|
+
const check = (value) => {
|
|
3229
|
+
const item = select(idKey, value);
|
|
3230
|
+
if (!item) {
|
|
3231
|
+
return false;
|
|
3232
|
+
}
|
|
3233
|
+
itemIdSignal.value = item[idKey];
|
|
3234
|
+
return true;
|
|
3235
|
+
};
|
|
3236
|
+
if (!check(idValueSignal.peek())) {
|
|
3237
|
+
effect(function () {
|
|
3238
|
+
const idValue = idValueSignal.value;
|
|
3239
|
+
if (check(idValue)) {
|
|
3240
|
+
this.dispose();
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
// When the id itself is renamed, keep itemIdSignal in sync.
|
|
3245
|
+
observeProperties(
|
|
3246
|
+
(mutationsArray) => {
|
|
3247
|
+
const currentId = itemIdSignal.peek();
|
|
3248
|
+
if (currentId === null) return;
|
|
3249
|
+
for (const mutations of mutationsArray) {
|
|
3250
|
+
const mutation = mutations[idKey];
|
|
3251
|
+
if (mutation.oldValue === currentId) {
|
|
3252
|
+
itemIdSignal.value = mutation.newValue;
|
|
3253
|
+
break;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
},
|
|
3257
|
+
{ properties: [idKey] },
|
|
3258
|
+
);
|
|
3185
3259
|
return computed(() => {
|
|
3186
3260
|
return select(itemIdSignal.value);
|
|
3187
3261
|
});
|
|
@@ -3195,6 +3269,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
3195
3269
|
};
|
|
3196
3270
|
|
|
3197
3271
|
Object.assign(store, {
|
|
3272
|
+
idKey,
|
|
3198
3273
|
uniqueKeys,
|
|
3199
3274
|
arraySignal,
|
|
3200
3275
|
select,
|
|
@@ -3207,11 +3282,63 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
3207
3282
|
observeRemovals,
|
|
3208
3283
|
observeIdChanges,
|
|
3209
3284
|
registerItemMatchLifecycle,
|
|
3210
|
-
|
|
3285
|
+
signalForKey,
|
|
3211
3286
|
});
|
|
3212
3287
|
return store;
|
|
3213
3288
|
};
|
|
3214
3289
|
|
|
3290
|
+
const syncStoreToSignals = (store, propertyToSignalMap) => {
|
|
3291
|
+
const { idKey } = store;
|
|
3292
|
+
const cleanupCallbackSet = new Set();
|
|
3293
|
+
for (const [propertyName, targetSignal] of Object.entries(
|
|
3294
|
+
propertyToSignalMap,
|
|
3295
|
+
)) {
|
|
3296
|
+
if (propertyName === idKey) {
|
|
3297
|
+
const unsubscribe = store.observeProperties(
|
|
3298
|
+
(mutationsArray) => {
|
|
3299
|
+
for (const mutations of mutationsArray) {
|
|
3300
|
+
const mutation = mutations[idKey];
|
|
3301
|
+
if (mutation.oldValue === targetSignal.peek()) {
|
|
3302
|
+
targetSignal.value = mutation.newValue;
|
|
3303
|
+
break;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
},
|
|
3307
|
+
{ properties: [idKey] },
|
|
3308
|
+
);
|
|
3309
|
+
cleanupCallbackSet.add(unsubscribe);
|
|
3310
|
+
continue;
|
|
3311
|
+
}
|
|
3312
|
+
const itemSignal = store.signalForKey(propertyName, targetSignal);
|
|
3313
|
+
const unsubscribe = store.observeItemProperties(
|
|
3314
|
+
itemSignal,
|
|
3315
|
+
(propertyMutations) => {
|
|
3316
|
+
const mutation = propertyMutations[propertyName];
|
|
3317
|
+
targetSignal.value = mutation.newValue;
|
|
3318
|
+
},
|
|
3319
|
+
{ properties: [propertyName] },
|
|
3320
|
+
);
|
|
3321
|
+
cleanupCallbackSet.add(unsubscribe);
|
|
3322
|
+
}
|
|
3323
|
+
return () => {
|
|
3324
|
+
for (const cleanup of cleanupCallbackSet) {
|
|
3325
|
+
cleanup();
|
|
3326
|
+
}
|
|
3327
|
+
cleanupCallbackSet.clear();
|
|
3328
|
+
};
|
|
3329
|
+
};
|
|
3330
|
+
|
|
3331
|
+
// WeakMap<action, Set<string>> — tracks which top-level properties were present in
|
|
3332
|
+
// the GET response for a given action instance. Used by scoped_many_effect to check
|
|
3333
|
+
// whether the parent GET embedded the child sub-resource.
|
|
3334
|
+
const actionResultPropertiesMap = new WeakMap();
|
|
3335
|
+
const recordGetResultProperties = (action, resultKeys) => {
|
|
3336
|
+
actionResultPropertiesMap.set(action, new Set(resultKeys));
|
|
3337
|
+
};
|
|
3338
|
+
const getActionResultProperties = (action) => {
|
|
3339
|
+
return actionResultPropertiesMap.get(action);
|
|
3340
|
+
};
|
|
3341
|
+
|
|
3215
3342
|
/*
|
|
3216
3343
|
* Default autorerun behavior explanation:
|
|
3217
3344
|
* GET: false (RECOMMENDED)
|
|
@@ -3248,7 +3375,8 @@ const defaultRerunOn = {
|
|
|
3248
3375
|
// This handles ALL resource lifecycle logic (rerun/reset) across all resources
|
|
3249
3376
|
const createResourceLifecycleManager = () => {
|
|
3250
3377
|
const registeredResources = new Map(); // Map<resourceInstance, lifecycleConfig>
|
|
3251
|
-
const resourceDependencies = new Map(); // Map<resourceInstance, Set<dependentResources>>
|
|
3378
|
+
const resourceDependencies = new Map(); // Map<resourceInstance, Set<dependentResources>> — user-configured
|
|
3379
|
+
const scopedManyParents = new Map(); // Map<childResource, Set<{resource, propertyName}>> — auto from scopedMany
|
|
3252
3380
|
|
|
3253
3381
|
const registerResource = (resourceScope, config) => {
|
|
3254
3382
|
const {
|
|
@@ -3307,11 +3435,17 @@ const createResourceLifecycleManager = () => {
|
|
|
3307
3435
|
triggerVerb === "PATCH") &&
|
|
3308
3436
|
config.uniqueKeys.length > 0;
|
|
3309
3437
|
|
|
3438
|
+
const isKnownDependency =
|
|
3439
|
+
triggerResourceScope !== null &&
|
|
3440
|
+
triggerResourceScope !== undefined &&
|
|
3441
|
+
resourceDependencies.get(triggerResourceScope)?.has(resourceScope);
|
|
3442
|
+
|
|
3310
3443
|
if (
|
|
3311
3444
|
!shouldRerunGetMany &&
|
|
3312
3445
|
!shouldRerunGet &&
|
|
3313
3446
|
triggerVerb !== "DELETE" &&
|
|
3314
|
-
!hasUniqueKeyAutorerun
|
|
3447
|
+
!hasUniqueKeyAutorerun &&
|
|
3448
|
+
!isKnownDependency
|
|
3315
3449
|
) {
|
|
3316
3450
|
continue;
|
|
3317
3451
|
}
|
|
@@ -3431,22 +3565,63 @@ const createResourceLifecycleManager = () => {
|
|
|
3431
3565
|
}
|
|
3432
3566
|
}
|
|
3433
3567
|
|
|
3434
|
-
// Cross-resource dependency effects: rerun dependent GET_MANY
|
|
3568
|
+
// Cross-resource dependency effects: rerun dependent GET / GET_MANY
|
|
3569
|
+
// Fires on any mutating verb — user-configured dependencies express
|
|
3570
|
+
// "this resource depends on another resource's data", so any mutation
|
|
3571
|
+
// (POST, PUT, PATCH, DELETE) on the dependency should trigger a rerun.
|
|
3435
3572
|
{
|
|
3436
3573
|
if (
|
|
3437
3574
|
triggerResourceScope &&
|
|
3438
3575
|
resourceDependencies
|
|
3439
3576
|
.get(triggerResourceScope)
|
|
3440
3577
|
?.has(resourceScope) &&
|
|
3441
|
-
triggerVerb
|
|
3442
|
-
|
|
3443
|
-
|
|
3578
|
+
(triggerVerb === "POST" ||
|
|
3579
|
+
triggerVerb === "PUT" ||
|
|
3580
|
+
triggerVerb === "PATCH" ||
|
|
3581
|
+
triggerVerb === "DELETE") &&
|
|
3582
|
+
candidateVerb === "GET"
|
|
3444
3583
|
) {
|
|
3445
3584
|
actionsToRerun.add(actionCandidate);
|
|
3446
3585
|
reasonSet.add("dependency autorerun");
|
|
3447
3586
|
continue;
|
|
3448
3587
|
}
|
|
3449
3588
|
}
|
|
3589
|
+
|
|
3590
|
+
// scopedMany auto-dependency: only rerun parent singular GET on child POST,
|
|
3591
|
+
// and only when the parent GET previously returned the sub-resource embedded
|
|
3592
|
+
// inside its response (detected via action._resultProperties).
|
|
3593
|
+
// GET_MANY is excluded — a list of parents is not stale just because one
|
|
3594
|
+
// child item was added to one of them.
|
|
3595
|
+
scoped_many_effect: {
|
|
3596
|
+
if (
|
|
3597
|
+
triggerResourceScope &&
|
|
3598
|
+
triggerVerb === "POST" &&
|
|
3599
|
+
candidateVerb === "GET" &&
|
|
3600
|
+
!candidateIsPlural
|
|
3601
|
+
) {
|
|
3602
|
+
const parentEntries = scopedManyParents.get(triggerResourceScope);
|
|
3603
|
+
if (!parentEntries) {
|
|
3604
|
+
break scoped_many_effect;
|
|
3605
|
+
}
|
|
3606
|
+
for (const {
|
|
3607
|
+
resource: parentResource,
|
|
3608
|
+
propertyName,
|
|
3609
|
+
} of parentEntries) {
|
|
3610
|
+
if (parentResource !== resourceScope) {
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
// Only rerun if the last GET response included the embedded sub-resource.
|
|
3614
|
+
if (
|
|
3615
|
+
!getActionResultProperties(actionCandidate)?.has(propertyName)
|
|
3616
|
+
) {
|
|
3617
|
+
break scoped_many_effect;
|
|
3618
|
+
}
|
|
3619
|
+
actionsToRerun.add(actionCandidate);
|
|
3620
|
+
reasonSet.add("scopedMany parent autorerun");
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3450
3625
|
}
|
|
3451
3626
|
}
|
|
3452
3627
|
}
|
|
@@ -3478,6 +3653,16 @@ const createResourceLifecycleManager = () => {
|
|
|
3478
3653
|
registerResource,
|
|
3479
3654
|
registerAction,
|
|
3480
3655
|
onActionComplete,
|
|
3656
|
+
// Registers: when `triggerResource` fires, rerun `dependentResource`'s actions.
|
|
3657
|
+
// Used by scopedMany to make the parent GET rerun when a child mutation completes.
|
|
3658
|
+
addDependency: (triggerResource, dependentResource, propertyName) => {
|
|
3659
|
+
if (!scopedManyParents.has(triggerResource)) {
|
|
3660
|
+
scopedManyParents.set(triggerResource, new Set());
|
|
3661
|
+
}
|
|
3662
|
+
scopedManyParents
|
|
3663
|
+
.get(triggerResource)
|
|
3664
|
+
.add({ resource: dependentResource, propertyName });
|
|
3665
|
+
},
|
|
3481
3666
|
};
|
|
3482
3667
|
};
|
|
3483
3668
|
|
|
@@ -4265,7 +4450,14 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4265
4450
|
`${childActionName} callback must return [ownerId, props], received ${result}`,
|
|
4266
4451
|
);
|
|
4267
4452
|
}
|
|
4268
|
-
const [
|
|
4453
|
+
const [rawOwnerId, props] = result;
|
|
4454
|
+
const ownerId = resolveOwnerId(
|
|
4455
|
+
rawOwnerId,
|
|
4456
|
+
store,
|
|
4457
|
+
idKey,
|
|
4458
|
+
uniqueKeys,
|
|
4459
|
+
childActionName,
|
|
4460
|
+
);
|
|
4269
4461
|
const childItem = scopedItemMap.get(ownerId);
|
|
4270
4462
|
if (!childItem) {
|
|
4271
4463
|
throw new Error(
|
|
@@ -4340,21 +4532,56 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4340
4532
|
const scopedIdArraySignalMap = new Map(); // ownerId → childItemIdArraySignal
|
|
4341
4533
|
addItemSetup((item) => {
|
|
4342
4534
|
const ownerId = item[idKey];
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4535
|
+
|
|
4536
|
+
// Reuse an existing scoped store if one was already created via a uniqueKey
|
|
4537
|
+
// (e.g. rows were fetched by tablename before the full table was loaded).
|
|
4538
|
+
let childStore = scopedStoreMap.get(ownerId);
|
|
4539
|
+
let childItemIdArraySignal = scopedIdArraySignalMap.get(ownerId);
|
|
4540
|
+
if (!childStore) {
|
|
4541
|
+
for (const uniqueKey of uniqueKeys) {
|
|
4542
|
+
const uniqueKeyValue = item[uniqueKey];
|
|
4543
|
+
if (uniqueKeyValue !== undefined) {
|
|
4544
|
+
const existing = scopedStoreMap.get(uniqueKeyValue);
|
|
4545
|
+
if (existing) {
|
|
4546
|
+
childStore = existing;
|
|
4547
|
+
childItemIdArraySignal =
|
|
4548
|
+
scopedIdArraySignalMap.get(uniqueKeyValue);
|
|
4549
|
+
break;
|
|
4550
|
+
}
|
|
4350
4551
|
}
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
if (!childStore) {
|
|
4555
|
+
childStore = arraySignalStore([], childIdKey, {
|
|
4556
|
+
name: `${childName}#${ownerId} store`,
|
|
4557
|
+
createItem: (props) => {
|
|
4558
|
+
const childItem = {};
|
|
4559
|
+
Object.assign(childItem, props);
|
|
4560
|
+
for (const childSetup of childSetupCallbackSet) {
|
|
4561
|
+
childSetup(childItem);
|
|
4562
|
+
}
|
|
4563
|
+
return childItem;
|
|
4564
|
+
},
|
|
4565
|
+
});
|
|
4566
|
+
childItemIdArraySignal = signal([]);
|
|
4567
|
+
}
|
|
4354
4568
|
scopedStoreMap.set(ownerId, childStore);
|
|
4569
|
+
// Also register by each uniqueKey value so that resolveOwnerId works
|
|
4570
|
+
// when a callback returns { [uniqueKey]: value } before the full item is loaded.
|
|
4571
|
+
for (const uniqueKey of uniqueKeys) {
|
|
4572
|
+
const uniqueKeyValue = item[uniqueKey];
|
|
4573
|
+
if (uniqueKeyValue !== undefined) {
|
|
4574
|
+
scopedStoreMap.set(uniqueKeyValue, childStore);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4355
4577
|
|
|
4356
|
-
const childItemIdArraySignal = signal([]);
|
|
4357
4578
|
scopedIdArraySignalMap.set(ownerId, childItemIdArraySignal);
|
|
4579
|
+
for (const uniqueKey of uniqueKeys) {
|
|
4580
|
+
const uniqueKeyValue = item[uniqueKey];
|
|
4581
|
+
if (uniqueKeyValue !== undefined) {
|
|
4582
|
+
scopedIdArraySignalMap.set(uniqueKeyValue, childItemIdArraySignal);
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4358
4585
|
|
|
4359
4586
|
const updateChildItemIdArray = (valueArray) => {
|
|
4360
4587
|
const currentIdArray = childItemIdArraySignal.peek();
|
|
@@ -4431,12 +4658,32 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4431
4658
|
`${childActionName} callback must return [ownerId, ...] array, received ${result}`,
|
|
4432
4659
|
);
|
|
4433
4660
|
}
|
|
4434
|
-
const [
|
|
4435
|
-
const
|
|
4661
|
+
const [rawOwnerId, ...rest] = result;
|
|
4662
|
+
const ownerId = resolveOwnerId(
|
|
4663
|
+
rawOwnerId,
|
|
4664
|
+
store,
|
|
4665
|
+
idKey,
|
|
4666
|
+
uniqueKeys,
|
|
4667
|
+
childActionName,
|
|
4668
|
+
);
|
|
4669
|
+
let childStore = scopedStoreMap.get(ownerId);
|
|
4436
4670
|
if (!childStore) {
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4671
|
+
// Owner not yet in store — lazily create scoped store so actions can run
|
|
4672
|
+
// before the parent item has been fully loaded (e.g. rows fetched before table).
|
|
4673
|
+
childStore = arraySignalStore([], childIdKey, {
|
|
4674
|
+
name: `${childName}#${ownerId} store`,
|
|
4675
|
+
createItem: (props) => {
|
|
4676
|
+
const childItem = {};
|
|
4677
|
+
Object.assign(childItem, props);
|
|
4678
|
+
for (const childSetup of childSetupCallbackSet) {
|
|
4679
|
+
childSetup(childItem);
|
|
4680
|
+
}
|
|
4681
|
+
return childItem;
|
|
4682
|
+
},
|
|
4683
|
+
});
|
|
4684
|
+
scopedStoreMap.set(ownerId, childStore);
|
|
4685
|
+
const newIdArraySignal = signal([]);
|
|
4686
|
+
scopedIdArraySignalMap.set(ownerId, newIdArraySignal);
|
|
4440
4687
|
}
|
|
4441
4688
|
const childItemIdArraySignal = scopedIdArraySignalMap.get(ownerId);
|
|
4442
4689
|
|
|
@@ -4471,6 +4718,14 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4471
4718
|
: childStore.upsert(rest[0]);
|
|
4472
4719
|
return [ownerId, childItem[childIdKey]];
|
|
4473
4720
|
},
|
|
4721
|
+
valueToData: (value) => {
|
|
4722
|
+
if (!value) return isMany ? [] : undefined;
|
|
4723
|
+
const [ownerId, idOrIdArray] = value;
|
|
4724
|
+
const childStore = scopedStoreMap.get(ownerId);
|
|
4725
|
+
if (!childStore) return isMany ? [] : undefined;
|
|
4726
|
+
if (isMany) return childStore.selectAll(idOrIdArray);
|
|
4727
|
+
return childStore.select(idOrIdArray);
|
|
4728
|
+
},
|
|
4474
4729
|
completeSideEffect: (actionCompleted) => {
|
|
4475
4730
|
lifecycleCtx.onComplete(actionCompleted);
|
|
4476
4731
|
},
|
|
@@ -4478,6 +4733,11 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4478
4733
|
return childAction;
|
|
4479
4734
|
};
|
|
4480
4735
|
|
|
4736
|
+
// When a child (scopedMany) item is mutated via POST, the parent GET must
|
|
4737
|
+
// re-fetch because the parent embeds the child array and we cannot know the
|
|
4738
|
+
// new ordering without asking the backend again.
|
|
4739
|
+
// (scopedOne does NOT need this: the mutation result contains the updated
|
|
4740
|
+
// item directly, so no parent re-fetch is necessary.)
|
|
4481
4741
|
const childResource = createResource(childName, {
|
|
4482
4742
|
idKey: childIdKey,
|
|
4483
4743
|
restCallbacks: {
|
|
@@ -4499,6 +4759,13 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4499
4759
|
rerunOn: scopedManyRerunOn ?? rerunOn,
|
|
4500
4760
|
dependencies: scopedManyDependencies ?? dependencies,
|
|
4501
4761
|
});
|
|
4762
|
+
// Register: when childResource fires, rerun parent (stateFacade) GETs.
|
|
4763
|
+
resourceLifecycleManager.addDependency(
|
|
4764
|
+
childResource,
|
|
4765
|
+
stateFacade,
|
|
4766
|
+
propertyName,
|
|
4767
|
+
);
|
|
4768
|
+
childResource.getChildStore = (ownerKey) => scopedStoreMap.get(ownerKey);
|
|
4502
4769
|
return childResource;
|
|
4503
4770
|
};
|
|
4504
4771
|
|
|
@@ -4612,6 +4879,11 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
4612
4879
|
${originalActionName} source location: ${locationInfo}`,
|
|
4613
4880
|
);
|
|
4614
4881
|
}
|
|
4882
|
+
// Track which top-level properties the GET response contained so that
|
|
4883
|
+
// lifecycle rules can detect whether sub-resources were embedded.
|
|
4884
|
+
if (verb === "GET") {
|
|
4885
|
+
recordGetResultProperties(action, Object.keys(result));
|
|
4886
|
+
}
|
|
4615
4887
|
return applyResultToValue(result);
|
|
4616
4888
|
},
|
|
4617
4889
|
valueToData: (itemId) => store.select(itemId),
|
|
@@ -4702,6 +4974,110 @@ const isProps = (value) => {
|
|
|
4702
4974
|
return value !== null && typeof value === "object";
|
|
4703
4975
|
};
|
|
4704
4976
|
|
|
4977
|
+
const resolveOwnerId = (rawOwnerId, store, idKey, uniqueKeys, actionName) => {
|
|
4978
|
+
if (!isProps(rawOwnerId)) {
|
|
4979
|
+
// Already a primitive — use as-is.
|
|
4980
|
+
return rawOwnerId;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
const keys = Object.keys(rawOwnerId);
|
|
4984
|
+
|
|
4985
|
+
if (keys.length === 1) {
|
|
4986
|
+
const [propName] = keys;
|
|
4987
|
+
const propValue = rawOwnerId[propName];
|
|
4988
|
+
|
|
4989
|
+
if (propName === idKey) {
|
|
4990
|
+
return propValue;
|
|
4991
|
+
}
|
|
4992
|
+
if (uniqueKeys.includes(propName)) {
|
|
4993
|
+
const item = store.select(propName, propValue);
|
|
4994
|
+
if (!item) {
|
|
4995
|
+
// Owner not yet in store — the scoped maps may still be keyed by uniqueKey value
|
|
4996
|
+
// (registered during addItemSetup). Return the propValue as the owner key directly.
|
|
4997
|
+
return propValue;
|
|
4998
|
+
}
|
|
4999
|
+
return item[idKey];
|
|
5000
|
+
}
|
|
5001
|
+
throw new TypeError(
|
|
5002
|
+
`${actionName}: the first element of the returned array is { ${propName}: "${propValue}" } but "${propName}" is neither the idKey ("${idKey}") nor a declared uniqueKey (${uniqueKeys.length ? uniqueKeys.join(", ") : "none"}).
|
|
5003
|
+
Return a primitive id or a single-property object whose key is the idKey or a uniqueKey.`,
|
|
5004
|
+
);
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
// More than one property — try to recover via idKey, warn if successful.
|
|
5008
|
+
if (idKey in rawOwnerId) {
|
|
5009
|
+
const resolvedId = rawOwnerId[idKey];
|
|
5010
|
+
console.warn(
|
|
5011
|
+
`${actionName}: the first element of the returned array is an object with multiple properties.
|
|
5012
|
+
Only "${idKey}" is needed. Consider returning a primitive id or { ${idKey}: value } instead.`,
|
|
5013
|
+
);
|
|
5014
|
+
return resolvedId;
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
throw new TypeError(
|
|
5018
|
+
`${actionName}: the first element of the returned array must be a primitive id or a single-property object equal to { [idKey]: value } or { [uniqueKey]: value }.
|
|
5019
|
+
Received an object with keys: ${keys.join(", ")}.`,
|
|
5020
|
+
);
|
|
5021
|
+
};
|
|
5022
|
+
|
|
5023
|
+
/** so that when a tracked property changes
|
|
5024
|
+
* on an item the corresponding signal is updated automatically.
|
|
5025
|
+
*
|
|
5026
|
+
* Since signals are typically connected to route parameters via the route template
|
|
5027
|
+
* syntax, this keeps the URL in sync when a store item's mutable key is renamed.
|
|
5028
|
+
*
|
|
5029
|
+
* @example
|
|
5030
|
+
* const usernameSignal = stateSignal();
|
|
5031
|
+
* const USER_ROUTE = route(`/users/:username=${usernameSignal}/`);
|
|
5032
|
+
*
|
|
5033
|
+
* const USER = resource("user", {
|
|
5034
|
+
* idKey: "id",
|
|
5035
|
+
* uniqueKeys: ["username"],
|
|
5036
|
+
* PUT: async ({ id, username }) => ({ id, username }),
|
|
5037
|
+
* });
|
|
5038
|
+
*
|
|
5039
|
+
* syncResourceToSignals(USER, { username: usernameSignal });
|
|
5040
|
+
* // Now when a user item's username is updated via USER.PUT,
|
|
5041
|
+
* // usernameSignal.value is set to the new username,
|
|
5042
|
+
* // which in turn triggers the route Signal->URL sync and updates the browser URL.
|
|
5043
|
+
*/
|
|
5044
|
+
const syncResourceToSignals = (resource, propertyToSignalMap) => {
|
|
5045
|
+
if (resource.getChildStore) {
|
|
5046
|
+
throw new Error(
|
|
5047
|
+
`syncResourceToSignals: "${resource.name}" is a scoped resource (scopedMany/scopedOne). Use syncOwnedResourceToSignals instead.`,
|
|
5048
|
+
);
|
|
5049
|
+
}
|
|
5050
|
+
syncStoreToSignals(resource.store, propertyToSignalMap);
|
|
5051
|
+
};
|
|
5052
|
+
|
|
5053
|
+
const syncOwnedResourceToSignals = (
|
|
5054
|
+
resource,
|
|
5055
|
+
ownerSignal,
|
|
5056
|
+
propertyToSignalMap,
|
|
5057
|
+
) => {
|
|
5058
|
+
if (!resource.getChildStore) {
|
|
5059
|
+
throw new Error(
|
|
5060
|
+
`syncOwnedResourceToSignals: "${resource.name}" is not a scoped resource (scopedMany/scopedOne). Use syncResourceToSignals instead.`,
|
|
5061
|
+
);
|
|
5062
|
+
}
|
|
5063
|
+
effect(() => {
|
|
5064
|
+
// Always subscribe to the parent store so the effect re-runs when a new
|
|
5065
|
+
// owner item is added (which creates the child store).
|
|
5066
|
+
// eslint-disable-next-line no-unused-expressions
|
|
5067
|
+
resource.store.arraySignal.value;
|
|
5068
|
+
const ownerKey = ownerSignal.value;
|
|
5069
|
+
if (ownerKey === null || ownerKey === undefined) {
|
|
5070
|
+
return null;
|
|
5071
|
+
}
|
|
5072
|
+
const childStore = resource.getChildStore(ownerKey);
|
|
5073
|
+
if (!childStore) {
|
|
5074
|
+
return null;
|
|
5075
|
+
}
|
|
5076
|
+
const cleanup = syncStoreToSignals(childStore, propertyToSignalMap);
|
|
5077
|
+
return cleanup;
|
|
5078
|
+
});
|
|
5079
|
+
};
|
|
5080
|
+
|
|
4705
5081
|
const valueInLocalStorage = (key, { type = "any" } = {}) => {
|
|
4706
5082
|
const converter = TYPE_CONVERTERS[type];
|
|
4707
5083
|
|
|
@@ -5401,6 +5777,298 @@ const useStateArray = (
|
|
|
5401
5777
|
return [array, add, remove, reset];
|
|
5402
5778
|
};
|
|
5403
5779
|
|
|
5780
|
+
const promiseStateWeakMap = new WeakMap();
|
|
5781
|
+
const usePromiseAsyncData = (
|
|
5782
|
+
promise,
|
|
5783
|
+
{ loadingEffect, errorEffect },
|
|
5784
|
+
) => {
|
|
5785
|
+
const forceRender = useForceRender();
|
|
5786
|
+
|
|
5787
|
+
let promiseState = promiseStateWeakMap.get(promise);
|
|
5788
|
+
if (!promiseState) {
|
|
5789
|
+
promiseState = {
|
|
5790
|
+
data: undefined,
|
|
5791
|
+
error: undefined,
|
|
5792
|
+
settled: false,
|
|
5793
|
+
};
|
|
5794
|
+
promiseStateWeakMap.set(promise, promiseState);
|
|
5795
|
+
promise.then(
|
|
5796
|
+
(data) => {
|
|
5797
|
+
promiseState.data = data;
|
|
5798
|
+
promiseState.settled = true;
|
|
5799
|
+
forceRender();
|
|
5800
|
+
},
|
|
5801
|
+
(error) => {
|
|
5802
|
+
promiseState.error = error;
|
|
5803
|
+
promiseState.settled = true;
|
|
5804
|
+
forceRender();
|
|
5805
|
+
},
|
|
5806
|
+
);
|
|
5807
|
+
}
|
|
5808
|
+
if (!promiseState.settled) {
|
|
5809
|
+
if (loadingEffect === "use") {
|
|
5810
|
+
return [promiseState.data, true, undefined];
|
|
5811
|
+
}
|
|
5812
|
+
throw promise;
|
|
5813
|
+
}
|
|
5814
|
+
if (promiseState.error) {
|
|
5815
|
+
if (errorEffect === "use") {
|
|
5816
|
+
return [promiseState.data, false, promiseState.error];
|
|
5817
|
+
}
|
|
5818
|
+
throw promiseState.error;
|
|
5819
|
+
}
|
|
5820
|
+
return [promiseState.data, false, undefined];
|
|
5821
|
+
};
|
|
5822
|
+
|
|
5823
|
+
const useForceRender = () => {
|
|
5824
|
+
const [, setState] = useState(null);
|
|
5825
|
+
return () => {
|
|
5826
|
+
setState({});
|
|
5827
|
+
};
|
|
5828
|
+
};
|
|
5829
|
+
|
|
5830
|
+
// https://github.com/preactjs/preact/issues/4756
|
|
5831
|
+
|
|
5832
|
+
const useAsyncData = (promiseOrAction, {
|
|
5833
|
+
loading = "delegate",
|
|
5834
|
+
error = "delegate"
|
|
5835
|
+
} = {}) => {
|
|
5836
|
+
const isAction = Boolean(promiseOrAction && promiseOrAction.isAction);
|
|
5837
|
+
if (loading === true) {
|
|
5838
|
+
loading = "use";
|
|
5839
|
+
}
|
|
5840
|
+
if (error === true) {
|
|
5841
|
+
error = "use";
|
|
5842
|
+
}
|
|
5843
|
+
if (isAction) {
|
|
5844
|
+
return useActionAsyncData(promiseOrAction, {
|
|
5845
|
+
loadingEffect: loading,
|
|
5846
|
+
errorEffect: error
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
return usePromiseAsyncData(promiseOrAction, {
|
|
5850
|
+
loadingEffect: loading,
|
|
5851
|
+
errorEffect: error
|
|
5852
|
+
});
|
|
5853
|
+
};
|
|
5854
|
+
|
|
5855
|
+
// ─── useAction ────────────────────────────────────────────────────────────────
|
|
5856
|
+
|
|
5857
|
+
const LoadingContext$1 = createContext(null);
|
|
5858
|
+
const actionPendingPromiseWeakMap = new WeakMap();
|
|
5859
|
+
const dismissedActionWeakSet = new WeakSet();
|
|
5860
|
+
const dismissedActionPendingPromiseWeakMap = new WeakMap();
|
|
5861
|
+
const useActionAsyncData = (action, {
|
|
5862
|
+
loadingEffect,
|
|
5863
|
+
errorEffect
|
|
5864
|
+
}) => {
|
|
5865
|
+
const loadingRef = useContext(LoadingContext$1);
|
|
5866
|
+
if (!loadingRef) {
|
|
5867
|
+
throw new Error("Missing <Loading>");
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5870
|
+
// Use peek() instead of .value to avoid subscribing this component to the signal.
|
|
5871
|
+
// Reading .value would make Preact re-render the component reactively when the state
|
|
5872
|
+
// changes. When the action fails while Suspense is still holding the detached stale
|
|
5873
|
+
// DOM, this reactive re-render causes Suspense to move that stale DOM permanently
|
|
5874
|
+
// back into the document — the stale content then coexists with the error fallback
|
|
5875
|
+
// and never goes away. Manual subscription via useEffect + useState ensures
|
|
5876
|
+
// re-renders only happen after the pending promise resolves, at which point Suspense
|
|
5877
|
+
// has already processed the settlement and the detached DOM is discarded.
|
|
5878
|
+
const runningState = action.runningStateSignal.peek();
|
|
5879
|
+
const [, setTick] = useState(0);
|
|
5880
|
+
useEffect(() => {
|
|
5881
|
+
return action.runningStateSignal.subscribe(state => {
|
|
5882
|
+
if (state === RUNNING) {
|
|
5883
|
+
dismissedActionWeakSet.delete(action);
|
|
5884
|
+
}
|
|
5885
|
+
setTick(n => n + 1);
|
|
5886
|
+
});
|
|
5887
|
+
}, []);
|
|
5888
|
+
if (runningState === COMPLETED) {
|
|
5889
|
+
return [action.dataSignal.peek(), false, undefined];
|
|
5890
|
+
}
|
|
5891
|
+
if (runningState === FAILED) {
|
|
5892
|
+
if (dismissedActionWeakSet.has(action)) {
|
|
5893
|
+
const staleData = action.dataSignal.peek();
|
|
5894
|
+
if (staleData !== undefined) {
|
|
5895
|
+
// Dismissed with stale data — return it so children render normally
|
|
5896
|
+
return [staleData, false, undefined];
|
|
5897
|
+
}
|
|
5898
|
+
// Dismissed with no data — suspend until the action re-runs.
|
|
5899
|
+
// A never-resolving promise would leave the component stuck forever,
|
|
5900
|
+
// so we use an action-specific promise that resolves on RUNNING,
|
|
5901
|
+
// which lets the component re-render and go through the normal loading path.
|
|
5902
|
+
let dismissedPromise = dismissedActionPendingPromiseWeakMap.get(action);
|
|
5903
|
+
if (!dismissedPromise) {
|
|
5904
|
+
dismissedPromise = new Promise(resolve => {
|
|
5905
|
+
const unsubscribe = action.runningStateSignal.subscribe(state => {
|
|
5906
|
+
if (state === RUNNING) {
|
|
5907
|
+
dismissedActionPendingPromiseWeakMap.delete(action);
|
|
5908
|
+
unsubscribe();
|
|
5909
|
+
resolve();
|
|
5910
|
+
}
|
|
5911
|
+
});
|
|
5912
|
+
});
|
|
5913
|
+
dismissedActionPendingPromiseWeakMap.set(action, dismissedPromise);
|
|
5914
|
+
}
|
|
5915
|
+
throw dismissedPromise;
|
|
5916
|
+
}
|
|
5917
|
+
const actionError = action.errorSignal.peek();
|
|
5918
|
+
if (errorEffect === "use") {
|
|
5919
|
+
const dismissError = () => {
|
|
5920
|
+
dismissedActionWeakSet.add(action);
|
|
5921
|
+
setTick(n => n + 1);
|
|
5922
|
+
};
|
|
5923
|
+
return [undefined, false, actionError, dismissError];
|
|
5924
|
+
}
|
|
5925
|
+
actionError.action = action;
|
|
5926
|
+
throw actionError;
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
// RUNNING with loadingEffect: "use" — return stale data + loading flag, no suspend
|
|
5930
|
+
if (loadingEffect === "use" && runningState === RUNNING) {
|
|
5931
|
+
const staleData = action.dataSignal.peek();
|
|
5932
|
+
return [staleData, true, undefined];
|
|
5933
|
+
}
|
|
5934
|
+
|
|
5935
|
+
// IDLE or RUNNING with loadingEffect: "delegate" — suspend
|
|
5936
|
+
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
5937
|
+
loadingRef.current = {
|
|
5938
|
+
reason,
|
|
5939
|
+
action
|
|
5940
|
+
};
|
|
5941
|
+
let pendingPromise = actionPendingPromiseWeakMap.get(action);
|
|
5942
|
+
if (!pendingPromise) {
|
|
5943
|
+
pendingPromise = new Promise(resolve => {
|
|
5944
|
+
const unsubscribe = action.runningStateSignal.subscribe(state => {
|
|
5945
|
+
if (state === COMPLETED || state === FAILED) {
|
|
5946
|
+
actionPendingPromiseWeakMap.delete(action);
|
|
5947
|
+
unsubscribe();
|
|
5948
|
+
resolve();
|
|
5949
|
+
} else if (reason === "idle" && state === RUNNING) {
|
|
5950
|
+
// idle→running: unblock so loadingRef reason updates to "loading"
|
|
5951
|
+
actionPendingPromiseWeakMap.delete(action);
|
|
5952
|
+
unsubscribe();
|
|
5953
|
+
resolve();
|
|
5954
|
+
}
|
|
5955
|
+
});
|
|
5956
|
+
});
|
|
5957
|
+
actionPendingPromiseWeakMap.set(action, pendingPromise);
|
|
5958
|
+
}
|
|
5959
|
+
throw pendingPromise;
|
|
5960
|
+
};
|
|
5961
|
+
|
|
5962
|
+
// ─── Loading ──────────────────────────────────────────────────────────────────
|
|
5963
|
+
// Wraps Suspense. Provides LoadingContext so useAction can write the suspension
|
|
5964
|
+
// reason. LoadingFallback reads that reason and subscribes to the action so it
|
|
5965
|
+
// only shows the spinner when actually loading (not in the initial idle state).
|
|
5966
|
+
const Loading = ({
|
|
5967
|
+
children,
|
|
5968
|
+
fallback
|
|
5969
|
+
}) => {
|
|
5970
|
+
const loadingRef = useRef({
|
|
5971
|
+
reason: "idle",
|
|
5972
|
+
action: null
|
|
5973
|
+
});
|
|
5974
|
+
return jsx(LoadingContext$1.Provider, {
|
|
5975
|
+
value: loadingRef,
|
|
5976
|
+
children: jsx(Suspense, {
|
|
5977
|
+
fallback: jsx(LoadingFallback, {
|
|
5978
|
+
loadingRef: loadingRef,
|
|
5979
|
+
fallback: fallback
|
|
5980
|
+
}),
|
|
5981
|
+
children: children
|
|
5982
|
+
})
|
|
5983
|
+
});
|
|
5984
|
+
};
|
|
5985
|
+
const LoadingFallback = ({
|
|
5986
|
+
loadingRef,
|
|
5987
|
+
fallback
|
|
5988
|
+
}) => {
|
|
5989
|
+
const [, setTick] = useState(0);
|
|
5990
|
+
const {
|
|
5991
|
+
action
|
|
5992
|
+
} = loadingRef.current;
|
|
5993
|
+
useEffect(() => {
|
|
5994
|
+
if (!action) {
|
|
5995
|
+
return undefined;
|
|
5996
|
+
}
|
|
5997
|
+
return action.runningStateSignal.subscribe(() => {
|
|
5998
|
+
setTick(n => n + 1);
|
|
5999
|
+
});
|
|
6000
|
+
}, [action]);
|
|
6001
|
+
if (loadingRef.current.reason !== "loading") {
|
|
6002
|
+
return null;
|
|
6003
|
+
}
|
|
6004
|
+
if (typeof fallback === "function") {
|
|
6005
|
+
return h(fallback);
|
|
6006
|
+
}
|
|
6007
|
+
return fallback;
|
|
6008
|
+
};
|
|
6009
|
+
|
|
6010
|
+
// ─── ErrorBoundary ────────────────────────────────────────────────────────────
|
|
6011
|
+
// Catches errors thrown by useAction. Subscribes to error.action so it
|
|
6012
|
+
// auto-resets when the action runs again.
|
|
6013
|
+
const ErrorBoundary = ({
|
|
6014
|
+
children,
|
|
6015
|
+
fallback,
|
|
6016
|
+
onReset
|
|
6017
|
+
}) => {
|
|
6018
|
+
const [error, resetError] = useErrorBoundary();
|
|
6019
|
+
const [dismissed, setDismissed] = useState(false);
|
|
6020
|
+
const cleanupRef = useRef();
|
|
6021
|
+
useEffect(() => {
|
|
6022
|
+
return () => {
|
|
6023
|
+
cleanupRef.current?.();
|
|
6024
|
+
};
|
|
6025
|
+
}, []);
|
|
6026
|
+
if (error) {
|
|
6027
|
+
error.__handled_by__ = "<ErrorBoundary>"; // prevent jsenv from displaying it
|
|
6028
|
+
|
|
6029
|
+
const action = error.action;
|
|
6030
|
+
if (action) {
|
|
6031
|
+
cleanupRef.current?.();
|
|
6032
|
+
cleanupRef.current = action.runningStateSignal.subscribe(state => {
|
|
6033
|
+
if (state === RUNNING) {
|
|
6034
|
+
dismissedActionWeakSet.delete(action);
|
|
6035
|
+
setDismissed(false);
|
|
6036
|
+
resetError();
|
|
6037
|
+
}
|
|
6038
|
+
});
|
|
6039
|
+
const hasStaleData = action && action.dataSignal.peek() !== undefined;
|
|
6040
|
+
if (dismissed) {
|
|
6041
|
+
if (hasStaleData) {
|
|
6042
|
+
// Has stale data — children will render (useAction returns stale value)
|
|
6043
|
+
return children;
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
} else if (dismissed) {
|
|
6047
|
+
// stop rendering the error
|
|
6048
|
+
return null;
|
|
6049
|
+
}
|
|
6050
|
+
const dismiss = () => {
|
|
6051
|
+
if (action) {
|
|
6052
|
+
dismissedActionWeakSet.add(action);
|
|
6053
|
+
}
|
|
6054
|
+
onReset?.();
|
|
6055
|
+
setDismissed(true);
|
|
6056
|
+
resetError();
|
|
6057
|
+
};
|
|
6058
|
+
if (!fallback) {
|
|
6059
|
+
return null;
|
|
6060
|
+
}
|
|
6061
|
+
if (typeof fallback === "function") {
|
|
6062
|
+
return h(fallback, {
|
|
6063
|
+
error,
|
|
6064
|
+
resetError: dismiss
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
return fallback;
|
|
6068
|
+
}
|
|
6069
|
+
return children;
|
|
6070
|
+
};
|
|
6071
|
+
|
|
5404
6072
|
/**
|
|
5405
6073
|
* Creates a function that generates abort signals, automatically cancelling previous requests.
|
|
5406
6074
|
*
|
|
@@ -7084,7 +7752,9 @@ const Box = props => {
|
|
|
7084
7752
|
}
|
|
7085
7753
|
const propCssVar = propsCSSVars[name];
|
|
7086
7754
|
if (propCssVar) {
|
|
7087
|
-
|
|
7755
|
+
if (value !== undefined) {
|
|
7756
|
+
addCSSVar(value, propCssVar, boxStylesTarget);
|
|
7757
|
+
}
|
|
7088
7758
|
return;
|
|
7089
7759
|
}
|
|
7090
7760
|
const isPseudoStyle = styleOrigin === "pseudo_style";
|
|
@@ -10326,9 +10996,63 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
10326
10996
|
);
|
|
10327
10997
|
};
|
|
10328
10998
|
|
|
10329
|
-
//
|
|
10330
|
-
|
|
10331
|
-
|
|
10999
|
+
// Returns the pathname that this route's own literal prefix resolves to.
|
|
11000
|
+
// For route "/": "/"
|
|
11001
|
+
// For route "/profile/": "/profile/"
|
|
11002
|
+
// For route "/map/isochrone/compare": "/map/isochrone/compare"
|
|
11003
|
+
// For route "/map/isochrone/:tab=/": "/map/isochrone/" (literal prefix before first param)
|
|
11004
|
+
const getOwnBasePathname = () => {
|
|
11005
|
+
const segments = parsedPattern.segments;
|
|
11006
|
+
if (segments.length === 0) {
|
|
11007
|
+
return new URL(resolveRouteUrl("/")).pathname;
|
|
11008
|
+
}
|
|
11009
|
+
const literalSegments = [];
|
|
11010
|
+
for (const seg of segments) {
|
|
11011
|
+
if (seg.type !== "literal") {
|
|
11012
|
+
break;
|
|
11013
|
+
}
|
|
11014
|
+
literalSegments.push(seg.value);
|
|
11015
|
+
}
|
|
11016
|
+
if (literalSegments.length === 0) {
|
|
11017
|
+
return new URL(resolveRouteUrl("/")).pathname;
|
|
11018
|
+
}
|
|
11019
|
+
const prefix = `/${literalSegments.join("/")}/`;
|
|
11020
|
+
return new URL(resolveRouteUrl(prefix)).pathname;
|
|
11021
|
+
};
|
|
11022
|
+
|
|
11023
|
+
// Like buildMostPreciseUrl but takes the actual current browser URL into account.
|
|
11024
|
+
// When buildMostPreciseUrl returns the route's own base URL (catch-all matching an
|
|
11025
|
+
// unrepresentable path like "/404") the current pathname is preserved and only
|
|
11026
|
+
// search params are updated. When buildMostPreciseUrl performs an ancestor
|
|
11027
|
+
// optimisation (e.g. "/map/isochrone/compare" → "/map/isochrone") it is trusted
|
|
11028
|
+
// as-is because the built pathname will differ from the route's own base pathname.
|
|
11029
|
+
const buildUrlPreservingPath = (currentUrl, params = {}) => {
|
|
11030
|
+
const relativeBuiltUrl = buildMostPreciseUrl(params);
|
|
11031
|
+
if (!currentUrl) {
|
|
11032
|
+
return resolveRouteUrl(relativeBuiltUrl);
|
|
11033
|
+
}
|
|
11034
|
+
const absoluteBuiltUrl = resolveRouteUrl(relativeBuiltUrl);
|
|
11035
|
+
const builtPathname = new URL(absoluteBuiltUrl).pathname;
|
|
11036
|
+
const currentPathname = new URL(currentUrl).pathname;
|
|
11037
|
+
if (builtPathname === currentPathname) {
|
|
11038
|
+
return absoluteBuiltUrl;
|
|
11039
|
+
}
|
|
11040
|
+
const ownBasePathname = getOwnBasePathname();
|
|
11041
|
+
if (builtPathname === ownBasePathname) {
|
|
11042
|
+
// Catch-all: the route resolved to its own base pathname but the current URL
|
|
11043
|
+
// sits on a different path that this trailing-slash route caught. Keep the
|
|
11044
|
+
// current pathname and only update the search string.
|
|
11045
|
+
const correctedUrl = new URL(currentUrl);
|
|
11046
|
+
correctedUrl.search = new URL(absoluteBuiltUrl).search;
|
|
11047
|
+
return correctedUrl.href;
|
|
11048
|
+
}
|
|
11049
|
+
// Ancestor optimisation or descendant selection — trust buildMostPreciseUrl.
|
|
11050
|
+
return absoluteBuiltUrl;
|
|
11051
|
+
};
|
|
11052
|
+
|
|
11053
|
+
// Pattern object with unified data and methods
|
|
11054
|
+
const patternObject = {
|
|
11055
|
+
// Pattern data properties (formerly patternData)
|
|
10332
11056
|
urlPatternRaw: pattern,
|
|
10333
11057
|
cleanPattern,
|
|
10334
11058
|
connections,
|
|
@@ -10345,6 +11069,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
10345
11069
|
pattern: parsedPattern,
|
|
10346
11070
|
applyOn,
|
|
10347
11071
|
buildMostPreciseUrl,
|
|
11072
|
+
buildUrlPreservingPath,
|
|
10348
11073
|
resolveParams,
|
|
10349
11074
|
};
|
|
10350
11075
|
|
|
@@ -11591,7 +12316,7 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
11591
12316
|
route.setupCalled = true;
|
|
11592
12317
|
});
|
|
11593
12318
|
// methods
|
|
11594
|
-
registerSetup(({ routeSet }) => {
|
|
12319
|
+
registerSetup(({ routeSet, getUrl }) => {
|
|
11595
12320
|
route.buildRelativeUrl = (params) => {
|
|
11596
12321
|
// buildMostPreciseUrl now handles parameter resolution internally
|
|
11597
12322
|
return routePattern.buildMostPreciseUrl(params);
|
|
@@ -11660,7 +12385,20 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
11660
12385
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
11661
12386
|
});
|
|
11662
12387
|
}
|
|
11663
|
-
|
|
12388
|
+
|
|
12389
|
+
// This route is the most specific — compute the target URL.
|
|
12390
|
+
// buildUrlPreservingPath handles the catch-all case where this trailing-slash
|
|
12391
|
+
// route matched a path it cannot represent (e.g. "/" on "/404") without
|
|
12392
|
+
// corrupting the current URL. Ancestor optimisation is trusted as-is.
|
|
12393
|
+
if (!integration) {
|
|
12394
|
+
return Promise.resolve();
|
|
12395
|
+
}
|
|
12396
|
+
const targetUrl = routePattern.buildUrlPreservingPath(
|
|
12397
|
+
getUrl(),
|
|
12398
|
+
newParams,
|
|
12399
|
+
);
|
|
12400
|
+
return integration.navTo(targetUrl, {
|
|
12401
|
+
replace: true,
|
|
11664
12402
|
callReason,
|
|
11665
12403
|
});
|
|
11666
12404
|
};
|
|
@@ -11835,6 +12573,8 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
11835
12573
|
setupRoutesCalled = true;
|
|
11836
12574
|
|
|
11837
12575
|
const routeSet = new Set();
|
|
12576
|
+
let currentUrl = null;
|
|
12577
|
+
const getUrl = () => currentUrl;
|
|
11838
12578
|
// PHASE 1: Setup patterns with unified objects (includes all relationships and signal connections)
|
|
11839
12579
|
const routePatterns = [];
|
|
11840
12580
|
for (const route of routes) {
|
|
@@ -11847,7 +12587,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
11847
12587
|
// Setup routes now that patterns are correctly initialized
|
|
11848
12588
|
for (const route of routeSet) {
|
|
11849
12589
|
const { setup } = getRoutePrivateProperties(route);
|
|
11850
|
-
setup({ routeSet });
|
|
12590
|
+
setup({ routeSet, getUrl });
|
|
11851
12591
|
}
|
|
11852
12592
|
|
|
11853
12593
|
// Store previous route states to detect changes
|
|
@@ -11859,6 +12599,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
11859
12599
|
// state
|
|
11860
12600
|
} = {},
|
|
11861
12601
|
) => {
|
|
12602
|
+
currentUrl = url;
|
|
11862
12603
|
const returnValue = {};
|
|
11863
12604
|
const routeMatchInfoSet = new Set();
|
|
11864
12605
|
for (const route of routeSet) {
|
|
@@ -12850,22 +13591,67 @@ const Head = ({
|
|
|
12850
13591
|
};
|
|
12851
13592
|
|
|
12852
13593
|
/**
|
|
13594
|
+
* Route is the single primitive for URL-based rendering.
|
|
13595
|
+
*
|
|
13596
|
+
* ## Layout pattern
|
|
13597
|
+
* Use this when multiple routes share a common layout but have no shared URL prefix,
|
|
13598
|
+
* making it impossible to set a guard route on the parent container.
|
|
13599
|
+
* For example, `/profile` and `/settings` both live inside `AuthLayout` but there
|
|
13600
|
+
* is no `/auth/` prefix to match on. A container Route wraps them: the active
|
|
13601
|
+
* child's element is injected as `children` into the layout element.
|
|
13602
|
+
* If a page needs state owned by the layout, the layout must expose it via context.
|
|
13603
|
+
*
|
|
13604
|
+
* ```jsx
|
|
13605
|
+
* const PROFILE_ROUTE = route("/profile");
|
|
13606
|
+
* const SETTINGS_ROUTE = route("/settings");
|
|
13607
|
+
*
|
|
13608
|
+
* <Route element={AuthLayout}>
|
|
13609
|
+
* <Route route={PROFILE_ROUTE} element={ProfilePage} />
|
|
13610
|
+
* <Route route={SETTINGS_ROUTE} element={SettingsPage} />
|
|
13611
|
+
* <Route fallback element={AuthNotFoundPage} />
|
|
13612
|
+
* </Route>
|
|
13613
|
+
* ```
|
|
13614
|
+
*
|
|
13615
|
+
* ## Self-contained section pattern
|
|
13616
|
+
* Use this when routes share a common URL prefix (e.g. `/dashboard/`).
|
|
13617
|
+
* A single leaf Route in the top-level router matches the prefix; the component
|
|
13618
|
+
* it renders owns its sub-router and all related routes internally.
|
|
13619
|
+
* Everything about the section — routes, structure, sub-pages — is co-located.
|
|
13620
|
+
* The component is not a reusable layout; it is the section.
|
|
12853
13621
|
*
|
|
12854
|
-
*
|
|
12855
|
-
*
|
|
13622
|
+
* Compared to the layout pattern, a dedicated section component is more powerful:
|
|
13623
|
+
* - Wrapper elements (chrome, nav, containers) are part of the component's render,
|
|
13624
|
+
* not injected via `children`, so a layout is not needed.
|
|
13625
|
+
* - Local state can be passed directly via `elementProps` to sub-pages. With the
|
|
13626
|
+
* layout pattern, sub-pages receive state only through `children` or context.
|
|
12856
13627
|
*
|
|
12857
|
-
*
|
|
12858
|
-
*
|
|
13628
|
+
* The layout pattern remains necessary when a shared prefix does not exist
|
|
13629
|
+
* (see "Layout pattern" above).
|
|
12859
13630
|
*
|
|
12860
|
-
*
|
|
12861
|
-
*
|
|
12862
|
-
*
|
|
12863
|
-
*
|
|
12864
|
-
* pour que react ne re-render pas tout
|
|
13631
|
+
* ```jsx
|
|
13632
|
+
* const DASHBOARD_SECTION_ROUTE = route("/dashboard/");
|
|
13633
|
+
* const DASHBOARD_HOME_ROUTE = route("/dashboard");
|
|
13634
|
+
* const DASHBOARD_POSTS_ROUTE = route("/dashboard/posts");
|
|
12865
13635
|
*
|
|
12866
|
-
*
|
|
13636
|
+
* // top-level router — only knows about the prefix
|
|
13637
|
+
* <Route route={DASHBOARD_SECTION_ROUTE} element={DashboardSection} />
|
|
12867
13638
|
*
|
|
13639
|
+
* // Dashboard owns the rest
|
|
13640
|
+
* const DashboardSection = () => {
|
|
13641
|
+
* const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
12868
13642
|
*
|
|
13643
|
+
* return <div
|
|
13644
|
+
* style="background: lightblue; padding: 10px;"
|
|
13645
|
+
* onClick={() => setSidebarOpen(o => !o)}
|
|
13646
|
+
* >
|
|
13647
|
+
* <Route>
|
|
13648
|
+
* <Route route={DASHBOARD_HOME_ROUTE} element={DashboardHomePage} elementProps={{ sidebarOpen}} />
|
|
13649
|
+
* <Route route={DASHBOARD_POSTS_ROUTE} element={DashboardPostsPage} elementProps={{ sidebarOpen }} />
|
|
13650
|
+
* <Route fallback element={DashboardNotFound} />
|
|
13651
|
+
* </Route>
|
|
13652
|
+
* </div>;
|
|
13653
|
+
* }
|
|
13654
|
+
* ```
|
|
12869
13655
|
*/
|
|
12870
13656
|
|
|
12871
13657
|
const debug$1 = (...args) => {
|
|
@@ -12974,11 +13760,7 @@ const RouteContainer = ({
|
|
|
12974
13760
|
return null;
|
|
12975
13761
|
}
|
|
12976
13762
|
if (element) {
|
|
12977
|
-
|
|
12978
|
-
return jsx(Element, {
|
|
12979
|
-
...elementProps,
|
|
12980
|
-
children: content
|
|
12981
|
-
});
|
|
13763
|
+
return h(element, elementProps, content);
|
|
12982
13764
|
}
|
|
12983
13765
|
return content;
|
|
12984
13766
|
};
|
|
@@ -13007,17 +13789,12 @@ const RouteLeafFallback = props => {
|
|
|
13007
13789
|
};
|
|
13008
13790
|
const RouteActive = ({
|
|
13009
13791
|
element,
|
|
13010
|
-
elementProps
|
|
13011
|
-
action
|
|
13792
|
+
elementProps
|
|
13012
13793
|
}) => {
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
}) : typeof element === "function" ? jsx(Element, {
|
|
13018
|
-
...elementProps
|
|
13019
|
-
}) : element;
|
|
13020
|
-
return renderedElement;
|
|
13794
|
+
if (typeof element === "function") {
|
|
13795
|
+
return h(element, elementProps);
|
|
13796
|
+
}
|
|
13797
|
+
return element;
|
|
13021
13798
|
};
|
|
13022
13799
|
|
|
13023
13800
|
const routeAction = (
|
|
@@ -13039,48 +13816,6 @@ const routeAction = (
|
|
|
13039
13816
|
options,
|
|
13040
13817
|
);
|
|
13041
13818
|
|
|
13042
|
-
// If the action is related to a store of items
|
|
13043
|
-
// we want to keep the url in sync with the item id of the store when it changes
|
|
13044
|
-
// This way whenever an item with a mutable id is updated, the url is also updated
|
|
13045
|
-
// (renaming a user while being on the user page)
|
|
13046
|
-
// In case the route does not use this param, then this code won't have an effect
|
|
13047
|
-
// To work the route params MUST use the same name (case sensitive) as the mutable id key
|
|
13048
|
-
// so "/users/:id/" with mutableIdKey "id" will work but "/users/:userId/" with mutableIdKey "id" won't work
|
|
13049
|
-
sync_url_and_item_id: {
|
|
13050
|
-
const { store } = actionBoundToRoute.meta;
|
|
13051
|
-
if (!store) {
|
|
13052
|
-
break sync_url_and_item_id;
|
|
13053
|
-
}
|
|
13054
|
-
const { uniqueKeys } = store;
|
|
13055
|
-
const [firstUniqueKey] = uniqueKeys;
|
|
13056
|
-
if (!firstUniqueKey) {
|
|
13057
|
-
break sync_url_and_item_id;
|
|
13058
|
-
}
|
|
13059
|
-
const uniqueValueSignal = computed(() => {
|
|
13060
|
-
const params = route.paramsSignal.value;
|
|
13061
|
-
const uniqueKeyValue = params[firstUniqueKey];
|
|
13062
|
-
return uniqueKeyValue;
|
|
13063
|
-
});
|
|
13064
|
-
const routeItemSignal = store.signalForUniqueKey(
|
|
13065
|
-
firstUniqueKey,
|
|
13066
|
-
uniqueValueSignal,
|
|
13067
|
-
);
|
|
13068
|
-
store.observeItemProperties(routeItemSignal, (propertyMutations) => {
|
|
13069
|
-
const uniquePropertyMutation = propertyMutations[firstUniqueKey];
|
|
13070
|
-
if (!uniquePropertyMutation) {
|
|
13071
|
-
return;
|
|
13072
|
-
}
|
|
13073
|
-
route.replaceParams(
|
|
13074
|
-
{
|
|
13075
|
-
[firstUniqueKey]: uniquePropertyMutation.newValue,
|
|
13076
|
-
},
|
|
13077
|
-
{
|
|
13078
|
-
callReason: `store item ${firstUniqueKey} change on ${route}`,
|
|
13079
|
-
},
|
|
13080
|
-
);
|
|
13081
|
-
});
|
|
13082
|
-
}
|
|
13083
|
-
|
|
13084
13819
|
return actionBoundToRoute;
|
|
13085
13820
|
};
|
|
13086
13821
|
|
|
@@ -15184,6 +15919,10 @@ const openCallout = (
|
|
|
15184
15919
|
`anchor element is not visually visible (${anchorVisuallyVisibleInfo.reason}) -> will be anchored to first visually visible ancestor`,
|
|
15185
15920
|
);
|
|
15186
15921
|
anchorElement = getFirstVisuallyVisibleAncestor(anchorElement);
|
|
15922
|
+
if (!anchorElement) {
|
|
15923
|
+
// anchorElement is not in the DOM anymore, fallback to body
|
|
15924
|
+
anchorElement = document.body;
|
|
15925
|
+
}
|
|
15187
15926
|
}
|
|
15188
15927
|
|
|
15189
15928
|
allowWheelThrough(calloutElement, anchorElement);
|
|
@@ -17377,8 +18116,15 @@ const installCustomConstraintValidation = (
|
|
|
17377
18116
|
closeElementValidationMessage("cleanup");
|
|
17378
18117
|
});
|
|
17379
18118
|
|
|
17380
|
-
const anchorElement =
|
|
17381
|
-
|
|
18119
|
+
const anchorElement = (() => {
|
|
18120
|
+
const base =
|
|
18121
|
+
failedConstraintInfo.target || elementReceivingValidationMessage;
|
|
18122
|
+
const renderedBy = base.getAttribute("data-rendered-by");
|
|
18123
|
+
if (renderedBy) {
|
|
18124
|
+
return base.closest(renderedBy) || base;
|
|
18125
|
+
}
|
|
18126
|
+
return base;
|
|
18127
|
+
})();
|
|
17382
18128
|
validationInterface.validationMessage = openCallout(
|
|
17383
18129
|
failedConstraintInfo.message,
|
|
17384
18130
|
{
|
|
@@ -19442,7 +20188,6 @@ const useExecuteAction = (
|
|
|
19442
20188
|
const resetErrorBoundary = useResetErrorBoundary();
|
|
19443
20189
|
useLayoutEffect(() => {
|
|
19444
20190
|
if (error) {
|
|
19445
|
-
error.__handled__ = true; // prevent jsenv from displaying it
|
|
19446
20191
|
throw error;
|
|
19447
20192
|
}
|
|
19448
20193
|
}, [error]);
|
|
@@ -20656,6 +21401,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
|
|
|
20656
21401
|
border-radius: inherit;
|
|
20657
21402
|
pointer-events: none;
|
|
20658
21403
|
}
|
|
21404
|
+
|
|
21405
|
+
& > img {
|
|
21406
|
+
border-radius: inherit;
|
|
21407
|
+
}
|
|
20659
21408
|
}
|
|
20660
21409
|
|
|
20661
21410
|
&[data-reveal-on-interaction] {
|
|
@@ -20746,13 +21495,10 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
|
|
|
20746
21495
|
--x-button-border-color: transparent;
|
|
20747
21496
|
}
|
|
20748
21497
|
}
|
|
20749
|
-
|
|
20750
|
-
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
}
|
|
20754
|
-
.navi_button > img {
|
|
20755
|
-
border-radius: inherit;
|
|
21498
|
+
/* Callout (info, warning, error) */
|
|
21499
|
+
&[data-callout] {
|
|
21500
|
+
--x-button-border-color: var(--callout-color);
|
|
21501
|
+
}
|
|
20756
21502
|
}
|
|
20757
21503
|
`;
|
|
20758
21504
|
const Button = props => {
|
|
@@ -24585,6 +25331,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
|
|
|
24585
25331
|
/* Callout (info, warning, error) */
|
|
24586
25332
|
&[data-callout] {
|
|
24587
25333
|
--x-border-color: var(--callout-color);
|
|
25334
|
+
--x-outline-color: var(--callout-color);
|
|
24588
25335
|
}
|
|
24589
25336
|
}
|
|
24590
25337
|
|
|
@@ -26001,219 +26748,6 @@ const filterTableSelection = (selection, predicate) => {
|
|
|
26001
26748
|
return matching;
|
|
26002
26749
|
};
|
|
26003
26750
|
|
|
26004
|
-
// https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
|
|
26005
|
-
// https://github.com/pacocoursey/use-descendants/tree/master
|
|
26006
|
-
|
|
26007
|
-
const createIsolatedItemTracker = () => {
|
|
26008
|
-
// Producer contexts (ref-based, no re-renders)
|
|
26009
|
-
const ProducerTrackerContext = createContext();
|
|
26010
|
-
const ProducerItemCountRefContext = createContext();
|
|
26011
|
-
const ProducerListRenderIdContext = createContext();
|
|
26012
|
-
|
|
26013
|
-
// Consumer contexts (state-based, re-renders)
|
|
26014
|
-
const ConsumerItemsContext = createContext();
|
|
26015
|
-
const useIsolatedItemTrackerProvider = () => {
|
|
26016
|
-
const itemsRef = useRef([]);
|
|
26017
|
-
const items = itemsRef.current;
|
|
26018
|
-
const itemCountRef = useRef();
|
|
26019
|
-
const pendingFlushRef = useRef(false);
|
|
26020
|
-
const producerIsRenderingRef = useRef(false);
|
|
26021
|
-
const itemTracker = useMemo(() => {
|
|
26022
|
-
const registerItem = (index, value) => {
|
|
26023
|
-
const hasValue = index in items;
|
|
26024
|
-
if (hasValue) {
|
|
26025
|
-
const currentValue = items[index];
|
|
26026
|
-
if (compareTwoJsValues(currentValue, value)) {
|
|
26027
|
-
return;
|
|
26028
|
-
}
|
|
26029
|
-
}
|
|
26030
|
-
items[index] = value;
|
|
26031
|
-
if (producerIsRenderingRef.current) {
|
|
26032
|
-
// Consumer will sync after producer render completes
|
|
26033
|
-
return;
|
|
26034
|
-
}
|
|
26035
|
-
pendingFlushRef.current = true;
|
|
26036
|
-
};
|
|
26037
|
-
const getProducerItem = itemIndex => {
|
|
26038
|
-
return items[itemIndex];
|
|
26039
|
-
};
|
|
26040
|
-
const ItemProducerProvider = ({
|
|
26041
|
-
children
|
|
26042
|
-
}) => {
|
|
26043
|
-
items.length = 0;
|
|
26044
|
-
itemCountRef.current = 0;
|
|
26045
|
-
pendingFlushRef.current = false;
|
|
26046
|
-
producerIsRenderingRef.current = true;
|
|
26047
|
-
const listRenderId = {};
|
|
26048
|
-
useLayoutEffect(() => {
|
|
26049
|
-
producerIsRenderingRef.current = false;
|
|
26050
|
-
});
|
|
26051
|
-
|
|
26052
|
-
// CRITICAL: Sync consumer state on subsequent renders
|
|
26053
|
-
const renderedOnce = useRef(false);
|
|
26054
|
-
useLayoutEffect(() => {
|
|
26055
|
-
if (!renderedOnce.current) {
|
|
26056
|
-
renderedOnce.current = true;
|
|
26057
|
-
return;
|
|
26058
|
-
}
|
|
26059
|
-
pendingFlushRef.current = true;
|
|
26060
|
-
itemTracker.flushToConsumers();
|
|
26061
|
-
}, [listRenderId]);
|
|
26062
|
-
return jsx(ProducerItemCountRefContext.Provider, {
|
|
26063
|
-
value: itemCountRef,
|
|
26064
|
-
children: jsx(ProducerListRenderIdContext.Provider, {
|
|
26065
|
-
value: listRenderId,
|
|
26066
|
-
children: jsx(ProducerTrackerContext.Provider, {
|
|
26067
|
-
value: itemTracker,
|
|
26068
|
-
children: children
|
|
26069
|
-
})
|
|
26070
|
-
})
|
|
26071
|
-
});
|
|
26072
|
-
};
|
|
26073
|
-
const ItemConsumerProvider = ({
|
|
26074
|
-
children
|
|
26075
|
-
}) => {
|
|
26076
|
-
const [consumerItems, setConsumerItems] = useState(items);
|
|
26077
|
-
const flushToConsumers = () => {
|
|
26078
|
-
if (!pendingFlushRef.current) {
|
|
26079
|
-
return;
|
|
26080
|
-
}
|
|
26081
|
-
const itemsCopy = [...items];
|
|
26082
|
-
pendingFlushRef.current = false;
|
|
26083
|
-
setConsumerItems(itemsCopy);
|
|
26084
|
-
};
|
|
26085
|
-
itemTracker.flushToConsumers = flushToConsumers;
|
|
26086
|
-
useLayoutEffect(() => {
|
|
26087
|
-
flushToConsumers();
|
|
26088
|
-
});
|
|
26089
|
-
return jsx(ConsumerItemsContext.Provider, {
|
|
26090
|
-
value: consumerItems,
|
|
26091
|
-
children: children
|
|
26092
|
-
});
|
|
26093
|
-
};
|
|
26094
|
-
return {
|
|
26095
|
-
pendingFlushRef,
|
|
26096
|
-
registerItem,
|
|
26097
|
-
getProducerItem,
|
|
26098
|
-
ItemProducerProvider,
|
|
26099
|
-
ItemConsumerProvider
|
|
26100
|
-
};
|
|
26101
|
-
}, []);
|
|
26102
|
-
const {
|
|
26103
|
-
ItemProducerProvider,
|
|
26104
|
-
ItemConsumerProvider
|
|
26105
|
-
} = itemTracker;
|
|
26106
|
-
return [ItemProducerProvider, ItemConsumerProvider, items];
|
|
26107
|
-
};
|
|
26108
|
-
|
|
26109
|
-
// Hook for producers to register items (ref-based, no re-renders)
|
|
26110
|
-
const useTrackIsolatedItem = data => {
|
|
26111
|
-
const listRenderId = useContext(ProducerListRenderIdContext);
|
|
26112
|
-
const itemCountRef = useContext(ProducerItemCountRefContext);
|
|
26113
|
-
const itemTracker = useContext(ProducerTrackerContext);
|
|
26114
|
-
const listRenderIdRef = useRef();
|
|
26115
|
-
const itemIndexRef = useRef();
|
|
26116
|
-
const dataRef = useRef();
|
|
26117
|
-
const prevListRenderId = listRenderIdRef.current;
|
|
26118
|
-
useLayoutEffect(() => {
|
|
26119
|
-
if (itemTracker.pendingFlushRef.current) {
|
|
26120
|
-
itemTracker.flushToConsumers();
|
|
26121
|
-
}
|
|
26122
|
-
});
|
|
26123
|
-
if (prevListRenderId === listRenderId) {
|
|
26124
|
-
const itemIndex = itemIndexRef.current;
|
|
26125
|
-
itemTracker.registerItem(itemIndex, data);
|
|
26126
|
-
dataRef.current = data;
|
|
26127
|
-
return itemIndex;
|
|
26128
|
-
}
|
|
26129
|
-
listRenderIdRef.current = listRenderId;
|
|
26130
|
-
const itemCount = itemCountRef.current;
|
|
26131
|
-
const itemIndex = itemCount;
|
|
26132
|
-
itemCountRef.current = itemIndex + 1;
|
|
26133
|
-
itemIndexRef.current = itemIndex;
|
|
26134
|
-
dataRef.current = data;
|
|
26135
|
-
itemTracker.registerItem(itemIndex, data);
|
|
26136
|
-
return itemIndex;
|
|
26137
|
-
};
|
|
26138
|
-
const useTrackedIsolatedItem = itemIndex => {
|
|
26139
|
-
const items = useTrackedIsolatedItems();
|
|
26140
|
-
const item = items[itemIndex];
|
|
26141
|
-
return item;
|
|
26142
|
-
};
|
|
26143
|
-
|
|
26144
|
-
// Hooks for consumers to read items (state-based, re-renders)
|
|
26145
|
-
const useTrackedIsolatedItems = () => {
|
|
26146
|
-
const consumerItems = useContext(ConsumerItemsContext);
|
|
26147
|
-
if (!consumerItems) {
|
|
26148
|
-
throw new Error("useTrackedIsolatedItems must be used within <ItemConsumerProvider />");
|
|
26149
|
-
}
|
|
26150
|
-
return consumerItems;
|
|
26151
|
-
};
|
|
26152
|
-
return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
|
|
26153
|
-
};
|
|
26154
|
-
|
|
26155
|
-
const createItemTracker = () => {
|
|
26156
|
-
const ItemTrackerContext = createContext();
|
|
26157
|
-
const useItemTrackerProvider = () => {
|
|
26158
|
-
const itemsRef = useRef([]);
|
|
26159
|
-
const items = itemsRef.current;
|
|
26160
|
-
const itemCountRef = useRef(0);
|
|
26161
|
-
const tracker = useMemo(() => {
|
|
26162
|
-
const ItemTrackerProvider = ({
|
|
26163
|
-
children
|
|
26164
|
-
}) => {
|
|
26165
|
-
// Reset on each render to start fresh
|
|
26166
|
-
tracker.reset();
|
|
26167
|
-
return jsx(ItemTrackerContext.Provider, {
|
|
26168
|
-
value: tracker,
|
|
26169
|
-
children: children
|
|
26170
|
-
});
|
|
26171
|
-
};
|
|
26172
|
-
ItemTrackerProvider.items = items;
|
|
26173
|
-
return {
|
|
26174
|
-
ItemTrackerProvider,
|
|
26175
|
-
items,
|
|
26176
|
-
registerItem: data => {
|
|
26177
|
-
const index = itemCountRef.current++;
|
|
26178
|
-
items[index] = data;
|
|
26179
|
-
return index;
|
|
26180
|
-
},
|
|
26181
|
-
getItem: index => {
|
|
26182
|
-
return items[index];
|
|
26183
|
-
},
|
|
26184
|
-
getAllItems: () => {
|
|
26185
|
-
return items;
|
|
26186
|
-
},
|
|
26187
|
-
reset: () => {
|
|
26188
|
-
items.length = 0;
|
|
26189
|
-
itemCountRef.current = 0;
|
|
26190
|
-
}
|
|
26191
|
-
};
|
|
26192
|
-
}, []);
|
|
26193
|
-
return tracker.ItemTrackerProvider;
|
|
26194
|
-
};
|
|
26195
|
-
const useTrackItem = data => {
|
|
26196
|
-
const tracker = useContext(ItemTrackerContext);
|
|
26197
|
-
if (!tracker) {
|
|
26198
|
-
throw new Error("useTrackItem must be used within SimpleItemTrackerProvider");
|
|
26199
|
-
}
|
|
26200
|
-
return tracker.registerItem(data);
|
|
26201
|
-
};
|
|
26202
|
-
const useTrackedItem = index => {
|
|
26203
|
-
const trackedItems = useTrackedItems();
|
|
26204
|
-
const item = trackedItems[index];
|
|
26205
|
-
return item;
|
|
26206
|
-
};
|
|
26207
|
-
const useTrackedItems = () => {
|
|
26208
|
-
const tracker = useContext(ItemTrackerContext);
|
|
26209
|
-
if (!tracker) {
|
|
26210
|
-
throw new Error("useTrackedItems must be used within SimpleItemTrackerProvider");
|
|
26211
|
-
}
|
|
26212
|
-
return tracker.items;
|
|
26213
|
-
};
|
|
26214
|
-
return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
|
|
26215
|
-
};
|
|
26216
|
-
|
|
26217
26751
|
const Z_INDEX_EDITING = 1; /* To go above neighbours, but should not be too big to stay under the sticky cells */
|
|
26218
26752
|
|
|
26219
26753
|
/* needed because cell uses position:relative, sticky must win even if before in DOM order */
|
|
@@ -26669,18 +27203,205 @@ const createTableAttributeSync = (table, tableClone) => {
|
|
|
26669
27203
|
return observer;
|
|
26670
27204
|
};
|
|
26671
27205
|
|
|
26672
|
-
|
|
27206
|
+
// https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
|
|
27207
|
+
// https://github.com/pacocoursey/use-descendants/tree/master
|
|
26673
27208
|
|
|
26674
|
-
const
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26678
|
-
|
|
26679
|
-
|
|
26680
|
-
|
|
26681
|
-
|
|
26682
|
-
|
|
26683
|
-
|
|
27209
|
+
const createIsolatedItemTracker = () => {
|
|
27210
|
+
// Producer contexts (ref-based, no re-renders)
|
|
27211
|
+
const ProducerTrackerContext = createContext();
|
|
27212
|
+
const ProducerItemCountRefContext = createContext();
|
|
27213
|
+
const ProducerListRenderIdContext = createContext();
|
|
27214
|
+
|
|
27215
|
+
// Consumer contexts (state-based, re-renders)
|
|
27216
|
+
const ConsumerItemsContext = createContext();
|
|
27217
|
+
const useIsolatedItemTrackerProvider = () => {
|
|
27218
|
+
const itemsRef = useRef([]);
|
|
27219
|
+
const items = itemsRef.current;
|
|
27220
|
+
const itemCountRef = useRef();
|
|
27221
|
+
const itemTracker = useMemo(() => {
|
|
27222
|
+
// Snapshot taken by FlushSentinel after all producer children rendered.
|
|
27223
|
+
// Consumers read from this — always up-to-date within the same render pass.
|
|
27224
|
+
const itemsSnapshotRef = {
|
|
27225
|
+
current: items
|
|
27226
|
+
};
|
|
27227
|
+
const registerItem = (index, value) => {
|
|
27228
|
+
const hasValue = index in items;
|
|
27229
|
+
if (hasValue) {
|
|
27230
|
+
const currentValue = items[index];
|
|
27231
|
+
if (compareTwoJsValues(currentValue, value)) {
|
|
27232
|
+
return;
|
|
27233
|
+
}
|
|
27234
|
+
}
|
|
27235
|
+
items[index] = value;
|
|
27236
|
+
};
|
|
27237
|
+
const getProducerItem = itemIndex => {
|
|
27238
|
+
return items[itemIndex];
|
|
27239
|
+
};
|
|
27240
|
+
const ItemProducerProvider = ({
|
|
27241
|
+
children
|
|
27242
|
+
}) => {
|
|
27243
|
+
items.length = 0;
|
|
27244
|
+
itemCountRef.current = 0;
|
|
27245
|
+
const listRenderId = {};
|
|
27246
|
+
return jsx(ProducerItemCountRefContext.Provider, {
|
|
27247
|
+
value: itemCountRef,
|
|
27248
|
+
children: jsx(ProducerListRenderIdContext.Provider, {
|
|
27249
|
+
value: listRenderId,
|
|
27250
|
+
children: jsxs(ProducerTrackerContext.Provider, {
|
|
27251
|
+
value: itemTracker,
|
|
27252
|
+
children: [children, jsx(FlushSentinel, {})]
|
|
27253
|
+
})
|
|
27254
|
+
})
|
|
27255
|
+
});
|
|
27256
|
+
};
|
|
27257
|
+
|
|
27258
|
+
// Renders after all producer children (e.g. <Col>) have registered their
|
|
27259
|
+
// items. Taking a snapshot here guarantees the consumer sees the correct
|
|
27260
|
+
// item list within the same render pass, without any heuristic.
|
|
27261
|
+
const FlushSentinel = () => {
|
|
27262
|
+
itemsSnapshotRef.current = items;
|
|
27263
|
+
return null;
|
|
27264
|
+
};
|
|
27265
|
+
const ItemConsumerProvider = ({
|
|
27266
|
+
children
|
|
27267
|
+
}) => {
|
|
27268
|
+
// FlushSentinel (last child of ItemProducerProvider) already set
|
|
27269
|
+
// itemsSnapshotRef.current to the up-to-date items array before any
|
|
27270
|
+
// consumer rendered. Reading from the snapshot is always correct.
|
|
27271
|
+
return jsx(ConsumerItemsContext.Provider, {
|
|
27272
|
+
value: itemsSnapshotRef.current,
|
|
27273
|
+
children: children
|
|
27274
|
+
});
|
|
27275
|
+
};
|
|
27276
|
+
return {
|
|
27277
|
+
registerItem,
|
|
27278
|
+
getProducerItem,
|
|
27279
|
+
ItemProducerProvider,
|
|
27280
|
+
ItemConsumerProvider
|
|
27281
|
+
};
|
|
27282
|
+
}, []);
|
|
27283
|
+
const {
|
|
27284
|
+
ItemProducerProvider,
|
|
27285
|
+
ItemConsumerProvider
|
|
27286
|
+
} = itemTracker;
|
|
27287
|
+
return [ItemProducerProvider, ItemConsumerProvider, items];
|
|
27288
|
+
};
|
|
27289
|
+
|
|
27290
|
+
// Hook for producers to register items (ref-based, no re-renders)
|
|
27291
|
+
const useTrackIsolatedItem = data => {
|
|
27292
|
+
const listRenderId = useContext(ProducerListRenderIdContext);
|
|
27293
|
+
const itemCountRef = useContext(ProducerItemCountRefContext);
|
|
27294
|
+
const itemTracker = useContext(ProducerTrackerContext);
|
|
27295
|
+
const listRenderIdRef = useRef();
|
|
27296
|
+
const itemIndexRef = useRef();
|
|
27297
|
+
const dataRef = useRef();
|
|
27298
|
+
const prevListRenderId = listRenderIdRef.current;
|
|
27299
|
+
if (prevListRenderId === listRenderId) {
|
|
27300
|
+
const itemIndex = itemIndexRef.current;
|
|
27301
|
+
itemTracker.registerItem(itemIndex, data);
|
|
27302
|
+
dataRef.current = data;
|
|
27303
|
+
return itemIndex;
|
|
27304
|
+
}
|
|
27305
|
+
listRenderIdRef.current = listRenderId;
|
|
27306
|
+
const itemCount = itemCountRef.current;
|
|
27307
|
+
const itemIndex = itemCount;
|
|
27308
|
+
itemCountRef.current = itemIndex + 1;
|
|
27309
|
+
itemIndexRef.current = itemIndex;
|
|
27310
|
+
dataRef.current = data;
|
|
27311
|
+
itemTracker.registerItem(itemIndex, data);
|
|
27312
|
+
return itemIndex;
|
|
27313
|
+
};
|
|
27314
|
+
const useTrackedIsolatedItem = itemIndex => {
|
|
27315
|
+
const items = useTrackedIsolatedItems();
|
|
27316
|
+
const item = items[itemIndex];
|
|
27317
|
+
return item;
|
|
27318
|
+
};
|
|
27319
|
+
|
|
27320
|
+
// Hooks for consumers to read items (state-based, re-renders)
|
|
27321
|
+
const useTrackedIsolatedItems = () => {
|
|
27322
|
+
const consumerItems = useContext(ConsumerItemsContext);
|
|
27323
|
+
if (!consumerItems) {
|
|
27324
|
+
throw new Error("useTrackedIsolatedItems must be used within <ItemConsumerProvider />");
|
|
27325
|
+
}
|
|
27326
|
+
return consumerItems;
|
|
27327
|
+
};
|
|
27328
|
+
return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
|
|
27329
|
+
};
|
|
27330
|
+
|
|
27331
|
+
const createItemTracker = () => {
|
|
27332
|
+
const ItemTrackerContext = createContext();
|
|
27333
|
+
const useItemTrackerProvider = () => {
|
|
27334
|
+
const itemsRef = useRef([]);
|
|
27335
|
+
const items = itemsRef.current;
|
|
27336
|
+
const itemCountRef = useRef(0);
|
|
27337
|
+
const tracker = useMemo(() => {
|
|
27338
|
+
const ItemTrackerProvider = ({
|
|
27339
|
+
children
|
|
27340
|
+
}) => {
|
|
27341
|
+
// Reset on each render to start fresh
|
|
27342
|
+
tracker.reset();
|
|
27343
|
+
return jsx(ItemTrackerContext.Provider, {
|
|
27344
|
+
value: tracker,
|
|
27345
|
+
children: children
|
|
27346
|
+
});
|
|
27347
|
+
};
|
|
27348
|
+
ItemTrackerProvider.items = items;
|
|
27349
|
+
return {
|
|
27350
|
+
ItemTrackerProvider,
|
|
27351
|
+
items,
|
|
27352
|
+
registerItem: data => {
|
|
27353
|
+
const index = itemCountRef.current++;
|
|
27354
|
+
items[index] = data;
|
|
27355
|
+
return index;
|
|
27356
|
+
},
|
|
27357
|
+
getItem: index => {
|
|
27358
|
+
return items[index];
|
|
27359
|
+
},
|
|
27360
|
+
getAllItems: () => {
|
|
27361
|
+
return items;
|
|
27362
|
+
},
|
|
27363
|
+
reset: () => {
|
|
27364
|
+
items.length = 0;
|
|
27365
|
+
itemCountRef.current = 0;
|
|
27366
|
+
}
|
|
27367
|
+
};
|
|
27368
|
+
}, []);
|
|
27369
|
+
return tracker.ItemTrackerProvider;
|
|
27370
|
+
};
|
|
27371
|
+
const useTrackItem = data => {
|
|
27372
|
+
const tracker = useContext(ItemTrackerContext);
|
|
27373
|
+
if (!tracker) {
|
|
27374
|
+
throw new Error("useTrackItem must be used within SimpleItemTrackerProvider");
|
|
27375
|
+
}
|
|
27376
|
+
return tracker.registerItem(data);
|
|
27377
|
+
};
|
|
27378
|
+
const useTrackedItem = index => {
|
|
27379
|
+
const trackedItems = useTrackedItems();
|
|
27380
|
+
const item = trackedItems[index];
|
|
27381
|
+
return item;
|
|
27382
|
+
};
|
|
27383
|
+
const useTrackedItems = () => {
|
|
27384
|
+
const tracker = useContext(ItemTrackerContext);
|
|
27385
|
+
if (!tracker) {
|
|
27386
|
+
throw new Error("useTrackedItems must be used within SimpleItemTrackerProvider");
|
|
27387
|
+
}
|
|
27388
|
+
return tracker.items;
|
|
27389
|
+
};
|
|
27390
|
+
return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
|
|
27391
|
+
};
|
|
27392
|
+
|
|
27393
|
+
const TableSizeContext = createContext();
|
|
27394
|
+
|
|
27395
|
+
const useTableSizeContextValue = ({
|
|
27396
|
+
onColumnSizeChange,
|
|
27397
|
+
onRowSizeChange,
|
|
27398
|
+
columns,
|
|
27399
|
+
rows,
|
|
27400
|
+
columnResizerRef,
|
|
27401
|
+
rowResizerRef,
|
|
27402
|
+
}) => {
|
|
27403
|
+
onColumnSizeChange = useStableCallback(onColumnSizeChange);
|
|
27404
|
+
onRowSizeChange = useStableCallback(onRowSizeChange);
|
|
26684
27405
|
|
|
26685
27406
|
const tableSizeContextValue = useMemo(() => {
|
|
26686
27407
|
const onColumnSizeChangeWithColumn = onColumnSizeChange
|
|
@@ -28824,6 +29545,8 @@ const TableCell = props => {
|
|
|
28824
29545
|
onClick,
|
|
28825
29546
|
action,
|
|
28826
29547
|
name,
|
|
29548
|
+
children,
|
|
29549
|
+
value = children,
|
|
28827
29550
|
valueSignal,
|
|
28828
29551
|
// appeareance
|
|
28829
29552
|
style,
|
|
@@ -28831,8 +29554,7 @@ const TableCell = props => {
|
|
|
28831
29554
|
bold,
|
|
28832
29555
|
selfAlignX = column.selfAlignX,
|
|
28833
29556
|
selfAlignY = column.selfAlignY,
|
|
28834
|
-
backgroundColor = column.backgroundColor || row.backgroundColor
|
|
28835
|
-
children
|
|
29557
|
+
backgroundColor = column.backgroundColor || row.backgroundColor
|
|
28836
29558
|
} = props;
|
|
28837
29559
|
const ref = props.ref || cellDefaultRef;
|
|
28838
29560
|
const isFirstRow = rowIndex === 0;
|
|
@@ -29005,9 +29727,9 @@ const TableCell = props => {
|
|
|
29005
29727
|
children: [editable ? jsx(Editable, {
|
|
29006
29728
|
editing: editing,
|
|
29007
29729
|
onEditEnd: stopEditing,
|
|
29008
|
-
value: children,
|
|
29009
29730
|
action: action,
|
|
29010
29731
|
name: name,
|
|
29732
|
+
value: value,
|
|
29011
29733
|
valueSignal: valueSignal,
|
|
29012
29734
|
height: "100%",
|
|
29013
29735
|
width: "100%",
|
|
@@ -29178,6 +29900,9 @@ const createColumnOrdering = (columnIdKey, setOrderedColumnIds) => {
|
|
|
29178
29900
|
const stableId = stableIdByExternalIdMap.get(id);
|
|
29179
29901
|
stableIdByExternalIdMap.delete(id);
|
|
29180
29902
|
externalIdByStableIdMap.delete(stableId);
|
|
29903
|
+
currentOrderedColumnIds = currentOrderedColumnIds.filter(
|
|
29904
|
+
(orderedId) => orderedId !== id,
|
|
29905
|
+
);
|
|
29181
29906
|
}
|
|
29182
29907
|
for (const id of purelyAdded) {
|
|
29183
29908
|
const stableId = nextStableId++;
|
|
@@ -30642,6 +31367,39 @@ const Paragraph = props => {
|
|
|
30642
31367
|
});
|
|
30643
31368
|
};
|
|
30644
31369
|
|
|
31370
|
+
installImportMetaCss(import.meta);import.meta.css = /* css */`
|
|
31371
|
+
.navi_text_placeholder {
|
|
31372
|
+
display: inline-block;
|
|
31373
|
+
width: 100%;
|
|
31374
|
+
height: 1em;
|
|
31375
|
+
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
|
31376
|
+
background-size: 200% 100%;
|
|
31377
|
+
border-radius: 4px;
|
|
31378
|
+
|
|
31379
|
+
&[data-loading] {
|
|
31380
|
+
animation: shimmer 1.2s infinite;
|
|
31381
|
+
}
|
|
31382
|
+
}
|
|
31383
|
+
@keyframes shimmer {
|
|
31384
|
+
0% {
|
|
31385
|
+
background-position: 200% 0;
|
|
31386
|
+
}
|
|
31387
|
+
100% {
|
|
31388
|
+
background-position: -200% 0;
|
|
31389
|
+
}
|
|
31390
|
+
}
|
|
31391
|
+
`;
|
|
31392
|
+
const TextPlaceholder = ({
|
|
31393
|
+
loading,
|
|
31394
|
+
...props
|
|
31395
|
+
}) => {
|
|
31396
|
+
return jsx(Box, {
|
|
31397
|
+
...props,
|
|
31398
|
+
baseClassName: "navi_text_placeholder",
|
|
31399
|
+
"data-loading": loading ? "" : undefined
|
|
31400
|
+
});
|
|
31401
|
+
};
|
|
31402
|
+
|
|
30645
31403
|
const Image = props => {
|
|
30646
31404
|
return jsx(Box, {
|
|
30647
31405
|
...props,
|
|
@@ -30917,6 +31675,200 @@ const ViewportLayout = props => {
|
|
|
30917
31675
|
});
|
|
30918
31676
|
};
|
|
30919
31677
|
|
|
31678
|
+
installImportMetaCss(import.meta);import.meta.css = /* css */`
|
|
31679
|
+
@layer navi {
|
|
31680
|
+
.navi_side_panel {
|
|
31681
|
+
--side-panel-width: 400px;
|
|
31682
|
+
--side-panel-background: white;
|
|
31683
|
+
--side-panel-shadow: -4px 0 24px rgba(0, 0, 0, 0.18);
|
|
31684
|
+
--side-panel-animation-duration: 250ms;
|
|
31685
|
+
}
|
|
31686
|
+
}
|
|
31687
|
+
|
|
31688
|
+
.navi_side_panel {
|
|
31689
|
+
position: fixed;
|
|
31690
|
+
top: 0;
|
|
31691
|
+
right: 0;
|
|
31692
|
+
bottom: 0;
|
|
31693
|
+
z-index: 1000;
|
|
31694
|
+
pointer-events: none;
|
|
31695
|
+
|
|
31696
|
+
.navi_side_panel_overlay {
|
|
31697
|
+
position: fixed;
|
|
31698
|
+
inset: 0;
|
|
31699
|
+
background: rgba(0, 0, 0, 0.3);
|
|
31700
|
+
pointer-events: auto;
|
|
31701
|
+
}
|
|
31702
|
+
|
|
31703
|
+
.navi_side_panel_dialog {
|
|
31704
|
+
position: absolute;
|
|
31705
|
+
top: 0;
|
|
31706
|
+
right: 0;
|
|
31707
|
+
bottom: 0;
|
|
31708
|
+
width: var(--side-panel-width);
|
|
31709
|
+
background: var(--side-panel-background);
|
|
31710
|
+
outline: none;
|
|
31711
|
+
box-shadow: var(--side-panel-shadow);
|
|
31712
|
+
animation-duration: var(--side-panel-animation-duration);
|
|
31713
|
+
animation-timing-function: ease-out;
|
|
31714
|
+
animation-fill-mode: both;
|
|
31715
|
+
pointer-events: auto;
|
|
31716
|
+
overflow-y: auto;
|
|
31717
|
+
}
|
|
31718
|
+
|
|
31719
|
+
&[data-opening] {
|
|
31720
|
+
.navi_side_panel_dialog {
|
|
31721
|
+
animation-name: navi_side_panel_slide_in;
|
|
31722
|
+
}
|
|
31723
|
+
}
|
|
31724
|
+
|
|
31725
|
+
&[data-closing] {
|
|
31726
|
+
.navi_side_panel_dialog {
|
|
31727
|
+
animation-name: navi_side_panel_slide_out;
|
|
31728
|
+
}
|
|
31729
|
+
}
|
|
31730
|
+
}
|
|
31731
|
+
|
|
31732
|
+
.navi_side_panel_close_button {
|
|
31733
|
+
position: absolute;
|
|
31734
|
+
top: 12px;
|
|
31735
|
+
right: 12px;
|
|
31736
|
+
|
|
31737
|
+
z-index: 1; /* For some reason required to interact properly with the button */
|
|
31738
|
+
display: flex;
|
|
31739
|
+
width: 28px;
|
|
31740
|
+
height: 28px;
|
|
31741
|
+
padding: 0;
|
|
31742
|
+
align-items: center;
|
|
31743
|
+
justify-content: center;
|
|
31744
|
+
color: #6c757d;
|
|
31745
|
+
font-size: 18px;
|
|
31746
|
+
line-height: 1;
|
|
31747
|
+
background: transparent;
|
|
31748
|
+
border: none;
|
|
31749
|
+
border-radius: 4px;
|
|
31750
|
+
cursor: pointer;
|
|
31751
|
+
|
|
31752
|
+
&:hover {
|
|
31753
|
+
color: #212529;
|
|
31754
|
+
background: #f0f0f0;
|
|
31755
|
+
}
|
|
31756
|
+
}
|
|
31757
|
+
|
|
31758
|
+
@keyframes navi_side_panel_slide_in {
|
|
31759
|
+
from {
|
|
31760
|
+
transform: translateX(100%);
|
|
31761
|
+
}
|
|
31762
|
+
to {
|
|
31763
|
+
transform: translateX(0);
|
|
31764
|
+
}
|
|
31765
|
+
}
|
|
31766
|
+
|
|
31767
|
+
@keyframes navi_side_panel_slide_out {
|
|
31768
|
+
from {
|
|
31769
|
+
transform: translateX(0);
|
|
31770
|
+
}
|
|
31771
|
+
to {
|
|
31772
|
+
transform: translateX(100%);
|
|
31773
|
+
}
|
|
31774
|
+
}
|
|
31775
|
+
`;
|
|
31776
|
+
const SidePanelCloseContext = createContext(null);
|
|
31777
|
+
const useSidePanelClose = () => useContext(SidePanelCloseContext);
|
|
31778
|
+
const SidePanelStyleCSSVars = {
|
|
31779
|
+
width: "--side-panel-width"
|
|
31780
|
+
};
|
|
31781
|
+
const SidePanel = ({
|
|
31782
|
+
isOpen,
|
|
31783
|
+
onClose,
|
|
31784
|
+
children,
|
|
31785
|
+
closeOnClickOutside = false,
|
|
31786
|
+
hideCloseButton = false,
|
|
31787
|
+
width,
|
|
31788
|
+
...rest
|
|
31789
|
+
}) => {
|
|
31790
|
+
onClose = useStableCallback(onClose);
|
|
31791
|
+
const panelDialogRef = useRef(null);
|
|
31792
|
+
const [phase, setPhase] = useState(isOpen ? "open" : "closed");
|
|
31793
|
+
const previousFocusRef = useRef(null);
|
|
31794
|
+
const isMountedRef = useRef(false);
|
|
31795
|
+
useLayoutEffect(() => {
|
|
31796
|
+
if (!isMountedRef.current) {
|
|
31797
|
+
isMountedRef.current = true;
|
|
31798
|
+
return;
|
|
31799
|
+
}
|
|
31800
|
+
if (isOpen) {
|
|
31801
|
+
setPhase("opening");
|
|
31802
|
+
} else if (phase !== "closed") {
|
|
31803
|
+
setPhase("closing");
|
|
31804
|
+
}
|
|
31805
|
+
}, [isOpen]);
|
|
31806
|
+
useLayoutEffect(() => {
|
|
31807
|
+
if (phase === "opening" && panelDialogRef.current) {
|
|
31808
|
+
previousFocusRef.current = document.activeElement;
|
|
31809
|
+
panelDialogRef.current.focus();
|
|
31810
|
+
}
|
|
31811
|
+
}, [phase]);
|
|
31812
|
+
useKeyboardShortcuts(panelDialogRef, [{
|
|
31813
|
+
key: "escape",
|
|
31814
|
+
handler: () => {
|
|
31815
|
+
onClose();
|
|
31816
|
+
return true;
|
|
31817
|
+
}
|
|
31818
|
+
}]);
|
|
31819
|
+
if (phase === "closed") {
|
|
31820
|
+
return null;
|
|
31821
|
+
}
|
|
31822
|
+
const onAnimationEnd = () => {
|
|
31823
|
+
if (phase === "opening") {
|
|
31824
|
+
setPhase("open");
|
|
31825
|
+
} else if (phase === "closing") {
|
|
31826
|
+
setPhase("closed");
|
|
31827
|
+
const prev = previousFocusRef.current;
|
|
31828
|
+
if (prev && document.contains(prev)) {
|
|
31829
|
+
prev.focus({
|
|
31830
|
+
preventScroll: true
|
|
31831
|
+
});
|
|
31832
|
+
}
|
|
31833
|
+
previousFocusRef.current = null;
|
|
31834
|
+
}
|
|
31835
|
+
};
|
|
31836
|
+
return createPortal(jsx(SidePanelCloseContext.Provider, {
|
|
31837
|
+
value: onClose,
|
|
31838
|
+
children: jsxs(Box, {
|
|
31839
|
+
baseClassName: "navi_side_panel",
|
|
31840
|
+
propsCSSVars: SidePanelStyleCSSVars,
|
|
31841
|
+
width: width,
|
|
31842
|
+
"data-opening": phase === "opening" ? "" : undefined,
|
|
31843
|
+
"data-closing": phase === "closing" ? "" : undefined,
|
|
31844
|
+
...rest,
|
|
31845
|
+
children: [closeOnClickOutside && jsx("div", {
|
|
31846
|
+
className: "navi_side_panel_overlay",
|
|
31847
|
+
onClick: e => {
|
|
31848
|
+
onClose(e);
|
|
31849
|
+
}
|
|
31850
|
+
}), jsxs(Box, {
|
|
31851
|
+
ref: panelDialogRef,
|
|
31852
|
+
baseClassName: "navi_side_panel_dialog",
|
|
31853
|
+
tabIndex: -1,
|
|
31854
|
+
role: closeOnClickOutside ? "dialog" : "complementary",
|
|
31855
|
+
"aria-modal": closeOnClickOutside ? "true" : undefined,
|
|
31856
|
+
onAnimationEnd: onAnimationEnd,
|
|
31857
|
+
children: [!hideCloseButton && jsx(NaviSidePanelCloseButton, {}), children]
|
|
31858
|
+
})]
|
|
31859
|
+
})
|
|
31860
|
+
}), document.body);
|
|
31861
|
+
};
|
|
31862
|
+
const NaviSidePanelCloseButton = () => {
|
|
31863
|
+
const sidePanelClose = useSidePanelClose();
|
|
31864
|
+
return jsx("button", {
|
|
31865
|
+
className: "navi_side_panel_close_button",
|
|
31866
|
+
"aria-label": "Close panel",
|
|
31867
|
+
onClick: sidePanelClose,
|
|
31868
|
+
children: "\xD7"
|
|
31869
|
+
});
|
|
31870
|
+
};
|
|
31871
|
+
|
|
30920
31872
|
/*
|
|
30921
31873
|
* - Usage
|
|
30922
31874
|
* useEffect(() => {
|
|
@@ -31056,5 +32008,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
31056
32008
|
})
|
|
31057
32009
|
});
|
|
31058
32010
|
|
|
31059
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, MessageBox, Meter, Nav, Paragraph, Quantity, QuantityIntl, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue,
|
|
32011
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, Loading, MessageBox, Meter, Nav, Paragraph, Quantity, QuantityIntl, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextPlaceholder, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
|
|
31060
32012
|
//# sourceMappingURL=jsenv_navi.js.map
|