@jsenv/navi 0.29.43 → 0.29.44
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 +218 -46
- package/dist/jsenv_navi.js.map +21 -13
- package/docs/AI_INSTRUCTIONS.md +9 -2
- package/docs/control_group.md +5 -0
- package/docs/control_object.md +133 -0
- package/docs/control_value.md +19 -0
- package/docs/form_changed.md +2 -0
- package/docs/list_refresh.md +61 -12
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -23758,9 +23758,9 @@ const useUIGroupStateController = (
|
|
|
23758
23758
|
const delegatedChildrenRef = useRef(new Map());
|
|
23759
23759
|
|
|
23760
23760
|
const groupIsRenderingRef = useRef(false);
|
|
23761
|
-
const pendingChangeRef = useRef(
|
|
23761
|
+
const pendingChangeRef = useRef(null);
|
|
23762
23762
|
groupIsRenderingRef.current = true;
|
|
23763
|
-
pendingChangeRef.current =
|
|
23763
|
+
pendingChangeRef.current = null;
|
|
23764
23764
|
|
|
23765
23765
|
const isMonitoringChild = (childUIStateController) => {
|
|
23766
23766
|
if (childUIStateController.isProxy) return false;
|
|
@@ -23792,7 +23792,18 @@ const useUIGroupStateController = (
|
|
|
23792
23792
|
// signals/pubsub without needing external refs.
|
|
23793
23793
|
const onChange = (e, { notifyExternal }) => {
|
|
23794
23794
|
if (groupIsRenderingRef.current) {
|
|
23795
|
-
|
|
23795
|
+
// Held until the layout effect below, WITH what it asked for: a child
|
|
23796
|
+
// whose bound signal was written from the outside changes during the
|
|
23797
|
+
// render that follows, and replaying that as a mount sync is what
|
|
23798
|
+
// makes a group silently drift — its own state comes up to date while
|
|
23799
|
+
// the form around it is never told anything moved. A real change
|
|
23800
|
+
// deferred alongside a mount sync stays a real change.
|
|
23801
|
+
const pendingChange = pendingChangeRef.current;
|
|
23802
|
+
pendingChangeRef.current = {
|
|
23803
|
+
e,
|
|
23804
|
+
notifyExternal:
|
|
23805
|
+
pendingChange?.notifyExternal === true ? true : notifyExternal,
|
|
23806
|
+
};
|
|
23796
23807
|
return;
|
|
23797
23808
|
}
|
|
23798
23809
|
const aggChildState = resolvedAggregateChildStates(
|
|
@@ -24256,12 +24267,16 @@ const useUIGroupStateController = (
|
|
|
24256
24267
|
|
|
24257
24268
|
useLayoutEffect(() => {
|
|
24258
24269
|
groupIsRenderingRef.current = false;
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
|
|
24263
|
-
{
|
|
24270
|
+
const pendingChange = pendingChangeRef.current;
|
|
24271
|
+
if (pendingChange) {
|
|
24272
|
+
pendingChangeRef.current = null;
|
|
24273
|
+
const batchedEvent = new CustomEvent(
|
|
24274
|
+
`${controlType}_batched_ui_state_update`,
|
|
24264
24275
|
);
|
|
24276
|
+
chainEvent(batchedEvent, pendingChange.e);
|
|
24277
|
+
scope._onChange(batchedEvent, {
|
|
24278
|
+
notifyExternal: pendingChange.notifyExternal,
|
|
24279
|
+
});
|
|
24265
24280
|
}
|
|
24266
24281
|
});
|
|
24267
24282
|
|
|
@@ -24322,20 +24337,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
24322
24337
|
// not be treated as the picker's synced child.
|
|
24323
24338
|
return false;
|
|
24324
24339
|
}
|
|
24325
|
-
if (props.type === "form" && childController.controlType !== "form") {
|
|
24326
|
-
// Only a form: what a type="form" picker syncs with is the form in
|
|
24327
|
-
// its popup, not any control that happens to be in there (an input
|
|
24328
|
-
// sitting outside the form, a ControlGroup — which is a way of
|
|
24329
|
-
// grouping controls INSIDE a form, not a thing a picker talks to).
|
|
24330
|
-
return false;
|
|
24331
|
-
}
|
|
24332
|
-
if (
|
|
24333
|
-
props.type === "array" &&
|
|
24334
|
-
childController.controlType !== "checkbox_group"
|
|
24335
|
-
) {
|
|
24336
|
-
// only selectable list expose array, ignore others
|
|
24337
|
-
return false;
|
|
24338
|
-
}
|
|
24339
24340
|
return true;
|
|
24340
24341
|
};
|
|
24341
24342
|
|
|
@@ -24355,7 +24356,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
24355
24356
|
`[navi] a second control ("${childType}"${child.name ? ` name="${child.name}"` : ""}) registered in the ${describePicker(props)} popup. ` +
|
|
24356
24357
|
`A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
|
|
24357
24358
|
`so this one is neither filled nor collected. ` +
|
|
24358
|
-
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="
|
|
24359
|
+
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="object".`,
|
|
24359
24360
|
child,
|
|
24360
24361
|
);
|
|
24361
24362
|
} else {
|
|
@@ -32346,6 +32347,12 @@ const defaultRerunOn = {
|
|
|
32346
32347
|
GET: false,
|
|
32347
32348
|
GET_MANY: ["POST"],
|
|
32348
32349
|
};
|
|
32350
|
+
// What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
|
|
32351
|
+
// in there, unlike for GET_MANY: an action holds ids and the store drops the
|
|
32352
|
+
// deleted one out of every list holding it, while a list reading by slices
|
|
32353
|
+
// holds places — the row that left takes the ones after it one rank up, and
|
|
32354
|
+
// only the collection knows who fills the last one.
|
|
32355
|
+
const defaultInvalidateRangeOn = ["POST", "DELETE"];
|
|
32349
32356
|
|
|
32350
32357
|
// This handles ALL resource lifecycle logic (rerun/reset) across all resources
|
|
32351
32358
|
const createResourceLifecycleManager = () => {
|
|
@@ -32366,6 +32373,7 @@ const createResourceLifecycleManager = () => {
|
|
|
32366
32373
|
paramScope,
|
|
32367
32374
|
uniqueKeys,
|
|
32368
32375
|
restActionSet: new Set(),
|
|
32376
|
+
rangeReaderSet: new Set(),
|
|
32369
32377
|
});
|
|
32370
32378
|
|
|
32371
32379
|
// Register dependencies
|
|
@@ -32378,6 +32386,14 @@ const createResourceLifecycleManager = () => {
|
|
|
32378
32386
|
}
|
|
32379
32387
|
}
|
|
32380
32388
|
};
|
|
32389
|
+
// Only the reader the resource exposes is registered; the ones `bindParams`
|
|
32390
|
+
// makes share its signal (see resource_range_reader.js).
|
|
32391
|
+
const registerRangeReader = (resourceScope, rangeReader) => {
|
|
32392
|
+
const config = registeredResources.get(resourceScope);
|
|
32393
|
+
if (config) {
|
|
32394
|
+
config.rangeReaderSet.add(rangeReader);
|
|
32395
|
+
}
|
|
32396
|
+
};
|
|
32381
32397
|
const registerAction = (resourceScope, restAction) => {
|
|
32382
32398
|
const config = registeredResources.get(resourceScope);
|
|
32383
32399
|
if (config) {
|
|
@@ -32608,7 +32624,39 @@ const createResourceLifecycleManager = () => {
|
|
|
32608
32624
|
};
|
|
32609
32625
|
};
|
|
32610
32626
|
|
|
32627
|
+
// Which readers hold slices of a collection the mutation just changed: the
|
|
32628
|
+
// one of the resource itself, and the ones of the resources that said they
|
|
32629
|
+
// depend on it.
|
|
32630
|
+
const invalidateRangeReaders = (
|
|
32631
|
+
triggeringAction,
|
|
32632
|
+
triggeringActionContext,
|
|
32633
|
+
) => {
|
|
32634
|
+
const triggerVerb = triggeringAction.meta.verb;
|
|
32635
|
+
const triggerResourceScope = triggeringActionContext.resourceScope;
|
|
32636
|
+
for (const [resourceScope, config] of registeredResources) {
|
|
32637
|
+
if (config.rangeReaderSet.size === 0) {
|
|
32638
|
+
continue;
|
|
32639
|
+
}
|
|
32640
|
+
const isSameResource = resourceScope === triggerResourceScope;
|
|
32641
|
+
const isDependent = Boolean(
|
|
32642
|
+
triggerResourceScope &&
|
|
32643
|
+
resourceDependencies.get(triggerResourceScope)?.has(resourceScope),
|
|
32644
|
+
);
|
|
32645
|
+
if (!isSameResource && !isDependent) {
|
|
32646
|
+
continue;
|
|
32647
|
+
}
|
|
32648
|
+
const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
|
|
32649
|
+
if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
|
|
32650
|
+
continue;
|
|
32651
|
+
}
|
|
32652
|
+
for (const rangeReader of config.rangeReaderSet) {
|
|
32653
|
+
rangeReader.invalidate();
|
|
32654
|
+
}
|
|
32655
|
+
}
|
|
32656
|
+
};
|
|
32657
|
+
|
|
32611
32658
|
const onActionComplete = (restActionWhoJustCompleted, restActionContext) => {
|
|
32659
|
+
invalidateRangeReaders(restActionWhoJustCompleted, restActionContext);
|
|
32612
32660
|
const { actionsToRerun, actionsToReset, reasons } = findEffectOnActions(
|
|
32613
32661
|
restActionWhoJustCompleted,
|
|
32614
32662
|
restActionContext,
|
|
@@ -32627,6 +32675,7 @@ const createResourceLifecycleManager = () => {
|
|
|
32627
32675
|
return {
|
|
32628
32676
|
registerResource,
|
|
32629
32677
|
registerAction,
|
|
32678
|
+
registerRangeReader,
|
|
32630
32679
|
onActionComplete,
|
|
32631
32680
|
// Registers: when `triggerResource` fires, rerun `dependentResource`'s actions.
|
|
32632
32681
|
// Used by scopedMany to make the parent GET rerun when a child mutation completes.
|
|
@@ -32704,13 +32753,20 @@ const getParamScope = (params) => {
|
|
|
32704
32753
|
*
|
|
32705
32754
|
* The reader is a function, so a list feeds on it the way it feeds on any other
|
|
32706
32755
|
* source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
|
|
32756
|
+
*
|
|
32757
|
+
* Keeping nothing does not mean hearing nothing: a mutation that decides who
|
|
32758
|
+
* belongs to the collection (a POST, a DELETE, whatever `rerunOn.GET_RANGE`
|
|
32759
|
+
* says) bumps `invalidationSignal`, and whoever reads slices through it goes
|
|
32760
|
+
* and asks again — the counterpart, for a reader, of what a rerun is for an
|
|
32761
|
+
* action. Every reader made by `bindParams` shares the signal of the one it
|
|
32762
|
+
* comes from: the params say which slices are read, not which collection.
|
|
32707
32763
|
*/
|
|
32708
32764
|
|
|
32709
32765
|
|
|
32710
32766
|
const createRangeReader = (
|
|
32711
32767
|
actionName,
|
|
32712
32768
|
callback,
|
|
32713
|
-
{ store, params: boundParams },
|
|
32769
|
+
{ store, params: boundParams, invalidationSignal = signal(0) },
|
|
32714
32770
|
) => {
|
|
32715
32771
|
const readRange = async (range = {}) => {
|
|
32716
32772
|
const { signal, ...rangeParams } = range;
|
|
@@ -32739,10 +32795,17 @@ const createRangeReader = (
|
|
|
32739
32795
|
};
|
|
32740
32796
|
Object.defineProperty(readRange, "name", { value: actionName });
|
|
32741
32797
|
readRange.isRangeReader = true;
|
|
32798
|
+
// Bumped when the collection this reads has moved: the slices anyone holds
|
|
32799
|
+
// stand for a composition that is gone.
|
|
32800
|
+
readRange.invalidationSignal = invalidationSignal;
|
|
32801
|
+
readRange.invalidate = () => {
|
|
32802
|
+
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
32803
|
+
};
|
|
32742
32804
|
readRange.bindParams = (paramsToBind) => {
|
|
32743
32805
|
return createRangeReader(actionName, callback, {
|
|
32744
32806
|
store,
|
|
32745
32807
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
32808
|
+
invalidationSignal,
|
|
32746
32809
|
});
|
|
32747
32810
|
};
|
|
32748
32811
|
return readRange;
|
|
@@ -32809,9 +32872,11 @@ const debug$2 = (args) => {
|
|
|
32809
32872
|
* - GET_MANY / POST_MANY / … → an array of item objects
|
|
32810
32873
|
* - GET_RANGE → `{ items, start, count }`, one slice of the collection
|
|
32811
32874
|
*
|
|
32812
|
-
* `GET_RANGE` is a reader rather than an action: it keeps no value and
|
|
32813
|
-
*
|
|
32814
|
-
* (`itemsAction={USER.GET_RANGE.bindParams({ team })}`).
|
|
32875
|
+
* `GET_RANGE` is a reader rather than an action: it keeps no value and has nothing to
|
|
32876
|
+
* rerun, so a `<List.Items>` can feed on it slice by slice
|
|
32877
|
+
* (`itemsAction={USER.GET_RANGE.bindParams({ team })}`). A mutation listed in
|
|
32878
|
+
* `rerunOn.GET_RANGE` (`["POST", "DELETE"]` by default) tells it the collection moved,
|
|
32879
|
+
* and whoever holds slices reads them again.
|
|
32815
32880
|
*
|
|
32816
32881
|
* A sub-resource of the backend (`/games/:id/candidates`) must be modelled with a
|
|
32817
32882
|
* relationship method, never as an `op`/`type` discriminator dispatched inside one
|
|
@@ -33991,13 +34056,18 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
33991
34056
|
continue;
|
|
33992
34057
|
}
|
|
33993
34058
|
if (restCallbackKey === "GET_RANGE") {
|
|
33994
|
-
// A range is read, never kept: no action,
|
|
34059
|
+
// A range is read, never kept: no action, nothing to rerun — only a
|
|
34060
|
+
// signal saying the slices anyone holds are out of date
|
|
33995
34061
|
// (see resource_range_reader.js).
|
|
33996
34062
|
stateFacade.GET_RANGE = createRangeReader(
|
|
33997
34063
|
`${name}.GET_RANGE`,
|
|
33998
34064
|
restCallback,
|
|
33999
34065
|
{ store, params },
|
|
34000
34066
|
);
|
|
34067
|
+
resourceLifecycleManager.registerRangeReader(
|
|
34068
|
+
stateFacade,
|
|
34069
|
+
stateFacade.GET_RANGE,
|
|
34070
|
+
);
|
|
34001
34071
|
continue;
|
|
34002
34072
|
}
|
|
34003
34073
|
const isMany = restCallbackKey.endsWith("_MANY");
|
|
@@ -54720,6 +54790,7 @@ const ListUI = props => {
|
|
|
54720
54790
|
expand: expand,
|
|
54721
54791
|
"navi-nothing-to-display": nothingToDisplay ? "" : undefined,
|
|
54722
54792
|
"navi-loading": loading ? "" : undefined,
|
|
54793
|
+
"navi-refreshing": virtual.refreshingSignal.value ? "" : undefined,
|
|
54723
54794
|
"navi-error": error ? "" : undefined,
|
|
54724
54795
|
styleCSSVars: LIST_STYLE_CSS_VARS,
|
|
54725
54796
|
pseudoClasses: LIST_PSEUDO_CLASSES,
|
|
@@ -57044,6 +57115,10 @@ const createListVirtual = () => {
|
|
|
57044
57115
|
// registers, nothing is drawn), and yet they are what it may have been
|
|
57045
57116
|
// waiting for — the row it was told to open on, for one.
|
|
57046
57117
|
const pagesSignal = signal(0);
|
|
57118
|
+
// How many runs are re-reading rows they already show. The list wears it as
|
|
57119
|
+
// an attribute: what is drawn is from before, and the app may want to say so
|
|
57120
|
+
// without taking anything away.
|
|
57121
|
+
const refreshingSignal = signal(0);
|
|
57047
57122
|
const placeByOwner = new Map();
|
|
57048
57123
|
const locatorByOwner = new Map();
|
|
57049
57124
|
let passId = 0;
|
|
@@ -57051,6 +57126,7 @@ const createListVirtual = () => {
|
|
|
57051
57126
|
const virtual = {
|
|
57052
57127
|
totalSignal,
|
|
57053
57128
|
pagesSignal,
|
|
57129
|
+
refreshingSignal,
|
|
57054
57130
|
// What a run needs to know about the list it lives in: how many rows the
|
|
57055
57131
|
// list is willing to draw at once, which end it opens on, and how much
|
|
57056
57132
|
// room one row is given — a row whose content has not arrived must take
|
|
@@ -57153,7 +57229,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
57153
57229
|
* inside `<List.Group>`s; each takes its place in declaration order.
|
|
57154
57230
|
*
|
|
57155
57231
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
57156
|
-
* renderItem: (item: any, index: number) => import("ignore:preact").ComponentChildren,
|
|
57232
|
+
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
57157
57233
|
* itemsAction: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
57158
57234
|
* count?: number,
|
|
57159
57235
|
* groupBy?: (item: any, index: number) => any,
|
|
@@ -57163,6 +57239,10 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
57163
57239
|
* renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
|
|
57164
57240
|
* renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
|
|
57165
57241
|
* }>}
|
|
57242
|
+
* @param {(item: any, index: number, state: {refreshing: boolean}) => any} props.renderItem
|
|
57243
|
+
* What one row is, given the item and where it sits. `state.refreshing` says
|
|
57244
|
+
* the rows drawn are the ones from before while the run reads the collection
|
|
57245
|
+
* again — the list carries `navi-refreshing` for the same reason.
|
|
57166
57246
|
* @param {(item: any, index: number) => any} [props.groupBy]
|
|
57167
57247
|
* What tells rows that belong together apart from the others — the day of a
|
|
57168
57248
|
* message, the month of a game. Consecutive rows sharing it are wrapped in a
|
|
@@ -57369,6 +57449,9 @@ const ListItems = ({
|
|
|
57369
57449
|
virtualItemSize: virtualItemSize
|
|
57370
57450
|
}, "navi-list-filler-before"));
|
|
57371
57451
|
}
|
|
57452
|
+
const renderItemState = {
|
|
57453
|
+
refreshing: store.refreshing
|
|
57454
|
+
};
|
|
57372
57455
|
let rowIndex = windowFrom;
|
|
57373
57456
|
while (rowIndex < windowTo) {
|
|
57374
57457
|
if (rowIndex >= failureFrom && rowIndex <= failureTo) {
|
|
@@ -57396,7 +57479,7 @@ const ListItems = ({
|
|
|
57396
57479
|
const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
|
|
57397
57480
|
let rowVnode;
|
|
57398
57481
|
if (item !== undefined) {
|
|
57399
|
-
rowVnode = renderItem(item, rowIndex);
|
|
57482
|
+
rowVnode = renderItem(item, rowIndex, renderItemState);
|
|
57400
57483
|
} else if (renderRowSkeleton === false) {
|
|
57401
57484
|
// The row must still take its room: without it the rows below would
|
|
57402
57485
|
// climb up and slide back down as the answer arrives.
|
|
@@ -57497,6 +57580,22 @@ const useItemStore = ({
|
|
|
57497
57580
|
}
|
|
57498
57581
|
const pages = pagesRef.current;
|
|
57499
57582
|
const [, setPageVersion] = useState(0);
|
|
57583
|
+
// The rows held are out of date and the run has not asked for the new ones
|
|
57584
|
+
// yet. They stay on screen until the answer comes: what is drawn is from
|
|
57585
|
+
// before, which is not the same thing as nothing to draw.
|
|
57586
|
+
const staleRef = useRef(false);
|
|
57587
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
57588
|
+
// A source that says when what it reads has moved (a resource range reader
|
|
57589
|
+
// does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
|
|
57590
|
+
// to the collection is exactly what a run cannot deduce from the rows it
|
|
57591
|
+
// holds. A source that says nothing is read once and stays as it is.
|
|
57592
|
+
const invalidationSignal = typeof itemsAction === "function" ? itemsAction.invalidationSignal : null;
|
|
57593
|
+
const invalidation = invalidationSignal ? invalidationSignal.value : 0;
|
|
57594
|
+
const invalidationRef = useRef(invalidation);
|
|
57595
|
+
if (invalidationRef.current !== invalidation) {
|
|
57596
|
+
invalidationRef.current = invalidation;
|
|
57597
|
+
staleRef.current = true;
|
|
57598
|
+
}
|
|
57500
57599
|
// The one request in flight, with the means to call it off: a page asked for
|
|
57501
57600
|
// a window the list has left is work the server and the browser are doing for
|
|
57502
57601
|
// nothing.
|
|
@@ -57506,7 +57605,8 @@ const useItemStore = ({
|
|
|
57506
57605
|
end: -1,
|
|
57507
57606
|
held: -1,
|
|
57508
57607
|
controller: null,
|
|
57509
|
-
generation: 0
|
|
57608
|
+
generation: 0,
|
|
57609
|
+
revalidating: false
|
|
57510
57610
|
});
|
|
57511
57611
|
// The rows asked for that never came. Kept as a range so the list can say
|
|
57512
57612
|
// where the hole is, and cleared by a retry — which is what makes the same
|
|
@@ -57517,9 +57617,24 @@ const useItemStore = ({
|
|
|
57517
57617
|
// It stands for a windowful of them: a list that is about to be filled looks
|
|
57518
57618
|
// like rows on their way, not like an empty list.
|
|
57519
57619
|
const rowCount = pages.count ?? count ?? virtual.renderBudget;
|
|
57620
|
+
// A run that never received anything has nothing to keep on screen: asking
|
|
57621
|
+
// again is its first ask, not a refresh.
|
|
57622
|
+
if (staleRef.current && pages.count === undefined) {
|
|
57623
|
+
staleRef.current = false;
|
|
57624
|
+
}
|
|
57625
|
+
useLayoutEffect(() => {
|
|
57626
|
+
if (!refreshing) {
|
|
57627
|
+
return null;
|
|
57628
|
+
}
|
|
57629
|
+
virtual.refreshingSignal.value = virtual.refreshingSignal.peek() + 1;
|
|
57630
|
+
return () => {
|
|
57631
|
+
virtual.refreshingSignal.value = virtual.refreshingSignal.peek() - 1;
|
|
57632
|
+
};
|
|
57633
|
+
}, [refreshing]);
|
|
57520
57634
|
const store = {
|
|
57521
57635
|
rowCount,
|
|
57522
57636
|
failure,
|
|
57637
|
+
refreshing,
|
|
57523
57638
|
// JS memory is cheap next to the DOM, but a long enough scroll accumulates
|
|
57524
57639
|
// everything it ever went through. Rows far from what is on screen are
|
|
57525
57640
|
// dropped and simply asked for again if the user goes back — the same
|
|
@@ -57560,7 +57675,25 @@ const useItemStore = ({
|
|
|
57560
57675
|
let start = missingStart;
|
|
57561
57676
|
let end = missingEnd;
|
|
57562
57677
|
let around;
|
|
57563
|
-
|
|
57678
|
+
// Rows that are all there but out of date: the ask is the window itself,
|
|
57679
|
+
// anchored on the row at its top — a source paginating by cursor gets a
|
|
57680
|
+
// row to count from, and the reading position is what must survive.
|
|
57681
|
+
const revalidating = staleRef.current;
|
|
57682
|
+
if (revalidating) {
|
|
57683
|
+
start = windowFrom;
|
|
57684
|
+
end = windowTo - 1;
|
|
57685
|
+
if (end < start) {
|
|
57686
|
+
// Nothing of this run is on screen (the window frames another one, or
|
|
57687
|
+
// the list is scrolled past it): its own first rows are what it will
|
|
57688
|
+
// draw next.
|
|
57689
|
+
start = 0;
|
|
57690
|
+
end = budget - 1;
|
|
57691
|
+
}
|
|
57692
|
+
const firstHeld = pages.byIndex.get(windowFrom);
|
|
57693
|
+
if (firstHeld && firstHeld.id !== undefined) {
|
|
57694
|
+
around = firstHeld.id;
|
|
57695
|
+
}
|
|
57696
|
+
} else if (pages.count === undefined) {
|
|
57564
57697
|
const scrolled = virtual.scrolled;
|
|
57565
57698
|
if (scrolled === "end") {
|
|
57566
57699
|
// Counting back from the end, the way an HTTP range does: a list
|
|
@@ -57591,6 +57724,14 @@ const useItemStore = ({
|
|
|
57591
57724
|
return;
|
|
57592
57725
|
}
|
|
57593
57726
|
const request = requestRef.current;
|
|
57727
|
+
if (revalidating && request.busy) {
|
|
57728
|
+
if (request.revalidating) {
|
|
57729
|
+
return;
|
|
57730
|
+
}
|
|
57731
|
+
// A page for a window that is about to be replaced wholesale.
|
|
57732
|
+
request.controller?.abort();
|
|
57733
|
+
request.busy = false;
|
|
57734
|
+
}
|
|
57594
57735
|
if (request.busy) {
|
|
57595
57736
|
// Still worth waiting for as long as what it went to fetch is still
|
|
57596
57737
|
// what the list would draw. Once it is not, it is called off — and
|
|
@@ -57609,8 +57750,9 @@ const useItemStore = ({
|
|
|
57609
57750
|
}
|
|
57610
57751
|
const held = pages.byIndex.size;
|
|
57611
57752
|
// Asking again for a range that was already asked for, having received
|
|
57612
|
-
// nothing since, can only produce the same answer.
|
|
57613
|
-
|
|
57753
|
+
// nothing since, can only produce the same answer. A revalidation is
|
|
57754
|
+
// exactly the case where it produces another one.
|
|
57755
|
+
if (!revalidating && request.start === start && request.end === end && request.held === held) {
|
|
57614
57756
|
return;
|
|
57615
57757
|
}
|
|
57616
57758
|
request.start = start;
|
|
@@ -57625,17 +57767,33 @@ const useItemStore = ({
|
|
|
57625
57767
|
end,
|
|
57626
57768
|
around,
|
|
57627
57769
|
limit: end - start + 1,
|
|
57628
|
-
|
|
57629
|
-
|
|
57770
|
+
// A cursor names a row of the collection as it was; a revalidation
|
|
57771
|
+
// is asked precisely because that is what changed.
|
|
57772
|
+
before: revalidating ? undefined : cursor.before,
|
|
57773
|
+
after: revalidating ? undefined : cursor.after,
|
|
57630
57774
|
count: pages.count,
|
|
57631
57775
|
signal: controller.signal
|
|
57632
57776
|
};
|
|
57633
57777
|
request.busy = true;
|
|
57778
|
+
request.revalidating = revalidating;
|
|
57779
|
+
if (revalidating) {
|
|
57780
|
+
setRefreshing(true);
|
|
57781
|
+
}
|
|
57634
57782
|
const done = page => {
|
|
57635
57783
|
const current = generation === request.generation;
|
|
57636
57784
|
if (current) {
|
|
57637
57785
|
request.busy = false;
|
|
57786
|
+
request.revalidating = false;
|
|
57638
57787
|
setFailure(null);
|
|
57788
|
+
if (revalidating) {
|
|
57789
|
+
staleRef.current = false;
|
|
57790
|
+
setRefreshing(false);
|
|
57791
|
+
}
|
|
57792
|
+
}
|
|
57793
|
+
if (revalidating && !current) {
|
|
57794
|
+
// Rows of a composition already superseded by a newer ask: keeping
|
|
57795
|
+
// them would mix two states of the collection.
|
|
57796
|
+
return;
|
|
57639
57797
|
}
|
|
57640
57798
|
if (!page) {
|
|
57641
57799
|
return;
|
|
@@ -57646,6 +57804,12 @@ const useItemStore = ({
|
|
|
57646
57804
|
// Before the rows land: what is on screen has to stay where it is,
|
|
57647
57805
|
// and the DOM still shows the state to hold onto.
|
|
57648
57806
|
virtual.captureAnchor();
|
|
57807
|
+
if (revalidating) {
|
|
57808
|
+
// The rows held stood for a composition that has moved on; the
|
|
57809
|
+
// ones outside the window are forgotten and asked for again if the
|
|
57810
|
+
// user goes back to them.
|
|
57811
|
+
pages.byIndex = new Map();
|
|
57812
|
+
}
|
|
57649
57813
|
let i = 0;
|
|
57650
57814
|
while (i < pageItems.length) {
|
|
57651
57815
|
pages.byIndex.set(pageStart + i, pageItems[i]);
|
|
@@ -57661,6 +57825,14 @@ const useItemStore = ({
|
|
|
57661
57825
|
return;
|
|
57662
57826
|
}
|
|
57663
57827
|
request.busy = false;
|
|
57828
|
+
request.revalidating = false;
|
|
57829
|
+
if (revalidating) {
|
|
57830
|
+
// The rows from before stay: a revalidation that failed has
|
|
57831
|
+
// nothing better to put in their place.
|
|
57832
|
+
staleRef.current = false;
|
|
57833
|
+
setRefreshing(false);
|
|
57834
|
+
return;
|
|
57835
|
+
}
|
|
57664
57836
|
setFailure({
|
|
57665
57837
|
start,
|
|
57666
57838
|
end,
|
|
@@ -58265,8 +58437,8 @@ const PickerTypeResolver = props => {
|
|
|
58265
58437
|
...props
|
|
58266
58438
|
});
|
|
58267
58439
|
}
|
|
58268
|
-
if (props.type === "
|
|
58269
|
-
return jsx(
|
|
58440
|
+
if (props.type === "object") {
|
|
58441
|
+
return jsx(PickerObject, {
|
|
58270
58442
|
...props
|
|
58271
58443
|
});
|
|
58272
58444
|
}
|
|
@@ -58282,19 +58454,19 @@ const PickerText = props => {
|
|
|
58282
58454
|
});
|
|
58283
58455
|
};
|
|
58284
58456
|
|
|
58285
|
-
// The popup holds a group of named controls — a `<
|
|
58286
|
-
// when
|
|
58287
|
-
//
|
|
58288
|
-
// surface (see dialog.jsx), so there is nothing to tell it about the shape.
|
|
58289
|
-
const
|
|
58457
|
+
// The popup holds a group of named controls — a `<ControlGroup>`, or a `<Form>`
|
|
58458
|
+
// when that group is a question with a send of its own — and this picker's
|
|
58459
|
+
// value is the object that group aggregates. The popup itself holds nothing: it
|
|
58460
|
+
// is a surface (see dialog.jsx), so there is nothing to tell it about the shape.
|
|
58461
|
+
const PickerObject = props => {
|
|
58290
58462
|
const Next = useNextResolver();
|
|
58291
58463
|
return jsx(Next, {
|
|
58292
|
-
ui: jsx(
|
|
58464
|
+
ui: jsx(PickerObjectUI, {}),
|
|
58293
58465
|
...props,
|
|
58294
58466
|
type: "navi_js"
|
|
58295
58467
|
});
|
|
58296
58468
|
};
|
|
58297
|
-
const
|
|
58469
|
+
const PickerObjectUI = () => {
|
|
58298
58470
|
const {
|
|
58299
58471
|
value,
|
|
58300
58472
|
placeholder
|
|
@@ -59304,7 +59476,7 @@ Picker.UI.Week = PickerWeekUI;
|
|
|
59304
59476
|
Picker.UI.Datetime = PickerDatetimeUI;
|
|
59305
59477
|
Picker.UI.File = PickerFileUI;
|
|
59306
59478
|
Picker.UI.Color = PickerColorUI;
|
|
59307
|
-
Picker.UI.
|
|
59479
|
+
Picker.UI.Object = PickerObjectUI;
|
|
59308
59480
|
Picker.UI.Multiple = PickerArrayUI;
|
|
59309
59481
|
Picker.UI.PencilSvg = PencilSvg;
|
|
59310
59482
|
Picker.UI.ChevronDownSvg = ChevronDownSvg$1;
|