Add evidence preview url
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<7e91345490d4b70ef6668bb6504be1e3>>
|
||||
* @generated SignedSource<<286a7d808d7a9c793cd9e9b6830cb40b>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -598,6 +598,20 @@ return {
|
||||
"name": "filename",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "mimeType",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -619,20 +633,6 @@ return {
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "mimeType",
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -667,12 +667,12 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "4a923b265801da22c605af53e744d543",
|
||||
"cacheID": "c36bf27c7f4d6b3ef4a3e063e4bfccea",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MeasureGraphNodeQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n tasksInfos: tasks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureTasksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n fileUrl\n mimeType\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureTasksTabFragment on Measure {\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n"
|
||||
"text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n tasksInfos: tasks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureTasksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n filename\n fileUrl\n mimeType\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n fileUrl\n mimeType\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureTasksTabFragment on Measure {\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
IconArrowInbox,
|
||||
IconFilter,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
|
||||
type Props = {
|
||||
evidence: {
|
||||
filename: string;
|
||||
fileUrl?: string | null;
|
||||
mimeType: string;
|
||||
};
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function EvidencePreviewDialog({ evidence, onClose }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
return (
|
||||
<Dialog defaultOpen title={evidence.filename} onClose={onClose}>
|
||||
<DialogContent padded>
|
||||
<p>
|
||||
{__(
|
||||
"Preview of the evidence file. You can view or download the file from here."
|
||||
)}
|
||||
</p>
|
||||
<EvidencePreviewContent evidence={evidence} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EvidencePreviewContent({ evidence }: Pick<Props, "evidence">) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
if (!evidence.fileUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (evidence.mimeType.startsWith("image/")) {
|
||||
return (
|
||||
<img
|
||||
src={evidence.fileUrl}
|
||||
alt={evidence.filename}
|
||||
className="max-h-[70vh] object-contain"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (evidence.mimeType.includes("pdf")) {
|
||||
return (
|
||||
<iframe
|
||||
src={evidence.fileUrl}
|
||||
className="w-full h-[70vh]"
|
||||
title={evidence.filename}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 justify-center">
|
||||
<IconFilter size={20} />
|
||||
<p className="text-secondary text-center">
|
||||
{__("Preview not availeeable for this file type") +
|
||||
" " +
|
||||
evidence.mimeType}
|
||||
</p>
|
||||
<Button asChild variant="secondary" icon={IconArrowInbox}>
|
||||
<a href={evidence.fileUrl} target="_blank">
|
||||
{__("Download File")}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useOutletContext } from "react-router";
|
||||
import { useNavigate, useOutletContext, useParams } from "react-router";
|
||||
import type { MeasureEvidencesTabFragment$key } from "./__generated__/MeasureEvidencesTabFragment.graphql";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import {
|
||||
Button,
|
||||
ActionDropdown,
|
||||
DropdownItem,
|
||||
Dropzone,
|
||||
IconArrowInbox,
|
||||
IconTrashCan,
|
||||
@@ -18,8 +19,9 @@ import { graphql } from "relay-runtime";
|
||||
import { useFragment, useMutation, useRefetchableFragment } from "react-relay";
|
||||
import { SortableTable } from "/components/SortableTable";
|
||||
import type { MeasureEvidencesTabFragment_evidence$key } from "./__generated__/MeasureEvidencesTabFragment_evidence.graphql";
|
||||
import { fileSize, fileType } from "@probo/helpers";
|
||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
||||
import { fileSize, fileType, promisifyMutation, sprintf } from "@probo/helpers";
|
||||
import { EvidencePreviewDialog } from "../dialog/EvidencePreviewDialog";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
|
||||
export const evidencesFragment = graphql`
|
||||
fragment MeasureEvidencesTabFragment on Measure
|
||||
@@ -43,6 +45,9 @@ export const evidencesFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
filename
|
||||
fileUrl
|
||||
mimeType
|
||||
...MeasureEvidencesTabFragment_evidence
|
||||
}
|
||||
}
|
||||
@@ -50,7 +55,7 @@ export const evidencesFragment = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const evidenceFragment = graphql`
|
||||
export const evidenceFragment = graphql`
|
||||
fragment MeasureEvidencesTabFragment_evidence on Evidence {
|
||||
id
|
||||
filename
|
||||
@@ -93,12 +98,15 @@ export default function MeasureEvidencesTab() {
|
||||
const { measure } = useOutletContext<{
|
||||
measure: MeasureEvidencesTabFragment$key & { id: string; name: string };
|
||||
}>();
|
||||
const { evidenceId } = useParams<{ evidenceId: string }>();
|
||||
const [data, refetch] = useRefetchableFragment(evidencesFragment, measure);
|
||||
const connectionId = data.evidences.__id;
|
||||
const evidences = data.evidences?.edges?.map((edge) => edge.node) ?? [];
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { __ } = useTranslate();
|
||||
const [mutate, isUpdating] = useMutation(uploadEvidenceMutation);
|
||||
const evidence = evidences.find((e) => e.id === evidenceId);
|
||||
const organizationId = useOrganizationId();
|
||||
|
||||
usePageTitle(measure.name + " - " + __("Evidences"));
|
||||
|
||||
@@ -123,7 +131,7 @@ export default function MeasureEvidencesTab() {
|
||||
<div className="space-y-6">
|
||||
<Dropzone
|
||||
description={__(
|
||||
"Only PDF, DOCX, XLSX, PPTX, JPG, PNG, WEBP files up to 10MB are allowed"
|
||||
"Only PDF, DOCX, XLSX, PPTX, JPG, PNG, WEBP files up to 10MB are allowed",
|
||||
)}
|
||||
isUploading={isUpdating}
|
||||
onDrop={handleDrop}
|
||||
@@ -157,17 +165,32 @@ export default function MeasureEvidencesTab() {
|
||||
<EvidenceRow
|
||||
key={evidence.id}
|
||||
evidenceKey={evidence}
|
||||
measureId={measure.id}
|
||||
organizationId={organizationId}
|
||||
connectionId={connectionId}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
{evidence && (
|
||||
<EvidencePreviewDialog
|
||||
key={evidence?.id}
|
||||
onClose={() =>
|
||||
navigate(
|
||||
`/organizations/${organizationId}/measures/${measure.id}/evidences`,
|
||||
)
|
||||
}
|
||||
evidence={evidence}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EvidenceRow(props: {
|
||||
evidenceKey: MeasureEvidencesTabFragment_evidence$key;
|
||||
measureId: string;
|
||||
organizationId: string;
|
||||
connectionId: string;
|
||||
}) {
|
||||
const evidence = useFragment(evidenceFragment, props.evidenceKey);
|
||||
@@ -191,34 +214,40 @@ function EvidenceRow(props: {
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
'This will permanently delete the evidence "%s". This action cannot be undone.'
|
||||
'This will permanently delete the evidence "%s". This action cannot be undone.',
|
||||
),
|
||||
evidence.filename
|
||||
evidence.filename,
|
||||
),
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Tr
|
||||
to={`/organizations/${props.organizationId}/measures/${props.measureId}/evidences/${evidence.id}`}
|
||||
>
|
||||
<Td>{evidence.filename}</Td>
|
||||
<Td>{fileType(__, evidence)}</Td>
|
||||
<Td>{fileSize(__, evidence.size)}</Td>
|
||||
<Td>{dateFormat(evidence.createdAt)}</Td>
|
||||
<Td>
|
||||
<Td noLink>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="secondary">
|
||||
<a href={evidence.fileUrl ?? ""} target="_blank">
|
||||
<IconArrowInbox size={16} />
|
||||
{__("Download")}
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
/>
|
||||
<ActionDropdown>
|
||||
<DropdownItem asChild>
|
||||
<a href={evidence.fileUrl ?? ""} target="_blank">
|
||||
<IconArrowInbox size={16} />
|
||||
{__("Download")}
|
||||
</a>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<70954edefec64fedbc4f8c1ec1c1efab>>
|
||||
* @generated SignedSource<<f1947ff2accbc325c11c88fb343d82b2>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -15,7 +15,10 @@ export type MeasureEvidencesTabFragment$data = {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly fileUrl: string | null | undefined;
|
||||
readonly filename: string;
|
||||
readonly id: string;
|
||||
readonly mimeType: string;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MeasureEvidencesTabFragment_evidence">;
|
||||
};
|
||||
}>;
|
||||
@@ -135,6 +138,27 @@ return {
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "filename",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "mimeType",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
@@ -220,6 +244,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "619655a768981a51a00a71887dbcb227";
|
||||
(node as any).hash = "abae73e44e966689cd837465f93abcb3";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<65fd5218c9365e00e3127dfca328ad10>>
|
||||
* @generated SignedSource<<e2a2d3c5470f8878acedd5e4ecc05300>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -219,6 +219,20 @@ return {
|
||||
"name": "filename",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "mimeType",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -240,20 +254,6 @@ return {
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "mimeType",
|
||||
"storageKey": null
|
||||
},
|
||||
(v11/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -343,16 +343,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "28755a6b97ad50d18c25dea1b383d062",
|
||||
"cacheID": "61b9596f07d5d2516f50e2f646eb21a2",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MeasureEvidencesTabQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query MeasureEvidencesTabQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: EvidenceOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeasureEvidencesTabFragment_16fISc\n id\n }\n}\n\nfragment MeasureEvidencesTabFragment_16fISc on Measure {\n id\n evidences(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n fileUrl\n mimeType\n}\n"
|
||||
"text": "query MeasureEvidencesTabQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: EvidenceOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeasureEvidencesTabFragment_16fISc\n id\n }\n}\n\nfragment MeasureEvidencesTabFragment_16fISc on Measure {\n id\n evidences(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n filename\n fileUrl\n mimeType\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n fileUrl\n mimeType\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "619655a768981a51a00a71887dbcb227";
|
||||
(node as any).hash = "abae73e44e966689cd837465f93abcb3";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -61,7 +61,7 @@ export const measureRoutes = [
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "evidences",
|
||||
path: "evidences/:evidenceId?",
|
||||
fallback: LinkCardSkeleton,
|
||||
Component: lazy(
|
||||
() =>
|
||||
|
||||
Reference in New Issue
Block a user