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') }}
From 5e87b3dbb2a6c9ed040fb9fc2cd57cfa0d7c24c3 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 56df2d6cd6..a1934ab44a 100644
--- a/src/views/Validation.vue
+++ b/src/views/Validation.vue
@@ -35,7 +35,7 @@
-
+
{{ t('libresign', 'From UUID') }}
From 93c9e7a5aa0e51eb994fa4473b9722e73e030b7f 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 a1934ab44a..2795dfd7a1 100644
--- a/src/views/Validation.vue
+++ b/src/views/Validation.vue
@@ -43,7 +43,7 @@
-
+
{{ t('libresign', 'Upload') }}
From 8cbbefd8f0e8980d84dabdeff500774734f4796d 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 2795dfd7a1..ad9d9f73e3 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.') }}