From 7adf87045b3e1eb913f8878f78e5668f9675a6c0 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sat, 18 Jul 2026 07:27:10 -0700 Subject: [PATCH 1/5] feat(validation): support drag-and-drop PDF upload on the Validation page Signed-off-by: Sai Asish Y --- src/tests/views/Validation.spec.ts | 71 ++++++++++++++++++++ src/views/Validation.vue | 102 ++++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/src/tests/views/Validation.spec.ts b/src/tests/views/Validation.spec.ts index 1666ad38d3..da4201367a 100644 --- a/src/tests/views/Validation.spec.ts +++ b/src/tests/views/Validation.spec.ts @@ -24,7 +24,12 @@ type ValidationVm = { isAsyncSigning: boolean shouldFireAsyncConfetti: boolean isActiveView: boolean + isDraggingOver: boolean EXPIRATION_WARNING_DAYS: number + isPdfFile: (file: File) => boolean + handleDragOver: () => void + handleDragLeave: () => void + handleFileDrop: (event: Partial) => Promise isAfterSigned: boolean isEnvelope: boolean validationComponent: unknown @@ -63,6 +68,7 @@ vi.mock('@nextcloud/axios', () => ({ default: { get: vi.fn(), post: vi.fn(), + postForm: vi.fn(), put: vi.fn(), delete: vi.fn(), }, @@ -1077,6 +1083,71 @@ describe('Validation.vue - Business Logic', () => { }) }) + describe('drag-and-drop upload on the Validation page', () => { + const buildDropEvent = (files: File[]): Partial => ({ + dataTransfer: { files } as unknown as DataTransfer, + }) + + it('accepts a file whose mime type is application/pdf', () => { + const file = new File(['%PDF-1.7'], 'contract.pdf', { type: 'application/pdf' }) + expect(wrapper.vm.isPdfFile(file)).toBe(true) + }) + + it('accepts a .pdf file even when the browser reports no mime type', () => { + const file = new File(['%PDF-1.7'], 'contract.pdf', { type: '' }) + expect(wrapper.vm.isPdfFile(file)).toBe(true) + }) + + it('rejects a non-PDF file', () => { + const file = new File(['plain'], 'notes.txt', { type: 'text/plain' }) + expect(wrapper.vm.isPdfFile(file)).toBe(false) + }) + + it('marks the dropzone as active while a file is dragged over it', () => { + wrapper.vm.handleDragOver() + expect(wrapper.vm.isDraggingOver).toBe(true) + }) + + it('does not activate the dropzone while an upload is already in progress', async () => { + await setVmState({ loading: true }) + wrapper.vm.handleDragOver() + expect(wrapper.vm.isDraggingOver).toBe(false) + }) + + it('clears the active state when the drag leaves the dropzone', async () => { + await setVmState({ isDraggingOver: true }) + wrapper.vm.handleDragLeave() + expect(wrapper.vm.isDraggingOver).toBe(false) + }) + + it('validates a dropped PDF through the same endpoint as the upload button', async () => { + vi.mocked(axios.postForm).mockResolvedValueOnce({ data: { ocs: { data: {} } } }) + const file = new File(['%PDF-1.7'], 'contract.pdf', { type: 'application/pdf' }) + + await wrapper.vm.handleFileDrop(buildDropEvent([file])) + + expect(axios.postForm).toHaveBeenCalledOnce() + const formData = vi.mocked(axios.postForm).mock.calls[0][1] as FormData + expect(formData.get('file')).toBe(file) + expect(wrapper.vm.isDraggingOver).toBe(false) + expect(wrapper.vm.loading).toBe(false) + }) + + it('rejects a dropped non-PDF file without calling the validate endpoint', async () => { + const file = new File(['plain'], 'notes.txt', { type: 'text/plain' }) + + await wrapper.vm.handleFileDrop(buildDropEvent([file])) + + expect(axios.postForm).not.toHaveBeenCalled() + expect(wrapper.vm.validationErrorMessage).toBe('Please upload a PDF file') + }) + + it('ignores a drop that carries no file', async () => { + await wrapper.vm.handleFileDrop(buildDropEvent([])) + expect(axios.postForm).not.toHaveBeenCalled() + }) + }) + describe('status contract guards', () => { const createLoadedValidationDocument = (patch: Record = {}) => ({ id: 100, diff --git a/src/views/Validation.vue b/src/views/Validation.vue index 1608fd7e83..c341ddda23 100644 --- a/src/views/Validation.vue +++ b/src/views/Validation.vue @@ -24,23 +24,32 @@ {{ validationErrorMessage }} - - - - {{ t('libresign', 'From UUID') }} - - - - {{ t('libresign', 'Upload') }} - - - +
+

+ {{ t('libresign', 'Drag and drop a PDF here, or use the buttons below.') }} +

+ + + + {{ t('libresign', 'From UUID') }} + + + + {{ t('libresign', 'Upload') }} + + + +
@@ -302,6 +311,7 @@ const legalInformation = computed(() => { }) const clickedValidate = ref(false) const getUUID = ref(false) +const isDraggingOver = ref(false) const validationStatusOpenState = ref({}) const extensionsOpenState = ref({}) const tsaOpenState = ref({}) @@ -411,6 +421,42 @@ async function uploadFile() { input.click() } +function isPdfFile(file: File) { + return file.type === 'application/pdf' || /\.pdf$/i.test(file.name) +} + +function handleDragOver() { + if (loading.value) { + return + } + isDraggingOver.value = true +} + +function handleDragLeave() { + isDraggingOver.value = false +} + +async function handleFileDrop(event: DragEvent) { + isDraggingOver.value = false + if (loading.value) { + return + } + + const file = event.dataTransfer?.files?.[0] + if (!file) { + return + } + + if (!isPdfFile(file)) { + setValidationError(t('libresign', 'Please upload a PDF file')) + return + } + + loading.value = true + await upload(file) + loading.value = false +} + function dateFromSqlAnsi(date: string) { return Moment(Date.parse(date)).format('LL LTS') } @@ -923,6 +969,7 @@ defineExpose({ legalInformation, clickedValidate, getUUID, + isDraggingOver, EXPIRATION_WARNING_DAYS, validationStatusOpenState, extensionsOpenState, @@ -947,6 +994,10 @@ defineExpose({ crlStatusMap, upload, uploadFile, + isPdfFile, + handleDragOver, + handleDragLeave, + handleFileDrop, dateFromSqlAnsi, getSignerStatus, validate, @@ -1059,6 +1110,23 @@ defineExpose({ float: inline-end; align-self: flex-end; } + .upload-dropzone { + border: 2px dashed var(--color-border-dark); + border-radius: var(--border-radius-large); + padding: 20px; + transition: border-color 0.2s ease, background-color 0.2s ease; + + &--dragover { + border-color: var(--color-primary-element); + background-color: var(--color-primary-element-light); + } + + &__hint { + margin: 0 0 12px; + text-align: center; + color: var(--color-text-maxcontrast); + } + } .infor-container { width: 100%; margin: auto 0; From 4e06d33961be03f37c0c89fb818e78af71cf6416 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sat, 18 Jul 2026 10:35:41 -0700 Subject: [PATCH 2/5] feat(validation): add TRANSLATORS hints for drop-zone and upload strings Signed-off-by: Sai Asish Y --- src/views/Validation.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/Validation.vue b/src/views/Validation.vue index c341ddda23..56df2d6cd6 100644 --- a/src/views/Validation.vue +++ b/src/views/Validation.vue @@ -30,6 +30,7 @@ @dragleave.prevent="handleDragLeave" @drop.prevent="handleFileDrop">

+ {{ t('libresign', 'Drag and drop a PDF here, or use the buttons below.') }}

@@ -42,6 +43,7 @@ + {{ t('libresign', 'Upload') }}