Skip to content

Commit af62ddd

Browse files
authored
Merge pull request #7447 from LibreSign/backport/7446/stable32
[stable32] fix(Sign): submit each envelope file independently with its UUID
2 parents 4877b39 + cfa05cf commit af62ddd

7 files changed

Lines changed: 1185 additions & 112 deletions

File tree

src/components/Draw/Draw.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
<script setup lang="ts">
3535
import { t } from '@nextcloud/l10n'
36-
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
36+
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
3737
3838
import {
3939
mdiCloudUpload,
@@ -116,8 +116,8 @@ function close() {
116116
}
117117
118118
async function save(base64: string) {
119-
signatureElementsStore.loadSignatures()
120119
await signatureElementsStore.save(props.type, base64)
120+
await nextTick()
121121
emit('save')
122122
close()
123123
}

src/services/signSubmit.ts

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type {
7+
FileUuidReferenceRecord,
8+
SignActionResponseRecord,
9+
SignerDetailRecord,
10+
SigningJobRecord,
11+
UserElementRecord,
12+
VisibleElementRecord,
13+
} from '../types/index'
14+
15+
export type SignResultData = Omit<Partial<SignActionResponseRecord>, 'file' | 'job'> & {
16+
file?: Partial<FileUuidReferenceRecord>
17+
job?: Partial<SigningJobRecord> & {
18+
file?: Partial<FileUuidReferenceRecord>
19+
}
20+
} & Record<string, unknown>
21+
22+
export type SignResult = {
23+
status: 'signingInProgress' | 'signed' | 'unknown'
24+
data: SignResultData
25+
}
26+
27+
export type SubmitSignaturePayload = {
28+
method?: string
29+
token?: string
30+
elements?: Array<{
31+
documentElementId: number
32+
profileNodeId?: number
33+
}>
34+
}
35+
36+
export type SignatureMethodConfig = {
37+
method?: string
38+
modalCode?: string
39+
token?: string
40+
}
41+
42+
export type VisibleSignatureElement = Partial<Pick<VisibleElementRecord, 'elementId' | 'signRequestId' | 'type'>>
43+
44+
export type SignatureProfileMap = Record<string, {
45+
file?: Partial<Pick<UserElementRecord['file'], 'nodeId' | 'url'>>
46+
} | undefined>
47+
48+
export type EnvelopeSigner = Omit<Partial<Pick<SignerDetailRecord, 'me' | 'signRequestId' | 'sign_request_uuid'>>, 'sign_request_uuid'> & {
49+
sign_request_uuid?: string | null
50+
}
51+
52+
type EnvelopeOwnSigner = EnvelopeSigner & {
53+
me: true
54+
signRequestId: number
55+
sign_request_uuid: string
56+
level: 'top' | 'file'
57+
}
58+
59+
export type EnvelopeFileForSubmission = {
60+
signers?: EnvelopeSigner[]
61+
}
62+
63+
export type SignDocumentForSubmission = {
64+
nodeType?: string
65+
signers?: EnvelopeSigner[]
66+
files?: EnvelopeFileForSubmission[]
67+
}
68+
69+
export type SignSubmissionAttempt = {
70+
result: SignResult
71+
signRequestUuid: string
72+
}
73+
74+
export type SignSubmissionOutcome =
75+
| {
76+
type: 'signed'
77+
payload: Record<string, unknown> & {
78+
signRequestUuid: string
79+
}
80+
}
81+
| {
82+
type: 'signing-started'
83+
payload: {
84+
signRequestUuid: string
85+
async: true
86+
}
87+
}
88+
| null
89+
90+
export type EnvelopeSubmitRequest = {
91+
signRequestUuid: string
92+
payload: SubmitSignaturePayload
93+
}
94+
95+
export function createBaseSubmitSignaturePayload(methodConfig: SignatureMethodConfig = {}): SubmitSignaturePayload {
96+
const payload: SubmitSignaturePayload = {
97+
method: methodConfig.method,
98+
}
99+
100+
if (methodConfig.token) {
101+
payload.token = methodConfig.token
102+
}
103+
104+
return payload
105+
}
106+
107+
export function buildSubmitSignaturePayload({
108+
basePayload,
109+
elements,
110+
canCreateSignature,
111+
signatures,
112+
}: {
113+
basePayload: SubmitSignaturePayload
114+
elements: VisibleSignatureElement[]
115+
canCreateSignature: boolean
116+
signatures: SignatureProfileMap
117+
}): SubmitSignaturePayload {
118+
const payload: SubmitSignaturePayload = { ...basePayload }
119+
const mappedElements = mapSubmitSignatureElements(elements, canCreateSignature, signatures)
120+
121+
if (mappedElements.length > 0) {
122+
payload.elements = mappedElements
123+
}
124+
125+
return payload
126+
}
127+
128+
export function getEnvelopeSubmitRequests({
129+
document,
130+
basePayload,
131+
elements,
132+
canCreateSignature,
133+
signatures,
134+
}: {
135+
document: SignDocumentForSubmission | null | undefined
136+
basePayload: SubmitSignaturePayload
137+
elements: VisibleSignatureElement[]
138+
canCreateSignature: boolean
139+
signatures: SignatureProfileMap
140+
}): EnvelopeSubmitRequest[] {
141+
if (document?.nodeType !== 'envelope') {
142+
return []
143+
}
144+
145+
const requests: EnvelopeSubmitRequest[] = []
146+
147+
for (const signer of getEnvelopeOwnSigners(document)) {
148+
if (!isOwnEnvelopeSigner(signer)) {
149+
continue
150+
}
151+
152+
const matchingElements = elements.filter((element) => element.signRequestId === signer.signRequestId)
153+
const signerElements = signer.level === 'top' && matchingElements.length === 0
154+
? elements
155+
: matchingElements
156+
requests.push({
157+
signRequestUuid: signer.sign_request_uuid,
158+
payload: buildSubmitSignaturePayload({
159+
basePayload,
160+
elements: signerElements,
161+
canCreateSignature,
162+
signatures,
163+
}),
164+
})
165+
}
166+
167+
return requests
168+
}
169+
170+
function getEnvelopeOwnSigners(document: SignDocumentForSubmission): Array<EnvelopeSigner & {
171+
me: true
172+
signRequestId: number
173+
sign_request_uuid: string
174+
level: 'top' | 'file'
175+
}> {
176+
const ownSigners: EnvelopeOwnSigner[] = []
177+
const seen = new Set<string>()
178+
179+
const addSigner = (signer: EnvelopeSigner, level: 'top' | 'file') => {
180+
if (!isOwnEnvelopeSigner(signer)) {
181+
return
182+
}
183+
184+
if (seen.has(signer.sign_request_uuid)) {
185+
return
186+
}
187+
188+
seen.add(signer.sign_request_uuid)
189+
ownSigners.push({
190+
...signer,
191+
level,
192+
})
193+
}
194+
195+
const topOwnSigners: EnvelopeSigner[] = []
196+
for (const signer of document.signers ?? []) {
197+
if (isOwnEnvelopeSigner(signer)) {
198+
topOwnSigners.push(signer)
199+
}
200+
}
201+
202+
if (topOwnSigners.length > 0) {
203+
for (const signer of topOwnSigners) {
204+
addSigner(signer, 'top')
205+
}
206+
207+
return ownSigners
208+
}
209+
210+
const fileOwnSigners: EnvelopeOwnSigner[] = []
211+
for (const file of document.files ?? []) {
212+
for (const signer of file.signers ?? []) {
213+
if (isOwnEnvelopeSigner(signer)) {
214+
fileOwnSigners.push({
215+
...signer,
216+
level: 'file',
217+
})
218+
}
219+
}
220+
}
221+
222+
if (fileOwnSigners.length > 0) {
223+
for (const signer of fileOwnSigners) {
224+
addSigner(signer, 'file')
225+
}
226+
227+
return ownSigners
228+
}
229+
230+
return ownSigners
231+
}
232+
233+
export function resolveSignSubmissionOutcome(attempts: SignSubmissionAttempt[]): SignSubmissionOutcome {
234+
let signedAttempt: SignSubmissionAttempt | null = null
235+
let signingInProgressAttempt: SignSubmissionAttempt | null = null
236+
237+
for (const attempt of attempts) {
238+
if (attempt.result.status === 'signed') {
239+
signedAttempt = attempt
240+
continue
241+
}
242+
243+
if (attempt.result.status === 'signingInProgress' && !signingInProgressAttempt) {
244+
signingInProgressAttempt = attempt
245+
}
246+
}
247+
248+
if (signedAttempt) {
249+
return {
250+
type: 'signed',
251+
payload: {
252+
...signedAttempt.result.data,
253+
signRequestUuid: resolveNavigationUuid(signedAttempt.result.data, signedAttempt.signRequestUuid),
254+
},
255+
}
256+
}
257+
258+
if (signingInProgressAttempt) {
259+
return {
260+
type: 'signing-started',
261+
payload: {
262+
signRequestUuid: resolveNavigationUuid(
263+
signingInProgressAttempt.result.data,
264+
signingInProgressAttempt.signRequestUuid,
265+
),
266+
async: true,
267+
},
268+
}
269+
}
270+
271+
return null
272+
}
273+
274+
export function resolveNavigationUuid(
275+
data: SignResultData | null | undefined,
276+
fallbackUuid: string,
277+
): string {
278+
if (typeof data?.file?.uuid === 'string' && data.file.uuid.length > 0) {
279+
return data.file.uuid
280+
}
281+
282+
if (typeof data?.job?.file?.uuid === 'string' && data.job.file.uuid.length > 0) {
283+
return data.job.file.uuid
284+
}
285+
286+
return fallbackUuid
287+
}
288+
289+
function mapSubmitSignatureElements(
290+
elements: VisibleSignatureElement[],
291+
canCreateSignature: boolean,
292+
signatures: SignatureProfileMap,
293+
): NonNullable<SubmitSignaturePayload['elements']> {
294+
const payloadElements: NonNullable<SubmitSignaturePayload['elements']> = []
295+
296+
for (const element of elements) {
297+
if (typeof element.elementId !== 'number') {
298+
continue
299+
}
300+
301+
const payloadElement: NonNullable<SubmitSignaturePayload['elements']>[number] = {
302+
documentElementId: element.elementId,
303+
}
304+
305+
if (canCreateSignature && element.type) {
306+
const profileNodeId = signatures[element.type]?.file?.nodeId
307+
if (typeof profileNodeId === 'number') {
308+
payloadElement.profileNodeId = profileNodeId
309+
}
310+
}
311+
312+
payloadElements.push(payloadElement)
313+
}
314+
315+
return payloadElements
316+
}
317+
318+
function isOwnEnvelopeSigner(signer: EnvelopeSigner): signer is EnvelopeSigner & {
319+
me: true
320+
signRequestId: number
321+
sign_request_uuid: string
322+
} {
323+
if (signer.me !== true) {
324+
return false
325+
}
326+
327+
if (typeof signer.signRequestId !== 'number') {
328+
return false
329+
}
330+
331+
return typeof signer.sign_request_uuid === 'string' && signer.sign_request_uuid.length > 0
332+
}

src/tests/components/Draw/Draw.spec.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ describe('Draw.vue', () => {
247247
expect(wrapper.emitted('close')).toBeTruthy()
248248
})
249249

250-
it('calls store loadSignatures when save is triggered', async () => {
250+
it('persists the signature without triggering a parallel reload', async () => {
251251
const wrapper = mountDraw({
252252
props: {
253253
type: 'signature',
@@ -259,16 +259,14 @@ describe('Draw.vue', () => {
259259

260260
await wrapper.vm.$nextTick()
261261

262-
const store = useSignatureElementsStore()
263-
const originalLoadSignatures = store.loadSignatures
264-
store.loadSignatures = vi.fn()
265-
store.save = vi.fn()
266-
267262
wrapper.vm.signatureElementsStore.loadSignatures = vi.fn()
268263
wrapper.vm.signatureElementsStore.save = vi.fn()
269264

270265
const base64Data = 'data:image/png;base64,test'
271266
await wrapper.vm.save(base64Data)
267+
268+
expect(wrapper.vm.signatureElementsStore.loadSignatures).not.toHaveBeenCalled()
269+
expect(wrapper.vm.signatureElementsStore.save).toHaveBeenCalledWith('signature', base64Data)
272270
})
273271

274272
it('emits save event after complete flow', async () => {
@@ -283,7 +281,6 @@ describe('Draw.vue', () => {
283281

284282
await wrapper.vm.$nextTick()
285283
const store = wrapper.vm.signatureElementsStore
286-
store.loadSignatures = vi.fn()
287284
store.save = vi.fn()
288285

289286
await wrapper.vm.save('data:image/png;base64,test')
@@ -303,7 +300,6 @@ describe('Draw.vue', () => {
303300

304301
await wrapper.vm.$nextTick()
305302
const store = wrapper.vm.signatureElementsStore
306-
store.loadSignatures = vi.fn()
307303
store.save = vi.fn()
308304

309305
const closeEmits = wrapper.emitted('close') || []

0 commit comments

Comments
 (0)