mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-27 08:27:43 +00:00
fix: contain partial Apps loads and keep the uninstall flag current
- A partial installed-apps list (a page beyond the first failed) now renders only when the grid is empty — it must never replace a complete grid already on screen — and saveOrder refuses to persist while the list is partial, since overwriting the saved order with a truncated one would drop the missing apps' positions for good. - Opening an app from the grid now flips its tile's uninstallable flag: the open puts it in the server-side recent list, so a revoke from that moment on leaves the tile in place, and the modal must not act on the stale load-time snapshot. - The in-flight-load invalidation moved into _invalidateInFlightLoads() so the seq protocol lives in one place.
This commit is contained in:
@@ -179,13 +179,9 @@ function showUninstallModal ({ appName, appTitle, appUid, removesTile, self, $el
|
||||
// Keep the persisted order free of the now-uninstalled app, but
|
||||
// only if the user already has a custom order (don't create one).
|
||||
if ( self._hasCustomOrder ) self.saveOrder();
|
||||
// Mark any in-flight load stale: it fetched before the revoke,
|
||||
// and applying it would pop the just-removed tile back in.
|
||||
// Also stop sharing its promise, so the next activation
|
||||
// fetches fresh instead of joining the doomed load.
|
||||
self._loadSeq = (self._loadSeq || 0) + 1;
|
||||
self._appliedSeq = self._loadSeq;
|
||||
self._loadPromise = null;
|
||||
// A load fetched before the revoke must not apply — it would
|
||||
// pop the just-removed tile back in.
|
||||
self._invalidateInFlightLoads();
|
||||
self.renderApps($el_window, { preservePage: true });
|
||||
}
|
||||
} catch ( err ) {
|
||||
@@ -242,6 +238,7 @@ const TabApps = {
|
||||
_reduceMotionMQL: undefined,
|
||||
_loadPromise: null,
|
||||
_pendingLoad: null,
|
||||
_partialLoad: false,
|
||||
|
||||
html () {
|
||||
let h = '<div class="dashboard-tab-content myapps-tab">';
|
||||
@@ -294,6 +291,15 @@ const TabApps = {
|
||||
window.open(targetLink, '_blank', 'noopener,noreferrer');
|
||||
} else if ( appName ) {
|
||||
window.open(`/app/${appName}`, '_blank', 'noopener,noreferrer');
|
||||
// Opening records an app-open, which puts this app in the
|
||||
// server-side recent list — from now on a revoke leaves the
|
||||
// tile in place (see isUninstallable in _fetchAndRenderApps).
|
||||
// Flip the flag now so the uninstall modal doesn't act on a
|
||||
// stale load-time snapshot and remove a tile that will
|
||||
// silently reappear.
|
||||
const app = (self._apps || []).find(a => a.name === appName);
|
||||
if ( app ) app.uninstallable = false;
|
||||
$(this).attr('data-app-uninstallable', '0');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -924,6 +930,7 @@ const TabApps = {
|
||||
this._pendingLoad = null;
|
||||
if ( pending.loadSeq < (this._appliedSeq || 0) ) return;
|
||||
this._appliedSeq = pending.loadSeq;
|
||||
this._partialLoad = pending.partial;
|
||||
|
||||
const orderedNames = this._hasCustomOrder && Array.isArray(this._apps)
|
||||
? serializeAppOrder(this._apps)
|
||||
@@ -933,8 +940,27 @@ const TabApps = {
|
||||
this.renderApps(pending.$el_window, { preservePage: true, instant: true });
|
||||
},
|
||||
|
||||
// A local mutation of _apps (uninstall) must invalidate loads fetched
|
||||
// before it — applying one would resurrect the pre-mutation state. Loads
|
||||
// started after this call get a newer seq and still apply. Dropping the
|
||||
// shared promise lets the next activation fetch fresh instead of joining
|
||||
// the doomed load.
|
||||
_invalidateInFlightLoads () {
|
||||
this._loadSeq = (this._loadSeq || 0) + 1;
|
||||
this._appliedSeq = this._loadSeq;
|
||||
this._loadPromise = null;
|
||||
},
|
||||
|
||||
saveOrder () {
|
||||
this._hasCustomOrder = true;
|
||||
// Never persist an order backed by a partial app list — the saved
|
||||
// order is the only record of the missing apps' positions, and
|
||||
// overwriting it would drop them for good. The in-session order still
|
||||
// applies; the next complete load can persist again.
|
||||
if ( this._partialLoad ) {
|
||||
console.warn('App order not saved: the installed-apps list is incomplete (a page failed to load).');
|
||||
return;
|
||||
}
|
||||
const names = serializeAppOrder(this._apps);
|
||||
try {
|
||||
const p = puter.kv.set(APPS_ORDER_KV_KEY, JSON.stringify(names));
|
||||
@@ -974,6 +1000,11 @@ const TabApps = {
|
||||
|
||||
const $container = $el_window.find('.myapps-container');
|
||||
|
||||
// Whether the installed-apps list came back incomplete (a page beyond
|
||||
// the first failed). A partial list may render when the grid is empty,
|
||||
// but must never be persisted (see saveOrder).
|
||||
let installedAppsPartial = false;
|
||||
|
||||
try {
|
||||
// Fetch the two app lists and the saved order together. The
|
||||
// installedApps endpoint caps `limit` at 100 and paginates, so page
|
||||
@@ -1007,9 +1038,13 @@ const TabApps = {
|
||||
// A first-page failure is a failed load. A later page
|
||||
// failing must not discard the pages already fetched —
|
||||
// that would turn one flaky request among N into an
|
||||
// empty grid (the single-request load never had that).
|
||||
if ( page === 1 ) throw err;
|
||||
// empty grid — but a partial list is only better than
|
||||
// nothing: it must never REPLACE a complete grid
|
||||
// already on screen, so fail the refresh in that case
|
||||
// (the outer catch preserves what's showing).
|
||||
if ( page === 1 || this._apps ) throw err;
|
||||
console.error(`Failed to fetch installedApps page ${page}; showing the ${all.length} apps fetched so far:`, err);
|
||||
installedAppsPartial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1102,12 +1137,13 @@ const TabApps = {
|
||||
// away either — stash it for _endDrag to apply.
|
||||
if ( this._drag?.started ) {
|
||||
if ( ! this._pendingLoad || loadSeq > this._pendingLoad.loadSeq ) {
|
||||
this._pendingLoad = { $el_window, merged, orderedNames, loadSeq };
|
||||
this._pendingLoad = { $el_window, merged, orderedNames, loadSeq, partial: installedAppsPartial };
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._pendingLoad = null;
|
||||
this._appliedSeq = loadSeq;
|
||||
this._partialLoad = installedAppsPartial;
|
||||
|
||||
this._hasCustomOrder = Array.isArray(orderedNames) && orderedNames.length > 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user