From cd677bf373e46f2c235f80b2ec86a7aac71289cd 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 958c3a3a20..ad44f5de0c 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(),
},
@@ -1034,6 +1040,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 7f1950eb50..bd1e5ee33e 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') }}
+
+
+
+
+
+
+
@@ -275,6 +284,7 @@ const document = ref(null)
const legalInformation = ref(loadState('libresign', 'legal_information', ''))
const clickedValidate = ref(false)
const getUUID = ref(false)
+const isDraggingOver = ref(false)
const validationStatusOpenState = ref({})
const extensionsOpenState = ref({})
const tsaOpenState = ref({})
@@ -384,6 +394,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')
}
@@ -892,6 +938,7 @@ defineExpose({
legalInformation,
clickedValidate,
getUUID,
+ isDraggingOver,
EXPIRATION_WARNING_DAYS,
validationStatusOpenState,
extensionsOpenState,
@@ -916,6 +963,10 @@ defineExpose({
crlStatusMap,
upload,
uploadFile,
+ isPdfFile,
+ handleDragOver,
+ handleDragLeave,
+ handleFileDrop,
dateFromSqlAnsi,
getSignerStatus,
validate,
@@ -1028,6 +1079,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 de706edeecc1701d3c959181aa0fbae859a7b75e 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 bd1e5ee33e..6d9c978136 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') }}
From 0ff635e3ee928d5ec89ed009a096ed9980d381be Mon Sep 17 00:00:00 2001
From: Vitor Mattos
Date: Tue, 21 Jul 2026 17:02:19 -0300
Subject: [PATCH 3/5] chore: Update src/views/Validation.vue
Signed-off-by: Vitor Mattos
---
src/views/Validation.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/views/Validation.vue b/src/views/Validation.vue
index 6d9c978136..401524d17e 100644
--- a/src/views/Validation.vue
+++ b/src/views/Validation.vue
@@ -35,7 +35,7 @@
-
+
{{ t('libresign', 'From UUID') }}
From 4885981b9f2bb572bce01b77c11c1b74c7b4c300 Mon Sep 17 00:00:00 2001
From: Vitor Mattos
Date: Tue, 21 Jul 2026 17:05:59 -0300
Subject: [PATCH 4/5] chore: Update src/views/Validation.vue
Signed-off-by: Vitor Mattos
---
src/views/Validation.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/views/Validation.vue b/src/views/Validation.vue
index 401524d17e..de982ec65b 100644
--- a/src/views/Validation.vue
+++ b/src/views/Validation.vue
@@ -43,7 +43,7 @@
-
+
{{ t('libresign', 'Upload') }}
From 5fcdcee56c74a10b2737464d1e5c9d36912d8d27 Mon Sep 17 00:00:00 2001
From: Vitor Mattos
Date: Tue, 21 Jul 2026 17:10:15 -0300
Subject: [PATCH 5/5] chore: Apply suggestion from @vitormattos
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Signed-off-by: Vitor Mattos
---
src/views/Validation.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/views/Validation.vue b/src/views/Validation.vue
index de982ec65b..6bef2b2bfe 100644
--- a/src/views/Validation.vue
+++ b/src/views/Validation.vue
@@ -30,7 +30,7 @@
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleFileDrop">
-
+
{{ t('libresign', 'Drag and drop a PDF here, or use the buttons below.') }}