Prevent duplicate edit submissions

This commit is contained in:
thePR0M3TH3AN committed 2025-10-08 16:27:04 -04:00
1 parent eec5b58175
commit ebc306c928
3 files changed
+192 -1

No files matched your search

+9
View File
@@ -5343,6 +5343,9 @@ class Application {
if (!this.pubkey) {
this.showError("Please login to edit videos.");
if (this.editModal?.setSubmitState) {
this.editModal.setSubmitState({ pending: false });
}
return;
}
@@ -5355,11 +5358,17 @@ class Application {
await this.loadVideos();
this.videosMap.clear();
this.showSuccess("Video updated successfully!");
if (this.editModal?.setSubmitState) {
this.editModal.setSubmitState({ pending: false });
}
this.editModal.close();
this.forceRefreshAllProfiles();
} catch (error) {
console.error("Failed to edit video:", error);
this.showError("Failed to edit video. Please try again.");
if (this.editModal?.setSubmitState) {
this.editModal.setSubmitState({ pending: false });
}
}
}
+40 -1
View File
@@ -59,6 +59,9 @@ export class EditModal {
this.eventsBound = false;
this.loadPromise = null;
this.pendingSubmit = false;
this.pendingSubmitVideo = null;
this.nip71FormManager = new Nip71FormManager();
this.nip71SectionKey = "edit";
this.originalNip71Metadata = null;
@@ -143,6 +146,8 @@ export class EditModal {
this.cancelButton = context.querySelector("#cancelEditVideo") || null;
this.submitButton = context.querySelector("#submitEditVideo") || null;
this.updateSubmitButtonState();
this.fieldButtons = Array.from(context.querySelectorAll("[data-edit-target]"));
this.fields = {
@@ -251,6 +256,7 @@ export class EditModal {
this.nip71FormManager.resetSection(this.nip71SectionKey);
this.originalNip71Metadata = null;
this.originalNip71MetadataJson = null;
this.setSubmitState({ pending: false });
return;
}
@@ -295,6 +301,7 @@ export class EditModal {
this.originalNip71Metadata = null;
this.originalNip71MetadataJson = null;
this.activeVideo = null;
this.setSubmitState({ pending: false });
}
async open(video) {
@@ -677,11 +684,17 @@ export class EditModal {
}
submit() {
if (this.pendingSubmit) {
return;
}
if (!this.activeVideo || !this.root) {
this.showError("No video selected for editing.");
return;
}
this.setSubmitState({ pending: true, video: this.activeVideo });
const fieldValue = (key) => {
const input = this.fields[key];
if (!input || typeof input.value !== "string") {
@@ -696,7 +709,7 @@ export class EditModal {
return this.sanitizers.text(input.value);
};
const original = this.activeVideo;
const original = this.pendingSubmitVideo || this.activeVideo;
const titleInput = this.fields.title;
const urlInput = this.fields.url;
@@ -843,4 +856,30 @@ export class EditModal {
video: { ...original },
});
}
setSubmitState({ pending = false, video } = {}) {
const nextPending = Boolean(pending);
if (nextPending) {
this.pendingSubmitVideo = video || this.activeVideo || this.pendingSubmitVideo;
} else {
this.pendingSubmitVideo = null;
}
this.pendingSubmit = nextPending;
this.updateSubmitButtonState();
}
updateSubmitButtonState() {
if (!this.submitButton) {
return;
}
if (this.pendingSubmit) {
this.submitButton.disabled = true;
this.submitButton.setAttribute("disabled", "disabled");
} else {
this.submitButton.disabled = false;
this.submitButton.removeAttribute("disabled");
}
}
}
+143
View File
@@ -0,0 +1,143 @@
import test, { beforeEach, afterEach } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { JSDOM } from 'jsdom';
import { EditModal } from '../js/ui/components/EditModal.js';
const editModalHtml = await readFile(
new URL('../components/edit-video-modal.html', import.meta.url),
'utf8',
);
let dom;
let container;
beforeEach(() => {
dom = new JSDOM(
'<!DOCTYPE html><html><body><div id="modalContainer"></div></body></html>',
{
url: 'https://example.com',
pretendToBeVisual: true,
},
);
global.window = dom.window;
global.document = dom.window.document;
global.HTMLElement = dom.window.HTMLElement;
global.HTMLInputElement = dom.window.HTMLInputElement;
global.HTMLButtonElement = dom.window.HTMLButtonElement;
global.CustomEvent = dom.window.CustomEvent;
global.Event = dom.window.Event;
global.EventTarget = dom.window.EventTarget;
container = document.getElementById('modalContainer');
global.fetch = async (resource) => {
if (
typeof resource === 'string' &&
resource.includes('components/edit-video-modal.html')
) {
return {
ok: true,
status: 200,
text: async () => editModalHtml,
};
}
throw new Error(`Unexpected fetch: ${resource}`);
};
});
afterEach(() => {
delete global.fetch;
delete global.EventTarget;
delete global.Event;
delete global.CustomEvent;
delete global.HTMLButtonElement;
delete global.HTMLInputElement;
delete global.HTMLElement;
delete global.document;
delete global.window;
if (dom) {
dom.window.close();
dom = null;
}
container = null;
});
function createModal({
showError,
eventTarget = new EventTarget(),
} = {}) {
return new EditModal({
removeTrackingScripts: () => {},
setGlobalModalState: () => {},
showError: showError || (() => {}),
getMode: () => 'live',
eventTarget,
container,
});
}
test('ignores additional submissions while pending without spurious errors', async () => {
const errorMessages = [];
const modalEvents = new EventTarget();
const modal = createModal({
showError: (message) => {
if (message) {
errorMessages.push(message);
}
},
eventTarget: modalEvents,
});
await modal.load({ container });
const video = {
id: 'video1',
pubkey: 'pubkey1',
title: 'Sample video',
url: 'https://example.com/video.mp4',
thumbnail: '',
description: '',
enableComments: true,
isPrivate: false,
};
await modal.open(video);
assert.ok(modal.submitButton, 'expected submit button to be cached');
assert.equal(modal.submitButton.disabled, false);
const submissions = [];
modal.addEventListener('video:edit-submit', (event) => {
submissions.push(event.detail);
});
modal.submit();
assert.equal(submissions.length, 1, 'first submit should emit once');
assert.equal(modal.pendingSubmit, true, 'pending flag should be set after submit');
assert.equal(
modal.pendingSubmitVideo?.id,
video.id,
'pending video context should match the edited video',
);
assert.equal(modal.submitButton.disabled, true, 'button should disable while pending');
modal.submit();
assert.equal(submissions.length, 1, 'duplicate submit should be ignored');
assert.equal(
errorMessages.includes('No video selected for editing.'),
false,
'should not surface missing video error during pending submit',
);
modal.setSubmitState({ pending: false });
assert.equal(modal.pendingSubmit, false, 'pending flag should reset after release');
assert.equal(modal.submitButton.disabled, false, 'button should re-enable after release');
});