mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-20 20:26:21 +00:00
fix: warn before closing the tab while an upload is in flight
Uploads stream from the page, so closing the tab loses a nearly-finished one with no warning. The beforeunload handler only existed behind prompt_user_when_navigation_away_from_puter, which is off by default. Install it unconditionally and have it consult window.active_uploads, the registry the progress code already maintains. The feature flag keeps its open-window behavior. Uploads now register in active_uploads before the first await in the init callback: an upload failing while the progress window was still opening used to run its delete ahead of the insert, leaving a phantom entry that would have made the tab unclosable.
This commit is contained in:
@@ -1320,6 +1320,9 @@ const TabFiles = {
|
||||
thumbnailGenerator: createUploadThumbnailGenerator(),
|
||||
init: async (operation_id, xhr) => {
|
||||
opid = operation_id;
|
||||
// register before the first await, so a failure while the progress
|
||||
// window is still opening can't delete the entry before it exists
|
||||
window.active_uploads[opid] = 0;
|
||||
// create upload progress window
|
||||
upload_progress_window = await UIWindowProgress({
|
||||
title: i18n('upload'),
|
||||
@@ -1331,8 +1334,6 @@ const TabFiles = {
|
||||
xhr.abort();
|
||||
},
|
||||
});
|
||||
// add to active_uploads
|
||||
window.active_uploads[opid] = 0;
|
||||
},
|
||||
// start
|
||||
start: async function () {
|
||||
@@ -4806,6 +4807,9 @@ const TabFiles = {
|
||||
thumbnailGenerator: createUploadThumbnailGenerator(),
|
||||
init: async (operation_id, xhr) => {
|
||||
opid = operation_id;
|
||||
// register before the first await, so a failure while the progress
|
||||
// window is still opening can't delete the entry before it exists
|
||||
window.active_uploads[opid] = 0;
|
||||
upload_progress_window = await UIWindowProgress({
|
||||
title: i18n('upload'),
|
||||
icon: window.icons['app-icon-uploader.svg'],
|
||||
@@ -4816,7 +4820,6 @@ const TabFiles = {
|
||||
xhr.abort();
|
||||
},
|
||||
});
|
||||
window.active_uploads[opid] = 0;
|
||||
},
|
||||
start: async function () {
|
||||
upload_progress_window.set_status('Uploading');
|
||||
|
||||
@@ -2378,6 +2378,9 @@ window.upload_items = async function (items, dest_path) {
|
||||
// init
|
||||
init: async (operation_id, xhr) => {
|
||||
opid = operation_id;
|
||||
// register before the first await, so a failure while the progress
|
||||
// window is still opening can't delete the entry before it exists
|
||||
window.active_uploads[opid] = 0;
|
||||
// create upload progress window
|
||||
upload_progress_window = await UIWindowProgress({
|
||||
title: i18n('upload'),
|
||||
@@ -2389,8 +2392,6 @@ window.upload_items = async function (items, dest_path) {
|
||||
xhr.abort();
|
||||
},
|
||||
});
|
||||
// add to active_uploads
|
||||
window.active_uploads[opid] = 0;
|
||||
},
|
||||
// start
|
||||
start: async function () {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Whether closing the tab right now would destroy work in progress, so the
|
||||
* browser should put its "leave site?" dialog in the way first.
|
||||
*
|
||||
* Uploads stream from this page, so they die with it — a nearly-finished
|
||||
* upload has to start over. Downloads are handed to the browser's own
|
||||
* downloader and outlive the tab, so they don't count.
|
||||
*
|
||||
* @param {{ activeUploadCount?: number, openWindowCount?: number, promptOnOpenWindows?: boolean }} state
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const should_confirm_before_unload = ({
|
||||
activeUploadCount = 0,
|
||||
openWindowCount = 0,
|
||||
promptOnOpenWindows = false,
|
||||
} = {}) => {
|
||||
if ( activeUploadCount > 0 ) return true;
|
||||
return promptOnOpenWindows && openWindowCount > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* `beforeunload` handler reading the live globals. Returning a truthy value is
|
||||
* what triggers the dialog; its wording is the browser's, not ours.
|
||||
*
|
||||
* @returns {true|undefined}
|
||||
*/
|
||||
export const confirm_before_unload = () => {
|
||||
const confirm = should_confirm_before_unload({
|
||||
activeUploadCount: Object.keys(window.active_uploads ?? {}).length,
|
||||
// Explorer windows hold nothing unsaved, so they don't warrant a prompt.
|
||||
openWindowCount: $('.window:not(.window[data-app="explorer"])').length,
|
||||
promptOnOpenWindows: Boolean(window.feature_flags?.prompt_user_when_navigation_away_from_puter),
|
||||
});
|
||||
|
||||
return confirm ? true : undefined;
|
||||
};
|
||||
|
||||
export default confirm_before_unload;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { should_confirm_before_unload } from './confirmBeforeUnload.js';
|
||||
|
||||
describe('should_confirm_before_unload', () => {
|
||||
it('lets a quiet page close without a dialog', () => {
|
||||
expect(should_confirm_before_unload()).toBe(false);
|
||||
});
|
||||
|
||||
it('warns while an upload is still streaming', () => {
|
||||
expect(should_confirm_before_unload({ activeUploadCount: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it('warns during an upload even with the open-window prompt off', () => {
|
||||
// The bug: a nearly-finished upload used to die silently on close.
|
||||
expect(should_confirm_before_unload({
|
||||
activeUploadCount: 2,
|
||||
openWindowCount: 0,
|
||||
promptOnOpenWindows: false,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('stops warning once the last upload settles', () => {
|
||||
expect(should_confirm_before_unload({ activeUploadCount: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores open windows unless the feature flag asks for it', () => {
|
||||
expect(should_confirm_before_unload({ openWindowCount: 3 })).toBe(false);
|
||||
expect(should_confirm_before_unload({
|
||||
openWindowCount: 3,
|
||||
promptOnOpenWindows: true,
|
||||
})).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ import UIWindowSessionList from './UI/UIWindowSessionList.js';
|
||||
import UIWindowSignup from './UI/UIWindowSignup.js';
|
||||
import UIWindowRecoverPassword from './UI/UIWindowRecoverPassword.js';
|
||||
import { PROCESS_RUNNING } from './definitions.js';
|
||||
import confirm_before_unload from './helpers/confirmBeforeUnload.js';
|
||||
import create_access_token from './helpers/createAccessToken.js';
|
||||
import create_gui_token from './helpers/createGuiToken.js';
|
||||
import {
|
||||
@@ -2500,14 +2501,9 @@ window.initgui = async function (options) {
|
||||
}
|
||||
}
|
||||
|
||||
// if there is at least one window open (only non-Explorer windows), ask user for confirmation when navigating away from puter
|
||||
if (window.feature_flags.prompt_user_when_navigation_away_from_puter) {
|
||||
window.onbeforeunload = function () {
|
||||
if ($('.window:not(.window[data-app="explorer"])').length > 0) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
// ask the user to confirm before leaving while an upload is still in flight
|
||||
// (and, behind the feature flag, while any non-Explorer window is open)
|
||||
window.onbeforeunload = confirm_before_unload;
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// `login` event handler
|
||||
|
||||
Reference in New Issue
Block a user