From 9dbd66f79435ceba12d82a2b6a838783f5cf6a01 Mon Sep 17 00:00:00 2001 From: Phillip Date: Sun, 29 Mar 2026 16:03:57 +0200 Subject: [PATCH 1/4] fix(mobile): fix signature placement and coordinate serialization Signed-off-by: Phillip --- src/components/PdfEditor/PdfEditor.vue | 75 +++++++++++++++++++--- src/components/Request/VisibleElements.vue | 36 +++++++---- src/store/files.js | 9 +-- vite.config.mjs | 21 ++++++ 4 files changed, 115 insertions(+), 26 deletions(-) diff --git a/src/components/PdfEditor/PdfEditor.vue b/src/components/PdfEditor/PdfEditor.vue index 7977e15559..e74bb566d2 100644 --- a/src/components/PdfEditor/PdfEditor.vue +++ b/src/components/PdfEditor/PdfEditor.vue @@ -127,6 +127,12 @@ type PdfElementsInstance = { selectedDocIndex?: number autoFitZoom?: boolean } +type PdfElementsRuntimeInstance = PdfElementsInstance & { + handleMouseMove?: (event: { type: string, touches: Array<{ clientX: number, clientY: number }> }) => void + finishAdding?: () => void + previewElement?: Record | null + previewVisible?: boolean +} defineOptions({ name: 'PdfEditor', @@ -155,6 +161,7 @@ const pdfElements = ref(null) const pendingAddedObjectCount = ref(null) let pendingAddCheckTimer: ReturnType | null = null +let pendingAddCheckRetries = 0 const ignoreClickOutsideSelectors = computed(() => ['.action-item__popper', '.action-item']) @@ -271,6 +278,7 @@ function clearPendingAddCheck() { clearTimeout(pendingAddCheckTimer) pendingAddCheckTimer = null } + pendingAddCheckRetries = 0 pendingAddedObjectCount.value = null } @@ -283,11 +291,29 @@ function checkSignerAdded() { pendingAddCheckTimer = null const isAddingMode = pdfElements.value?.isAddingMode === true const objectsAfter = getTotalObjectsCount() - pendingAddedObjectCount.value = null - if (!isAddingMode && objectsAfter > objectsBefore) { + if (objectsAfter > objectsBefore) { + clearPendingAddCheck() emit('pdf-editor:signer-added') + return + } + + // Fallback: once add mode ends, unblock the UI even if the object count + // comparison was not conclusive due timing/reactivity. + if (!isAddingMode) { + clearPendingAddCheck() + emit('pdf-editor:signer-added') + return } + + // Poll while the external component still processes placement. + if (pendingAddCheckRetries < 300) { + pendingAddCheckRetries++ + pendingAddCheckTimer = setTimeout(checkSignerAdded, 100) + return + } + + clearPendingAddCheck() } function scheduleSignerAddedCheck() { @@ -300,6 +326,38 @@ function scheduleSignerAddedCheck() { pendingAddCheckTimer = setTimeout(checkSignerAdded, 0) } +function handleDocumentTouchEnd(event: Event) { + if (pendingAddedObjectCount.value === null) { + return + } + + const instance = pdfElements.value as PdfElementsRuntimeInstance | null + const touchEvent = event as TouchEvent + const touchPoint = touchEvent.changedTouches?.[0] + if (!instance || !touchPoint) { + scheduleSignerAddedCheck() + return + } + + // Work around mobile tap placement timing in pdf-elements: touchend has no + // touches[0], so preview may never become visible on first tap. + if (instance.isAddingMode && instance.previewElement && !instance.previewVisible && instance.handleMouseMove) { + instance.handleMouseMove({ + type: 'touchmove', + touches: [{ clientX: touchPoint.clientX, clientY: touchPoint.clientY }], + }) + requestAnimationFrame(() => { + if (instance.isAddingMode) { + instance.finishAdding?.() + } + scheduleSignerAddedCheck() + }) + return + } + + scheduleSignerAddedCheck() +} + function startAddingSigner(signer: SignerSummaryRecord | SignerDetailRecord | null | undefined, size: { width?: number, height?: number }) { if (!pdfElements.value || !size?.width || !size?.height) { return false @@ -319,6 +377,11 @@ function startAddingSigner(signer: SignerSummaryRecord | SignerDetailRecord | nu signer: signerPayload, }) pendingAddedObjectCount.value = getTotalObjectsCount() + pendingAddCheckRetries = 0 + if (pendingAddCheckTimer !== null) { + clearTimeout(pendingAddCheckTimer) + } + pendingAddCheckTimer = setTimeout(checkSignerAdded, 100) return true } @@ -383,15 +446,11 @@ async function waitForPageRender(docIndex: number, pageIndex: number) { onMounted(() => { ensurePdfWorker() - document.addEventListener('mouseup', scheduleSignerAddedCheck) - document.addEventListener('touchend', scheduleSignerAddedCheck) - document.addEventListener('keyup', scheduleSignerAddedCheck) + document.addEventListener('touchend', handleDocumentTouchEnd) }) onBeforeUnmount(() => { - document.removeEventListener('mouseup', scheduleSignerAddedCheck) - document.removeEventListener('touchend', scheduleSignerAddedCheck) - document.removeEventListener('keyup', scheduleSignerAddedCheck) + document.removeEventListener('touchend', handleDocumentTouchEnd) clearPendingAddCheck() }) diff --git a/src/components/Request/VisibleElements.vue b/src/components/Request/VisibleElements.vue index 8ed4b506ee..f8ae524338 100644 --- a/src/components/Request/VisibleElements.vue +++ b/src/components/Request/VisibleElements.vue @@ -51,20 +51,22 @@ - - {{ t('libresign', 'Save') }} - - - - {{ t('libresign', 'Sign') }} - +
+ + {{ t('libresign', 'Save') }} + + + + {{ t('libresign', 'Sign') }} + +
{ } const coordinates = element.coordinates && typeof element.coordinates === 'object' ? { - x: element.coordinates.x, - y: element.coordinates.y, - w: element.coordinates.w, - h: element.coordinates.h, + page: element.coordinates.page, + top: element.coordinates.top, + left: element.coordinates.left, + width: element.coordinates.width, + height: element.coordinates.height, } : undefined return { diff --git a/vite.config.mjs b/vite.config.mjs index a7cdd0d3a0..20a767810a 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -6,6 +6,26 @@ import { createAppConfig } from '@nextcloud/vite-config' import { resolve } from 'node:path' +const patchPdfElementsTouchmovePassive = { + name: 'patch-pdf-elements-touchmove-passive', + enforce: 'pre', + transform(code, id) { + if (!id.includes('/@libresign/pdf-elements/')) { + return null + } + if (!id.endsWith('/dist/index.mjs') && !id.endsWith('/src/components/DraggableElement.vue')) { + return null + } + + const replaced = code.replace( + /window\.addEventListener\((['"])touchmove\1,\s*this\.boundHandleMove\)/g, + 'window.addEventListener($1touchmove$1, this.boundHandleMove, { passive: false })', + ) + + return replaced === code ? null : { code: replaced, map: null } + }, +} + export default createAppConfig({ main: resolve('src/main.ts'), init: resolve('src/init.ts'), @@ -25,6 +45,7 @@ export default createAppConfig({ }, }, plugins: [ + patchPdfElementsTouchmovePassive, { name: 'vue-devtools', config(_, { mode }) { From 2a184787ed54bffd326e40e4b48f7e31ae21dcc5 Mon Sep 17 00:00:00 2001 From: Phillip Date: Sun, 29 Mar 2026 17:04:47 +0200 Subject: [PATCH 2/4] fix(mobile): harden touch placement and pdf-elements runtime behavior Signed-off-by: Phillip --- src/components/PdfEditor/PdfEditor.vue | 32 +++++++++++++++----- vite.config.mjs | 41 ++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/components/PdfEditor/PdfEditor.vue b/src/components/PdfEditor/PdfEditor.vue index e74bb566d2..ea25ae193f 100644 --- a/src/components/PdfEditor/PdfEditor.vue +++ b/src/components/PdfEditor/PdfEditor.vue @@ -342,15 +342,20 @@ function handleDocumentTouchEnd(event: Event) { // Work around mobile tap placement timing in pdf-elements: touchend has no // touches[0], so preview may never become visible on first tap. if (instance.isAddingMode && instance.previewElement && !instance.previewVisible && instance.handleMouseMove) { + touchEvent.preventDefault?.() + touchEvent.stopImmediatePropagation?.() + instance.handleMouseMove({ type: 'touchmove', touches: [{ clientX: touchPoint.clientX, clientY: touchPoint.clientY }], }) requestAnimationFrame(() => { - if (instance.isAddingMode) { - instance.finishAdding?.() - } - scheduleSignerAddedCheck() + requestAnimationFrame(() => { + if (instance.isAddingMode) { + instance.finishAdding?.() + } + scheduleSignerAddedCheck() + }) }) return } @@ -492,13 +497,14 @@ defineExpose({ } .action-btn { - border: none; - background: transparent; - color: #ffffff; + border: 1px solid #cbd5e1; + background: #f8fafc; + color: #0f172a; padding: 4px; min-height: 0; min-width: 0; border-radius: 4px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); cursor: pointer; display: inline-flex; align-items: center; @@ -506,7 +512,17 @@ defineExpose({ transition: background 120ms ease; &:hover { - background: rgba(255, 255, 255, 0.1); + background: #e2e8f0; + } + + :deep(svg), + :deep(.icon-vue), + :deep(.material-design-icon), + :deep([class*='icon']) { + color: currentColor; + fill: currentColor; + stroke: currentColor; + opacity: 1; } } diff --git a/vite.config.mjs b/vite.config.mjs index 20a767810a..13e9b692fd 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -6,22 +6,53 @@ import { createAppConfig } from '@nextcloud/vite-config' import { resolve } from 'node:path' -const patchPdfElementsTouchmovePassive = { - name: 'patch-pdf-elements-touchmove-passive', +const patchPdfElementsRuntimeFixes = { + name: 'patch-pdf-elements-runtime-fixes', enforce: 'pre', transform(code, id) { if (!id.includes('/@libresign/pdf-elements/')) { return null } - if (!id.endsWith('/dist/index.mjs') && !id.endsWith('/src/components/DraggableElement.vue')) { + if (!id.endsWith('/dist/index.mjs') + && !id.endsWith('/src/components/DraggableElement.vue') + && !id.endsWith('/src/components/PDFElements.vue')) { return null } - const replaced = code.replace( + let replaced = code + + // Drag/resize listeners must be non-passive because handleMove calls preventDefault. + replaced = replaced.replace( /window\.addEventListener\((['"])touchmove\1,\s*this\.boundHandleMove\)/g, 'window.addEventListener($1touchmove$1, this.boundHandleMove, { passive: false })', ) + // Adding-mode touchmove also needs to be non-passive. + replaced = replaced.replace( + /document\.addEventListener\((['"])touchmove\1,\s*this\.handleMouseMove,\s*\{\s*passive:\s*(?:!0|true)\s*\}\)/g, + 'document.addEventListener($1touchmove$1, this.handleMouseMove, { passive: false })', + ) + + // Guard against race where add mode ends while RAF callback is still queued. + replaced = replaced.replace( + /const s = this\.pendingHoverClientPos;\s*if \(!s\) return;/g, + 'if (!this.isAddingMode || !this.previewElement) { this.pendingHoverClientPos = null; return; } const s = this.pendingHoverClientPos; if (!s) return;', + ) + + // Defensive access to preview element dimensions in async mobile flow. + replaced = replaced.replace(/this\.previewElement\.width/g, '(this.previewElement?.width || 0)') + replaced = replaced.replace(/this\.previewElement\.height/g, '(this.previewElement?.height || 0)') + + // Keep toolbar above by default, but place below when signature is near top. + replaced = replaced.replace( + /const e = this\.pagesScale \|\| 1, t = this\.mode === "drag", i = this\.mode === "resize", s = t \? this\.offsetX : 0, n = t \? this\.offsetY : 0, a = i \? this\.resizeOffsetX : 0, o = i \? this\.resizeOffsetY : 0, r = i \? this\.resizeOffsetW : 0, h = this\.object\.x \+ s \+ a, l = this\.object\.y \+ n \+ o, u = this\.object\.width \+ r, d = l - 60, g = d < 0 \? l \+ 8 : d;/g, + 'const e = this.pagesScale || 1, t = this.mode === "drag", i = this.mode === "resize", s = t ? this.offsetX : 0, n = t ? this.offsetY : 0, a = i ? this.resizeOffsetX : 0, o = i ? this.resizeOffsetY : 0, r = i ? this.resizeOffsetW : 0, h = i ? this.resizeOffsetH : 0, l = this.object.x + s + a, u = this.object.y + n + o, c = this.object.width + r, d = this.object.height + h, g = u * e < 72, f = g ? u + d : u, b = g ? "translate(-50%, 8px)" : "translate(-50%, calc(-100% - 8px))";', + ) + replaced = replaced.replace( + /left: `\$\{\(h \+ u \/ 2\) \* e\}px`,\s*top: `\$\{g \* e\}px`,\s*transform: "translateX\(-50%\)"/g, + 'left: `${(l + c / 2) * e}px`, top: `${f * e}px`, transform: b', + ) + return replaced === code ? null : { code: replaced, map: null } }, } @@ -45,7 +76,7 @@ export default createAppConfig({ }, }, plugins: [ - patchPdfElementsTouchmovePassive, + patchPdfElementsRuntimeFixes, { name: 'vue-devtools', config(_, { mode }) { From 64bfae686daf98111077119bd2bf61d5e510422f Mon Sep 17 00:00:00 2001 From: Phillip Date: Mon, 30 Mar 2026 22:17:53 +0200 Subject: [PATCH 3/4] test(mobile): increase coverage for signature placement flows Signed-off-by: Phillip --- .../components/PdfEditor/PdfEditor.spec.ts | 202 ++++++++++++++++++ .../Request/VisibleElements.spec.ts | 20 ++ 2 files changed, 222 insertions(+) diff --git a/src/tests/components/PdfEditor/PdfEditor.spec.ts b/src/tests/components/PdfEditor/PdfEditor.spec.ts index cf36e9760c..1da8719a5b 100644 --- a/src/tests/components/PdfEditor/PdfEditor.spec.ts +++ b/src/tests/components/PdfEditor/PdfEditor.spec.ts @@ -80,6 +80,9 @@ type PdfEditorVm = { totalPages: number isAddingMode: boolean }) => string + getTotalObjectsCount: () => number + checkSignerAdded: () => void + scheduleSignerAddedCheck: () => void setProps: (props: Record) => Promise } @@ -244,6 +247,15 @@ describe('PdfEditor Component - Business Rules', () => { expect(result).toBe(false) }) + it('returns false when signer payload cannot be built', () => { + const result = wrapper.vm.startAddingSigner( + null, + { width: 200, height: 100 }, + ) + + expect(result).toBe(false) + }) + it('returns true and starts adding when valid params', () => { const signer = { email: 'test@example.com' } const size = { width: 200, height: 100 } @@ -264,6 +276,196 @@ describe('PdfEditor Component - Business Rules', () => { }), ) }) + + it('restarts pending add timer when startAddingSigner is called twice', () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: true, + pdfDocuments: [{ allObjects: [[]] }], + }) + + wrapper.vm.startAddingSigner({ email: 'first@example.com' }, { width: 120, height: 60 }) + expect(vi.getTimerCount()).toBe(1) + wrapper.vm.startAddingSigner({ email: 'second@example.com' }, { width: 120, height: 60 }) + + expect(vi.getTimerCount()).toBe(1) + vi.useRealTimers() + }) + + it('emits signer-added when object count increases after adding mode starts', () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: true, + pdfDocuments: [{ allObjects: [[]] }], + }) + + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + getPdfElements().pdfDocuments = [{ allObjects: [[{ id: 'obj-1' }]] }] + wrapper.vm.checkSignerAdded() + + expect(wrapper.emitted('pdf-editor:signer-added')).toHaveLength(1) + vi.useRealTimers() + }) + + it('emits signer-added when adding mode finishes without object delta', () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: true, + pdfDocuments: [{ allObjects: [[]] }], + }) + + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + getPdfElements().isAddingMode = false + wrapper.vm.checkSignerAdded() + + expect(wrapper.emitted('pdf-editor:signer-added')).toHaveLength(1) + vi.useRealTimers() + }) + + it('keeps polling while adding mode is active and count has not changed', () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: true, + pdfDocuments: [{ allObjects: [[]] }], + }) + + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + vi.clearAllTimers() + wrapper.vm.checkSignerAdded() + + expect(wrapper.emitted('pdf-editor:signer-added')).toBeFalsy() + expect(vi.getTimerCount()).toBeGreaterThan(0) + vi.useRealTimers() + }) + + it('stops polling after retry limit without emitting signer-added', () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: true, + pdfDocuments: [{ allObjects: [[]] }], + }) + + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + vi.clearAllTimers() + for (let i = 0; i < 301; i++) { + wrapper.vm.checkSignerAdded() + vi.clearAllTimers() + } + + expect(wrapper.emitted('pdf-editor:signer-added')).toBeFalsy() + vi.useRealTimers() + }) + }) + + describe('RULE: touchend handling for mobile placement', () => { + it('ignores touchend when no signer placement is pending', () => { + const handleMouseMove = vi.fn() + Object.assign(getPdfElements(), { + handleMouseMove, + isAddingMode: true, + previewElement: { id: 'preview-1' }, + previewVisible: false, + }) + const event = new Event('touchend') + Object.defineProperty(event, 'changedTouches', { + value: [{ clientX: 10, clientY: 20 }], + configurable: true, + }) + + document.dispatchEvent(event) + + expect(handleMouseMove).not.toHaveBeenCalled() + expect(wrapper.emitted('pdf-editor:signer-added')).toBeFalsy() + }) + + it('schedules signer check when touchend has no touch point', async () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: false, + pdfDocuments: [{ allObjects: [[]] }], + }) + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + vi.clearAllTimers() + + document.dispatchEvent(new Event('touchend')) + await vi.runOnlyPendingTimersAsync() + + expect(wrapper.emitted('pdf-editor:signer-added')).toHaveLength(1) + vi.useRealTimers() + }) + + it('schedules signer check when pdf-elements instance is unavailable', async () => { + vi.useFakeTimers() + Object.assign(getPdfElements(), { + isAddingMode: false, + pdfDocuments: [{ allObjects: [[]] }], + }) + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + vi.clearAllTimers() + wrapper.vm.pdfElements = null + + const event = new Event('touchend') + Object.defineProperty(event, 'changedTouches', { + value: [{ clientX: 10, clientY: 20 }], + configurable: true, + }) + document.dispatchEvent(event) + await vi.runOnlyPendingTimersAsync() + + expect(wrapper.emitted('pdf-editor:signer-added')).toHaveLength(1) + vi.useRealTimers() + }) + + it('uses preview fallback on touchend and finalizes adding flow', async () => { + vi.useFakeTimers() + const runtime = Object.assign(getPdfElements(), { + isAddingMode: true, + previewElement: { id: 'preview-1' }, + previewVisible: false, + handleMouseMove: vi.fn(), + pdfDocuments: [{ allObjects: [[]] }], + }) + runtime.finishAdding = vi.fn(() => { + runtime.isAddingMode = false + }) + + wrapper.vm.startAddingSigner({ email: 'test@example.com' }, { width: 120, height: 60 }) + vi.clearAllTimers() + + const event = new Event('touchend') + const preventDefaultSpy = vi.spyOn(event, 'preventDefault') + const stopImmediatePropagationSpy = vi.spyOn(event, 'stopImmediatePropagation') + Object.defineProperty(event, 'changedTouches', { + value: [{ clientX: 44, clientY: 88 }], + configurable: true, + }) + document.dispatchEvent(event) + await vi.runAllTimersAsync() + + expect(preventDefaultSpy).toHaveBeenCalledTimes(1) + expect(stopImmediatePropagationSpy).toHaveBeenCalledTimes(1) + expect(runtime.handleMouseMove).toHaveBeenCalledWith({ + type: 'touchmove', + touches: [{ clientX: 44, clientY: 88 }], + }) + expect(runtime.finishAdding).toHaveBeenCalledTimes(1) + expect(wrapper.emitted('pdf-editor:signer-added')).toHaveLength(1) + vi.useRealTimers() + }) + }) + + describe('RULE: document listener lifecycle', () => { + it('registers and unregisters touchend listener on mount/unmount', () => { + wrapper.unmount() + const addEventListenerSpy = vi.spyOn(document, 'addEventListener') + const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') + const localWrapper = createWrapper() + const touchendCall = addEventListenerSpy.mock.calls.find(([eventName]) => eventName === 'touchend') + + expect(touchendCall).toBeTruthy() + localWrapper.unmount() + expect(removeEventListenerSpy).toHaveBeenCalledWith('touchend', touchendCall?.[1] as EventListener) + }) }) describe('RULE: addSigner coordinate calculations', () => { diff --git a/src/tests/components/Request/VisibleElements.spec.ts b/src/tests/components/Request/VisibleElements.spec.ts index 81cf879606..0846801b16 100644 --- a/src/tests/components/Request/VisibleElements.spec.ts +++ b/src/tests/components/Request/VisibleElements.spec.ts @@ -588,6 +588,26 @@ describe('VisibleElements Component - Business Rules', () => { expect(wrapper.vm.modal).toBe(false) }) + it('renders the sign-details action wrapper inside the modal sidebar', async () => { + const wrapperWithModalContent = mount(VisibleElements, { + global: { + stubs: { + NcModal: { template: '' }, + NcNoteCard: true, + NcChip: true, + NcButton: true, + NcLoadingIcon: true, + PdfEditor: true, + Signer: true, + }, + }, + }) as unknown as VisibleElementsWrapper + wrapperWithModalContent.vm.modal = true + await wrapperWithModalContent.vm.$nextTick() + + expect(wrapperWithModalContent.find('.sign-details__actions').exists()).toBe(true) + }) + it('closeModal resets all modal state', () => { wrapper.vm.modal = true wrapper.vm.elementsLoaded = true From 46b1265a32198ac8985b7c526572686d2e9e70de Mon Sep 17 00:00:00 2001 From: Phillip Date: Mon, 30 Mar 2026 22:45:24 +0200 Subject: [PATCH 4/4] fix(mobile): restore sign flow fallback for self-signer requests Signed-off-by: Phillip --- .../RightSidebar/RequestSignatureTab.vue | 19 +++++++++- src/store/files.js | 7 +++- .../RightSidebar/RequestSignatureTab.spec.ts | 28 +++++++++++++++ src/tests/store/files.spec.ts | 36 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/components/RightSidebar/RequestSignatureTab.vue b/src/components/RightSidebar/RequestSignatureTab.vue index 59838204ee..546b8f130e 100644 --- a/src/components/RightSidebar/RequestSignatureTab.vue +++ b/src/components/RightSidebar/RequestSignatureTab.vue @@ -866,6 +866,18 @@ function getValidationFileUuid() { return null } +function getSignRouteUuid() { + const file = filesStore.getFile() + const signer = file?.signers?.find((row: EditableRequestSigner) => row.me) || file?.signers?.[0] + const fromFile = [file?.signUuid, signer?.sign_uuid] + .find((value): value is string => typeof value === 'string' && value.length > 0) + const fromSettings = typeof file?.settings?.signerFileUuid === 'string' && file.settings.signerFileUuid.length > 0 + ? file.settings.signerFileUuid + : null + const fromState = loadState('libresign', 'sign_request_uuid', null) + return fromFile || fromSettings || (typeof fromState === 'string' && fromState.length > 0 ? fromState : null) +} + function validationFile() { const targetUuid = getValidationFileUuid() if (!targetUuid) { @@ -1067,7 +1079,11 @@ async function sign() { return } - const uuid = 'signUuid' in file ? file.signUuid : null + const uuid = getSignRouteUuid() + if (!uuid) { + showError(t('libresign', 'Signer request not found')) + return + } if (props.useModal) { const absoluteUrl = generateUrl('/apps/libresign/p/sign/{uuid}/pdf', { uuid }) const route = router.resolve({ name: 'SignPDFExternal', params: { uuid } }) @@ -1293,6 +1309,7 @@ defineExpose({ isSignElementsAvailable, closeModal, getValidationFileUuid, + getSignRouteUuid, validationFile, addSigner, editSigner, diff --git a/src/store/files.js b/src/store/files.js index 91cc3ae3a4..0f69789917 100644 --- a/src/store/files.js +++ b/src/store/files.js @@ -714,13 +714,18 @@ const _filesStore = defineStore('files', () => { const isSigned = (signer) => Array.isArray(signer.signed) ? signer.signed.length > 0 : !!signer.signed + const signerFileUuid = typeof selectedFile?.settings?.signerFileUuid === 'string' + ? selectedFile.settings.signerFileUuid + : '' const mySigners = selectedFile?.signers?.filter(signer => signer.me) || [] if (isFullSigned(selectedFile) || selectedFile.status <= 0 - || mySigners.length === 0 || mySigners.some((signer) => isSigned(signer))) { return false } + if (mySigners.length === 0) { + return signerFileUuid.length > 0 + } const flow = selectedFile?.signatureFlow const isOrderedNumeric = flow === 'ordered_numeric' || flow === 2 diff --git a/src/tests/components/RightSidebar/RequestSignatureTab.spec.ts b/src/tests/components/RightSidebar/RequestSignatureTab.spec.ts index 43746fb946..5f8222302c 100644 --- a/src/tests/components/RightSidebar/RequestSignatureTab.spec.ts +++ b/src/tests/components/RightSidebar/RequestSignatureTab.spec.ts @@ -407,6 +407,34 @@ describe('RequestSignatureTab - Critical Business Rules', () => { expect(generateUrlMock).toHaveBeenCalledWith('/apps/libresign/p/sign/{uuid}/pdf', { uuid: 'sign-uuid' }) expect(wrapper.vm.modalSrc).toBe('/apps/libresign/p/sign/sign-uuid/pdf') }) + + it('falls back to signerFileUuid for signing modal links when signUuid is missing', async () => { + await wrapper.setProps({ useModal: true }) + await updateFile({ + signUuid: null, + settings: { signerFileUuid: 'mobile-fallback-uuid' }, + }) + generateUrlMock.mockClear() + + await wrapper.vm.sign() + + expect(generateUrlMock).toHaveBeenCalledWith('/apps/libresign/p/sign/{uuid}/pdf', { uuid: 'mobile-fallback-uuid' }) + expect(wrapper.vm.modalSrc).toBe('/apps/libresign/p/sign/mobile-fallback-uuid/pdf') + }) + + it('falls back to signer sign_uuid when signUuid is missing', async () => { + await wrapper.setProps({ useModal: true }) + await updateFile({ + signUuid: null, + signers: [{ me: true, sign_uuid: 'signer-uuid-123' }], + }) + generateUrlMock.mockClear() + + await wrapper.vm.sign() + + expect(generateUrlMock).toHaveBeenCalledWith('/apps/libresign/p/sign/{uuid}/pdf', { uuid: 'signer-uuid-123' }) + expect(wrapper.vm.modalSrc).toBe('/apps/libresign/p/sign/signer-uuid-123/pdf') + }) }) describe('RULE: canEditSigningOrder when using ordered flow', () => { diff --git a/src/tests/store/files.spec.ts b/src/tests/store/files.spec.ts index 31a8abb8d6..508f7663ae 100644 --- a/src/tests/store/files.spec.ts +++ b/src/tests/store/files.spec.ts @@ -338,6 +338,42 @@ describe('files store - critical business rules', () => { expect(store.canSign()).toBe(true) }) + + it('allows signing when signer me flag is missing but signerFileUuid exists', () => { + const store = useFilesStore() + store.selectedFileId = 1 + store.files[1] = { + id: 1, + status: 1, + signatureFlow: 'parallel', + signers: [ + { me: false, signingOrder: 1, signed: [] }, + ], + settings: { + signerFileUuid: '8af5bd0b-0776-4533-8d57-8ee88ed1f6bf', + }, + } + + expect(store.canSign()).toBe(true) + }) + + it('blocks signing when signer me flag is missing and signerFileUuid is empty', () => { + const store = useFilesStore() + store.selectedFileId = 1 + store.files[1] = { + id: 1, + status: 1, + signatureFlow: 'parallel', + signers: [ + { me: false, signingOrder: 1, signed: [] }, + ], + settings: { + signerFileUuid: '', + }, + } + + expect(store.canSign()).toBe(false) + }) }) describe('RULE: adding signers respects document state', () => {