diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx index 101ec0f52..1d1ac201f 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Box, CircularProgress, Tab, Tabs, styled } from "@mui/material"; import { useLocation, useNavigate } from "react-router-dom"; @@ -9,13 +9,7 @@ import { PDF_PATHS, } from "@consts"; import { useCardano, useDataActionsBar, useFeatureFlag } from "@context"; -import { - useGetDRepVotesQuery, - useGetProposalsQuery, - useGetVoterInfo, - useScreenDimension, - useTranslation, -} from "@hooks"; +import { useGetVoterInfo, useScreenDimension, useTranslation } from "@hooks"; import { DataActionsBar } from "@molecules"; import { GovernanceActionsToVote, @@ -29,10 +23,6 @@ type TabPanelProps = { value: number; }; -const defaultCategories = GOVERNANCE_ACTIONS_FILTERS.map( - (category) => category.key, -); - const CustomTabPanel = (props: TabPanelProps) => { const { children, value, index } = props; @@ -53,6 +43,8 @@ const CustomTabPanel = (props: TabPanelProps) => { ); }; +const temporaryProposeButtonRemove: boolean = true; + type StyledTabProps = { label: string; }; @@ -71,60 +63,16 @@ const StyledTab = styled((props: StyledTabProps) => ( })); export const DashboardGovernanceActions = () => { - const { debouncedSearchText, isAdjusting, ...dataActionsBarProps } = + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); - const { chosenFilters, chosenSorting } = dataActionsBarProps; const { voter } = useGetVoterInfo(); const { isMobile } = useScreenDimension(); const { t } = useTranslation(); const { isEnableLoading } = useCardano(); const { isProposalDiscussionForumEnabled } = useFeatureFlag(); const navigate = useNavigate(); - - const queryFilters = - chosenFilters.length > 0 ? chosenFilters : defaultCategories; - - const { proposals, isProposalsLoading } = useGetProposalsQuery({ - filters: queryFilters, - sorting: chosenSorting, - searchPhrase: debouncedSearchText, - enabled: !isAdjusting, - }); - const { data: votes, areDRepVotesLoading } = useGetDRepVotesQuery( - queryFilters, - chosenSorting, - debouncedSearchText, - ); - - // White Magic :) - const shouldFilter = - voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter; - - const filteredProposals = useMemo(() => { - if (!shouldFilter || !proposals || !votes) return proposals; - - return proposals - .map((proposalCategory) => { - const filteredActions = proposalCategory.actions.filter((action) => { - const hasVote = votes.some((voteCategory) => - voteCategory.actions.some( - (voteAction) => - voteAction.proposal.txHash === action.txHash && - voteAction.proposal.index === action.index, - ), - ); - return !hasVote; - }); - - return { - ...proposalCategory, - actions: filteredActions, - }; - }) - .filter((category) => category.actions.length > 0); - }, [proposals, votes, shouldFilter]); - const { state } = useLocation(); + const [content, setContent] = useState( state?.isVotedListOnLoad ? 1 : 0, ); @@ -160,7 +108,7 @@ export const DashboardGovernanceActions = () => { filtersTitle={t("govActions.filterTitle")} sortOptions={GOVERNANCE_ACTIONS_SORTING} /> - {!proposals || !voter || isEnableLoading || isProposalsLoading ? ( + {!voter || isEnableLoading ? ( { data-testid="proposal-discussion-link" onClick={onClickPropose} sx={{ - display: isMobile ? "none" : "block", + display: + isMobile || temporaryProposeButtonRemove + ? "none" + : "block", ml: "auto", }} > @@ -218,19 +169,11 @@ export const DashboardGovernanceActions = () => { - + diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx index 748970d17..670d50498 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx @@ -1,67 +1,65 @@ import { useMemo } from "react"; import { Box, Typography, CircularProgress } from "@mui/material"; -import { useCardano } from "@context"; -import { useScreenDimension, useTranslation } from "@hooks"; -import { Slider, ValidatedGovernanceVotedOnCard } from "@organisms"; -import { getFullGovActionId, getProposalTypeLabel } from "@utils"; -import { VotedProposal } from "@/models"; +import { useCardano, useDataActionsBar } from "@context"; +import { + useGetDRepVotesQuery, + useScreenDimension, + useTranslation, +} from "@hooks"; +import { ValidatedGovernanceVotedOnCard } from "@organisms"; +import { getFullGovActionId } from "@utils"; type DashboardGovernanceActionsVotedOnProps = { searchPhrase?: string; - votes: { - title: string; - actions: VotedProposal[]; - }[]; - areDRepVotesLoading: boolean; }; export const DashboardGovernanceActionsVotedOn = ({ searchPhrase, - votes, - areDRepVotesLoading, }: DashboardGovernanceActionsVotedOnProps) => { - const { isMobile } = useScreenDimension(); + const { isMobile, screenWidth } = useScreenDimension(); const { pendingTransaction } = useCardano(); const { t } = useTranslation(); + const { ...dataActionsBarProps } = useDataActionsBar(); + const { chosenSorting, chosenFilters } = dataActionsBarProps; - // TODO: Filtering here is some kind of craziness. It should be done on the backend. + const { + data: votes, + areDRepVotesLoading, + isFetching, + } = useGetDRepVotesQuery(chosenFilters, chosenSorting, searchPhrase); + // TODO: Filtering here is some kind of craziness. It should be done on the backend. const filteredData = useMemo(() => { if (!votes?.length) return []; - if (!searchPhrase) return votes; + if (!searchPhrase) return votes.flatMap((entry) => entry.actions); + const lowerSearch = searchPhrase.toLowerCase(); - return votes - .map((entry) => { - const filteredActions = entry.actions.filter((action) => { - const hash = getFullGovActionId( - action.proposal.txHash, - action.proposal.index, - ).toLowerCase(); - const title = action.proposal.title?.toLowerCase() || ""; - const motivation = action.proposal.motivation?.toLowerCase() || ""; - const rationale = action.proposal.rationale?.toLowerCase() || ""; - const abstract = action.proposal.abstract?.toLowerCase() || ""; + return votes.flatMap((entry) => + entry.actions.filter((action) => { + const hash = getFullGovActionId( + action.proposal.txHash, + action.proposal.index, + ).toLowerCase(); - return ( - hash.includes(lowerSearch) || - title.includes(lowerSearch) || - motivation.includes(lowerSearch) || - rationale.includes(lowerSearch) || - abstract.includes(lowerSearch) - ); - }); + const title = action.proposal.title?.toLowerCase() || ""; + const motivation = action.proposal.motivation?.toLowerCase() || ""; + const rationale = action.proposal.rationale?.toLowerCase() || ""; + const abstract = action.proposal.abstract?.toLowerCase() || ""; - return { - ...entry, - actions: filteredActions, - }; - }) - .filter((entry) => entry.actions.length > 0); + return ( + hash.includes(lowerSearch) || + title.includes(lowerSearch) || + motivation.includes(lowerSearch) || + rationale.includes(lowerSearch) || + abstract.includes(lowerSearch) + ); + }), + ); }, [votes, searchPhrase, pendingTransaction.vote]); - return areDRepVotesLoading ? ( + return areDRepVotesLoading || isFetching ? ( @@ -76,36 +74,25 @@ export const DashboardGovernanceActionsVotedOn = ({ {t("govActions.noResultsForTheSearch")} ) : ( - <> - {filteredData?.map((item) => ( -
- ( -
- -
- ))} + + {filteredData.map((item) => ( + + - -
+
))} - +
)} ); diff --git a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx index 95729b3fa..4154474cb 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx @@ -1,84 +1,157 @@ -import { Box } from "@mui/material"; +import { Box, CircularProgress } from "@mui/material"; import { Typography } from "@atoms"; -import { useCardano } from "@context"; -import { useScreenDimension, useTranslation } from "@hooks"; -import { ProposalData } from "@models"; -import { getProposalTypeTitle } from "@utils"; -import { Slider, ValidatedGovernanceActionCard } from "@organisms"; +import { useCardano, useDataActionsBar } from "@context"; +import { + useFetchNextPageDetector, + useGetDRepVotesQuery, + useGetProposalsInfiniteQuery, + useGetVoterInfo, + useSaveScrollPosition, + useScreenDimension, + useTranslation, +} from "@hooks"; +import { removeDuplicatedProposals } from "@utils"; +import { ValidatedGovernanceActionCard } from "@organisms"; +import { useMemo, useRef } from "react"; type GovernanceActionsToVoteProps = { - filters: string[]; - sorting: string; - proposals: { title: string; actions: ProposalData[] }[]; onDashboard?: boolean; - searchPhrase?: string; }; export const GovernanceActionsToVote = ({ - filters, onDashboard = true, - proposals, - searchPhrase, - sorting, }: GovernanceActionsToVoteProps) => { - const { pendingTransaction } = useCardano(); - const { isMobile, pagePadding } = useScreenDimension(); + const { pendingTransaction, isEnableLoading } = useCardano(); + const { isMobile, screenWidth } = useScreenDimension(); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenSorting, chosenFilters } = dataActionsBarProps; + const { voter } = useGetVoterInfo(); const { t } = useTranslation(); + const { + isProposalsFetching, + isProposalsFetchingNextPage, + isProposalsLoading, + proposals, + proposalsfetchNextPage, + proposalsHaveNextPage, + } = useGetProposalsInfiniteQuery({ + filters: chosenFilters, + sorting: chosenSorting, + searchPhrase: debouncedSearchText, + }); + const loadNextPageRef = useRef(null); + + useFetchNextPageDetector( + proposalsfetchNextPage, + isProposalsLoading || isProposalsFetchingNextPage, + proposalsHaveNextPage, + ); + + const { + data: votes, + areDRepVotesLoading, + isFetching: isFetchingVotes, + } = useGetDRepVotesQuery(chosenFilters, chosenSorting, debouncedSearchText); + + const saveScrollPosition = useSaveScrollPosition( + isProposalsLoading, + isProposalsFetching, + ); + + const mappedData = useMemo( + () => removeDuplicatedProposals(proposals), + [proposals, voter?.isRegisteredAsDRep, isProposalsFetchingNextPage], + ); + + // TODO: Filtering here is some kind of craziness. It should be done on the backend. + const filteredProposals = useMemo(() => { + const list = mappedData ?? []; + if (!votes?.length) return list; + + const proposalsFromVotes = votes + .flatMap((v) => v?.actions ?? []) + .map((a) => a?.proposal) + .filter(Boolean); + + const votedKeys = new Set( + proposalsFromVotes + .map((p) => ({ + id: p?.id ?? p?.id, + tx: p?.txHash ?? p?.txHash, + })) + .filter(({ id, tx }) => Boolean(id && tx)) + .map(({ id, tx }) => `${id}:${tx}`), + ); + + if (votedKeys.size === 0) return list; + + return list.filter((p) => { + const id = p?.id ?? p?.id; + const tx = p?.txHash ?? p?.txHash; + if (!id || !tx) return true; + return !votedKeys.has(`${id}:${tx}`); + }); + }, [mappedData, voter?.isRegisteredAsDRep, isProposalsFetchingNextPage]); + return ( <> - {!proposals?.length ? ( + {!filteredProposals || + isEnableLoading || + isProposalsLoading || + areDRepVotesLoading || + isFetchingVotes ? ( + + + + ) : !filteredProposals?.length ? ( {t("govActions.noResultsForTheSearch")} ) : ( - <> - {proposals?.map((item, index) => ( - - ( -
- -
- ))} - dataLength={item.actions.slice(0, 6)?.length} - filters={filters} - navigateKey={item.title} - notSlicedDataLength={item.actions?.length} - onDashboard={onDashboard} - searchPhrase={searchPhrase} - sorting={sorting} - title={getProposalTypeTitle(item.title)} + + {filteredProposals.map((item) => ( + + { + saveScrollPosition(); + }} + txHash={item.txHash} /> - {index < proposals.length - 1 && ( - - )} ))} - + {proposalsHaveNextPage && isProposalsFetchingNextPage && ( + + + + )} + )} ); diff --git a/govtool/frontend/src/context/dataActionsBar.tsx b/govtool/frontend/src/context/dataActionsBar.tsx index 2bf291edd..1d296e79e 100644 --- a/govtool/frontend/src/context/dataActionsBar.tsx +++ b/govtool/frontend/src/context/dataActionsBar.tsx @@ -75,8 +75,7 @@ const DataActionsBarProvider: FC = ({ children }) => { (!pathname.startsWith(lastPath) || lastPath === "" || lastPath === "/"); const userOpenedGADetails = gADetailsPathnameRegexp.test(pathname); const userOpenedGADetailsFromCategoryPage = - userOpenedGADetails && - lastPath.includes("governance_actions/category"); + userOpenedGADetails && lastPath.includes("governance_actions/category"); const userMovedFromGAListToCategoryPage = lastPath.endsWith("governance_actions") && pathname.includes("governance_actions/category"); @@ -84,6 +83,10 @@ const DataActionsBarProvider: FC = ({ children }) => { (gADetailsPathnameRegexp.test(lastPath) && pathname.includes("governance_actions")) || pathname.includes("governance_actions/category"); + const isSearchOrFilterSet = + debouncedSearchText.length > 0 || + chosenSorting !== "" || + chosenFilters.length > 0; useEffect(() => { isAdjusting.current = true; @@ -91,7 +94,7 @@ const DataActionsBarProvider: FC = ({ children }) => { return; } - if (userMovedFromGADetailsToListOrCategoryPage && debouncedSearchText.length > 0) { + if (userMovedFromGADetailsToListOrCategoryPage && isSearchOrFilterSet) { isAdjusting.current = false; return; } diff --git a/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts index 0878191bc..4d1082f12 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepVotesQuery.ts @@ -8,11 +8,11 @@ import { VotedProposal } from "@/models"; export const useGetDRepVotesQuery = ( type?: string[], sort?: string, - search?: string, + search?: string ) => { const { dRepID, pendingTransaction } = useCardano(); - const { data, isLoading, refetch, isRefetching } = useQuery({ + const { data, isLoading, refetch, isFetching } = useQuery({ queryKey: [ QUERY_KEYS.useGetDRepVotesKey, pendingTransaction.vote?.transactionHash, @@ -62,6 +62,6 @@ export const useGetDRepVotesQuery = ( }[], areDRepVotesLoading: isLoading, refetch, - isRefetching, + isFetching, }; }; diff --git a/govtool/frontend/src/pages/GovernanceActions.tsx b/govtool/frontend/src/pages/GovernanceActions.tsx index a461c1a6a..5dbea1e2b 100644 --- a/govtool/frontend/src/pages/GovernanceActions.tsx +++ b/govtool/frontend/src/pages/GovernanceActions.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import { Box, CircularProgress, Divider } from "@mui/material"; +import { Box, Divider } from "@mui/material"; import { Background, ScrollToManage, Typography } from "@atoms"; import { @@ -9,38 +9,19 @@ import { PATHS, } from "@consts"; import { useCardano, useDataActionsBar } from "@context"; -import { - useGetProposalsQuery, - useScreenDimension, - useTranslation, -} from "@hooks"; +import { useScreenDimension, useTranslation } from "@hooks"; import { DataActionsBar } from "@molecules"; import { Footer, TopNav, GovernanceActionsToVote } from "@organisms"; import { WALLET_LS_KEY, getItemFromLocalStorage } from "@utils"; -const defaultCategories = GOVERNANCE_ACTIONS_FILTERS.map( - (category) => category.key, -); - export const GovernanceActions = () => { - const { debouncedSearchText, isAdjusting, ...dataActionsBarProps } = + const { ...dataActionsBarProps } = useDataActionsBar(); - const { chosenFilters, chosenSorting } = dataActionsBarProps; const { isMobile, pagePadding } = useScreenDimension(); const { isEnabled } = useCardano(); const navigate = useNavigate(); const { t } = useTranslation(); - const queryFilters = - chosenFilters.length > 0 ? chosenFilters : defaultCategories; - - const { proposals, isProposalsLoading } = useGetProposalsQuery({ - filters: queryFilters, - sorting: chosenSorting, - searchPhrase: debouncedSearchText, - enabled: !isAdjusting, - }); - useEffect(() => { if (isEnabled && getItemFromLocalStorage(`${WALLET_LS_KEY}_stake_key`)) { navigate(PATHS.dashboardGovernanceActions); @@ -96,29 +77,8 @@ export const GovernanceActions = () => { filtersTitle={t("govActions.filterTitle")} sortOptions={GOVERNANCE_ACTIONS_SORTING} /> - {!proposals || isProposalsLoading ? ( - - - - ) : ( - <> - - - - )} + + {/* FIXME: Footer should be on top of the layout.