Repository navigation
Show signature rejection information on the validation page #8162
Description
Update the validation page to support the signature rejection workflow defined by #7832.
Backend rejection support was added by #8159.
This issue is blocked by #8388, which defines the signer visibility and presentation contract used by the validation page.
#8161 is related, but it is not a technical blocker.
Goal
Make the validation page correctly represent:
- visible rejection;
- rejection date and comment when available;
- hidden signer state without discovering which signer rejected;
- canceled workflows;
- workflows that continue after rejection;
- normal files and envelopes.
The backend is the source of truth for visibility.
The frontend must render the response without reconstructing hidden workflow state.
Backend contract
#8388 introduces a separate presentation state:
displayStatus
Possible values are:
draft
ready_to_sign
signed
rejected
not_signed
displayStatus is not a workflow enum.
The real signer workflow state continues to use SignRequestStatus.
Visible rejection
A visible rejected signer may be returned as:
{
"status": 3,
"displayStatus": "rejected",
"rejection": {
"rejectedAt": "2026-09-09T12:00:00+00:00"
}
}With a visible comment:
{
"status": 3,
"displayStatus": "rejected",
"rejection": {
"rejectedAt": "2026-09-09T12:00:00+00:00",
"comment": "I do not agree with this document",
"commentPrivate": false
}
}For a privileged viewer, a private comment may also be returned.
Hidden rejection
When a file contains a rejection that the current viewer cannot see, every unsigned signer in that file receives:
{
"displayStatus": "not_signed"
}This may represent a signer whose real state is:
DRAFT;ABLE_TO_SIGN;REJECTED.
The frontend must not determine which real state is behind not_signed.
Signed signers continue to receive:
displayStatus: "signed"
Status constants
Add the missing real workflow constants:
FILE_STATUS.CANCELED = 6
SIGN_REQUEST_STATUS.REJECTED = 3These values must stay synchronized with:
lib/Enum/FileStatus.php;lib/Enum/SignRequestStatus.php.
Do not add not_signed to SIGN_REQUEST_STATUS.
It belongs only to the response presentation type.
TypeScript contract
Update:
src/services/validationDocument.ts
and related/generated types to support:
type SignerDisplayStatus =
| 'draft'
| 'ready_to_sign'
| 'signed'
| 'rejected'
| 'not_signed'The validation parser must accept the displayStatus field and continue validating all other values.
Do not loosen validation to arbitrary strings.
Support:
FileStatus::CANCELED;SignRequestStatus::REJECTEDwhen exposed;- optional
rejection; - the defined
displayStatusvalues.
File status presentation
Update the shared file status helpers so:
FILE_STATUS.CANCELED
renders as:
Canceled
and never as Unknown.
Reuse the existing shared status infrastructure.
Normal file validation
Use the existing components:
src/components/validation/DocumentValidationDetails.vue
src/components/validation/SignerDetails.vue
Do not create a second signer component only for rejection.
Signed
When:
displayStatus === "signed"
keep the existing signed presentation.
Pending
When:
displayStatus === "ready_to_sign"
keep the current pending presentation.
Draft
When:
displayStatus === "draft"
keep the appropriate existing unsigned presentation.
Rejected
When:
displayStatus === "rejected"
show:
Rejected
instead of Not signed yet.
When available:
- show
rejection.rejectedAt; - show
rejection.comment; - indicate a visible private comment when
commentPrivate === true.
Do not show certificate information for rejected signers.
Not signed
When:
displayStatus === "not_signed"
show:
Not signed
using the frontend translation system.
This state is intentionally neutral.
Do not infer whether the signer:
- is pending;
- is draft;
- rejected;
- can sign.
Do not inspect another field to reconstruct the hidden state.
Canceled workflow
When:
document.status === FILE_STATUS.CANCELED
show the workflow as:
Canceled
Every signer that has not signed must be presented consistently as:
No longer able to sign
This applies regardless of whether their displayStatus is:
draft
ready_to_sign
not_signed
rejected
if rejection itself is not visible.
Signed signers keep their signed presentation.
Do not render not_signed differently from another unsigned signer in a canceled workflow, because that could identify the rejecting signer.
Workflow continues after rejection
When the document is not canceled and rejection is visible:
- rejected signer shows
Rejected; - other eligible signers keep their normal presentation;
- workflow remains active.
When rejection is hidden:
- unsigned signers may all have
displayStatus: "not_signed"; - render all of them as
Not signed; - do not try to recover
ready_to_sign; - do not identify the rejecting signer.
This loss of detail is intentional and required for privacy.
Envelope validation
Apply the same presentation rules to envelopes.
EnvelopeValidation.vue has its own signer summary, so updating only SignerDetails.vue is not sufficient.
For envelope validation:
- support
displayStatus; - support visible rejection;
- support
not_signed; - support canceled child files;
- keep signed signer progress working;
- do not identify a hidden rejecting signer through differences between unsigned entries.
Normal files and envelopes must consume the same backend contract.
Privacy boundary
The frontend must not evaluate backend rejection policy.
Do not:
- evaluate
public_status; - evaluate
show_comment_on_validation; - decide whether comments should be visible;
- use
commentPrivateto decide authorization; - derive real
SignRequestStatusfromdisplayStatus; - infer rejection from
not_signed; - infer rejection from
FILE_STATUS.CANCELED; - infer which signer caused cancellation.
If the backend response leaks private information, fix #8388 instead of hiding it only in Vue.
Existing list/sidebar UI
file/list and sidebar components may also receive displayStatus after #8388.
Changing those interfaces is not part of this issue unless the backend contract introduces a regression.
#8162 remains focused on validation-page behavior.
Main implementation areas
Review at least:
src/constants.js;src/utils/fileStatus.js;src/services/validationDocument.ts;- generated/related TypeScript types;
src/components/validation/SignerDetails.vue;src/components/validation/DocumentValidationDetails.vue;src/components/validation/EnvelopeValidation.vue;- related frontend tests.
Tests
This behavior must be covered by frontend automated tests.
Parser
Test that:
- every valid
displayStatusis accepted; - arbitrary
displayStatusstrings are rejected; SIGN_REQUEST_STATUS.REJECTEDis accepted;FILE_STATUS.CANCELEDis accepted;- visible rejection without comment is accepted;
- public comment is accepted;
- private visible comment is accepted;
- malformed rejection data is rejected.
Display status
Test each presentation mapping:
draft -> existing draft/unsigned presentation
ready_to_sign -> existing pending presentation
signed -> existing signed presentation
rejected -> Rejected
not_signed -> Not signed
Rejected signer
Test:
Rejectedlabel;- rejection date;
- visible rejection comment;
- visible private marker;
- no signed certificate presentation.
Hidden rejection
With multiple unsigned signers using:
displayStatus: "not_signed"
test that:
- each receives the same presentation;
- no rejection UI is shown;
- no signer is presented as
Ready to sign; - the frontend does not attempt to identify which signer rejected.
Canceled workflow
Test that:
- workflow shows
Canceled; - every non-signed signer shows
No longer able to sign; draft,ready_to_sign, andnot_signedget the same canceled presentation;- signed signers remain signed;
- no UI difference reveals the rejecting signer.
Continued workflow
Visible rejection:
- rejected signer shows
Rejected; - eligible signers keep normal state;
- workflow remains active.
Hidden rejection:
- all redacted unsigned signers show
Not signed; - no signer is identified as the rejecter;
- frontend does not reconstruct
ready_to_sign.
Envelope
Cover:
- normal signed/pending progress;
- visible rejection;
- hidden rejection;
- multiple
not_signedsigners; - canceled workflow;
- continued workflow;
- visible rejection comment.
Use fixtures generated from the final backend contract implemented by #8388.
Do not reproduce backend policy logic in frontend tests.
Regression
Existing validation must continue to work for:
- unsigned documents without rejection;
- partially signed documents;
- fully signed documents;
- normal files;
- envelopes;
- anonymous validation;
- authenticated validation.
Quality gates
The implementation must pass:
- ESLint;
- type checking;
- frontend tests.
Out of scope
This issue does not implement:
- rejection persistence;
- rejection authorization;
- rejection visibility decisions;
- Policy Workbench configuration;
- requester rejection configuration;
- signer Reject action/dialog;
- general sidebar or file-list redesign.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Start date
Target date
Projects
- StatusShow more project fields4. Done
Activity
maia-andre commentedon Sep 13, 2026
I would like to take this one. Before touching the page I checked the precondition this issue states for the backend ("rejection cannot be discovered through
status/statusTextwhenpublic_statusdoes not allow it"), onmainafter #8257, with two signers (joao,maria), the requesteradmin, and the system policy{"enabled":true,"comment_mode":"optional","cancel_workflow":false,"public_status":false,"show_comment_on_validation":false}.joaorejects with a public comment. Then:GET /file/validate/uuid/{uuid}—joaoentryGET /file/list?details=1—joaoentryadmin(requester)status: 3,statusText: "Rejected", norejectionstatus: 3,rejection: {rejectedAt, comment, commentPrivate: false}joao(who rejected,me: true)status: 3, norejectionmaria(other signer)status: 3,"Rejected", norejectionstatus: 3,"Rejected", norejectionstatus: 3,"Rejected", norejectionWith
cancel_workflow: truethe anonymous validation payload isfile.status: 6 "Canceled",joao: 3 "Rejected",maria: 1 "Ready to sign".So the precondition is not met, and there are two distinct gaps in the backend contract:
rejection.SignatureRejectionVisibilityService::buildSignerRejection()is only wired intoFileListService(list/sidebar). The validation page is built byFileService→File\SignersLoader::loadLibreSignSigners(), which copiesstatus/statusTextstraight from the entity and knows nothing about rejection. Even the requester gets nothing on the page this issue is about.status/statusTextdisclose the rejection to everyone, on both payloads. The visibility service hides therejectionobject, but3/ Rejected stays; the frontend cannot fix that without inferring, which this issue forbids.Neither is covered by tests today:
tests/integration/features/sign/reject.featureonly asserts onfile/list, and every scenario that reads as another signer usespublic_status: true.Proposal
Two PRs under this issue, backend first, so the page is built on the contract the issue describes:
PR 1 — backend contract (small)
SignersLoaderbuildsrejectionthrough the existingSignatureRejectionVisibilityService, privileged when the entry ismeor the viewer is the file's requester — the same ruleFileListServiceapplies.REJECTEDand the viewer is not allowed to see it, the signer is presented with a maskedstatus/statusText— inSignersLoaderand in bothFileListServiceformatters, through one method on the visibility service so the rule lives in one place.public_status: false(other signer and anonymous get norejectionand no rejected status; requester and the rejecting signer do), withpublic_status: true, withshow_comment_on_validation, and withcancel_workflow. Unit tests for the loader and the new visibility method.PR 2 — this issue (frontend):
SignerDetails/FileValidationrenderrejection(state, timestamp, comment, private marker for privileged viewers), the canceled workflow (file.status === 6: unsigned signers shown as no longer able to sign, no hint of who caused it), and the continued workflow; tests as listed in the issue.Two decisions I need from you
1/ Ready to sign is the pre-rejection state and needs no new value inLibresignSignerSummary.status: 0|1|2|3. On a canceled workflow the page would then show every unsigned signer the same way ("no longer able to sign", FR7 of Configurable signature rejection workflow #7832), which is exactly the non-disclosure this issue asks for. The alternative is a new explicit state (e.g.closed), which is a contract change and a hint that something happened. I lean to masking to1, and to leavingfile.status: 6 "Canceled"public as it is today.vitormattos commentedon Sep 13, 2026
@maia-andre thanks for checking this in detail.
I split the backend visibility fix into #8388 and updated #8162 to depend on that contract. I also adjusted the validation issue to handle the redacted signer state without duplicating privacy rules in the frontend.
Could you review both issues and point out anything that still looks unclear or inconsistent before implementation starts?
maia-andre commentedon Sep 13, 2026
Thanks — read both against the code. The split and the rule "one shared visibility decision, consumed by every serializer" are clear, and the response paths listed in #8388 match what I found (
SignersLoader,FileListService×2,FileService::mapSignerDetailsToSummary()— which today maps3toDRAFTthroughnormalizeSignerSummaryStatus()— andEnvelopeAssembler, which already receivesFileResponseOptions, so the privileged decision can come fromgetMe()/getSignRequest()/isSignerIdentified()there too). Two things look inconsistent to me, plus a few small ones.1. The redacted representation still identifies the signer who rejected
Today rejection is the only reason a signer status is ever redacted. So for any reader who knows the contract (which #8162 documents),
status: nullmeans "this signer rejected and I am not allowed to see it" — the redaction hides the timestamp and the comment, but not the fact. Two concrete cases:cancel_workflow: false,public_status: false):mariais1 "Ready to sign",joaoisnull "Not signed".joaois singled out by elimination.FILE_STATUS.CANCELED) while the redacted one stays Not signed. The different label alone identifies who rejected, which is exactly what both issues forbid ("must not be able to identify which signer rejected", "do not infer the rejecting signer from the canceled state").I see three ways to make the redaction actually non-disclosing; it is your call which one:
null/"Not signed", sonullno longer points at anyone. Signed signers are untouched. In a canceled workflow this costs nothing (they are all "no longer able to sign" anyway); in a continued workflow the public page loses "Ready to sign" for the pending signers of that one file, which seems an acceptable price for the privacy the policy promises.nullonly on the rejected signer and state in both issues that redaction hides when and why, not who. Cheaper, but thenpublic_status: falsedoes less than its name says.1). You already ruled this out because it misstates the workflow, so I only list it for completeness.I would go with (a). Whatever the choice, #8162's canceled-workflow section should render every unsigned signer the same way (the same label for
status: 1andstatus: nullwhendocument.status === CANCELED), otherwise the frontend reintroduces the leak.2. Small points on #8388
LibresignSignerSummary.status: 0|1|2|3is shared by the three contracts you list, and all three paths can return a redacted entry, so the type becomes0|1|2|3|nullin all of them (and in the generated TypeScript). Fine, just noting it is not "only where needed" in practice."Not signed"will be a new translated backend string produced by the visibility service (the current labels come fromSignRequestStatus::getLabel()), so the service needsIL10N. Trivial, but it means the label lives in the backend; see 3.file/listis also consumed by an authenticated signer who is not the requester (mariasees this file in her list). Herjoaoentry will be redacted there too; the list/sidebar components (Signer.vuechip usesstatusText;getSignRequestStatusText()switches onstatus) are outside Show signature rejection information on the validation page #8162's scope. I checked and they degrade to the backendstatusText, so nothing breaks, but it may be worth one line in Apply signature rejection visibility rules to signer status and validation API #8388's tests ("detailed file response as another signer") — which is already there — and one line in Show signature rejection information on the validation page #8162 saying the list is intentionally not touched.reject.featuretoday; the rest (requester, other signer) can extend the existing scenarios.3. Small points on #8162
main:SIGN_REQUEST_STATUShas noREJECTED,FILE_STATUShas noCANCELED,getStatusLabel(6)returns Unknown, andisSignerStatus()invalidationDocument.tsaccepts only0|1|2— so the items are right.statusTextor its ownt('libresign', 'Not signed')keyed onstatus === null.SignerDetails.vueprints its own Not signed yet for pending signers today, so the two labels will sit next to each other; I would key it onstatus === nullin the frontend and treatstatusTextas informative only, so the wording of the two states is decided in one place.DRAFT(0) counts as "remaining unsigned".Nothing else looks inconsistent to me. If it helps sequencing, I can take #8388 first (it is the contract #8162 needs, and I already have the reproduction set up), then #8162 on top of it.
vitormattos commentedon Sep 13, 2026
Good catch. I agree with option (a).
If a rejection is hidden, redacting only the rejected signer still identifies them by comparison. We should redact all unsigned signers of that file for that viewer, while keeping signed signers unchanged.
For a canceled workflow, the validation page should then render every non-signed signer the same way as no longer able to sign.
I will update #8388 and #8162 with this contract. You can take #8388 first, then #8162 on top of it.
vitormattos commentedon Sep 13, 2026
I updated #8388 and #8162 accordingly. The contract now uses a separate
displayStatus, and a hidden rejection makes all unsigned signers use the samenot_signedpresentation without exposing their real status.You can proceed with #8388 first, then #8162.
maia-andre commentedon Oct 2, 2026
Starting on this now that #8388 is closed by #8398, as agreed above.
Part of the groundwork already landed with #8398:
FILE_STATUS.CANCELED,SIGN_REQUEST_STATUS.REJECTEDand thedisplayStatustype are in place, and the validation parser acceptsREJECTED. What is still missing for this issue:src/utils/fileStatus.jshas noCANCELEDentry, so a canceled document still renders as Unknown;SignerDetails.vueonly knows signed / "Not signed yet" and does not readdisplayStatusorrejection;EnvelopeValidation.vuebuilds its own signer summary fromsigned/status, so it needs the same mapping;displayStatusvalues and for malformedrejectiondata.I'll keep to the contract in the issue: presentation keyed only on
displayStatusanddocument.status, every non-signed signer rendered the same way in a canceled workflow, and no field inspected to reconstruct a hidden state. Tests follow the list in the issue, with fixtures taken from real responses of the endpoint after #8398.One wording question: in a continued workflow, should a pending signer (
ready_to_sign) keep the current "Not signed yet" label? A hidden one (not_signed) would show "Not signed". The two labels are close enough that I'd like your confirmation they are meant to differ.feat(validation): show signature rejection on the validation page