Skip to content

Commit ad34281

Browse files
committed
feat(web): case-duplication-in-resolver
1 parent 7cf13b4 commit ad34281

File tree

5 files changed

+190
-5
lines changed

5 files changed

+190
-5
lines changed

web/src/hooks/queries/usePopulatedDisputeData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActio
55
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
66
import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate";
77

8-
import { useGraphqlBatcher } from "context/GraphqlBatcher";
98
import { DEFAULT_CHAIN } from "consts/chains";
9+
import { useGraphqlBatcher } from "context/GraphqlBatcher";
1010
import { debounceErrorToast } from "utils/debounceErrorToast";
1111
import { isUndefined } from "utils/index";
1212

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
3+
import { REFETCH_INTERVAL, STALE_TIME } from "consts/index";
4+
import { useGraphqlBatcher } from "context/GraphqlBatcher";
5+
6+
import { graphql } from "src/graphql";
7+
import { RoundDetailsQuery } from "src/graphql/graphql";
8+
import { isUndefined } from "src/utils";
9+
10+
const roundDetailsQuery = graphql(`
11+
query RoundDetails($roundID: ID!) {
12+
round(id: $roundID) {
13+
court {
14+
id
15+
}
16+
nbVotes
17+
}
18+
}
19+
`);
20+
21+
export const useRoundDetailsQuery = (disputeId?: string, roundIndex?: number) => {
22+
const isEnabled = !isUndefined(disputeId) && !isUndefined(roundIndex);
23+
const { graphqlBatcher } = useGraphqlBatcher();
24+
25+
return useQuery<RoundDetailsQuery>({
26+
queryKey: [`roundDetailsQuery${disputeId}-${roundIndex}`],
27+
enabled: isEnabled,
28+
refetchInterval: REFETCH_INTERVAL,
29+
staleTime: STALE_TIME,
30+
queryFn: async () =>
31+
await graphqlBatcher.fetch({
32+
id: crypto.randomUUID(),
33+
document: roundDetailsQuery,
34+
variables: { roundID: `${disputeId}-${roundIndex}` },
35+
}),
36+
});
37+
};

web/src/pages/Resolver/Landing.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useEffect, useState } from "react";
2+
import styled, { css } from "styled-components";
3+
4+
import { useNavigate } from "react-router-dom";
5+
import { useDebounce } from "react-use";
6+
7+
import { Button } from "@kleros/ui-components-library";
8+
9+
import { AliasArray, Answer, useNewDisputeContext } from "context/NewDisputeContext";
10+
11+
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
12+
import { usePopulatedDisputeData } from "queries/usePopulatedDisputeData";
13+
import { useRoundDetailsQuery } from "queries/useRoundDetailsQuery";
14+
15+
import { isUndefined } from "src/utils";
16+
17+
import { landscapeStyle } from "styles/landscapeStyle";
18+
import { responsiveSize } from "styles/responsiveSize";
19+
20+
import { Divider } from "components/Divider";
21+
import LabeledInput from "components/LabeledInput";
22+
23+
import Header from "./Header";
24+
25+
const Container = styled.div`
26+
display: flex;
27+
flex-direction: column;
28+
gap: 24px;
29+
align-items: center;
30+
width: 84vw;
31+
32+
${landscapeStyle(
33+
() => css`
34+
width: ${responsiveSize(442, 700, 900)};
35+
36+
padding-bottom: 240px;
37+
gap: 48px;
38+
`
39+
)}
40+
`;
41+
42+
const ErrorMsg = styled.small`
43+
font-size: 16px;
44+
color: ${({ theme }) => theme.error};
45+
`;
46+
47+
const Landing: React.FC = () => {
48+
const navigate = useNavigate();
49+
50+
const [disputeID, setDisputeID] = useState<string>();
51+
const [debouncedDisputeID, setDebouncedDisputeID] = useState<string>();
52+
const { disputeData, setDisputeData } = useNewDisputeContext();
53+
useDebounce(() => setDebouncedDisputeID(disputeID), 500, [disputeID]);
54+
55+
const { data: dispute } = useDisputeDetailsQuery(debouncedDisputeID);
56+
const {
57+
data: populatedDispute,
58+
isError: isErrorPopulatedDisputeQuery,
59+
isLoading,
60+
} = usePopulatedDisputeData(debouncedDisputeID, dispute?.dispute?.arbitrated.id as `0x${string}`);
61+
62+
// we want the genesis round's court and numberOfJurors
63+
const { data: roundData, isError: isErrorRoundQuery } = useRoundDetailsQuery(debouncedDisputeID, 0);
64+
65+
const isInvalidDispute =
66+
!isLoading &&
67+
!isUndefined(populatedDispute) &&
68+
(isErrorRoundQuery || isErrorPopulatedDisputeQuery || Object.keys(populatedDispute).length === 0);
69+
70+
useEffect(() => {
71+
if (isUndefined(populatedDispute) || isUndefined(roundData) || isInvalidDispute) return;
72+
73+
const answers = populatedDispute.answers.reduce<Answer[]>((acc, val) => {
74+
acc.push({ ...val, id: parseInt(val.id, 16).toString() });
75+
return acc;
76+
}, []);
77+
78+
let aliasesArray: AliasArray[] | undefined;
79+
if (!isUndefined(populatedDispute.aliases)) {
80+
aliasesArray = Object.entries(populatedDispute.aliases).map(([key, value], index) => ({
81+
name: key,
82+
address: value,
83+
id: (index + 1).toString(),
84+
}));
85+
}
86+
87+
setDisputeData({
88+
...disputeData,
89+
title: populatedDispute.title,
90+
description: populatedDispute.description,
91+
category: populatedDispute.category,
92+
policyURI: populatedDispute.policyURI,
93+
question: populatedDispute.question,
94+
courtId: roundData.round?.court.id,
95+
numberOfJurors: roundData.round?.nbVotes,
96+
answers,
97+
aliasesArray,
98+
});
99+
}, [populatedDispute, roundData, isInvalidDispute]);
100+
101+
const showContinueButton =
102+
!isUndefined(disputeData) && !isUndefined(populatedDispute) && !isInvalidDispute && !isUndefined(roundData);
103+
return (
104+
<Container>
105+
<Header text="Create a case" />
106+
<Button text="Create from Scratch" onClick={() => navigate("/resolver/title")} />
107+
108+
<Divider />
109+
<LabeledInput
110+
value={disputeID}
111+
onChange={(e) => setDisputeID(e.target.value)}
112+
placeholder="Dispute ID"
113+
label="Duplicate an exiting case."
114+
type="number"
115+
onInput={(e) => {
116+
const value = e.currentTarget.value.replace(/[^0-9]/g, "");
117+
118+
e.currentTarget.value = value;
119+
return e;
120+
}}
121+
/>
122+
{isInvalidDispute ? <ErrorMsg>Error loading dispute data. Please use another dispute.</ErrorMsg> : null}
123+
{showContinueButton ? <Button small text="Continue" onClick={() => navigate("/resolver/title")} /> : null}
124+
</Container>
125+
);
126+
};
127+
128+
export default Landing;

