diff --git a/apps/console2/src/hooks/forms/useRiskForm.tsx b/apps/console2/src/hooks/forms/useRiskForm.tsx index 8a5658172..9a8b022f4 100644 --- a/apps/console2/src/hooks/forms/useRiskForm.tsx +++ b/apps/console2/src/hooks/forms/useRiskForm.tsx @@ -34,13 +34,7 @@ export const riskSchema = z.object({ name: z.string(), description: z.string(), ownerId: z.string(), - treatment: z.enum([ - "AVOIDED", - "MITIGATED", - "TRANSFERRED", - "ACCEPTED", - "%future added value", - ]), + treatment: z.enum(["AVOIDED", "MITIGATED", "TRANSFERRED", "ACCEPTED"]), inherentLikelihood: z.number({ coerce: true }).min(1).max(5), inherentImpact: z.number({ coerce: true }).min(1).max(5), residualLikelihood: z.number({ coerce: true }).min(1).max(5), diff --git a/apps/console2/src/hooks/forms/useVendorForm.tsx b/apps/console2/src/hooks/forms/useVendorForm.tsx index 7f0d36821..b063a1017 100644 --- a/apps/console2/src/hooks/forms/useVendorForm.tsx +++ b/apps/console2/src/hooks/forms/useVendorForm.tsx @@ -1,7 +1,7 @@ import { z } from "zod"; import { useFormWithSchema } from "../useFormWithSchema"; import { graphql } from "relay-runtime"; -import { useFragment, useMutation } from "react-relay"; +import { useFragment } from "react-relay"; import type { useVendorFormFragment$key } from "./__generated__/useVendorFormFragment.graphql"; import { useMutationWithToasts } from "../useMutationWithToasts"; import { useTranslate } from "@probo/i18n"; diff --git a/apps/console2/src/hooks/graph/VendorGraph.ts b/apps/console2/src/hooks/graph/VendorGraph.ts index 643b7ca70..b013b91fd 100644 --- a/apps/console2/src/hooks/graph/VendorGraph.ts +++ b/apps/console2/src/hooks/graph/VendorGraph.ts @@ -131,6 +131,7 @@ export const vendorNodeQuery = graphql` name websiteUrl ...useVendorFormFragment + ...VendorComplianceTabFragment } } } diff --git a/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts index e3eb43ba7..7ff554f66 100644 --- a/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<57ff86dca8786cdaf42f34f9cbe379a1>> + * @generated SignedSource<<6d50678dccfbe35e4ade26b58cda2ad3>> * @lightSyntaxTransform * @nogrep */ @@ -18,7 +18,7 @@ export type VendorGraphNodeQuery$data = { readonly id?: string; readonly name?: string; readonly websiteUrl?: string | null | undefined; - readonly " $fragmentSpreads": FragmentRefs<"useVendorFormFragment">; + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment" | "useVendorFormFragment">; }; }; export type VendorGraphNodeQuery = { @@ -62,8 +62,22 @@ v4 = { "name": "websiteUrl", "storageKey": null }, -v5 = [ +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v6 = [ (v2/*: any*/) +], +v7 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } ]; return { "fragment": { @@ -90,6 +104,11 @@ return { "args": null, "kind": "FragmentSpread", "name": "useVendorFormFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "VendorComplianceTabFragment" } ], "type": "Vendor", @@ -116,13 +135,7 @@ return { "name": "node", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, + (v5/*: any*/), (v2/*: any*/), { "kind": "InlineFragment", @@ -213,7 +226,7 @@ return { "kind": "LinkedField", "name": "businessOwner", "plural": false, - "selections": (v5/*: any*/), + "selections": (v6/*: any*/), "storageKey": null }, { @@ -223,8 +236,147 @@ return { "kind": "LinkedField", "name": "securityOwner", "plural": false, - "selections": (v5/*: any*/), + "selections": (v6/*: any*/), "storageKey": null + }, + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": "VendorComplianceReportConnection", + "kind": "LinkedField", + "name": "complianceReports", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReportEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReport", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportDate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileSize", + "storageKey": null + }, + (v5/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": "complianceReports(first:50)" + }, + { + "alias": null, + "args": (v7/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "VendorComplianceTabFragment_complianceReports", + "kind": "LinkedHandle", + "name": "complianceReports" } ], "type": "Vendor", @@ -236,16 +388,16 @@ return { ] }, "params": { - "cacheID": "3e7b2628c65e007a1603b982e41fea12", + "cacheID": "f4827b073b1e66d0f2fa29283f276ac1", "id": null, "metadata": {}, "name": "VendorGraphNodeQuery", "operationKind": "query", - "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n }\n id\n }\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" + "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" } }; })(); -(node as any).hash = "9a718caa45c75f17f4b910ca2c555781"; +(node as any).hash = "97226bc4f316909815617a30a3a88cd9"; export default node; diff --git a/apps/console2/src/pages/organizations/SettingsPage.tsx b/apps/console2/src/pages/organizations/SettingsPage.tsx index 8706ff8e0..1dcda521f 100644 --- a/apps/console2/src/pages/organizations/SettingsPage.tsx +++ b/apps/console2/src/pages/organizations/SettingsPage.tsx @@ -5,7 +5,6 @@ import { Card, Field, FileButton, - IconPlusLarge, Label, PageHeader, Spinner, @@ -23,7 +22,7 @@ import type { SettingsPageFragment$key, } from "./__generated__/SettingsPageFragment.graphql"; import type { ChangeEventHandler } from "react"; -import { groupBy, sprintf } from "@probo/helpers"; +import { sprintf } from "@probo/helpers"; import type { NodeOf } from "/types"; type Props = { diff --git a/apps/console2/src/pages/organizations/people/PeopleListPage.tsx b/apps/console2/src/pages/organizations/people/PeopleListPage.tsx index ee94eebeb..b6eed72ce 100644 --- a/apps/console2/src/pages/organizations/people/PeopleListPage.tsx +++ b/apps/console2/src/pages/organizations/people/PeopleListPage.tsx @@ -12,23 +12,17 @@ import { DropdownItem, IconPencil, IconTrashCan, - useConfirm, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { PeopleGraphPaginatedQuery } from "/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql"; -import { useMutation, type PreloadedQuery } from "react-relay"; -import { - deletePeopleMutation, - useDeletePeople, - usePeopleQuery, -} from "/hooks/graph/PeopleGraph"; +import { type PreloadedQuery } from "react-relay"; +import { useDeletePeople, usePeopleQuery } from "/hooks/graph/PeopleGraph"; import { SortableTable, SortableTh } from "/components/SortableTable"; import type { PeopleGraphPaginatedFragment$data } from "/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql"; import type { NodeOf } from "/types"; import { usePageTitle } from "@probo/hooks"; -import { getRole, sprintf } from "@probo/helpers"; +import { getRole } from "@probo/helpers"; import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog"; -import type { PeopleGraphDeleteMutation } from "/hooks/graph/__generated__/PeopleGraphDeleteMutation.graphql"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { Link } from "react-router"; diff --git a/apps/console2/src/pages/organizations/policies/dialogs/PolicySignaturesDialog.tsx b/apps/console2/src/pages/organizations/policies/dialogs/PolicySignaturesDialog.tsx index 19a970f50..3a7537731 100644 --- a/apps/console2/src/pages/organizations/policies/dialogs/PolicySignaturesDialog.tsx +++ b/apps/console2/src/pages/organizations/policies/dialogs/PolicySignaturesDialog.tsx @@ -38,6 +38,11 @@ export function PolicySignaturesDialog(props: Props) { versions[0].id ); const selectedVersion = versions.find((v) => v.id === selectedVersionId); + + if (!selectedVersion) { + return null; + } + return ( @@ -68,7 +73,7 @@ export function PolicySignaturesDialog(props: Props) { - + ); } diff --git a/apps/console2/src/pages/organizations/policies/dialogs/UpdateVersionDialog.tsx b/apps/console2/src/pages/organizations/policies/dialogs/UpdateVersionDialog.tsx index 530fc5751..c8e6e3198 100644 --- a/apps/console2/src/pages/organizations/policies/dialogs/UpdateVersionDialog.tsx +++ b/apps/console2/src/pages/organizations/policies/dialogs/UpdateVersionDialog.tsx @@ -10,7 +10,7 @@ import { useDialogRef, useToast, } from "@probo/ui"; -import { useState, type RefObject } from "react"; +import { type RefObject } from "react"; import { graphql } from "relay-runtime"; import { useMutation } from "react-relay"; import type { PolicyPagePolicyFragment$data } from "../__generated__/PolicyPagePolicyFragment.graphql"; diff --git a/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx b/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx index cbdfbadda..9971235c0 100644 --- a/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx +++ b/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx @@ -13,7 +13,7 @@ import { useVendorSearch } from "/hooks/useVendorSearch"; import { faviconUrl } from "@probo/helpers"; import type { Vendor } from "@probo/vendors"; import { useCreateVendorMutation } from "/hooks/graph/VendorGraph"; -import { type ReactNode, useState } from "react"; +import { type ReactNode } from "react"; type Props = { children: ReactNode; diff --git a/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx b/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx index 997e8487b..c77685514 100644 --- a/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx +++ b/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx @@ -11,7 +11,6 @@ import { } from "/hooks/graph/VendorGraph"; import { ActionDropdown, - Avatar, Breadcrumb, DropdownItem, IconTrashCan, diff --git a/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx b/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx index fc24a62af..018a0c539 100644 --- a/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx +++ b/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx @@ -3,6 +3,24 @@ import { graphql } from "relay-runtime"; import type { VendorComplianceTabFragment$key } from "./__generated__/VendorComplianceTabFragment.graphql"; import { useTranslate } from "@probo/i18n"; import { usePageTitle } from "@probo/hooks"; +import { + ActionDropdown, + DropdownItem, + Dropzone, + IconTrashCan, + Tbody, + Td, + Th, + Thead, + Tr, + useConfirm, +} from "@probo/ui"; +import { useFragment, useMutation, useRefetchableFragment } from "react-relay"; +import type { VendorComplianceTabFragment_report$key } from "./__generated__/VendorComplianceTabFragment_report.graphql"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { sprintf } from "@probo/helpers"; +import { fileSize } from "@probo/helpers/src/file.ts"; +import { SortableTable, SortableTh } from "/components/SortableTable"; const complianceReportsFragment = graphql` fragment VendorComplianceTabFragment on Vendor @@ -21,8 +39,10 @@ const complianceReportsFragment = graphql` before: $before orderBy: $order ) @connection(key: "VendorComplianceTabFragment_complianceReports") { + __id edges { node { + id ...VendorComplianceTabFragment_report } } @@ -32,6 +52,7 @@ const complianceReportsFragment = graphql` const complianceReportFragment = graphql` fragment VendorComplianceTabFragment_report on VendorComplianceReport { + id reportDate validUntil reportName @@ -40,12 +61,161 @@ const complianceReportFragment = graphql` } `; +const uploadReportMutation = graphql` + mutation VendorComplianceTabUploadReportMutation( + $input: UploadVendorComplianceReportInput! + $connections: [ID!]! + ) { + uploadVendorComplianceReport(input: $input) { + vendorComplianceReportEdge @appendEdge(connections: $connections) { + node { + id + ...VendorComplianceTabFragment_report + } + } + } + } +`; + +const deleteReportMutation = graphql` + mutation VendorComplianceTabDeleteReportMutation( + $input: DeleteVendorComplianceReportInput! + $connections: [ID!]! + ) { + deleteVendorComplianceReport(input: $input) { + deletedVendorComplianceReportId @deleteEdge(connections: $connections) + } + } +`; + export default function VendorComplianceTab() { const { vendor } = useOutletContext<{ - vendor: VendorComplianceTabFragment$key & { name: string }; + vendor: VendorComplianceTabFragment$key & { name: string; id: string }; }>(); + const [data, refetch] = useRefetchableFragment( + complianceReportsFragment, + vendor + ); + const connectionId = data.complianceReports.__id; + const reports = data.complianceReports.edges.map((edge) => edge.node); const { __ } = useTranslate(); usePageTitle(vendor.name + " - " + __("Compliance reports")); - return
Vendor Compliance
; + + const [mutate, isMutating] = useMutation(uploadReportMutation); + + const handleDrop = (files: File[]) => { + const reportDate = new Date().toISOString(); + for (const file of files) { + mutate({ + variables: { + connections: [connectionId], + input: { + vendorId: vendor.id, + reportDate, + reportName: file.name, + file: null, + }, + }, + uploadables: { + "input.file": file, + }, + }); + } + }; + + return ( +
+ + + + + {__("Report name")} + {__("Report date")} + {__("Valid until")} + {__("File size")} + {__("Actions")} + + + + {reports.map((report) => ( + + ))} + + +
+ ); +} + +type ReportRowProps = { + reportKey: VendorComplianceTabFragment_report$key; + connectionId: string; +}; + +function ReportRow(props: ReportRowProps) { + const { __ } = useTranslate(); + const report = useFragment( + complianceReportFragment, + props.reportKey + ); + const { dateFormat } = useTranslate(); + const confirm = useConfirm(); + const [deleteReport] = useMutationWithToasts(deleteReportMutation, { + successMessage: __("Report deleted successfully"), + errorMessage: __("Failed to delete report"), + }); + + const handleDelete = () => { + confirm( + () => + deleteReport({ + variables: { + connections: [props.connectionId], + input: { + reportId: report.id, + }, + }, + }), + { + message: sprintf( + __( + 'This will permanently delete the report "%s". This action cannot be undone.' + ), + report.reportName + ), + } + ); + }; + + return ( + + {report.reportName} + {dateFormat(report.reportDate)} + {dateFormat(report.validUntil)} + {fileSize(__, report.fileSize)} + + + + {__("Delete")} + + + + + ); } diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts index 314e7c1c6..58eba5b49 100644 --- a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<75defbef64a4ee18b514465a44c8eea1>> + * @generated SignedSource<<10fdd09e95fc5f2995b91c73cf9fb945>> * @lightSyntaxTransform * @nogrep */ @@ -211,6 +211,7 @@ return { "name": "node", "plural": false, "selections": [ + (v12/*: any*/), { "alias": null, "args": null, @@ -246,7 +247,6 @@ return { "name": "fileSize", "storageKey": null }, - (v12/*: any*/), (v11/*: any*/) ], "storageKey": null @@ -299,6 +299,18 @@ return { } ], "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] } ], "storageKey": null @@ -324,16 +336,16 @@ return { ] }, "params": { - "cacheID": "76e0b5dbc01424695b8c642e27c0f4ed", + "cacheID": "c819e002dfe9f20004fb5bfb4f95b143", "id": null, "metadata": {}, "name": "ComplianceReportListQuery", "operationKind": "query", - "text": "query ComplianceReportListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorComplianceReportOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorComplianceTabFragment_16fISc\n id\n }\n}\n\nfragment VendorComplianceTabFragment_16fISc on Vendor {\n complianceReports(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n ...VendorComplianceTabFragment_report\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n" + "text": "query ComplianceReportListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorComplianceReportOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorComplianceTabFragment_16fISc\n id\n }\n}\n\nfragment VendorComplianceTabFragment_16fISc on Vendor {\n complianceReports(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n" } }; })(); -(node as any).hash = "7aae49a4b414ed4db5e7b39f8969c02f"; +(node as any).hash = "1fe4e0b1f9fb781c3b01e539145d610a"; export default node; diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabDeleteReportMutation.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabDeleteReportMutation.graphql.ts new file mode 100644 index 000000000..20b6bb83a --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabDeleteReportMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<6fc7bf1e239dfeccb007ee5a9beefaff>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteVendorComplianceReportInput = { + reportId: string; +}; +export type VendorComplianceTabDeleteReportMutation$variables = { + connections: ReadonlyArray; + input: DeleteVendorComplianceReportInput; +}; +export type VendorComplianceTabDeleteReportMutation$data = { + readonly deleteVendorComplianceReport: { + readonly deletedVendorComplianceReportId: string; + }; +}; +export type VendorComplianceTabDeleteReportMutation = { + response: VendorComplianceTabDeleteReportMutation$data; + variables: VendorComplianceTabDeleteReportMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedVendorComplianceReportId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "VendorComplianceTabDeleteReportMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteVendorComplianceReportPayload", + "kind": "LinkedField", + "name": "deleteVendorComplianceReport", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "VendorComplianceTabDeleteReportMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteVendorComplianceReportPayload", + "kind": "LinkedField", + "name": "deleteVendorComplianceReport", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedVendorComplianceReportId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3bfaddbf09d2cd72a0bb72687c1e187e", + "id": null, + "metadata": {}, + "name": "VendorComplianceTabDeleteReportMutation", + "operationKind": "mutation", + "text": "mutation VendorComplianceTabDeleteReportMutation(\n $input: DeleteVendorComplianceReportInput!\n) {\n deleteVendorComplianceReport(input: $input) {\n deletedVendorComplianceReportId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "7151aea21838384dc10781b64f0209ae"; + +export default node; diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts index 02c81fda7..d09432f83 100644 --- a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<079a3066496976bd1238ba194f5a0660>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,8 +12,10 @@ import { ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type VendorComplianceTabFragment$data = { readonly complianceReports: { + readonly __id: string; readonly edges: ReadonlyArray<{ readonly node: { + readonly id: string; readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment_report">; }; }>; @@ -31,7 +33,14 @@ import ComplianceReportListQuery_graphql from './ComplianceReportListQuery.graph const node: ReaderFragment = (function(){ var v0 = [ "complianceReports" -]; +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; return { "argumentDefinitions": [ { @@ -124,6 +133,7 @@ return { "name": "node", "plural": false, "selections": [ + (v1/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -187,23 +197,29 @@ return { } ], "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] } ], "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - } + (v1/*: any*/) ], "type": "Vendor", "abstractKey": null }; })(); -(node as any).hash = "7aae49a4b414ed4db5e7b39f8969c02f"; +(node as any).hash = "1fe4e0b1f9fb781c3b01e539145d610a"; export default node; diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts index 297116129..ad4487cde 100644 --- a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<01d2d791b86ae99330d4ba2ecb01f1ed>> + * @generated SignedSource<<8f56e9dbd8ce8cf0c81d8f2e9f1ad06e>> * @lightSyntaxTransform * @nogrep */ @@ -13,6 +13,7 @@ import { FragmentRefs } from "relay-runtime"; export type VendorComplianceTabFragment_report$data = { readonly fileSize: number; readonly fileUrl: string; + readonly id: string; readonly reportDate: any; readonly reportName: string; readonly validUntil: any | null | undefined; @@ -29,6 +30,13 @@ const node: ReaderFragment = { "metadata": null, "name": "VendorComplianceTabFragment_report", "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, { "alias": null, "args": null, @@ -69,6 +77,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "d39b70dfd0a316b158d7dc7253d0ff5e"; +(node as any).hash = "92ad78c9e4b4add9b1f622485d81a6ff"; export default node; diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabUploadReportMutation.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabUploadReportMutation.graphql.ts new file mode 100644 index 000000000..b23b32ae5 --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabUploadReportMutation.graphql.ts @@ -0,0 +1,226 @@ +/** + * @generated SignedSource<<6093b0bc54fa90962376639ec87fa149>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type UploadVendorComplianceReportInput = { + file: any; + reportDate: any; + reportName: string; + validUntil?: any | null | undefined; + vendorId: string; +}; +export type VendorComplianceTabUploadReportMutation$variables = { + connections: ReadonlyArray; + input: UploadVendorComplianceReportInput; +}; +export type VendorComplianceTabUploadReportMutation$data = { + readonly uploadVendorComplianceReport: { + readonly vendorComplianceReportEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment_report">; + }; + }; + }; +}; +export type VendorComplianceTabUploadReportMutation = { + response: VendorComplianceTabUploadReportMutation$data; + variables: VendorComplianceTabUploadReportMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "VendorComplianceTabUploadReportMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "UploadVendorComplianceReportPayload", + "kind": "LinkedField", + "name": "uploadVendorComplianceReport", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReportEdge", + "kind": "LinkedField", + "name": "vendorComplianceReportEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReport", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "VendorComplianceTabFragment_report" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "VendorComplianceTabUploadReportMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "UploadVendorComplianceReportPayload", + "kind": "LinkedField", + "name": "uploadVendorComplianceReport", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReportEdge", + "kind": "LinkedField", + "name": "vendorComplianceReportEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReport", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportDate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileSize", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "appendEdge", + "key": "", + "kind": "LinkedHandle", + "name": "vendorComplianceReportEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "fb9be687deba8567502e207b0969b2b5", + "id": null, + "metadata": {}, + "name": "VendorComplianceTabUploadReportMutation", + "operationKind": "mutation", + "text": "mutation VendorComplianceTabUploadReportMutation(\n $input: UploadVendorComplianceReportInput!\n) {\n uploadVendorComplianceReport(input: $input) {\n vendorComplianceReportEdge {\n node {\n id\n ...VendorComplianceTabFragment_report\n }\n }\n }\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n" + } +}; +})(); + +(node as any).hash = "4ed863a250b0686340056f4f77eb8ab8"; + +export default node; diff --git a/packages/helpers/src/file.test.ts b/packages/helpers/src/file.test.ts new file mode 100644 index 000000000..8752fa857 --- /dev/null +++ b/packages/helpers/src/file.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from "vitest"; +import { isEmpty } from "./array"; +import { fileSize } from "./file"; + +describe("file", () => { + it("should display a file size in a human readable format", () => { + const fakeTranslator = (s: string) => s; + expect(fileSize(fakeTranslator, 4911)).toMatchInlineSnapshot( + `"4.8 KB"`, + ); + expect(fileSize(fakeTranslator, 20)).toMatchInlineSnapshot(`"20 B"`); + }); +}); diff --git a/packages/helpers/src/file.ts b/packages/helpers/src/file.ts new file mode 100644 index 000000000..9fefa547c --- /dev/null +++ b/packages/helpers/src/file.ts @@ -0,0 +1,19 @@ +/** + * Return the file size in a human readable format + */ +export function fileSize(__: (s: string) => string, size: number): string { + if (size < 0) return ""; + if (size === 0) return `0 ${__("B")}`; + + const units = [__("B"), __("KB"), __("MB"), __("GB"), __("TB")]; + const i = Math.floor(Math.log(size) / Math.log(1024)); + + // Don't go beyond available units + const unitIndex = Math.min(i, units.length - 1); + + // Convert to the appropriate unit with 2 decimal places + const convertedSize = size / Math.pow(1024, unitIndex); + const formattedSize = Math.round(convertedSize * 100) / 100; + + return `${formattedSize} ${units[unitIndex]}`; +} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index ee2bbe2c1..74bacdd55 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,2 +1,3 @@ export { usePageTitle } from "./usePageTitle"; export { useToggle } from "./useToggle"; +export { useRefSync } from "./useRefSync"; diff --git a/packages/hooks/src/useRefSync.ts b/packages/hooks/src/useRefSync.ts new file mode 100644 index 000000000..728807718 --- /dev/null +++ b/packages/hooks/src/useRefSync.ts @@ -0,0 +1,7 @@ +import { useRef } from "react"; + +export function useRefSync(value: T) { + const ref = useRef(value); + ref.current = value; + return ref; +} diff --git a/packages/ui/src/Atoms/Dropdown/Dropdown.tsx b/packages/ui/src/Atoms/Dropdown/Dropdown.tsx index 03624961a..db67d07a7 100644 --- a/packages/ui/src/Atoms/Dropdown/Dropdown.tsx +++ b/packages/ui/src/Atoms/Dropdown/Dropdown.tsx @@ -13,7 +13,7 @@ type Props = PropsWithChildren<{ }>; export const dropdown = tv({ - base: "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin] z-50 p-2 shadow-mid min-w-[8rem] bg-level-1 overflow-y-auto overflow-x-hidden rounded-2xl border-border-low", + base: "z-50 p-2 shadow-mid min-w-[8rem] bg-level-1 overflow-y-auto overflow-x-hidden rounded-2xl border-border-low", }); export function Dropdown({ children, toggle, className, open }: Props) { diff --git a/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx b/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx index daf4a4100..dd88e22b0 100644 --- a/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx +++ b/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx @@ -9,4 +9,24 @@ export default { type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + args: { + description: "Maximum 500 MB file size", + isUploading: false, + disabled: false, + }, +}; + +export const Disabled: Story = { + args: { + ...Default.args, + disabled: true, + }, +}; + +export const Uploading: Story = { + args: { + ...Default.args, + isUploading: true, + }, +}; diff --git a/packages/ui/src/Atoms/Dropzone/Dropzone.tsx b/packages/ui/src/Atoms/Dropzone/Dropzone.tsx index 70bccd76b..7ed72d04e 100644 --- a/packages/ui/src/Atoms/Dropzone/Dropzone.tsx +++ b/packages/ui/src/Atoms/Dropzone/Dropzone.tsx @@ -1,24 +1,129 @@ +import { useRefSync } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; import { useCallback } from "react"; -import { useDropzone } from "react-dropzone"; +import { useDropzone, type FileRejection } from "react-dropzone"; +import { tv } from "tailwind-variants"; +import { IconPageCross, IconUpload } from "../Icons"; +import { Spinner } from "../Spinner/Spinner"; -type Props = {}; +type Props = { + description: string; + isUploading: boolean; + disabled?: boolean; + accept?: Record; + maxSize?: number; // maxSize in MB + onDrop: (acceptedFiles: File[]) => void; +}; -export function Dropzone({}: Props) { - const onDrop = useCallback((acceptedFiles) => { - // Do something with the files - }, []); - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, +export const dropzone = tv({ + slots: { + wrapper: + "bg-subtle border border border-border-low p-2 rounded-[20px] outline-none focus-visible:shadow-focus", + zone: "bg-secondary min-h-46 border border-border-low border-dashed rounded-2xl flex items-center justify-center", + title: "flex gap-2 text-sm font-medium text-center justify-center", + description: "text-xs text-txt-tertiary text-center", + }, + variants: { + isDragActive: { + true: { + wrapper: "border-border-success shadow-focus", + zone: "border-border-success", + }, + }, + hasError: { + true: { + wrapper: "bg-danger", + zone: "bg-invert", + title: "text-txt-danger", + description: "text-txt-danger", + }, + }, + disabled: { + true: { + wrapper: "opacity-60 cursor-default", + zone: "bg-highlight", + }, + false: { + zone: "hover:bg-secondary-hover", + }, + }, + }, + defaultVariants: { + isDragActive: false, + disabled: false, + hasError: false, + }, +}); + +const MB = 1024; + +export function Dropzone(props: Props) { + const { __ } = useTranslate(); + const onDropRef = useRefSync(props.onDrop); + const onDrop = useCallback( + (files: File[]) => { + onDropRef.current(files); + }, + [onDropRef], + ); + const { getRootProps, getInputProps, isDragActive, fileRejections } = + useDropzone({ + disabled: props.disabled || props.isUploading, + accept: props.accept, + onDrop, + maxSize: props.maxSize ? props.maxSize * MB : undefined, + }); + const error = getDropzoneError(__, fileRejections); + const { wrapper, zone, title, description } = dropzone({ + ...props, + isDragActive, + hasError: !!error, }); + console.log(error); + return ( -
- - {isDragActive ? ( -

Drop the files here ...

- ) : ( -

Drag 'n' drop some files here, or click to select files

- )} +
+
+ + {props.isUploading ? ( +
+ + {__("Uploading file")} +
+ ) : ( +
+
+ {error ? ( + + ) : ( + + )} + {error ?? __("Drag and drop or browse files")} +
+
{props.description}
+
+ )} +
); } + +function getDropzoneError( + __: (s: string) => string, + fileRejections: readonly FileRejection[], +) { + if (fileRejections.length === 0) { + return null; + } + + const code = fileRejections[0].errors[0].code; + switch (code) { + case "file-invalid-type": + return __("This file is not supported"); + case "file-too-large": + return __("This file is too large"); + default: + return __("Something went wrong"); + } +} diff --git a/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx b/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx index b5c7c1a57..2cb95c3d4 100644 --- a/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx +++ b/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx @@ -1,21 +1,13 @@ import { useTranslate } from "@probo/i18n"; import { Root, - Trigger, Overlay, Content, Title, Description, Cancel, } from "@radix-ui/react-alert-dialog"; -import { - useCallback, - useRef, - useState, - type ComponentProps, - type ReactNode, - type RefObject, -} from "react"; +import { useCallback, useState, type ComponentProps } from "react"; import { Button } from "../../Atoms/Button/Button"; import { Root as Portal } from "@radix-ui/react-portal"; import { dialog } from "./Dialog"; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 36f95aabb..381f4cd61 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -32,6 +32,7 @@ export { PropertyRow } from "./Atoms/PropertyRow/PropertyRow"; export { Table, Thead, Tr, Tbody, Td, Th } from "./Atoms/Table/Table"; export { Tabs, TabLink } from "./Atoms/Tabs/Tabs"; export { Markdown } from "./Atoms/Markdown/Markdown"; +export { Dropzone } from "./Atoms/Dropzone/Dropzone"; // Molecules export {