Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dependencies": {
"@base-ui/react": "^1.6.0",
"@dnd-kit/react": "^0.4.0",
"@polinetwork/backend": "^1.10.0",
"@polinetwork/backend": "^1.11.0",
"@t3-oss/env-core": "^0.13.10",
"@tanstack/charts": "1.1.0",
"@tanstack/react-charts": "1.1.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 44 additions & 3 deletions src/features/esign/request-page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link, useRouter } from "@tanstack/react-router"
import { Link, useNavigate, useRouter } from "@tanstack/react-router"
import { useServerFn } from "@tanstack/react-start"
import { Ban, Download, Files, FolderUp, Mail, RotateCw, X } from "lucide-react"
import { Ban, Download, Files, FolderUp, Mail, RotateCw, Trash2, X } from "lucide-react"
import { useEffect, useMemo, useState } from "react"

import {
Expand All @@ -17,14 +17,15 @@ import { appToast, PageBar, PageContent, useCan } from "@/components/shell"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import type { EsignDrive, EsignPdfInfo, EsignRequestDetail } from "@/lib/api/types"
import { errorHasCode } from "@/lib/errors"
import { formatDateTime } from "@/lib/format"
import { cn } from "@/lib/utils"

import { declineEsignRequest, getEsignDocumentContent, uploadSignedEsignDocument } from "./esign.functions"
import { PdfViewer, usePdfBytes } from "./pdf-viewer"
import { kindsLabel, type Place, pagesLabel, placesLeft } from "./places"
import { RequestStatusBadge, SignerStatusBadge, signedProgress, YouChip } from "./request-status"
import { cancelEsignRequest, retryEsignDelivery } from "./requests.functions"
import { cancelEsignRequest, deleteEsignRequest, retryEsignDelivery } from "./requests.functions"
import { SignWithCie } from "./sign-with-cie"
import { SignatureBox } from "./signature-box"

Expand Down Expand Up @@ -77,10 +78,12 @@ function downloadBytes(bytes: Uint8Array, fileName: string) {

export function EsignRequestPage({ detail }: { detail: Detail }) {
const router = useRouter()
const navigate = useNavigate()
const canManage = useCan("esign:requests:manage")
const contentFn = useServerFn(getEsignDocumentContent)
const declineFn = useServerFn(declineEsignRequest)
const cancelFn = useServerFn(cancelEsignRequest)
const deleteFn = useServerFn(deleteEsignRequest)
const retryFn = useServerFn(retryEsignDelivery)
const uploadSignedFn = useServerFn(uploadSignedEsignDocument)
const [uploadingSigned, setUploadingSigned] = useState(false)
Expand Down Expand Up @@ -149,6 +152,8 @@ export function EsignRequestPage({ detail }: { detail: Detail }) {
}, [request])

const canCancel = request.status === "in_progress" && canManage
// A request in progress is cancelled first; a pending delivery still needs the request.
const canDelete = request.status !== "in_progress" && request.delivery?.status !== "pending" && canManage
const pages = pdf?.pages ?? []

const [declineOpen, setDeclineOpen] = useState(false)
Expand Down Expand Up @@ -522,6 +527,42 @@ export function EsignRequestPage({ detail }: { detail: Detail }) {
}}
/>
)}

{canDelete && (
<ConfirmDialog
title="Delete request?"
description={
request.delivery?.driveItem
? "Its signers, activity and every version of the document are deleted. The copy saved to OneDrive stays."
: "Its signers, activity and every version of the document are deleted. Download the PDF first if you need it."
}
confirmLabel="Delete request"
tone="danger"
icon={Trash2}
trigger={
<Button variant="outline" className="h-10 self-start text-(--pn-danger-fg)">
<Trash2 aria-hidden /> Delete request
</Button>
}
onConfirm={async () => {
try {
await deleteFn({ data: { id: request.id } })
} catch (error) {
console.error(error)
// Already gone: the outcome the user asked for.
if (!errorHasCode(error, "NOT_FOUND")) throw error
}
try {
// Leave first: reloading this page would look the deleted request up again.
await navigate({ to: "/dashboard/esign/requests" })
await router.invalidate({ sync: true })
} catch (error) {
console.error(error)
}
appToast.success("Request deleted.")
}}
/>
)}
</div>
</div>
</PageContent>
Expand Down
5 changes: 5 additions & 0 deletions src/features/esign/requests.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export const cancelEsignRequest = createServerFn({ method: "POST" })
.validator(z.object({ id: z.uuid() }))
.handler(({ data, context }) => context.backend.esign.requests.cancel.mutate(data))

export const deleteEsignRequest = createServerFn({ method: "POST" })
.middleware([esignRequestsManageMiddleware])
.validator(z.object({ id: z.uuid() }))
.handler(({ data, context }) => context.backend.esign.requests.delete.mutate(data))

export const retryEsignDelivery = createServerFn({ method: "POST" })
.middleware([esignRequestsManageMiddleware])
.validator(z.object({ id: z.uuid() }))
Expand Down
1 change: 1 addition & 0 deletions tests/server-security.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ test("event handlers integrate protected server-function redirects with the rout
"src/features/esign/request-page.tsx": [
"declineEsignRequest",
"cancelEsignRequest",
"deleteEsignRequest",
"retryEsignDelivery",
"uploadSignedEsignDocument",
],
Expand Down
Loading