fix: keep hidden apps' saved ranks through the mid-drag stash path

- _applyPendingLoad reconciled a stashed load against the visible-only
  on-screen order, tail-appending any app returning to the grid and
  contradicting the order the drag had just saved — the rank-demotion
  defect resurfacing through one more path. It now prefers
  _savedOrderNames, the canonical saved list that saveOrder keeps
  merged with hidden apps at their ranks, over the possibly pre-drag kv
  snapshot the stashed load fetched.
- The post-uninstall optimistic render now also skips the load fade;
  it was the one in-place rebuild still hiding the grid behind the
  opacity-0 icon gate.
- mergeSavedOrder's rank rescan collapsed to a single forward
  insertion pointer (verified output-identical; the 20 unit tests pin
  the behavior).
This commit is contained in:
jelveh
2026-07-18 18:28:00 -07:00
parent e188048f97
commit 35e40e0062
2 changed files with 19 additions and 22 deletions
+10 -4
View File
@@ -183,7 +183,7 @@ function showUninstallModal ({ appName, appTitle, appUid, removesTile, self, $el
// and restores its position if it comes back.
if ( removesTile ) {
self._apps = self._apps.filter(a => a.name !== appName);
self.renderApps($el_window, { preservePage: true });
self.renderApps($el_window, { preservePage: true, instant: true });
}
// Keep the user's page and skip the load fade — this is a
// background sync, not a fresh visit.
@@ -925,11 +925,17 @@ const TabApps = {
this._pendingLoad = null;
if ( pending.loadSeq < (this._appliedSeq || 0) ) return;
this._appliedSeq = pending.loadSeq;
this._savedOrderNames = pending.orderedNames;
const orderedNames = this._hasCustomOrder && Array.isArray(this._apps)
? serializeAppOrder(this._apps)
// The drag that deferred this load may have just saved a newer order;
// _savedOrderNames tracks the canonical saved list (saveOrder keeps
// it merged, with hidden apps at their ranks). Prefer it over the kv
// snapshot the stashed load fetched — that may predate the drag — and
// never reconcile against the visible-only on-screen order, which
// would tail-append any app returning to the grid.
const orderedNames = this._hasCustomOrder && Array.isArray(this._savedOrderNames)
? this._savedOrderNames
: pending.orderedNames;
this._savedOrderNames = orderedNames;
this._hasCustomOrder = Array.isArray(orderedNames) && orderedNames.length > 0;
this._apps = reconcileAppOrder(pending.merged, orderedNames);
this.renderApps(pending.$el_window, { preservePage: true, instant: true });
+9 -18
View File
@@ -94,35 +94,26 @@ export function mergeSavedOrder (currentNames, previousNames) {
const currentSet = new Set(result);
const prevSet = new Set(previousNames);
// A survivor is a name present in both lists; re-inserted missing names
// and brand-new names never count when locating the k-th survivor.
// and brand-new names never count when advancing past a survivor.
const isSurvivor = name => currentSet.has(name) && prevSet.has(name);
const seen = new Set();
let rank = 0; // survivors encountered so far in the saved order
let lastInsert = -1; // keeps runs of missing names in their saved order
// Single forward pointer over `result`: for each survivor in the saved
// order it advances just past the next survivor; each missing name is
// spliced in at the pointer, which puts it right after the same number
// of survivors that preceded it in the saved order — its rank.
let at = 0;
for ( const name of previousNames ) {
if ( typeof name !== 'string' || name.length === 0 ) continue;
if ( seen.has(name) ) continue;
seen.add(name);
if ( currentSet.has(name) ) {
rank++;
lastInsert = -1;
while ( at < result.length && ! isSurvivor(result[at]) ) at++;
at++;
continue;
}
// Find the index just past the rank-th survivor in the result.
let at = 0;
if ( rank > 0 ) {
let survivors = 0;
for ( let i = 0; i < result.length; i++ ) {
if ( isSurvivor(result[i]) && ++survivors === rank ) {
at = i + 1;
break;
}
}
}
if ( lastInsert >= at ) at = lastInsert + 1;
result.splice(at, 0, name);
lastInsert = at;
at++;
}
return result;
}