From bb4bcae454687f7034c8d01475f5eda133217fd0 Mon Sep 17 00:00:00 2001 From: Benoit Devos Date: Thu, 28 Mar 2024 17:41:33 +0100 Subject: [PATCH] feat: request Transaction LOC in same way as Collection; factor into generic components. logion-network/logion-internal#1196 --- src/loc/CollectionLocCreation.tsx | 29 ---- src/loc/CollectionLocRequest.css | 3 - src/loc/DataLocRequest.css | 3 + ...quest.test.tsx => DataLocRequest.test.tsx} | 64 +++++--- ...ctionLocRequest.tsx => DataLocRequest.tsx} | 54 ++++--- src/loc/TransactionLocCreation.css | 8 - src/loc/TransactionLocCreation.test.tsx | 102 ------------- src/loc/TransactionLocCreation.tsx | 139 ------------------ src/loc/TransactionLocRequestForm.tsx | 124 +++++++--------- src/wallet-user/Home.tsx | 13 +- ...dentityLocCreation.tsx => LocCreation.tsx} | 15 +- src/wallet-user/UserRouter.tsx | 38 +++-- .../__snapshots__/UserRouter.test.tsx.snap | 30 +++- 13 files changed, 199 insertions(+), 423 deletions(-) delete mode 100644 src/loc/CollectionLocCreation.tsx delete mode 100644 src/loc/CollectionLocRequest.css create mode 100644 src/loc/DataLocRequest.css rename src/loc/{CollectionLocRequest.test.tsx => DataLocRequest.test.tsx} (65%) rename src/loc/{CollectionLocRequest.tsx => DataLocRequest.tsx} (69%) delete mode 100644 src/loc/TransactionLocCreation.css delete mode 100644 src/loc/TransactionLocCreation.test.tsx delete mode 100644 src/loc/TransactionLocCreation.tsx rename src/wallet-user/{IdentityLocCreation.tsx => LocCreation.tsx} (60%) diff --git a/src/loc/CollectionLocCreation.tsx b/src/loc/CollectionLocCreation.tsx deleted file mode 100644 index 5dd8ab0e..00000000 --- a/src/loc/CollectionLocCreation.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useCallback } from 'react'; -import { useNavigate } from "react-router-dom"; -import { COLLECTION_REQUEST_PATH } from "../wallet-user/UserRouter"; -import Button from '../common/Button'; - -export interface Props { - renderButton?: (onClick: () => void) => React.ReactNode; -} - -export default function CollectionLocCreation(props: Props) { - const navigate = useNavigate(); - - const onSelect = useCallback(() => { - navigate(COLLECTION_REQUEST_PATH); - }, [ navigate ]); - - return ( - <> - { - props.renderButton === undefined && - - } - { - props.renderButton !== undefined && - props.renderButton( onSelect ) - } - - ); -} diff --git a/src/loc/CollectionLocRequest.css b/src/loc/CollectionLocRequest.css deleted file mode 100644 index 7582f565..00000000 --- a/src/loc/CollectionLocRequest.css +++ /dev/null @@ -1,3 +0,0 @@ -.CollectionLocRequest .request-additional-id-loc-frame { - margin-top: 30px; -} diff --git a/src/loc/DataLocRequest.css b/src/loc/DataLocRequest.css new file mode 100644 index 00000000..e528c83f --- /dev/null +++ b/src/loc/DataLocRequest.css @@ -0,0 +1,3 @@ +.DataLocRequest .request-additional-id-loc-frame { + margin-top: 30px; +} diff --git a/src/loc/CollectionLocRequest.test.tsx b/src/loc/DataLocRequest.test.tsx similarity index 65% rename from src/loc/CollectionLocRequest.test.tsx rename to src/loc/DataLocRequest.test.tsx index f5ca266f..06c8ea42 100644 --- a/src/loc/CollectionLocRequest.test.tsx +++ b/src/loc/DataLocRequest.test.tsx @@ -5,7 +5,7 @@ import { setLocsState, setMutateLocsState } from "../wallet-user/__mocks__/UserC import { render, waitFor, screen, getByText } from "@testing-library/react"; import { clickByName, typeByLabel } from "../tests"; import { navigate } from "../__mocks__/ReactRouterMock"; -import CollectionLocRequest from "./CollectionLocRequest"; +import DataLocRequest from "./DataLocRequest"; import userEvent from "@testing-library/user-event"; import { LegalOfficerClass } from "@logion/client/dist/Types"; @@ -15,42 +15,61 @@ jest.mock('../common/CommonContext'); const locId = new UUID("a2b9dfa7-cbde-414b-8cda-cdd221a57643"); -describe("CollectionLocRequest", () => { +describe("DataLocRequest", () => { + + it("should disable form submission when no valid identity locs", async () => { + + setupLocsState([]); + render(); + await checkFormDisabled(); + }); + + it("should disable form submission when no legal officer selected", async () => { + + setupLocsState(twoLegalOfficers); + render(); + await checkFormDisabled(); + }); +}) + +describe("DataLocRequest (Transaction)", () => { it("should redirect when form is correctly filled in and submitted", async () => { setupLocsState(twoLegalOfficers); - render(); + render(); await selectLegalOfficer(); - await fillInForm(); + await fillInForm("Transaction"); + expect(screen.getByRole("button", { name: "Create Draft" })).toBeEnabled(); await clickByName("Create Draft"); - await waitFor(() => expect(navigate).toBeCalledWith(`/user/loc/collection/${locId.toString()}`)); + await waitFor(() => expect(navigate).toBeCalledWith(`/user/loc/transaction/${locId.toString()}`)); }); +}) - it("should disable form submission when no valid identity locs", async () => { +describe("DataLocRequest (Collection)", () => { - setupLocsState([]); - render(); - await checkFormDisabled(); - }); - - it("should disable form submission when no legal officer selected", async () => { + it("should redirect when form is correctly filled in and submitted", async () => { setupLocsState(twoLegalOfficers); - render(); - await checkFormDisabled(); + render(); + + await selectLegalOfficer(); + await fillInForm("Collection"); + expect(screen.getByRole("button", { name: "Create Draft" })).toBeEnabled(); + await clickByName("Create Draft"); + + await waitFor(() => expect(navigate).toBeCalledWith(`/user/loc/collection/${ locId.toString() }`)); }); it("should disable form submission when no collection limit selected", async () => { setupLocsState(twoLegalOfficers); - render(); + render(); await selectLegalOfficer(); await checkFormDisabled(); }); - }) async function checkFormDisabled() { @@ -70,6 +89,7 @@ function setupLocsState(legalOfficersWithValidIdentityLoc: LegalOfficerClass[]) } as DraftRequest; const locsState = { legalOfficersWithValidIdentityLoc, + requestTransactionLoc: () => Promise.resolve(draftRequest), requestCollectionLoc: () => Promise.resolve(draftRequest), } as unknown as LocsState; setLocsState(locsState); @@ -85,12 +105,14 @@ async function selectLegalOfficer() { await waitFor(() => userEvent.click(getByText(legalOfficer1Select, "Patrick Gielen (workload: 1)"))); } -async function fillInForm() { +async function fillInForm(locType: 'Transaction' | 'Collection') { await typeByLabel("Description", "description"); - await typeByLabel("Value fee", "10"); - await typeByLabel("Collection item fee", "10"); - await typeByLabel("Tokens record fee", "10"); - await selectAndEnterText(1, "Data number limit", "999"); + if (locType === 'Collection') { + await typeByLabel("Value fee", "10"); + await typeByLabel("Collection item fee", "10"); + await typeByLabel("Tokens record fee", "10"); + await selectAndEnterText(1, "Data number limit", "999"); + } } async function selectAndEnterText(index: number, name: string, value: string) { diff --git a/src/loc/CollectionLocRequest.tsx b/src/loc/DataLocRequest.tsx similarity index 69% rename from src/loc/CollectionLocRequest.tsx rename to src/loc/DataLocRequest.tsx index 81c61f13..5d457254 100644 --- a/src/loc/CollectionLocRequest.tsx +++ b/src/loc/DataLocRequest.tsx @@ -8,16 +8,20 @@ import { LegalOfficerClass } from "@logion/client"; import { useUserContext } from "../wallet-user/UserContext"; import { useCommonContext } from "../common/CommonContext"; import CollectionLocRequestForm from "./CollectionLocRequestForm"; -import IdentityLocCreation from "../wallet-user/IdentityLocCreation"; -import "./CollectionLocRequest.css"; +import LocCreation from "../wallet-user/LocCreation"; +import "./DataLocRequest.css"; import ButtonGroup from "../common/ButtonGroup"; +import TransactionLocRequestForm from "./TransactionLocRequestForm"; export interface Props { backPath: string, + locType: 'Transaction' | 'Collection' + iconId: string, } -export default function CollectionLocRequest(props: Props) { +export default function DataLocRequest(props: Props) { const { colorTheme } = useCommonContext(); + const { locType, iconId } = props; const [ legalOfficer, setLegalOfficer ] = useState(null); const { locsState } = useUserContext(); const navigate = useNavigate(); @@ -31,9 +35,9 @@ export default function CollectionLocRequest(props: Props) { return ( navigate(props.backPath) } > @@ -59,31 +63,39 @@ export default function CollectionLocRequest(props: Props) { please request an Identity LOC to the Logion Legal Officer of your choice by clicking on the related button below:

