diff --git a/subgraph/core-neo/subgraph.yaml b/subgraph/core-neo/subgraph.yaml
index 0b07fb708..e98508cc4 100644
--- a/subgraph/core-neo/subgraph.yaml
+++ b/subgraph/core-neo/subgraph.yaml
@@ -1,6 +1,9 @@
specVersion: 0.0.4
schema:
file: ./schema.graphql
+features:
+ - fullTextSearch
+
dataSources:
- kind: ethereum
name: KlerosCore
diff --git a/subgraph/core-university/subgraph.yaml b/subgraph/core-university/subgraph.yaml
index 2970fb804..03c1f6c72 100644
--- a/subgraph/core-university/subgraph.yaml
+++ b/subgraph/core-university/subgraph.yaml
@@ -1,6 +1,9 @@
specVersion: 0.0.4
schema:
file: ./schema.graphql
+features:
+ - fullTextSearch
+
dataSources:
- kind: ethereum
name: KlerosCore
diff --git a/subgraph/core/schema.graphql b/subgraph/core/schema.graphql
index df39fad86..597914610 100644
--- a/subgraph/core/schema.graphql
+++ b/subgraph/core/schema.graphql
@@ -52,7 +52,9 @@ interface Evidence {
id: ID!
evidence: String!
evidenceGroup: EvidenceGroup!
+ evidenceIndex: String!
sender: User!
+ senderAddress: String!
timestamp: BigInt!
name: String
description: String
@@ -300,7 +302,9 @@ type ClassicEvidence implements Evidence @entity(immutable: true) {
id: ID! # classicEvidenceGroup.id-nextEvidenceIndex
evidence: String!
evidenceGroup: EvidenceGroup!
+ evidenceIndex: String!
sender: User!
+ senderAddress: String!
timestamp: BigInt!
name: String
description: String
@@ -319,3 +323,11 @@ type ClassicContribution implements Contribution @entity {
choice: BigInt!
rewardWithdrawn: Boolean!
}
+
+type _Schema_
+ @fulltext(
+ name: "evidenceSearch"
+ language: en
+ algorithm: rank
+ include: [{ entity: "ClassicEvidence", fields: [{ name: "name" }, { name: "description" },{ name: "senderAddress"},{ name: "evidenceIndex"}] }]
+ )
\ No newline at end of file
diff --git a/subgraph/core/src/EvidenceModule.ts b/subgraph/core/src/EvidenceModule.ts
index 4af1cee9a..a4dfd8e70 100644
--- a/subgraph/core/src/EvidenceModule.ts
+++ b/subgraph/core/src/EvidenceModule.ts
@@ -14,11 +14,13 @@ export function handleEvidenceEvent(event: EvidenceEvent): void {
evidenceGroup.save();
const evidenceId = `${evidenceGroupID}-${evidenceIndex.toString()}`;
const evidence = new ClassicEvidence(evidenceId);
+ evidence.evidenceIndex = evidenceIndex.plus(ONE).toString();
const userId = event.params._party.toHexString();
evidence.timestamp = event.block.timestamp;
evidence.evidence = event.params._evidence;
evidence.evidenceGroup = evidenceGroupID.toString();
evidence.sender = userId;
+ evidence.senderAddress = userId;
ensureUser(userId);
let jsonObjValueAndSuccess = json.try_fromString(event.params._evidence);
diff --git a/subgraph/core/subgraph.yaml b/subgraph/core/subgraph.yaml
index 8bd10e01a..f431077d9 100644
--- a/subgraph/core/subgraph.yaml
+++ b/subgraph/core/subgraph.yaml
@@ -1,6 +1,9 @@
specVersion: 0.0.4
schema:
file: ./schema.graphql
+features:
+ - fullTextSearch
+
dataSources:
- kind: ethereum
name: KlerosCore
diff --git a/subgraph/package.json b/subgraph/package.json
index 837a88185..0f62c24d0 100644
--- a/subgraph/package.json
+++ b/subgraph/package.json
@@ -1,6 +1,6 @@
{
"name": "@kleros/kleros-v2-subgraph",
- "version": "0.6.2",
+ "version": "0.7.0",
"license": "MIT",
"scripts": {
"update:core:arbitrum-sepolia-devnet": "./scripts/update.sh arbitrumSepoliaDevnet arbitrum-sepolia core/subgraph.yaml",
diff --git a/web/src/assets/svgs/icons/arrow-down.svg b/web/src/assets/svgs/icons/arrow-down.svg
new file mode 100644
index 000000000..97be1a02a
--- /dev/null
+++ b/web/src/assets/svgs/icons/arrow-down.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/web/src/components/EvidenceCard.tsx b/web/src/components/EvidenceCard.tsx
index 4b298fa7d..cf30bb0bc 100644
--- a/web/src/components/EvidenceCard.tsx
+++ b/web/src/components/EvidenceCard.tsx
@@ -62,20 +62,6 @@ const BottomShade = styled.div`
}
`;
-const StyledA = styled.a`
- display: flex;
- margin-left: auto;
- gap: ${responsiveSize(5, 6)};
- ${landscapeStyle(
- () => css`
- > svg {
- width: 16px;
- fill: ${({ theme }) => theme.primaryBlue};
- }
- `
- )}
-`;
-
const AccountContainer = styled.div`
display: flex;
flex-direction: row;
diff --git a/web/src/hooks/queries/useEvidences.ts b/web/src/hooks/queries/useEvidences.ts
index 1586c99b3..e6e6782ad 100644
--- a/web/src/hooks/queries/useEvidences.ts
+++ b/web/src/hooks/queries/useEvidences.ts
@@ -4,39 +4,60 @@ import { REFETCH_INTERVAL } from "consts/index";
import { useGraphqlBatcher } from "context/GraphqlBatcher";
import { graphql } from "src/graphql";
-import { EvidencesQuery } from "src/graphql/graphql";
+import { EvidenceDetailsFragment, EvidencesQuery } from "src/graphql/graphql";
export type { EvidencesQuery };
+export const evidenceFragment = graphql(`
+ fragment EvidenceDetails on ClassicEvidence {
+ id
+ evidence
+ sender {
+ id
+ }
+ timestamp
+ name
+ description
+ fileURI
+ fileTypeExtension
+ evidenceIndex
+ }
+`);
+
const evidencesQuery = graphql(`
query Evidences($evidenceGroupID: String) {
- evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: desc) {
- id
- evidence
- sender {
- id
- }
- timestamp
- name
- description
- fileURI
- fileTypeExtension
+ evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: asc) {
+ ...EvidenceDetails
}
}
`);
-export const useEvidences = (evidenceGroup?: string) => {
+const evidenceSearchQuery = graphql(`
+ query EvidenceSearch($keywords: String!, $evidenceGroupID: String) {
+ evidenceSearch(text: $keywords, where: { evidenceGroup: $evidenceGroupID }) {
+ ...EvidenceDetails
+ }
+ }
+`);
+
+export const useEvidences = (evidenceGroup?: string, keywords?: string) => {
const isEnabled = evidenceGroup !== undefined;
const { graphqlBatcher } = useGraphqlBatcher();
- return useQuery({
- queryKey: [`evidencesQuery${evidenceGroup}`],
+ const document = keywords ? evidenceSearchQuery : evidencesQuery;
+ return useQuery<{ evidences: EvidenceDetailsFragment[] }>({
+ queryKey: [
+ keywords ? `evidenceSearchQuery${evidenceGroup}-${keywords}` : `evidencesQuery${evidenceGroup}`,
+ ],
enabled: isEnabled,
refetchInterval: REFETCH_INTERVAL,
- queryFn: async () =>
- await graphqlBatcher.fetch({
+ queryFn: async () => {
+ const result = await graphqlBatcher.fetch({
id: crypto.randomUUID(),
- document: evidencesQuery,
- variables: { evidenceGroupID: evidenceGroup?.toString() },
- }),
+ document: document,
+ variables: { evidenceGroupID: evidenceGroup?.toString(), keywords: keywords },
+ });
+
+ return keywords ? { evidences: [...result.evidenceSearch] } : result;
+ },
});
};
diff --git a/web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx b/web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx
new file mode 100644
index 000000000..7aa1a3671
--- /dev/null
+++ b/web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx
@@ -0,0 +1,69 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+
+import { useAccount } from "wagmi";
+
+import { Button, Searchbar } from "@kleros/ui-components-library";
+
+import { isUndefined } from "src/utils";
+
+import { responsiveSize } from "styles/responsiveSize";
+
+import { EnsureChain } from "components/EnsureChain";
+
+import SubmitEvidenceModal from "./SubmitEvidenceModal";
+
+const SearchContainer = styled.div`
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: ${responsiveSize(16, 28)};
+`;
+
+const StyledSearchBar = styled(Searchbar)`
+ min-width: 220px;
+ flex: 1;
+`;
+
+const StyledButton = styled(Button)`
+ align-self: flex-end;
+`;
+
+interface IEvidenceSearch {
+ search?: string;
+ setSearch: (search: string) => void;
+ evidenceGroup?: bigint;
+}
+
+const EvidenceSearch: React.FC = ({ search, setSearch, evidenceGroup }) => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { address } = useAccount();
+
+ return (
+ <>
+ {!isUndefined(evidenceGroup) && (
+ setIsModalOpen(false)} {...{ evidenceGroup }} />
+ )}
+
+
+ setSearch(e.target.value)}
+ value={search}
+ />
+
+
+ setIsModalOpen(true)}
+ />
+
+
+ >
+ );
+};
+
+export default EvidenceSearch;
diff --git a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx
index 5f5897e1c..8c088edfc 100644
--- a/web/src/pages/Cases/CaseDetails/Evidence/index.tsx
+++ b/web/src/pages/Cases/CaseDetails/Evidence/index.tsx
@@ -1,23 +1,22 @@
-import React, { useState } from "react";
+import React, { useCallback, useRef, useState } from "react";
import styled from "styled-components";
import { useParams } from "react-router-dom";
-import { useAccount } from "wagmi";
+import { useDebounce } from "react-use";
-import { Button, Searchbar } from "@kleros/ui-components-library";
+import { Button } from "@kleros/ui-components-library";
-import { isUndefined } from "utils/index";
+import DownArrow from "svgs/icons/arrow-down.svg";
import { useEvidenceGroup } from "queries/useEvidenceGroup";
import { useEvidences } from "queries/useEvidences";
import { responsiveSize } from "styles/responsiveSize";
-import { EnsureChain } from "components/EnsureChain";
import EvidenceCard from "components/EvidenceCard";
import { SkeletonEvidenceCard } from "components/StyledSkeleton";
-import SubmitEvidenceModal from "./SubmitEvidenceModal";
+import EvidenceSearch from "./EvidenceSearch";
const Container = styled.div`
width: 100%;
@@ -29,43 +28,61 @@ const Container = styled.div`
padding: ${responsiveSize(16, 32)};
`;
-const StyledButton = styled(Button)`
- align-self: flex-end;
-`;
-
const StyledLabel = styled.label`
display: flex;
margin-top: 16px;
font-size: 16px;
`;
+const ScrollButton = styled(Button)`
+ align-self: flex-end;
+ background-color: transparent;
+ padding: 0;
+ flex-direction: row-reverse;
+ margin: 0 0 18px;
+ gap: 8px;
+ .button-text {
+ color: ${({ theme }) => theme.primaryBlue};
+ font-weight: 400;
+ }
+ .button-svg {
+ margin: 0;
+ }
+ :focus,
+ :hover {
+ background-color: transparent;
+ }
+`;
+
const Evidence: React.FC<{ arbitrable?: `0x${string}` }> = ({ arbitrable }) => {
- const [isModalOpen, setIsModalOpen] = useState(false);
const { id } = useParams();
const { data: evidenceGroup } = useEvidenceGroup(id, arbitrable);
- const { data } = useEvidences(evidenceGroup?.toString());
- const { address } = useAccount();
+ const ref = useRef(null);
+ const [search, setSearch] = useState();
+ const [debouncedSearch, setDebouncedSearch] = useState();
+
+ const { data } = useEvidences(evidenceGroup?.toString(), debouncedSearch);
+
+ useDebounce(() => setDebouncedSearch(search), 500, [search]);
+
+ const scrollToLatest = useCallback(() => {
+ if (!ref.current) return;
+ const latestEvidence = ref.current.lastElementChild;
+
+ if (!latestEvidence) return;
+
+ latestEvidence.scrollIntoView({ behavior: "smooth" });
+ }, [ref]);
return (
-
- {!isUndefined(evidenceGroup) && (
- setIsModalOpen(false)} {...{ evidenceGroup }} />
- )}
-
-
- setIsModalOpen(true)}
- />
-
+
+
+
{data ? (
- data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI }, i) => (
+ data.evidences.map(({ evidence, sender, timestamp, name, description, fileURI, evidenceIndex }) => (