web/src/pages/Resolver/Parameters/NotablePersons/PersonFields.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef } from "react";
1+
import React, { useEffect, useRef } from "react";
22
import styled, { css } from "styled-components";
33

44
import { usePublicClient } from "wagmi";
@@ -44,6 +44,7 @@ const PersonFields: React.FC = () => {
4444
const publicClient = usePublicClient({ chainId: 1 });
4545

4646
const debounceValidateAddress = (address: string, key: number) => {
47+
if (isUndefined(publicClient)) return;
4748
// Clear the existing timer
4849
if (validationTimerRef.current) {
4950
clearTimeout(validationTimerRef.current);
@@ -59,6 +60,22 @@ const PersonFields: React.FC = () => {
5960
setDisputeData({ ...disputeData, aliasesArray: updatedAliases });
6061
}, 500);
6162
};
63+
64+
// in case of duplicate creation flow, aliasesArray will already be populated.
65+
// validating addresses in case it is
66+
useEffect(() => {
67+
if (disputeData.aliasesArray && publicClient) {
68+
disputeData.aliasesArray.map(async (alias, key) => {
69+
const isValid = await validateAddress(alias.address, publicClient);
70+
const updatedAliases = disputeData.aliasesArray;
71+
if (isUndefined(updatedAliases) || isUndefined(updatedAliases[key])) return;
72+
updatedAliases[key].isValid = isValid;
73+
74+
setDisputeData({ ...disputeData, aliasesArray: updatedAliases });
75+
});
76+
}
77+
}, []);
78+
6279
const handleAliasesWrite = (event: React.ChangeEvent<HTMLInputElement>) => {
6380
const key = parseInt(event.target.id.replace(/\D/g, ""), 10) - 1;
6481
const aliases = disputeData.aliasesArray;
@@ -68,7 +85,7 @@ const PersonFields: React.FC = () => {
6885
setDisputeData({ ...disputeData, aliasesArray: aliases });
6986

7087
//since resolving ens is async, we update asynchronously too with debounce
71-
event.target.name === "address" && debounceValidateAddress(event.target.value, key);
88+
if (event.target.name === "address") debounceValidateAddress(event.target.value, key);
7289
};
7390

7491
const showError = (alias: AliasArray) => {

web/src/pages/Resolver/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ScrollTop from "components/ScrollTop";
1717

1818
import Description from "./Briefing/Description";
1919
import Title from "./Briefing/Title";
20+
import Landing from "./Landing";
2021
import Category from "./Parameters/Category";
2122
import Court from "./Parameters/Court";
2223
import Jurors from "./Parameters/Jurors";
@@ -79,6 +80,7 @@ const DisputeResolver: React.FC = () => {
7980
const [isDisputeResolverMiniGuideOpen, toggleDisputeResolverMiniGuide] = useToggle(false);
8081
const { isConnected } = useAccount();
8182
const isPreviewPage = location.pathname.includes("/preview");
83+
const isLandingPage = location.pathname.includes("/create");
8284

8385
return (
8486
<Wrapper>
@@ -87,7 +89,7 @@ const DisputeResolver: React.FC = () => {
8789
{isConnected ? (
8890
<StyledEnsureAuth>
8991
<MiddleContentContainer>
90-
{isConnected && !isPreviewPage ? (
92+
{isConnected && !isPreviewPage && !isLandingPage ? (
9193
<HowItWorksAndTimeline>
9294
<HowItWorks
9395
isMiniGuideOpen={isDisputeResolverMiniGuideOpen}
@@ -98,7 +100,8 @@ const DisputeResolver: React.FC = () => {
98100
</HowItWorksAndTimeline>
99101
) : null}
100102
<Routes>
101-
<Route index element={<Navigate to="title" replace />} />
103+
<Route index element={<Navigate to="create" replace />} />
104+
<Route path="/create/*" element={<Landing />} />
102105
<Route path="/title/*" element={<Title />} />
103106
<Route path="/description/*" element={<Description />} />
104107
<Route path="/court/*" element={<Court />} />

0 commit comments

Comments
 (0)