mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
Merge pull request #366 from PR0M3TH3AN/codex/track-pending-submit-state-in-editmodal
Prevent duplicate edit submissions
This commit is contained in:
3 files changed
+192
-1
No files matched your search
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user