From e6717a111f8330d8aff343df19af98d2ac769b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Dethier?= Date: Thu, 4 Jul 2024 11:33:11 +0200 Subject: [PATCH] feat: enable full-draft mode. logion-network/logion-internal#1318 --- src/loc/DataLocRequest.test.tsx | 4 +- src/loc/DataLocRequest.tsx | 12 ++-- src/loc/DraftLocInstructions.tsx | 10 ++++ src/loc/LocDetailsTab.test.tsx | 42 ++++++++++++- src/loc/LocDetailsTab.tsx | 60 ++++++++++++++++++- src/loc/LocLinkButton.test.tsx | 17 ++++-- src/loc/LocLinkExistingLocDialog.tsx | 10 ++-- .../DraftLocInstructions.test.tsx.snap | 8 +++ .../__snapshots__/LocDetailsTab.test.tsx.snap | 14 +++++ 9 files changed, 156 insertions(+), 21 deletions(-) diff --git a/src/loc/DataLocRequest.test.tsx b/src/loc/DataLocRequest.test.tsx index 06c8ea42..962a8e3a 100644 --- a/src/loc/DataLocRequest.test.tsx +++ b/src/loc/DataLocRequest.test.tsx @@ -79,7 +79,7 @@ async function checkFormDisabled() { }); } -function setupLocsState(legalOfficersWithValidIdentityLoc: LegalOfficerClass[]) { +function setupLocsState(legalOfficersWithNonVoidIdentityLoc: LegalOfficerClass[]) { const draftRequest = { locId, locsState: () => locsState, @@ -88,7 +88,7 @@ function setupLocsState(legalOfficersWithValidIdentityLoc: LegalOfficerClass[]) }), } as DraftRequest; const locsState = { - legalOfficersWithValidIdentityLoc, + legalOfficersWithNonVoidIdentityLoc, requestTransactionLoc: () => Promise.resolve(draftRequest), requestCollectionLoc: () => Promise.resolve(draftRequest), } as unknown as LocsState; diff --git a/src/loc/DataLocRequest.tsx b/src/loc/DataLocRequest.tsx index b63d229d..9a52834f 100644 --- a/src/loc/DataLocRequest.tsx +++ b/src/loc/DataLocRequest.tsx @@ -25,9 +25,9 @@ export default function DataLocRequest(props: Props) { const [ legalOfficer, setLegalOfficer ] = useState(null); const { locsState } = useUserContext(); const navigate = useNavigate(); - const legalOfficersWithValidIdentityLoc = useMemo(() => { + const legalOfficersWithNonVoidIdentityLoc = useMemo(() => { if (locsState !== undefined) { - return locsState.legalOfficersWithValidIdentityLoc + return locsState.legalOfficersWithNonVoidIdentityLoc } else { return [] } @@ -42,12 +42,12 @@ export default function DataLocRequest(props: Props) { > - { legalOfficersWithValidIdentityLoc.length > 0 && + { legalOfficersWithNonVoidIdentityLoc.length > 0 && } - { legalOfficersWithValidIdentityLoc.length > 0 && + { legalOfficersWithNonVoidIdentityLoc.length > 0 &&