- + } { legalOfficersWithValidIdentityLoc.length === 0 && -

To submit a Collection LOC request, you must select a Logion Legal +

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

Please request an Identity LOC to the Logion Legal Officer of your choice:

- + } - - - - - - -
-
-) + + + + { locType === "Collection" && + + } + { locType === "Transaction" && + + } + + + + + ) } diff --git a/src/loc/TransactionLocCreation.css b/src/loc/TransactionLocCreation.css deleted file mode 100644 index f32f3c03..00000000 --- a/src/loc/TransactionLocCreation.css +++ /dev/null @@ -1,8 +0,0 @@ -.LocCreation .info-text { - margin-top: 25px; - text-align: left; -} - -.LocCreation .modal-body { - padding-bottom: 5px; -} diff --git a/src/loc/TransactionLocCreation.test.tsx b/src/loc/TransactionLocCreation.test.tsx deleted file mode 100644 index 2b52c1d7..00000000 --- a/src/loc/TransactionLocCreation.test.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event' -import { clickByName, typeByLabel } from '../tests'; -import TransactionLocCreation from './TransactionLocCreation'; -import { setHasValidIdentityLoc, setLocsState, setMutateLocsState } from "../wallet-user/__mocks__/UserContextMock"; -import { oneLegalOfficer, twoLegalOfficers } from "../common/TestData"; -import { DraftRequest, LocsState } from '@logion/client'; -import { UUID } from '@logion/node-api'; -import { navigate } from '../__mocks__/ReactRouterMock'; - -jest.mock('../common/CommonContext'); -jest.mock('../common/Model'); -jest.mock('../wallet-user/UserContext'); -jest.mock('../logion-chain'); -jest.unmock("@logion/client"); - -const requestButtonLabel = "Request a Transaction Protection" - -describe("LocCreation", () => { - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it("should disable 'Submit' when user has valid id LOC but the form is empty", async () => { - setHasValidIdentityLoc(oneLegalOfficer) - await openDialog(); - - await waitFor(() => { - expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled(); - expect(screen.getByRole("button", { name: "Submit" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "Request an Identity Protection" })).toBeEnabled(); - }); - }) - - it("should enable 'Submit' and remove 'Request an Identity Protection' when an LO is selected", async () => { - setHasValidIdentityLoc([ twoLegalOfficers[1] ]) - await openDialog(); - - await userEvent.click(screen.getByText("Select...")); - await waitFor(() => userEvent.click(screen.getByText("Guillaume Grain (workload: 3)"))); - - await waitFor(() => { - expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled(); - expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled(); - expect(screen.queryByRole("button", { name: "Request an Identity Protection" })).toBeNull(); - }); - }) - - it("should remove 'Submit' and enable 'Request an Identity Protection' when there are no LO's", async () => { - setHasValidIdentityLoc([ ]) - await openDialog(); - - await waitFor(() => { - expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled(); - expect(screen.queryByRole("button", { name: "Submit" })).toBeNull(); - expect(screen.getByRole("button", { name: "Request an Identity Protection" })).toBeEnabled(); - }); - }) - - it("should close dialog and not create a request when cancel is pressed", async () => { - setHasValidIdentityLoc(oneLegalOfficer) - await openDialog(); - - const dialog = screen.getByRole("dialog"); - await clickByName("Cancel"); - await waitFor(() => expect(dialog).not.toBeInTheDocument()); - }) - - it("should create the request if the selected LO has a valid identity LOC", async () => { - const locId = new UUID("a2b9dfa7-cbde-414b-8cda-cdd221a57643"); - const draftRequest = { - locId, - locsState: () => locsState, - } as DraftRequest; - const locsState = { - legalOfficersWithValidIdentityLoc: twoLegalOfficers, - requestTransactionLoc: () => Promise.resolve(draftRequest), - } as unknown as LocsState; - setLocsState(locsState); - setMutateLocsState(async (mutator: (current: LocsState) => Promise): Promise => { - await mutator(locsState); - return Promise.resolve(); - }); - - await openDialog(); - - const description = "description"; - await typeByLabel("Description", description) - - await userEvent.click(screen.getByText("Select...")); - await waitFor(() => userEvent.click(screen.getByText("Patrick Gielen (workload: 1)"))); - await clickByName("Submit"); - - await waitFor(() => expect(navigate).toBeCalledWith(`/user/loc/transaction/${locId.toString()}`)); - }) - - async function openDialog() { - render(); - await clickByName(requestButtonLabel); - } -}) diff --git a/src/loc/TransactionLocCreation.tsx b/src/loc/TransactionLocCreation.tsx deleted file mode 100644 index 5034f69a..00000000 --- a/src/loc/TransactionLocCreation.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useCallback, useEffect, useState, useMemo } from 'react'; -import { useForm } from 'react-hook-form'; -import { Numbers, Lgnt } from "@logion/node-api"; - -import { useCommonContext } from '../common/CommonContext'; -import { LocsState, LegalOfficerClass, DraftRequest } from "@logion/client"; -import Button, { Action } from '../common/Button'; -import Dialog from '../common/Dialog'; - -import { useUserContext } from '../wallet-user/UserContext'; -import ButtonGroup from "../common/ButtonGroup"; -import TransactionLocRequestForm, { FormValues } from './TransactionLocRequestForm'; -import { useLogionChain } from '../logion-chain'; -import { useNavigate } from "react-router-dom"; -import { locDetailsPath } from "../wallet-user/UserRouter"; -import './TransactionLocCreation.css'; -import IdentityLocCreation from '../wallet-user/IdentityLocCreation'; - -export interface Props { - requestButtonLabel?: string; - renderButton?: (onClick: () => void) => React.ReactNode; -} - -export default function TransactionLocCreation(props: Props) { - const { getOfficer } = useLogionChain(); - const { colorTheme } = useCommonContext(); - const { locsState, mutateLocsState } = useUserContext(); - const [ requestLoc, setRequestLoc ] = useState(false); - const { requestButtonLabel } = props; - const navigate = useNavigate(); - const { control, handleSubmit, formState: { errors }, reset, watch } = useForm({ - defaultValues: { - description: "", - legalOfficer: "", - legalFee: undefined, - } - }); - const [ selectedLegalOfficer, setSelectedLegalOfficer ] = useState(); - - const legalOfficersWithValidIdentityLoc = useMemo( - () => (locsState && !locsState.discarded) ? locsState.legalOfficersWithValidIdentityLoc : undefined, - [ locsState ] - ); - - const clear = useCallback(() => { - reset(); - setRequestLoc(false); - }, [ reset ]); - - const submit = useCallback(async (formValues: FormValues) => { - let draftRequest: DraftRequest; - await mutateLocsState(async (locsState: LocsState) => { - draftRequest = await locsState!.requestTransactionLoc({ - legalOfficerAddress: selectedLegalOfficer!.address, - description: formValues.description, - draft: true, - template: undefined, - legalFee: formValues.legalFee ? Lgnt.fromPrefixedNumber(new Numbers.PrefixedNumber(formValues.legalFee.value, formValues.legalFee.unit)) : undefined, - }) as DraftRequest; - return draftRequest.locsState(); - }); - clear(); - navigate(locDetailsPath(draftRequest!.locId, "Transaction")); - }, [ selectedLegalOfficer, mutateLocsState, clear, navigate ]); - - useEffect(() => { - if (getOfficer !== undefined && locsState !== undefined) { - const subscription = watch(({ legalOfficer }) => { - setSelectedLegalOfficer(getOfficer(legalOfficer)); - }); - return () => subscription.unsubscribe(); - } - }, [ watch, getOfficer, locsState, setSelectedLegalOfficer ]); - - if (legalOfficersWithValidIdentityLoc === undefined) { - return null; - } - - const requestIdLocAction = ; - - const cancelAction: Action = { - id: "cancel", - callback: clear, - buttonText: 'Cancel', - buttonVariant: 'secondary', - type: "button", - }; - - return ( - <> - { - props.renderButton === undefined && - - } - { - props.renderButton !== undefined && - props.renderButton(() => setRequestLoc(true)) - } - { legalOfficersWithValidIdentityLoc?.length === 0 && - -

