From 35b1bd5d8b5dc4dddbd9aaa6073d2a163c0598e8 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Sun, 28 Jan 2024 00:24:19 +0100 Subject: [PATCH] Add support for searching for network proposal by name --- .changelog/1192.feature.md | 1 + .../NetworkProposalsList/ProposalDetails.tsx | 51 +++++++++++++++++++ src/app/components/Search/search-utils.ts | 5 ++ .../SearchResultsPage/SearchResultsList.tsx | 10 ++++ src/app/pages/SearchResultsPage/hooks.ts | 33 +++++++++++- .../useRedirectIfSingleResult.ts | 3 ++ src/locales/en/translation.json | 3 ++ src/oasis-nexus/api.ts | 18 +++++++ 8 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 .changelog/1192.feature.md create mode 100644 src/app/components/NetworkProposalsList/ProposalDetails.tsx diff --git a/.changelog/1192.feature.md b/.changelog/1192.feature.md new file mode 100644 index 0000000000..ca8eb8eaaa --- /dev/null +++ b/.changelog/1192.feature.md @@ -0,0 +1 @@ +Add support for searching for network proposal by name diff --git a/src/app/components/NetworkProposalsList/ProposalDetails.tsx b/src/app/components/NetworkProposalsList/ProposalDetails.tsx new file mode 100644 index 0000000000..3679e686a5 --- /dev/null +++ b/src/app/components/NetworkProposalsList/ProposalDetails.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react' +import { Proposal } from '../../../oasis-nexus/api' +import { StyledDescriptionList } from '../StyledDescriptionList' +import { useScreenSize } from '../../hooks/useScreensize' +import { DashboardLink } from '../../pages/ParatimeDashboardPage/DashboardLink' +import { useTranslation } from 'react-i18next' +import { RoundedBalance } from '../RoundedBalance' +import { ProposalStatusIcon } from '../ProposalStatusIcon' + +export const ProposalDetails: FC<{ proposal: Proposal; showLayer?: boolean; standalone?: boolean }> = ({ + proposal, + showLayer = false, + standalone = false, +}) => { + const { t } = useTranslation() + const { isMobile } = useScreenSize() + return ( + + {showLayer && ( + <> +
{t('common.network')}
+
+ +
+ + )} + +
{t('networkProposal.id')}
+
{proposal.id}
+ +
{t('networkProposal.handler')}
+
{proposal.handler}
+ +
{t('networkProposal.deposit')}
+
+ +
+ +
{t('networkProposal.create')}
+
{proposal.created_at}
+ +
{t('networkProposal.close')}
+
{proposal.closes_at}
+ +
{t('common.status')}
+
+ +
+
+ ) +} diff --git a/src/app/components/Search/search-utils.ts b/src/app/components/Search/search-utils.ts index 9bb21e9c7e..76abd2e038 100644 --- a/src/app/components/Search/search-utils.ts +++ b/src/app/components/Search/search-utils.ts @@ -100,6 +100,11 @@ export const validateAndNormalize = { return searchTerm.toLowerCase() } }, + networkProposalNameFragment: (searchTerm: string) => { + if (searchTerm?.length >= textSearchMininumLength) { + return searchTerm.toLowerCase() + } + }, } satisfies { [name: string]: (searchTerm: string) => string | undefined } export function isSearchValid(searchTerm: string) { diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 8079f8fc5c..07d54edc04 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -9,6 +9,7 @@ import { AccountResult, BlockResult, ContractResult, + ProposalResult, SearchResults, TokenResult, TransactionResult, @@ -19,6 +20,7 @@ import { SubPageCard } from '../../components/SubPageCard' import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' import { TokenDetails } from '../../components/Tokens/TokenDetails' +import { ProposalDetails } from '../../components/NetworkProposalsList/ProposalDetails' /** * Component for displaying a list of search results @@ -114,6 +116,14 @@ export const SearchResultsList: FC<{ link={token => RouteUtils.getTokenRoute(token, token.eth_contract_addr ?? token.contract_addr)} linkLabel={t('search.results.tokens.viewLink')} /> + + item.resultType === 'proposal')} + resultComponent={item => } + link={() => ''} + linkLabel="" + /> ) diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts index 72b12c9eab..722078f52d 100644 --- a/src/app/pages/SearchResultsPage/hooks.ts +++ b/src/app/pages/SearchResultsPage/hooks.ts @@ -13,6 +13,8 @@ import { EvmTokenList, EvmToken, useGetRuntimeBlockByHash, + Proposal, + useGetConsensusProposalsByName, } from '../../../oasis-nexus/api' import { RouteUtils } from '../../utils/route-utils' import { SearchParams } from '../../components/Search/search-utils' @@ -24,7 +26,7 @@ function isDefined(item: T): item is NonNullable { export type ConditionalResults = { isLoading: boolean; results: T[] } type SearchResultItemCore = HasScope & { - resultType: 'block' | 'transaction' | 'account' | 'contract' | 'token' + resultType: 'block' | 'transaction' | 'account' | 'contract' | 'token' | 'proposal' } export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' } @@ -37,7 +39,15 @@ export type ContractResult = SearchResultItemCore & RuntimeAccount & { resultTyp export type TokenResult = SearchResultItemCore & EvmToken & { resultType: 'token' } -export type SearchResultItem = BlockResult | TransactionResult | AccountResult | ContractResult | TokenResult +export type ProposalResult = SearchResultItemCore & Proposal & { resultType: 'proposal' } + +export type SearchResultItem = + | BlockResult + | TransactionResult + | AccountResult + | ContractResult + | TokenResult + | ProposalResult export type SearchResults = SearchResultItem[] @@ -161,6 +171,22 @@ export function useRuntimeTokenConditionally( } } +export function useNetworkProposalsConditionally( + nameFragment: string | undefined, +): ConditionalResults { + const queries = RouteUtils.getEnabledNetworks().map(network => + // eslint-disable-next-line react-hooks/rules-of-hooks + useGetConsensusProposalsByName(network, nameFragment), + ) + return { + isLoading: queries.some(query => query.isInitialLoading), + results: queries + .map(query => query.results) + .filter(isDefined) + .flat(), + } +} + export const useSearch = (q: SearchParams) => { const queries = { blockHeight: useBlocksByHeightConditionally(q.blockHeight), @@ -170,6 +196,7 @@ export const useSearch = (q: SearchParams) => { // TODO: remove evmBech32Account and use evmAccount when API is ready evmBech32Account: useRuntimeAccountConditionally(q.evmBech32Account), tokens: useRuntimeTokenConditionally(q.evmTokenNameFragment), + proposals: useNetworkProposalsConditionally(q.networkProposalNameFragment), } const isLoading = Object.values(queries).some(query => query.isLoading) const blocks = [...queries.blockHeight.results, ...queries.blockHash.results] @@ -182,6 +209,7 @@ export const useSearch = (q: SearchParams) => { .map(l => l.evm_tokens) .flat() .sort((t1, t2) => t2.num_holders - t1.num_holders) + const proposals = queries.proposals.results const results: SearchResultItem[] = isLoading ? [] @@ -195,6 +223,7 @@ export const useSearch = (q: SearchParams) => { .filter(account => account.evm_contract) .map((account): ContractResult => ({ ...account, resultType: 'contract' })), ...tokens.map((token): TokenResult => ({ ...token, resultType: 'token' })), + ...proposals.map((proposal): ProposalResult => ({ ...proposal, resultType: 'proposal' })), ] return { isLoading, diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts index 5854f9e45b..a10cc02f40 100644 --- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts +++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts @@ -36,6 +36,9 @@ export function useRedirectIfSingleResult(scope: SearchScope | undefined, result case 'token': redirectTo = RouteUtils.getTokenRoute(item, item.eth_contract_addr || item.contract_addr) break + case 'proposal': + // We don't want to redirect to proposals + break default: exhaustedTypeWarning('Unexpected result type', item) } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b175fc4074..61fc345794 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -497,6 +497,9 @@ "title": "Transactions", "viewLink": "View Transaction" }, + "proposals": { + "title": "Proposals" + }, "count_one": "1 result", "count_other": "{{ count }} results", "moreCount_one": "1 more result", diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index af9ca703ec..df3e3b0894 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -817,6 +817,7 @@ export const useGetConsensusProposals: typeof generated.useGetConsensusProposals return { ...proposal, network, + layer: Layer.consensus, deposit: fromBaseUnits(proposal.deposit, consensusDecimals), } }), @@ -854,6 +855,23 @@ export const useGetConsensusProposalsProposalId: typeof generated.useGetConsensu }) } +export const useGetConsensusProposalsByName = (network: Network, nameFragment: string | undefined) => { + const query = useGetConsensusProposals(network, {}, { query: { enabled: !!nameFragment } }) + const { isLoading, isInitialLoading, data, status, error } = query + const textMatcher = nameFragment + ? (proposal: generated.Proposal): boolean => + !!proposal.handler && proposal.handler?.includes(nameFragment) + : () => false + const results = data ? query.data.data.proposals.filter(textMatcher) : undefined + return { + isLoading, + isInitialLoading, + status, + error, + results, + } +} + export const useGetConsensusValidators: typeof generated.useGetConsensusValidators = ( network, params?,