diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js index 16756ba55..1ef2ae5d0 100644 --- a/src/gui/src/helpers.js +++ b/src/gui/src/helpers.js @@ -2168,6 +2168,53 @@ window.updateSubdomainsForItems = async function (fsentries, container) { } }; +// This flow owns its own file input rather than borrowing the shell's +// #upload-file-dialog. That element is shared, and on the dashboard the Files +// tab assigns an onchange PROPERTY to it — which jQuery's unbind() cannot +// remove — so a single selection used to fire both handlers and start two +// concurrent uploads of the same files. When the two destinations coincided +// (both default to Desktop) the signed batch writes raced and the loser failed +// with "Entry already exists"; when they differed, the files also landed in +// the Files tab's directory. A private input keeps the two flows independent. +let el_upload_dialog_input = null; + +// Destination for the pending selection. Module-level rather than captured per +// call because the input's change handler is bound once, and only one native +// file dialog can be open at a time — matching the old behaviour, where +// re-binding meant the most recent caller's target won. +let upload_dialog_target_path = null; + +const get_upload_dialog_input = () => { + if ( el_upload_dialog_input?.isConnected ) { + return el_upload_dialog_input; + } + + el_upload_dialog_input = document.createElement('input'); + el_upload_dialog_input.type = 'file'; + el_upload_dialog_input.name = 'file'; + el_upload_dialog_input.multiple = true; + el_upload_dialog_input.style.display = 'none'; + document.body.appendChild(el_upload_dialog_input); + + el_upload_dialog_input.addEventListener('change', function () { + // Snapshot into an array before clearing: `value = ''` empties the live + // FileList in place, and upload_items consumes it asynchronously. + const files = Array.from(el_upload_dialog_input.files ?? []); + el_upload_dialog_input.value = ''; + if ( files.length === 0 ) { + return; + } + try { + window.upload_items(files, upload_dialog_target_path); + } + catch ( err ) { + UIAlert(err.message ?? err); + } + }); + + return el_upload_dialog_input; +}; + /** * * @param {*} el_target_container @@ -2175,29 +2222,15 @@ window.updateSubdomainsForItems = async function (fsentries, container) { */ window.init_upload_using_dialog = function (el_target_container, target_path = null) { - $('#upload-file-dialog').unbind('onchange'); - $('#upload-file-dialog').unbind('change'); - $('#upload-file-dialog').unbind('onChange'); + upload_dialog_target_path = target_path === null + ? $(el_target_container).attr('data-path') + : path.resolve(target_path); - target_path = target_path === null ? $(el_target_container).attr('data-path') : path.resolve(target_path); - $('#upload-file-dialog').trigger('click'); - $('#upload-file-dialog').on('change', async function (e) { - if ( $('#upload-file-dialog').val() !== '' ) { - const files = $('#upload-file-dialog')[0].files; - if ( files.length > 0 ) { - try { - window.upload_items(files, target_path); - } - catch ( err ) { - UIAlert(err.message ?? err); - } - $('#upload-file-dialog').val(''); - } - } - else { - return; - } - }); + const el_input = get_upload_dialog_input(); + // Clearing before opening lets the same file be picked twice in a row; an + // unchanged value fires no change event. + el_input.value = ''; + el_input.click(); }; window.upload_items = async function (items, dest_path) {