From 2d23ed76a4b2fba1d405c7c3b44d0cd481cff14b Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 2 Jul 2024 17:37:44 +0530 Subject: [PATCH 1/2] feat(subgraph): evidence-fulltext-search --- subgraph/core-neo/subgraph.yaml | 3 +++ subgraph/core-university/subgraph.yaml | 3 +++ subgraph/core/schema.graphql | 12 ++++++++++++ subgraph/core/src/EvidenceModule.ts | 2 ++ subgraph/core/subgraph.yaml | 3 +++ subgraph/package.json | 2 +- 6 files changed, 24 insertions(+), 1 deletion(-) 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", From 8637c5e67753d7a6efd4f5f8238eb6367c159aef Mon Sep 17 00:00:00 2001 From: Harman-singh-waraich Date: Tue, 2 Jul 2024 17:39:19 +0530 Subject: [PATCH 2/2] feat(web): evidence-search --- web/src/assets/svgs/icons/arrow-down.svg | 10 +++ web/src/components/EvidenceCard.tsx | 14 ---- web/src/hooks/queries/useEvidences.ts | 62 ++++++++++----- .../CaseDetails/Evidence/EvidenceSearch.tsx | 69 +++++++++++++++++ .../Cases/CaseDetails/Evidence/index.tsx | 75 ++++++++++++------- 5 files changed, 167 insertions(+), 63 deletions(-) create mode 100644 web/src/assets/svgs/icons/arrow-down.svg create mode 100644 web/src/pages/Cases/CaseDetails/Evidence/EvidenceSearch.tsx 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 1070c6b08..17143657b 100644 --- a/web/src/hooks/queries/useEvidences.ts +++ b/web/src/hooks/queries/useEvidences.ts @@ -3,38 +3,60 @@ import { useQuery } from "@tanstack/react-query"; 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: ["refetchOnBlock", `evidencesQuery${evidenceGroup}`], + const document = keywords ? evidenceSearchQuery : evidencesQuery; + return useQuery<{ evidences: EvidenceDetailsFragment[] }>({ + queryKey: [ + "refetchOnBlock", + keywords ? `evidenceSearchQuery${evidenceGroup}-${keywords}` : `evidencesQuery${evidenceGroup}`, + ], enabled: isEnabled, - 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 }) => (