Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tokens records search. #546

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/__mocks__/LogionClientMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { toIsoString, fromIsoString } from "@logion/client/dist/DateTimeUtil.js"
import { api } from "./LogionMock";
import { VerifiedIssuerWithSelect } from "@logion/client/dist/Loc";
import { Hash } from "@logion/node-api";
import { TokensRecord } from "@logion/client";

export const axiosMock = {
post: jest.fn().mockReturnValue(undefined),
Expand Down Expand Up @@ -123,6 +124,12 @@ export class LocRequestState {

export class ClosedCollectionLoc extends LocRequestState {
requestSof: jest.Mock<Promise<EditableRequest>> | undefined;
getTokensRecord(_: { recordId: Hash } ): Promise<TokensRecord | undefined> {
return Promise.resolve(undefined)
}
getTokensRecords(): Promise<TokensRecord[]> {
return Promise.resolve([])
}
}

export class ClosedLoc extends LocRequestState {
Expand Down
3 changes: 1 addition & 2 deletions src/certificate/Certificate.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LogionClient, PublicLoc, LocData, CollectionItem, HashString, TokensRecord } from '@logion/client';
import { LogionClient, PublicLoc, LocData, CollectionItem, HashString, ClientTokensRecord } from '@logion/client';
import { UUID, Hash } from '@logion/node-api';
import { render, screen, waitFor } from "@testing-library/react";
import { act } from 'react-test-renderer';
Expand All @@ -10,7 +10,6 @@ import Certificate from './Certificate';
import { setClientMock } from 'src/logion-chain/__mocks__/LogionChainMock';
import { PATRICK } from 'src/common/TestData';
import { URLSearchParams } from 'url';
import { ClientTokensRecord } from "@logion/client/dist/LocClient";

jest.mock("react-router");
jest.mock("react-router-dom");
Expand Down
5 changes: 0 additions & 5 deletions src/loc/CollectionLocItemChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,6 @@ interface CheckResultProps {

function CheckResultFeedback(props: CheckResultProps) {
const { state } = props;
const { client } = useLogionChain();

if(!client) {
return null;
}

switch (state) {
case "POSITIVE":
Expand Down
2 changes: 1 addition & 1 deletion src/loc/record/TokensRecordFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function TokensRecordFiles(props: Props) {

return (<div className="TokensRecordFiles">
{ record.files.map(file => (
<TokensRecordFileCell { ...props } loc={ loc } file={ file } />
<TokensRecordFileCell { ...props } loc={ loc } file={ file } key={`tokens-record-file-${ loc.id }-${ file.hash.toHex() }`}/>
)) }
</div>)
}
Expand Down
51 changes: 51 additions & 0 deletions src/loc/record/TokensRecordFrame.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.TokensRecordFrame {
}

.TokensRecordFrame .IconTextRow .text {
width: calc(100% - 80px);
}

.TokensRecordFrame .tokens-record-id-input {
display: inline-block;
width: calc(100% - 32px);
}
.TokensRecordFrame .clear-button {
cursor: pointer;
margin-left: 7px;
}

.TokensRecordFrame .clear-button:hover {
opacity: 0.5;
}

.TokensRecordFrame .Button {
margin-left: 20px;
}

.TokensRecordFrame .IconTextRow .text {
width: calc(100% - 80px);
}

.TokensRecordFrame .CheckResultFeedback {
margin-top: 20px;
}

.TokensRecordFrame .CheckResultFeedback .label-positive {
color: #37ad4b;
font-weight: bold;
}

.TokensRecordFrame .CheckResultFeedback .label-negative {
color: #ea1f46;
font-weight: bold;
}

.TokensRecordFrame .CheckResultFeedback .Col {
padding-right: 20px;
min-width: 65px;
}

.TokensRecordFrame div.IconTextRow div.text div.FormGroup div div.Row div.Col {
width: 50rem;
}

81 changes: 81 additions & 0 deletions src/loc/record/TokensRecordFrame.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
jest.mock("../../common/CommonContext");
jest.mock("../UserLocContext");
jest.mock("../LocContext");

import { render, screen, waitFor, } from "@testing-library/react";
import TokensRecordFrame from "./TokensRecordFrame";
import userEvent from "@testing-library/user-event";
import { clickByName } from "../../tests";
import { HashString, LocClient } from "@logion/client";
import { Hash, UUID } from "@logion/node-api";
import { TokensRecord } from "@logion/client/dist/TokensRecord";
import { setLocState } from "../__mocks__/LocContextMock";
import { ClosedCollectionLoc } from "../../__mocks__/LogionClientMock";

describe("TokensRecordFrame", () => {

it("renders", () => {
const result = render(<TokensRecordFrame/>);
expect(result).toMatchSnapshot();
})

it("shows positive match with item ID", async () => {
setLocState(mockLocState());
await testWithInput("record-matched");
await waitFor(() => expect(screen.getByText("positive")).toBeVisible());
})

it("shows negative match with item ID", async () => {
setLocState(mockLocState());
await testWithInput("non-existing-record");
await waitFor(() => expect(screen.getByText("negative")).toBeVisible());
})

})

async function testWithInput(inputValue: string) {
render(<TokensRecordFrame/>);
const inputField = screen.getByTestId("tokens-record-id");
await userEvent.type(inputField, inputValue);
await clickByName(content => /Check Tokens Record ID/.test(content));
}

function mockLocState(): ClosedCollectionLoc {

const matched = mockTokensRecord("record-matched");
const other = mockTokensRecord("record-other");

const locState = new ClosedCollectionLoc();
locState.getTokensRecord = (args: { recordId: Hash }) => {
if (args.recordId.equalTo(matched.id)) {
return Promise.resolve(matched);
} else {
return Promise.resolve(undefined);
}
};
locState.getTokensRecords = () => {
return Promise.resolve([ matched, other ]);
};
return locState;
}

function mockTokensRecord(idSeed: string): TokensRecord {
const tokensRecord = {
id: Hash.of(idSeed),
description: HashString.fromValue("Record Description"),
addedOn: "2022-08-23T07:27:46.128Z",
issuer: "record-issuer",
files: [ {
name: HashString.fromValue("record-file-name.txt"),
hash: Hash.of("record-file-content"),
size: 10n,
uploaded: true,
contentType: HashString.fromValue("text/plain")
} ]
};
return new TokensRecord({
locId: new UUID("d97c99fd-9bcc-4f92-b9ea-b6be93abbbcd"),
locClient: {} as LocClient,
tokensRecord
});
}
155 changes: 147 additions & 8 deletions src/loc/record/TokensRecordFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { TokensRecord, ClosedCollectionLoc } from "@logion/client";
import { useEffect, useState } from "react";
import { Spinner } from "react-bootstrap";
import { useEffect, useState, useCallback, useMemo } from "react";
import { Spinner, Form } from "react-bootstrap";
import ButtonGroup from "src/common/ButtonGroup";
import { POLKADOT } from "src/common/ColorTheme";
import { useCommonContext } from "src/common/CommonContext";
import Frame from "src/common/Frame";
import Icon from "src/common/Icon";
import IconTextRow from "src/common/IconTextRow";
import { useLocContext } from "../LocContext";
import { ContributionMode } from "../types";
import { ContributionMode, toItemId } from "../types";
import IssuerSelectionButton from "../issuer/IssuerSelectionButton";
import AddTokensRecordButton from "./AddTokensRecordButton";
import TokensRecordTable from "./TokensRecordTable";
import FormGroup from "../../common/FormGroup";
import { Row, Col } from "../../common/Grid";
import Button from "../../common/Button";
import { Hash } from "@logion/node-api";
import "./TokensRecordFrame.css";

export type CheckResult = 'NONE' | 'POSITIVE' | 'NEGATIVE';

export default function TokensRecordFrame(props: { contributionMode?: ContributionMode }) {
const { viewer } = useCommonContext();
const { viewer, colorTheme } = useCommonContext();
const { locState } = useLocContext();
const [ records, setRecords ] = useState<TokensRecord[]>();
const [ state, setState ] = useState<CheckResult>('NONE');
const [ recordId, setRecordId ] = useState<string>("");
const [ record, setRecord ] = useState<TokensRecord>();
const [ managedCheck, setManagedCheck ] = useState<{ recordId: Hash, active: boolean }>();

useEffect(() => {
if(locState instanceof ClosedCollectionLoc) {
Expand All @@ -27,24 +38,102 @@ export default function TokensRecordFrame(props: { contributionMode?: Contributi
}
}, [ locState ]);

const checkData = useCallback(async () => {
if (recordId && locState instanceof ClosedCollectionLoc) {
const actualId = toItemId(recordId);
if (actualId === undefined) {
setState('NEGATIVE');
} else {
try {
const record = await locState.getTokensRecord({
recordId: actualId
})
setRecord(record);
if (record) {
setState('POSITIVE');
} else {
setState('NEGATIVE');
}
} catch (e) {
console.log(e)
setState('NEGATIVE');
}
}
}
}, [ recordId, locState ]);

const resetCheck = useCallback(() => {
setManagedCheck(undefined);
setRecordId("");
setState('NONE');
setRecord(undefined);
}, []);

const title = useMemo(() => {
if (records === undefined || records.length === 0) {
return "Tokens records";
} else {
return `Tokens records (${ records.length })`;
}
}, [ records ]);

return (
<Frame
className="TokensRecordFrame"
titleIcon={{
icon: {
id: "records_polka"
},
width: "64px",
}}
title="Tokens records"
title={ title }
border={`2px solid ${POLKADOT}`}
>
<IconTextRow
icon={ <Icon icon={ { id: "tip" } } width="45px" /> }
text={
text={ <>
<p>The entire Tokens record list below will be visible on each token public certificate for all the owners of tokens declared in this LOC.
If the restricted delivery option is activated, token owners will be able to get a copy of the related file.
</p>
}
<FormGroup
id="tokenRecordId"
noFeedback={ true }
control={
<Row>
<Col className={ managedCheck?.active ? "matched" : undefined }>
<Form.Control
className="tokens-record-id-input"
type="text"
value={ recordId }
onChange={ value => {
setState('NONE');
if (managedCheck) {
setManagedCheck({
recordId: managedCheck.recordId,
active: false
});
}
setRecordId(value.target.value);
} }
data-testid="tokens-record-id"
/>
{
state !== "NONE" &&
<span className="clear-button" onClick={ resetCheck }><Icon
icon={ { id: "clear", hasVariants: true } } height="24px" /></span>
}
</Col>
<Col className="buttons">
<Button onClick={ checkData } disabled={ !recordId }>
<Icon icon={ { id: "search" } } /> Check Tokens Record ID
</Button>
</Col>
</Row>
}
colors={ colorTheme.frame }
/>
<CheckResultFeedback state = { state }/>
</>}
/>
{
records === undefined &&
Expand All @@ -53,7 +142,8 @@ export default function TokensRecordFrame(props: { contributionMode?: Contributi
{
records !== undefined &&
<>
<TokensRecordTable records={records} contributionMode={props.contributionMode}/>
<TokensRecordTable records={ records } record={ record }
contributionMode={ props.contributionMode } />
<ButtonGroup
align="left"
>
Expand All @@ -65,3 +155,52 @@ export default function TokensRecordFrame(props: { contributionMode?: Contributi
</Frame>
);
}

interface CheckResultProps {
state: CheckResult,
}

function CheckResultFeedback(props: CheckResultProps) {
const { state } = props;

switch (state) {
case "POSITIVE":
return (
<>
<Row className="CheckResultFeedback result-positive" id={ `feedback-${ state }` }>
<Col>
<p>
Check result: <span className="label-positive">positive</span><br />
The Tokens Record - defined by the ID you submitted - is part of the current Collection
LOC.
</p>
</Col>
<Col>
<Icon icon={ { id: "ok" } } height='45px' />
</Col>
</Row>
</>
)
case "NEGATIVE":
return (
<Row className="CheckResultFeedback result-negative" id={ `feedback-${ state }` }>
<Col>
<p>
Check result: <span className="label-negative">negative</span><br />
The Tokens Record - defined by the ID you submitted - has no match and is NOT part of
the current<br />
Collection LOC. Please be careful and execute a deeper due diligence.
</p>
</Col>
<Col>
<Icon icon={ { id: "ko" } } height='45px' />
</Col>
</Row>
)
case "NONE":
return (
<Row className="CheckResultFeedback result-none" children="" id={ `feedback-${ state }` } />
)
}
}

Loading
Loading