Fix document pages permissions handling
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -65,7 +65,7 @@ import {
|
||||
PdfDownloadDialog,
|
||||
type PdfDownloadDialogRef,
|
||||
} from "/components/documents/PdfDownloadDialog";
|
||||
import { use, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import type { NodeOf } from "/types.ts";
|
||||
import clsx from "clsx";
|
||||
import { PeopleSelectField } from "/components/form/PeopleSelectField";
|
||||
@@ -74,7 +74,6 @@ import { DocumentTypeOptions } from "/components/form/DocumentTypeOptions";
|
||||
import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions";
|
||||
import { z } from "zod";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<DocumentGraphNodeQuery>;
|
||||
@@ -90,6 +89,15 @@ const documentFragment = graphql`
|
||||
id
|
||||
fullName
|
||||
}
|
||||
canUpdate: permission(action: "core:document:update")
|
||||
canDelete: permission(action: "core:document:delete")
|
||||
canPublish: permission(action: "core:document-version:publish")
|
||||
# canSendSigningNotifications: permission(
|
||||
# action: "core:document:send-signing-notifications"
|
||||
# )
|
||||
# canRequestSignatures: permission(
|
||||
# action: "core:document-version:request-signature"
|
||||
# )
|
||||
...DocumentControlsTabFragment
|
||||
controlsInfo: controls(first: 0) {
|
||||
totalCount
|
||||
@@ -109,6 +117,9 @@ const documentFragment = graphql`
|
||||
id
|
||||
fullName
|
||||
}
|
||||
canDeleteDraft: permission(
|
||||
action: "core:document-version:delete-draft"
|
||||
)
|
||||
...DocumentSignaturesTab_version
|
||||
signatures(first: 1000)
|
||||
@connection(key: "DocumentDetailPage_signatures", filters: []) {
|
||||
@@ -176,6 +187,7 @@ const documentUpdateSchema = z.object({
|
||||
classification: z.enum(documentClassifications),
|
||||
});
|
||||
|
||||
// FIXME
|
||||
// const UserEmailQuery = graphql`
|
||||
// query DocumentDetailPageUserEmailQuery {
|
||||
// viewer {
|
||||
@@ -194,7 +206,6 @@ export default function DocumentDetailPage(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const navigate = useNavigate();
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [isEditingOwner, setIsEditingOwner] = useState(false);
|
||||
@@ -461,7 +472,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
]}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{isDraft && isAuthorized("Document", "publishDocumentVersion") && (
|
||||
{isDraft && document.canPublish && (
|
||||
<Button
|
||||
onClick={handlePublish}
|
||||
icon={IconCheckmark1}
|
||||
@@ -491,7 +502,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
</Dropdown>
|
||||
|
||||
<ActionDropdown variant="secondary">
|
||||
{isAuthorized("Document", "updateDocument") && (
|
||||
{document.canUpdate && (
|
||||
<DropdownItem
|
||||
onClick={() => updateDialogRef.current?.open()}
|
||||
icon={IconPencil}
|
||||
@@ -501,7 +512,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
)}
|
||||
{isDraft &&
|
||||
versions.length > 1 &&
|
||||
isAuthorized("Document", "deleteDraftDocumentVersion") && (
|
||||
currentVersion.canDeleteDraft && (
|
||||
<DropdownItem
|
||||
onClick={handleDeleteDraft}
|
||||
icon={IconTrashCan}
|
||||
@@ -517,7 +528,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
>
|
||||
{__("Download PDF")}
|
||||
</DropdownItem>
|
||||
{isAuthorized("Document", "deleteDocument") && (
|
||||
{document.canDelete && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
@@ -567,7 +578,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{document.title}</span>
|
||||
{isAuthorized("Document", "updateDocument") && (
|
||||
{document.canUpdate && (
|
||||
<Button
|
||||
variant="quaternary"
|
||||
icon={IconPencil}
|
||||
@@ -620,7 +631,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
) : (
|
||||
<ReadOnlyPropertyContent
|
||||
onEdit={() => setIsEditingOwner(true)}
|
||||
canEdit={isAuthorized("Document", "updateDocument")}
|
||||
canEdit={document.canUpdate}
|
||||
>
|
||||
<Badge variant="highlight" size="md" className="gap-2">
|
||||
<Avatar name={currentVersion.owner?.fullName ?? ""} />
|
||||
@@ -650,7 +661,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
) : (
|
||||
<ReadOnlyPropertyContent
|
||||
onEdit={() => setIsEditingType(true)}
|
||||
canEdit={isAuthorized("Document", "updateDocument")}
|
||||
canEdit={document.canUpdate}
|
||||
>
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{getDocumentTypeLabel(__, document.documentType)}
|
||||
@@ -679,7 +690,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
) : (
|
||||
<ReadOnlyPropertyContent
|
||||
onEdit={() => setIsEditingClassification(true)}
|
||||
canEdit={isAuthorized("Document", "updateDocument")}
|
||||
canEdit={document.canUpdate}
|
||||
>
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{getDocumentClassificationLabel(
|
||||
@@ -792,7 +803,7 @@ function VersionItem({
|
||||
<div className="flex gap-3 w-full overflow-hidden">
|
||||
<div
|
||||
className={clsx(
|
||||
"flex-shrink-0 flex items-center justify-center size-10",
|
||||
"shrink-0 flex items-center justify-center size-10",
|
||||
active && "bg-active rounded",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
usePreloadedQuery,
|
||||
type PreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { use, useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { DocumentGraphListQuery } from "/__generated__/core/DocumentGraphListQuery.graphql";
|
||||
import {
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
BulkExportDialog,
|
||||
type BulkExportDialogRef,
|
||||
} from "/components/documents/BulkExportDialog";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
const documentsFragment = graphql`
|
||||
fragment DocumentsPageListFragment on Organization
|
||||
@@ -81,6 +80,14 @@ const documentsFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
canUpdate: permission(action: "core:document:update")
|
||||
canDelete: permission(action: "core:document:delete")
|
||||
canSendSigningNotifications: permission(
|
||||
action: "core:document:send-signing-notifications"
|
||||
)
|
||||
canRequestSignatures: permission(
|
||||
action: "core:document-version:request-signature"
|
||||
)
|
||||
...DocumentsPageRowFragment
|
||||
}
|
||||
}
|
||||
@@ -104,13 +111,13 @@ type Props = {
|
||||
|
||||
export default function DocumentsPage(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
|
||||
const organization = usePreloadedQuery(
|
||||
documentsQuery,
|
||||
props.queryRef,
|
||||
).organization;
|
||||
|
||||
// FIXME
|
||||
// const userEmailData = useLazyLoadQuery<DocumentsPageUserEmailQuery>(
|
||||
// UserEmailQuery,
|
||||
// {}
|
||||
@@ -135,9 +142,15 @@ export default function DocumentsPage(props: Props) {
|
||||
|
||||
usePageTitle(__("Documents"));
|
||||
|
||||
const hasAnyAction =
|
||||
isAuthorized("Document", "updateDocument") ||
|
||||
isAuthorized("Document", "deleteDocument");
|
||||
const canDeleteAny = documents.some(({ canDelete }) => canDelete);
|
||||
const canUpdateAny = documents.some(({ canUpdate }) => canUpdate);
|
||||
const canSendAnySignatureNotifications = documents.some(
|
||||
({ canSendSigningNotifications }) => canSendSigningNotifications,
|
||||
);
|
||||
const canRequestAnySignatures = documents.some(
|
||||
({ canRequestSignatures }) => canRequestSignatures,
|
||||
);
|
||||
const hasAnyAction = canDeleteAny || canUpdateAny;
|
||||
|
||||
const handleSendSigningNotifications = () => {
|
||||
sendSigningNotifications({
|
||||
@@ -197,7 +210,7 @@ export default function DocumentsPage(props: Props) {
|
||||
description={__("Manage your organization's documents")}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
{isAuthorized("Document", "sendSigningNotifications") && (
|
||||
{canSendAnySignatureNotifications && (
|
||||
<Button
|
||||
icon={IconBell2}
|
||||
variant="secondary"
|
||||
@@ -206,7 +219,7 @@ export default function DocumentsPage(props: Props) {
|
||||
{__("Send signing notifications")}
|
||||
</Button>
|
||||
)}
|
||||
{isAuthorized("Organization", "createDocument") && (
|
||||
{organization.canCreateDocument && (
|
||||
<CreateDocumentDialog
|
||||
connection={connectionId}
|
||||
trigger={
|
||||
@@ -259,7 +272,7 @@ export default function DocumentsPage(props: Props) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{isAuthorized("Document", "updateDocument") && (
|
||||
{canUpdateAny && (
|
||||
<PublishDocumentsDialog
|
||||
documentIds={selection}
|
||||
onSave={clear}
|
||||
@@ -272,7 +285,7 @@ export default function DocumentsPage(props: Props) {
|
||||
</Button>
|
||||
</PublishDocumentsDialog>
|
||||
)}
|
||||
{isAuthorized("Document", "bulkRequestSignatures") && (
|
||||
{canRequestAnySignatures && (
|
||||
<SignatureDocumentsDialog
|
||||
documentIds={selection}
|
||||
onSave={clear}
|
||||
@@ -301,7 +314,7 @@ export default function DocumentsPage(props: Props) {
|
||||
{__("Export")}
|
||||
</Button>
|
||||
</BulkExportDialog>
|
||||
{isAuthorized("Document", "deleteDocument") && (
|
||||
{canDeleteAny && (
|
||||
<Button
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
@@ -355,6 +368,7 @@ const rowFragment = graphql`
|
||||
documentType
|
||||
classification
|
||||
updatedAt
|
||||
canDelete: permission(action: "core:document:delete")
|
||||
owner {
|
||||
id
|
||||
fullName
|
||||
@@ -393,7 +407,6 @@ function DocumentRow({
|
||||
onCheck: () => void;
|
||||
hasAnyAction: boolean;
|
||||
}) {
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const document = useFragment<DocumentsPageRowFragment$key>(
|
||||
rowFragment,
|
||||
documentKey,
|
||||
@@ -469,7 +482,7 @@ function DocumentRow({
|
||||
{hasAnyAction && (
|
||||
<Td noLink width={50} className="text-end w-18">
|
||||
<ActionDropdown>
|
||||
{isAuthorized("Document", "deleteDocument") && (
|
||||
{document.canDelete && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Suspense, useState, useEffect, useRef, use } from "react";
|
||||
import { Suspense, useState, useEffect, useRef } from "react";
|
||||
import type { ItemOf, NodeOf } from "/types";
|
||||
import { graphql, useFragment, useRefetchableFragment } from "react-relay";
|
||||
import { usePeople } from "/hooks/graph/PeopleGraph.ts";
|
||||
@@ -20,7 +20,6 @@ import { useOutletContext } from "react-router";
|
||||
import type { DocumentSignaturesTab_signature$key } from "/__generated__/core/DocumentSignaturesTab_signature.graphql.ts";
|
||||
import type { DocumentSignaturesTab_version$key } from "/__generated__/core/DocumentSignaturesTab_version.graphql.ts";
|
||||
import type { DocumentSignaturesTabRefetchQuery } from "/__generated__/core/DocumentSignaturesTabRefetchQuery.graphql";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
|
||||
|
||||
@@ -34,6 +33,9 @@ const versionFragment = graphql`
|
||||
) {
|
||||
id
|
||||
status
|
||||
canRequestSignature: permission(
|
||||
action: "core:document-version:request-signature"
|
||||
)
|
||||
signatures(first: $count, after: $cursor, filter: $signatureFilter)
|
||||
@connection(
|
||||
key: "DocumentSignaturesTab_signatures"
|
||||
@@ -186,6 +188,7 @@ function SignatureList(props: {
|
||||
people={p}
|
||||
connectionId={version.signatures.__id}
|
||||
signable={signable}
|
||||
canRequestSignature={version.canRequestSignature}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -202,6 +205,7 @@ const signatureFragment = graphql`
|
||||
fullName
|
||||
primaryEmailAddress
|
||||
}
|
||||
canCancel: permission(action: "core:document-version:request-signature")
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -244,10 +248,10 @@ function SignatureItem(props: {
|
||||
people: ItemOf<ReturnType<typeof usePeople>>;
|
||||
connectionId: string;
|
||||
signable: boolean;
|
||||
canRequestSignature: boolean;
|
||||
}) {
|
||||
const signature = useFragment(signatureFragment, props.signature);
|
||||
const { __, dateTimeFormat } = useTranslate();
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const [requestSignature, isSendingRequest] = useMutationWithToasts(
|
||||
requestSignatureMutation,
|
||||
{
|
||||
@@ -276,7 +280,7 @@ function SignatureItem(props: {
|
||||
{props.people.primaryEmailAddress}
|
||||
</div>
|
||||
</div>
|
||||
{props.signable && isAuthorized("Document", "requestSignature") && (
|
||||
{props.signable && props.canRequestSignature && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
@@ -331,7 +335,7 @@ function SignatureItem(props: {
|
||||
{__("Signed")}
|
||||
</Badge>
|
||||
) : (
|
||||
isAuthorized("DocumentVersionSignature", "cancelSignatureRequest") && (
|
||||
signature.canCancel && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="ml-auto"
|
||||
|
||||
Reference in New Issue
Block a user