If you do not see the Logion Legal officer you are looking for, please request an Identity LOC to the Logion Legal Officer of your choice by @@ -67,7 +67,7 @@ export default function DataLocRequest(props: Props) { } - { legalOfficersWithValidIdentityLoc.length === 0 && + { legalOfficersWithNonVoidIdentityLoc.length === 0 &&

To submit a { locType } LOC request, you must select a Logion Legal Officer who already executed an Identity LOC linked to your Polkadot address.

diff --git a/src/loc/DraftLocInstructions.tsx b/src/loc/DraftLocInstructions.tsx index f2099eed..3689a55a 100644 --- a/src/loc/DraftLocInstructions.tsx +++ b/src/loc/DraftLocInstructions.tsx @@ -32,6 +32,16 @@ export default function DraftLocInstructions(props: Props) {
  • Public data will be publicly available on the logion blockchain and public certificate.
  • Confidential documents will not be publicly available and will stay confidential between you and your Legal Officer.
  • + { + props.locType !== "Identity" && +

    You must have a valid Identity LOC In order to submit this LOC for review.{" "} + Also, if your LOC contains a link, its target must be closed and not void.

    + } +

    You won't be able to cancel this request if it is the target of a link in another request.

    + { + props.locType === "Identity" && +

    You won't be able to cancel this request as long as other requests rely on it.

    + } } /> diff --git a/src/loc/LocDetailsTab.test.tsx b/src/loc/LocDetailsTab.test.tsx index a6827e52..36ddc359 100644 --- a/src/loc/LocDetailsTab.test.tsx +++ b/src/loc/LocDetailsTab.test.tsx @@ -182,6 +182,7 @@ function buildLocMock(params: MockParameters): { loc: LocData, locState: LocRequ closed: params.status === "CLOSED", createdOn, closedOn: params.status === "CLOSED" ? closedOn : undefined, + links: [], } as unknown as LocData; if(params.voidLoc) { @@ -190,10 +191,49 @@ function buildLocMock(params: MockParameters): { loc: LocData, locState: LocRequ }; } - const locState = { + const locsState = { + draftRequests: { + Collection: [], + Identity: [], + Transaction: [], + }, + openLocs: { + Collection: [], + Identity: [], + Transaction: [], + }, + closedLocs: { + Collection: [], + Identity: [], + Transaction: [], + }, + voidedLocs: { + Collection: [], + Identity: [], + Transaction: [], + }, + pendingRequests: { + Collection: [], + Identity: [], + Transaction: [], + }, + rejectedRequests: { + Collection: [], + Identity: [], + Transaction: [], + }, + acceptedRequests: { + Collection: [], + Identity: [], + Transaction: [], + }, + }; + const locState = { isLogionIdentity: () => params.isLogionIdentityLoc === true, isLogionData: () => params.locType === "Collection" || params.locType === "Transaction", + locsState: () => locsState, + data: () => loc, } as unknown as LocRequestState; return { loc, locState }; diff --git a/src/loc/LocDetailsTab.tsx b/src/loc/LocDetailsTab.tsx index f042c9d9..1a34d96d 100644 --- a/src/loc/LocDetailsTab.tsx +++ b/src/loc/LocDetailsTab.tsx @@ -21,7 +21,7 @@ import { Row } from "src/common/Grid"; import { Viewer } from "src/common/CommonContext"; import Button from "src/common/Button"; import Icon from "src/common/Icon"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import WarningDialog from "src/common/WarningDialog"; import { useLocContext } from "./LocContext"; import { useNavigate } from "react-router-dom"; @@ -242,6 +242,25 @@ export function LocDetailsTabContent(props: ContentProps) { }); }, [ mutateLocState ]); + const canSubmit = useMemo(() => { + return loc + && (loc.locType === "Identity" || (legalOfficer && locState?.locsState().hasValidIdentityLoc(legalOfficer))) + && allLinkTargetsValid(locState); + }, [ loc, legalOfficer, locState ]); + + const canCancel = useMemo(() => { + return loc && locState + && hasNoIncomingLink(locState) + && ( + loc.locType !== "Identity" + || ( + legalOfficer + && locState?.locsState().draftRequests["Transaction"].find(loc => loc.isOwner(legalOfficer.account)) === undefined + && locState?.locsState().draftRequests["Collection"].find(loc => loc.isOwner(legalOfficer.account)) === undefined + ) + ) + }, [ loc, legalOfficer, locState ]); + if(!loc) { return null; } @@ -389,6 +408,7 @@ export function LocDetailsTabContent(props: ContentProps) { @@ -397,6 +417,7 @@ export function LocDetailsTabContent(props: ContentProps) { loc.status === "DRAFT" && @@ -461,3 +482,40 @@ export function LocDetailsTabContent(props: ContentProps) { ); } + +function allLinkTargetsValid(locState: LocRequestState | null) { + if(locState) { + return locState.data().links.find(link => !targetValid(locState, link.target)) === undefined; + } else { + return false; + } +} + +function targetValid(locState: LocRequestState, targetId: UUID) { + const target = locState.locsState().findByIdOrUndefined(targetId); + return target && target.data().status === "CLOSED" && target.data().voidInfo === undefined; +} + +function hasNoIncomingLink(locState: LocRequestState) { + const locsState = locState.locsState(); + const all = mergeRequests(locsState.draftRequests) + .concat(mergeRequests(locsState.acceptedRequests)) + .concat(mergeRequests(locsState.rejectedRequests)) + .concat(mergeRequests(locsState.openLocs)) + .concat(mergeRequests(locsState.closedLocs)) + .concat(mergeRequests(locsState.voidedLocs)) + ; + return all.find(request => hasLinkTo(request, locState)) === undefined; +} + +function mergeRequests(requests: Record) { + return ([] as LocRequestState[]) + .concat(requests["Collection"]) + .concat(requests["Identity"]) + .concat(requests["Transaction"]) + ; +} + +function hasLinkTo(request: LocRequestState, target: LocRequestState) { + return request.data().links.find(link => link.target.equalTo(target.data().id)) !== undefined; +} diff --git a/src/loc/LocLinkButton.test.tsx b/src/loc/LocLinkButton.test.tsx index ee0fe072..19db676f 100644 --- a/src/loc/LocLinkButton.test.tsx +++ b/src/loc/LocLinkButton.test.tsx @@ -2,11 +2,12 @@ import LocLinkButton from "./LocLinkButton"; import { clickByName, shallowRender, typeByLabel } from "../tests"; import { render, screen, waitFor } from "@testing-library/react"; import { setClientMock } from "src/logion-chain/__mocks__/LogionChainMock"; -import { LocsState, LogionClient } from "@logion/client"; +import { LogionClient } from "@logion/client"; import { buildLocRequest } from "./TestData"; import { UUID } from "@logion/node-api"; import { setupQueriesGetLegalOfficerCase } from "src/test/Util"; import { setupApiMock, api, OPEN_IDENTITY_LOC, OPEN_IDENTITY_LOC_ID } from "src/__mocks__/LogionMock"; +import { setLocState } from "./__mocks__/LocContextMock"; jest.mock("./LocContext"); jest.mock("../logion-chain"); @@ -21,17 +22,21 @@ describe("LocLinkButton", () => { }) it("links to an existing LOC", async () => { + const locsState = { + findByIdOrUndefined: () => ({ + data: () => buildLocRequest(UUID.fromDecimalStringOrThrow(OPEN_IDENTITY_LOC_ID), OPEN_IDENTITY_LOC), + }), + }; setClientMock({ - locsState: () => Promise.resolve({ - findById: () => ({ - data: () => buildLocRequest(UUID.fromDecimalStringOrThrow(OPEN_IDENTITY_LOC_ID), OPEN_IDENTITY_LOC), - }), - }) as unknown as LocsState, + locsState: () => Promise.resolve(locsState), logionApi: api.object(), } as unknown as LogionClient); setupApiMock(api => { setupQueriesGetLegalOfficerCase(api, UUID.fromDecimalStringOrThrow(OPEN_IDENTITY_LOC_ID), OPEN_IDENTITY_LOC); }); + setLocState({ + locsState: () => locsState, + }); render(); await clickByName(content => /Link to an existing LOC/i.test(content)); diff --git a/src/loc/LocLinkExistingLocDialog.tsx b/src/loc/LocLinkExistingLocDialog.tsx index b01d3c55..1671b1b2 100644 --- a/src/loc/LocLinkExistingLocDialog.tsx +++ b/src/loc/LocLinkExistingLocDialog.tsx @@ -17,8 +17,8 @@ export interface Props { } export default function LocLinkExistingDialog(props: Props) { - const { api, client } = useLogionChain(); - const { mutateLocState, locItems } = useLocContext(); + const { client } = useLogionChain(); + const { mutateLocState, locItems, locState } = useLocContext(); const { control, handleSubmit, setError, clearErrors, formState: { errors }, reset } = useForm({ defaultValues: { locId: "" @@ -32,9 +32,9 @@ export default function LocLinkExistingDialog(props: Props) { setError("locId", { type: "value", message: "Invalid LOC ID" }) return } - const loc = await api!.queries.getLegalOfficerCase(locId); + const loc = locState?.locsState().findByIdOrUndefined(locId); if (!loc) { - setError("locId", { type: "value", message: "LOC not found on chain" }) + setError("locId", { type: "value", message: "LOC not found" }) return } const alreadyLinked = locItems.find(item => item.type === 'Linked LOC' && item.as().linkedLoc.id.toString() === locId.toString()) @@ -55,7 +55,7 @@ export default function LocLinkExistingDialog(props: Props) { }); reset(); props.exit(); - }, [ props, locItems, api, setError, clearErrors, reset, client, mutateLocState ]) + }, [ props, locItems, locState, setError, clearErrors, reset, client, mutateLocState ]) return ( <> diff --git a/src/loc/__snapshots__/DraftLocInstructions.test.tsx.snap b/src/loc/__snapshots__/DraftLocInstructions.test.tsx.snap index 465ca6a1..778e95e3 100644 --- a/src/loc/__snapshots__/DraftLocInstructions.test.tsx.snap +++ b/src/loc/__snapshots__/DraftLocInstructions.test.tsx.snap @@ -36,6 +36,14 @@ exports[`DraftLocInstructions renders 1`] = ` Confidential documents will not be publicly available and will stay confidential between you and your Legal Officer. +

    + You must have a valid Identity LOC In order to submit this LOC for review. + + Also, if your LOC contains a link, its target must be closed and not void. +

    +

    + You won't be able to cancel this request if it is the target of a link in another request. +

    } /> diff --git a/src/loc/__snapshots__/LocDetailsTab.test.tsx.snap b/src/loc/__snapshots__/LocDetailsTab.test.tsx.snap index 9c89215a..5aedcf06 100644 --- a/src/loc/__snapshots__/LocDetailsTab.test.tsx.snap +++ b/src/loc/__snapshots__/LocDetailsTab.test.tsx.snap @@ -301,6 +301,7 @@ exports[`LocDetailsTabContent renders closed data LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "CLOSED", } @@ -344,6 +345,7 @@ exports[`LocDetailsTabContent renders closed data LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "CLOSED", } @@ -549,6 +551,7 @@ exports[`LocDetailsTabContent renders closed data LOC for User 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "CLOSED", } @@ -592,6 +595,7 @@ exports[`LocDetailsTabContent renders closed data LOC for User 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "CLOSED", } @@ -790,6 +794,7 @@ exports[`LocDetailsTabContent renders open data LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", } @@ -833,6 +838,7 @@ exports[`LocDetailsTabContent renders open data LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", } @@ -1044,6 +1050,7 @@ exports[`LocDetailsTabContent renders open data LOC for User 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", } @@ -1087,6 +1094,7 @@ exports[`LocDetailsTabContent renders open data LOC for User 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", } @@ -1299,6 +1307,7 @@ exports[`LocDetailsTabContent renders open identity LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Identity", "status": "OPEN", } @@ -1342,6 +1351,7 @@ exports[`LocDetailsTabContent renders open identity LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Identity", "status": "OPEN", } @@ -1553,6 +1563,7 @@ exports[`LocDetailsTabContent renders void data LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", "voidInfo": Object {}, @@ -1597,6 +1608,7 @@ exports[`LocDetailsTabContent renders void data LOC for LO 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", "voidInfo": Object {}, @@ -1793,6 +1805,7 @@ exports[`LocDetailsTabContent renders void data LOC for User 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", "voidInfo": Object {}, @@ -1837,6 +1850,7 @@ exports[`LocDetailsTabContent renders void data LOC for User 1`] = ` 14, ], }, + "links": Array [], "locType": "Transaction", "status": "OPEN", "voidInfo": Object {},