Skip to content

Commit d1b6fe2

Browse files
authoredJul 18, 2024··
Merge pull request #1640 from kleros/feat/evidence-search
Feat/evidence search
2 parents 653f505 + 78a063f commit d1b6fe2

File tree

11 files changed

+190
-64
lines changed

11 files changed

+190
-64
lines changed
 

‎subgraph/core-neo/subgraph.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
specVersion: 0.0.4
22
schema:
33
file: ./schema.graphql
4+
features:
5+
- fullTextSearch
6+
47
dataSources:
58
- kind: ethereum
69
name: KlerosCore

‎subgraph/core-university/subgraph.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
specVersion: 0.0.4
22
schema:
33
file: ./schema.graphql
4+
features:
5+
- fullTextSearch
6+
47
dataSources:
58
- kind: ethereum
69
name: KlerosCore

‎subgraph/core/schema.graphql

+12
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ interface Evidence {
5252
id: ID!
5353
evidence: String!
5454
evidenceGroup: EvidenceGroup!
55+
evidenceIndex: String!
5556
sender: User!
57+
senderAddress: String!
5658
timestamp: BigInt!
5759
name: String
5860
description: String
@@ -300,7 +302,9 @@ type ClassicEvidence implements Evidence @entity(immutable: true) {
300302
id: ID! # classicEvidenceGroup.id-nextEvidenceIndex
301303
evidence: String!
302304
evidenceGroup: EvidenceGroup!
305+
evidenceIndex: String!
303306
sender: User!
307+
senderAddress: String!
304308
timestamp: BigInt!
305309
name: String
306310
description: String
@@ -319,3 +323,11 @@ type ClassicContribution implements Contribution @entity {
319323
choice: BigInt!
320324
rewardWithdrawn: Boolean!
321325
}
326+
327+
type _Schema_
328+
@fulltext(
329+
name: "evidenceSearch"
330+
language: en
331+
algorithm: rank
332+
include: [{ entity: "ClassicEvidence", fields: [{ name: "name" }, { name: "description" },{ name: "senderAddress"},{ name: "evidenceIndex"}] }]
333+
)

‎subgraph/core/src/EvidenceModule.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ export function handleEvidenceEvent(event: EvidenceEvent): void {
1414
evidenceGroup.save();
1515
const evidenceId = `${evidenceGroupID}-${evidenceIndex.toString()}`;
1616
const evidence = new ClassicEvidence(evidenceId);
17+
evidence.evidenceIndex = evidenceIndex.plus(ONE).toString();
1718
const userId = event.params._party.toHexString();
1819
evidence.timestamp = event.block.timestamp;
1920
evidence.evidence = event.params._evidence;
2021
evidence.evidenceGroup = evidenceGroupID.toString();
2122
evidence.sender = userId;
23+
evidence.senderAddress = userId;
2224
ensureUser(userId);
2325

2426
let jsonObjValueAndSuccess = json.try_fromString(event.params._evidence);

‎subgraph/core/subgraph.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
specVersion: 0.0.4
22
schema:
33
file: ./schema.graphql
4+
features:
5+
- fullTextSearch
6+
47
dataSources:
58
- kind: ethereum
69
name: KlerosCore

‎subgraph/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kleros/kleros-v2-subgraph",
3-
"version": "0.6.2",
3+
"version": "0.7.0",
44
"license": "MIT",
55
"scripts": {
66
"update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml",
+10
Loading

‎web/src/components/EvidenceCard.tsx

-14
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,6 @@ const BottomShade = styled.div`
6262
}
6363
`;
6464

65-
const StyledA = styled.a`
66-
display: flex;
67-
margin-left: auto;
68-
gap: ${responsiveSize(5, 6)};
69-
${landscapeStyle(
70-
() => css`
71-
> svg {
72-
width: 16px;
73-
fill: ${({ theme }) => theme.primaryBlue};
74-
}
75-
`
76-
)}
77-
`;
78-
7965
const AccountContainer = styled.div`
8066
display: flex;
8167
flex-direction: row;

‎web/src/hooks/queries/useEvidences.ts

+41-20
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,60 @@ import { REFETCH_INTERVAL } from "consts/index";
44
import { useGraphqlBatcher } from "context/GraphqlBatcher";
55

66
import { graphql } from "src/graphql";
7-
import { EvidencesQuery } from "src/graphql/graphql";
7+
import { EvidenceDetailsFragment, EvidencesQuery } from "src/graphql/graphql";
88
export type { EvidencesQuery };
99

10+
export const evidenceFragment = graphql(`
11+
fragment EvidenceDetails on ClassicEvidence {
12+
id
13+
evidence
14+
sender {
15+
id
16+
}
17+
timestamp
18+
name
19+
description
20+
fileURI
21+
fileTypeExtension
22+
evidenceIndex
23+
}
24+
`);
25+
1026
const evidencesQuery = graphql(`
1127
query Evidences($evidenceGroupID: String) {
12-
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: desc) {
13-
id
14-
evidence
15-
sender {
16-
id
17-
}
18-
timestamp
19-
name
20-
description
21-
fileURI
22-
fileTypeExtension
28+
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: asc) {
29+
...EvidenceDetails
2330
}
2431
}
2532
`);
2633

27-
export const useEvidences = (evidenceGroup?: string) => {
34+
const evidenceSearchQuery = graphql(`
35+
query EvidenceSearch($keywords: String!, $evidenceGroupID: String) {
36+
evidenceSearch(text: $keywords, where: { evidenceGroup: $evidenceGroupID }) {
37+
...EvidenceDetails
38+
}
39+
}
40+
`);
41+
42+
export const useEvidences = (evidenceGroup?: string, keywords?: string) => {
2843
const isEnabled = evidenceGroup !== undefined;
2944
const { graphqlBatcher } = useGraphqlBatcher();
3045

31-
return useQuery<EvidencesQuery>({
32-
queryKey: [`evidencesQuery${evidenceGroup}`],
46+
const document = keywords ? evidenceSearchQuery : evidencesQuery;
47+
return useQuery<{ evidences: EvidenceDetailsFragment[] }>({
48+
queryKey: [
49+
keywords ? `evidenceSearchQuery${evidenceGroup}-${keywords}` : `evidencesQuery${evidenceGroup}`,
50+
],
3351
enabled: isEnabled,
3452
refetchInterval: REFETCH_INTERVAL,
35-
queryFn: async () =>
36-
await graphqlBatcher.fetch({
53+
queryFn: async () => {
54+
const result = await graphqlBatcher.fetch({
3755
id: crypto.randomUUID(),
38-
document: evidencesQuery,
39-
variables: { evidenceGroupID: evidenceGroup?.toString() },
40-
}),
56+
document: document,
57+
variables: { evidenceGroupID: evidenceGroup?.toString(), keywords: keywords },
58+
});
59+
60+
return keywords ? { evidences: [...result.evidenceSearch] } : result;
61+
},
4162
});
4263
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { useAccount } from "wagmi";
5+
6+
import { Button, Searchbar } from "@kleros/ui-components-library";
7+
8+
import { isUndefined } from "src/utils";
9+
10+
import { responsiveSize } from "styles/responsiveSize";
11+
12+
import { EnsureChain } from "components/EnsureChain";
13+
14+
import SubmitEvidenceModal from "./SubmitEvidenceModal";
15+
16+
const SearchContainer = styled.div`
17+
width: 100%;
18+
display: flex;
19+
flex-wrap: wrap;
20+
align-items: center;
21+
gap: ${responsiveSize(16, 28)};
22+
`;
23+
24+
const StyledSearchBar = styled(Searchbar)`
25+
min-width: 220px;
26+
flex: 1;
27+
`;
28+
29+
const StyledButton = styled(Button)`
30+
align-self: flex-end;
31+
`;
32+
33+
interface IEvidenceSearch {
34+
search?: string;
35+
setSearch: (search: string) => void;
36+
evidenceGroup?: bigint;
37+
}
38+
39+
const EvidenceSearch: React.FC<IEvidenceSearch> = ({ search, setSearch, evidenceGroup }) => {
40+
const [isModalOpen, setIsModalOpen] = useState(false);
41+
const { address } = useAccount();
42+
43+
return (
44+
<>
45+
{!isUndefined(evidenceGroup) && (
46+
<SubmitEvidenceModal isOpen={isModalOpen} close={() => setIsModalOpen(false)} {...{ evidenceGroup }} />
47+
)}
48+
49+
<SearchContainer>
50+
<StyledSearchBar
51+
placeholder="Search evidence by number, word, or submitter."
52+
onChange={(e) => setSearch(e.target.value)}
53+
value={search}
54+
/>
55+
56+
<EnsureChain>
57+
<StyledButton
58+
text="Submit Evidence"
59+
disabled={typeof address === "undefined" || isModalOpen}
60+
isLoading={isModalOpen}
61+
onClick={() => setIsModalOpen(true)}
62+
/>
63+
</EnsureChain>
64+
</SearchContainer>
65+
</>
66+
);
67+
};
68+
69+
export default EvidenceSearch;

‎web/src/pages/Cases/CaseDetails/Evidence/index.tsx

+46-29
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
import React, { useState } from "react";
1+
import React, { useCallback, useRef, useState } from "react";
22
import styled from "styled-components";
33

44
import { useParams } from "react-router-dom";
5-
import { useAccount } from "wagmi";
5+
import { useDebounce } from "react-use";
66

7-
import { Button, Searchbar } from "@kleros/ui-components-library";
7+
import { Button } from "@kleros/ui-components-library";
88

9-
import { isUndefined } from "utils/index";
9+
import DownArrow from "svgs/icons/arrow-down.svg";
1010

1111
import { useEvidenceGroup } from "queries/useEvidenceGroup";
1212
import { useEvidences } from "queries/useEvidences";
1313

1414
import { responsiveSize } from "styles/responsiveSize";
1515

16-
import { EnsureChain } from "components/EnsureChain";
1716
import EvidenceCard from "components/EvidenceCard";
1817
import { SkeletonEvidenceCard } from "components/StyledSkeleton";
1918

20-
import SubmitEvidenceModal from "./SubmitEvidenceModal";
19+
import EvidenceSearch from "./EvidenceSearch";
2120

2221
const Container = styled.div`
2322
width: 100%;
@@ -29,43 +28,61 @@ const Container = styled.div`
2928
padding: ${responsiveSize(16, 32)};
3029
`;
3130

32-
const StyledButton = styled(Button)`
33-
align-self: flex-end;
34-
`;
35-
3631
const StyledLabel = styled.label`
3732
display: flex;
3833
margin-top: 16px;
3934
font-size: 16px;
4035
`;
4136

37+
const ScrollButton = styled(Button)`
38+
align-self: flex-end;
39+
background-color: transparent;
40+
padding: 0;
41+
flex-direction: row-reverse;
42+
margin: 0 0 18px;
43+
gap: 8px;
44+
.button-text {
45+
color: ${({ theme }) => theme.primaryBlue};
46+
font-weight: 400;
47+
}
48+
.button-svg {
49+
margin: 0;
50+
}
51+
:focus,
52+
:hover {
53+
background-color: transparent;
54+
}
55+
`;
56+
4257
const Evidence: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable }) => {
43-
const [isModalOpen, setIsModalOpen] = useState(false);
4458
const { id } = useParams();
4559
const { data: evidenceGroup } = useEvidenceGroup(id, arbitrable);
46-
const { data } = useEvidences(evidenceGroup?.toString());
47-
const { address } = useAccount();
60+
const ref = useRef<HTMLDivElement>(null);
61+
const [search, setSearch] = useState<string>();
62+
const [debouncedSearch, setDebouncedSearch] = useState<string>();
63+
64+
const { data } = useEvidences(evidenceGroup?.toString(), debouncedSearch);
65+
66+
useDebounce(() => setDebouncedSearch(search), 500, [search]);
67+
68+
const scrollToLatest = useCallback(() => {
69+
if (!ref.current) return;
70+
const latestEvidence = ref.current.lastElementChild;
71+
72+
if (!latestEvidence) return;
73+
74+
latestEvidence.scrollIntoView({ behavior: "smooth" });
75+
}, [ref]);
4876

4977
return (
50-
<Container>
51-
{!isUndefined(evidenceGroup) && (
52-
<SubmitEvidenceModal isOpen={isModalOpen} close={() => setIsModalOpen(false)} {...{ evidenceGroup }} />
53-
)}
54-
<Searchbar />
55-
<EnsureChain>
56-
<StyledButton
57-
small
58-
text="Submit Evidence"
59-
disabled={typeof address === "undefined" || isModalOpen}
60-
isLoading={isModalOpen}
61-
onClick={() => setIsModalOpen(true)}
62-
/>
63-
</EnsureChain>
78+
<Container ref={ref}>
79+
<EvidenceSearch {...{ search, setSearch, evidenceGroup }} />
80+
<ScrollButton small Icon={DownArrow} text="Scroll to latest" onClick={scrollToLatest} />
6481
{data ? (
65-
data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI }, i) => (
82+
data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI, evidenceIndex }) => (
6683
<EvidenceCard
6784
key={timestamp}
68-
index={i + 1}
85+
index={parseInt(evidenceIndex)}
6986
sender={sender?.id}
7087
{...{ evidence, timestamp, name, description, fileURI }}
7188
/>

0 commit comments

Comments
 (0)
Please sign in to comment.