Transaction LOC Request

-

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

-

Please request an Identity LOC to the Logion Legal Officer of your choice:

-
- } - { legalOfficersWithValidIdentityLoc?.length > 0 && - -

Transaction LOC Request

- - - - - - { selectedLegalOfficer === undefined && -

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 clicking on the related button below:

- } -
- } - - ); -} diff --git a/src/loc/TransactionLocRequestForm.tsx b/src/loc/TransactionLocRequestForm.tsx index 978aa5a8..34c99cc3 100644 --- a/src/loc/TransactionLocRequestForm.tsx +++ b/src/loc/TransactionLocRequestForm.tsx @@ -1,86 +1,66 @@ -import { Controller, Control, FieldErrors } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import Form from "react-bootstrap/Form"; -import { Lgnt } from "@logion/node-api"; - -import { BackgroundAndForegroundColors } from '../common/ColorTheme'; +import { Lgnt, Numbers } from "@logion/node-api"; import FormGroup from '../common/FormGroup'; -import Select, { OptionType } from '../common/Select'; - -import { buildOptions } from '../wallet-user/trust-protection/SelectLegalOfficer'; +import AmountControl, { Amount, validateAmount } from 'src/common/AmountControl'; +import CollapsePane from 'src/components/collapsepane/CollapsePane'; +import { BackgroundAndForegroundColors } from "../common/ColorTheme"; +import { useCallback } from "react"; +import { DraftRequest, LocsState } from "@logion/client"; import { useUserContext } from "../wallet-user/UserContext"; -import AmountControl, { Amount, validateAmount } from '../common/AmountControl'; -import CollapsePane from '../components/collapsepane/CollapsePane'; -import { useState, useEffect } from "react"; +import ButtonGroup from "../common/ButtonGroup"; +import Button from "../common/Button"; +import { locDetailsPath } from "../wallet-user/UserRouter"; +import { useNavigate } from "react-router"; export interface FormValues { description: string; - legalOfficer: string; legalFee: Amount | undefined; } export interface Props { - control: Control; - errors: FieldErrors; colors: BackgroundAndForegroundColors; - legalOfficer: string | null; + legalOfficer: string | undefined; } export default function TransactionLocRequestForm(props: Props) { - const { locsState } = useUserContext(); - const [ legalOfficersOptions, setLegalOfficersOptions ] = useState[]>([]); - - useEffect(() => { - if (locsState !== undefined && legalOfficersOptions.length === 0) { - buildOptions(locsState.legalOfficersWithValidIdentityLoc) - .then(options => setLegalOfficersOptions(options)); + const { mutateLocsState } = useUserContext(); + const navigate = useNavigate(); + const { control, handleSubmit, formState: { errors } } = useForm({ + defaultValues: { + description: "", + legalFee: undefined, } - }, [ legalOfficersOptions, locsState ]); + }); - if(locsState === undefined) { - return null; - } + const submit = useCallback(async (formValues: FormValues) => { - return ( - <> - { + draftRequest = await locsState!.requestTransactionLoc({ + legalOfficerAddress: props.legalOfficer!, + description: formValues.description, + draft: true, + template: undefined, + legalFee: formValues.legalFee ? Lgnt.fromPrefixedNumber(new Numbers.PrefixedNumber(formValues.legalFee.value, formValues.legalFee.unit)) : undefined, + }) as DraftRequest; + return draftRequest.locsState(); + }) + navigate(locDetailsPath(draftRequest!.data().id, "Transaction")); - }} - render={({ field }) => ( -