From 6198303b24f9f62bbf0b42a3c3ef97f5fab7767b Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Wed, 25 Sep 2024 14:02:35 -0400 Subject: [PATCH] add DAO proposal search bar --- packages/state/indexer/search.ts | 38 ++++++-- packages/state/query/queries/dao.ts | 10 +++ packages/state/recoil/selectors/indexer.ts | 11 ++- .../actions/ExecuteProposal/Component.tsx | 1 - .../Component.tsx | 1 - .../core/actions/VetoProposal/Component.tsx | 2 - packages/stateful/components/ProposalLine.tsx | 9 +- packages/stateful/components/ProposalList.tsx | 87 +++++++++++++++---- .../feed/sources/OpenProposals/state.ts | 1 - packages/stateful/recoil/selectors/dao.ts | 1 - .../components/proposal/ProposalList.tsx | 4 +- packages/types/components/ProposalLine.ts | 1 - .../types/contracts/CwProposalSingle.v1.ts | 3 + .../types/contracts/DaoProposalMultiple.ts | 4 + .../types/contracts/DaoProposalSingle.v2.ts | 3 + 15 files changed, 136 insertions(+), 40 deletions(-) diff --git a/packages/state/indexer/search.ts b/packages/state/indexer/search.ts index 2f2d17f748..0878dc3a4e 100644 --- a/packages/state/indexer/search.ts +++ b/packages/state/indexer/search.ts @@ -90,12 +90,35 @@ export type DaoProposalSearchResult = { } export type SearchDaoProposalsOptions = WithChainId<{ - limit: number + /** + * Search query. + */ + query?: string + /** + * Limit number of search results. + */ + limit?: number + /** + * Only search proposals in a specific DAO. + */ + dao?: string + /** + * Whether or not to sort by recently created first. Defaults to false. + */ + recentFirst?: boolean + /** + * Exclude hidden DAOs. Defaults to false. + */ + excludeHidden?: boolean }> -export const getRecentDaoProposals = async ({ +export const searchDaoProposals = async ({ chainId, + query, limit, + dao, + recentFirst, + excludeHidden, }: SearchDaoProposalsOptions): Promise => { const client = await loadMeilisearchClient() @@ -106,20 +129,21 @@ export const getRecentDaoProposals = async ({ const index = client.index(chainId + '_proposals') const results = await index.search>( - null, + query, { limit, filter: [ // Exclude hidden DAOs. - 'value.hideFromSearch NOT EXISTS OR value.hideFromSearch != true', + ...(excludeHidden + ? ['value.hideFromSearch NOT EXISTS OR value.hideFromSearch != true'] + : []), // Ensure DAO and proposal ID exist. - 'value.dao EXISTS', + dao ? `value.dao = "${dao}"` : 'value.dao EXISTS', 'value.daoProposalId EXISTS', ] .map((filter) => `(${filter})`) .join(' AND '), - // Most recently created first. - sort: ['value.proposal.start_height:desc'], + sort: recentFirst ? ['value.proposal.start_height:desc'] : undefined, } ) diff --git a/packages/state/query/queries/dao.ts b/packages/state/query/queries/dao.ts index dbea47268d..12e6a16488 100644 --- a/packages/state/query/queries/dao.ts +++ b/packages/state/query/queries/dao.ts @@ -1,6 +1,7 @@ import { FetchQueryOptions, QueryClient, + queryOptions, skipToken, } from '@tanstack/react-query' @@ -22,6 +23,7 @@ import { isConfiguredChainName, } from '@dao-dao/utils' +import { SearchDaoProposalsOptions, searchDaoProposals } from '../../indexer' import { accountQueries } from './account' import { chainQueries } from './chain' import { contractQueries } from './contract' @@ -410,4 +412,12 @@ export const daoQueries = { formula: 'daoCore/approvalDaos', noFallback: true, }), + /** + * Search DAO proposals. + */ + searchProposals: (options: SearchDaoProposalsOptions) => + queryOptions({ + queryKey: ['dao', 'searchProposals', options], + queryFn: () => searchDaoProposals(options), + }), } diff --git a/packages/state/recoil/selectors/indexer.ts b/packages/state/recoil/selectors/indexer.ts index 2b63800a49..92b7c8d84e 100644 --- a/packages/state/recoil/selectors/indexer.ts +++ b/packages/state/recoil/selectors/indexer.ts @@ -22,11 +22,11 @@ import { QuerySnapperOptions, SearchDaoProposalsOptions, SearchDaosOptions, - getRecentDaoProposals, loadMeilisearchClient, queryIndexer, queryIndexerUpStatus, querySnapper, + searchDaoProposals, searchDaos, } from '../../indexer' import { @@ -224,10 +224,15 @@ export const searchDaosSelector = selectorFamily< */ export const chainRecentDaoProposalsSelector = selectorFamily< DaoProposalSearchResult[], - SearchDaoProposalsOptions + Omit >({ key: 'chainRecentDaoProposals', - get: (options) => async () => await getRecentDaoProposals(options), + get: (options) => async () => + await searchDaoProposals({ + ...options, + recentFirst: true, + excludeHidden: true, + }), }) /** diff --git a/packages/stateful/actions/core/actions/ExecuteProposal/Component.tsx b/packages/stateful/actions/core/actions/ExecuteProposal/Component.tsx index aa6ce1f473..30e0edcc92 100644 --- a/packages/stateful/actions/core/actions/ExecuteProposal/Component.tsx +++ b/packages/stateful/actions/core/actions/ExecuteProposal/Component.tsx @@ -121,7 +121,6 @@ export const ExecuteProposalComponent: ActionComponent< = ({ = ({ { const existingDao = useDaoContextIfAvailable()?.dao const content = ( - + ) @@ -51,13 +50,13 @@ export const ProposalLine = ({ type InnerProposalLineProps = Pick< StatefulProposalLineProps, - 'proposalViewUrl' | 'isPreProposeProposal' | 'onClick' | 'openInNewTab' + 'proposalId' | 'proposalViewUrl' | 'onClick' | 'openInNewTab' > const InnerProposalLine = ({ + proposalId, proposalViewUrl, onClick, - isPreProposeProposal, openInNewTab, }: InnerProposalLineProps) => { const { t } = useTranslation() @@ -65,7 +64,7 @@ const InnerProposalLine = ({ components: { ProposalLine, PreProposeApprovalProposalLine }, } = useProposalModuleAdapter() - const Component = isPreProposeProposal + const Component = proposalId.includes('*') ? PreProposeApprovalProposalLine : ProposalLine if (!Component) { diff --git a/packages/stateful/components/ProposalList.tsx b/packages/stateful/components/ProposalList.tsx index 1de0d1b5c5..319c71d3f1 100644 --- a/packages/stateful/components/ProposalList.tsx +++ b/packages/stateful/components/ProposalList.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRecoilCallback, useSetRecoilState } from 'recoil' +import { daoQueries } from '@dao-dao/state/query' import { daoVetoableDaosSelector, refreshProposalsIdAtom, @@ -25,6 +26,7 @@ import { } from '@dao-dao/types' import { NEUTRON_GOVERNANCE_DAO, + chainIsIndexed, webSocketChannelNameForDao, } from '@dao-dao/utils' @@ -32,6 +34,7 @@ import { useMembership, useOnCurrentDaoWebSocketMessage, useOnWebSocketMessage, + useQueryLoadingDataWithError, } from '../hooks' import { matchAndLoadCommon } from '../proposal-module-adapter' import { daosWithDropdownVetoableProposalListSelector } from '../recoil' @@ -157,7 +160,7 @@ export const ProposalList = ({ limit: PROP_PAGINATE_LIMIT, }) ) - : undefined + : [] const preProposeCompletedProposalInfos = selectors.reversePreProposeCompletedProposalInfos @@ -167,7 +170,7 @@ export const ProposalList = ({ limit: PROP_PAGINATE_LIMIT, }) ) - : undefined + : [] return [ ...proposalInfos.map( @@ -176,18 +179,18 @@ export const ProposalList = ({ ...info, }) ), - ...(preProposePendingProposalInfos?.map( + ...preProposePendingProposalInfos.map( (info): CommonProposalListInfoWithType => ({ type: ProposalType.PreProposePending, ...info, }) - ) ?? []), - ...(preProposeCompletedProposalInfos?.map( + ), + ...preProposeCompletedProposalInfos.map( (info): CommonProposalListInfoWithType => ({ type: ProposalType.PreProposeCompleted, ...info, }) - ) ?? []), + ), ].map((info) => ({ ...info, proposalModule, @@ -270,7 +273,6 @@ export const ProposalList = ({ const transformIntoProps = ({ id, - type, status, }: typeof newProposalInfos[number]): StatefulProposalLineProps & { status: ProposalStatus @@ -284,9 +286,6 @@ export const ProposalList = ({ onClick: onClickRef.current ? () => onClickRef.current?.({ proposalId: id }) : undefined, - isPreProposeProposal: - type === ProposalType.PreProposePending || - type === ProposalType.PreProposeCompleted, status, }) @@ -374,6 +373,21 @@ export const ProposalList = ({ () => setRefreshProposalsId((id) => id + 1) ) + const [search, setSearch] = useState('') + // Cannot search without an indexer on the chain. + const canSearch = chainIsIndexed(dao.chainId) + const showingSearchResults = canSearch && !!search && search.length > 0 + const searchedProposals = useQueryLoadingDataWithError( + showingSearchResults + ? daoQueries.searchProposals({ + chainId: dao.chainId, + dao: dao.coreAddress, + query: search, + limit: 20, + }) + : undefined + ) + return ( loadMore() } - loadingMore={loading} + loadingMore={ + showingSearchResults + ? searchedProposals.loading || !!searchedProposals.updating + : loading + } openProposals={ - // Show executable proposals at the top in place of open proposals. - onlyExecutable + showingSearchResults + ? [] + : // Show executable proposals at the top in place of open proposals. + onlyExecutable ? historyProposals.filter( ({ status }) => status === ProposalStatusEnum.Passed ) : openProposals } + searchBarProps={ + canSearch + ? { + value: search, + onChange: (e) => setSearch(e.target.value), + } + : undefined + } sections={ - // Show executable proposals at the top in place of open proposals. - onlyExecutable + showingSearchResults + ? [ + { + title: t('title.results'), + proposals: + searchedProposals.loading || searchedProposals.errored + ? [] + : searchedProposals.data.flatMap( + (proposal): StatefulProposalLineProps | [] => + proposal.value.daoProposalId + ? { + chainId: dao.chainId, + coreAddress: dao.coreAddress, + proposalId: proposal.value.daoProposalId, + proposalViewUrl: getDaoProposalPath( + dao.coreAddress, + proposal.value.daoProposalId + ), + } + : [] + ), + }, + ] + : // Show executable proposals at the top in place of open proposals. + onlyExecutable ? [] : [ { @@ -422,6 +478,7 @@ export const ProposalList = ({ }, ] } + showingSearchResults={showingSearchResults} /> ) } diff --git a/packages/stateful/feed/sources/OpenProposals/state.ts b/packages/stateful/feed/sources/OpenProposals/state.ts index 6f07be7aec..32d6e6d011 100644 --- a/packages/stateful/feed/sources/OpenProposals/state.ts +++ b/packages/stateful/feed/sources/OpenProposals/state.ts @@ -308,7 +308,6 @@ export const feedOpenProposalsSelector = selectorFamily< coreAddress, `${proposalModule.prefix}${id}` ), - isPreProposeProposal: false, }, }, pending: diff --git a/packages/stateful/recoil/selectors/dao.ts b/packages/stateful/recoil/selectors/dao.ts index f994b29eb2..5b8e84e548 100644 --- a/packages/stateful/recoil/selectors/dao.ts +++ b/packages/stateful/recoil/selectors/dao.ts @@ -327,7 +327,6 @@ export const daosWithDropdownVetoableProposalListSelector = selectorFamily< dao, `${prefix}${id}` ), - isPreProposeProposal: false, }) ) ), diff --git a/packages/stateless/components/proposal/ProposalList.tsx b/packages/stateless/components/proposal/ProposalList.tsx index 00dac86e63..83c8948fcc 100644 --- a/packages/stateless/components/proposal/ProposalList.tsx +++ b/packages/stateless/components/proposal/ProposalList.tsx @@ -58,9 +58,7 @@ export const ProposalList = ({

{t('title.proposals')}

- {DiscordNotifierConfigureModal && proposalsExist && ( - - )} + {DiscordNotifierConfigureModal && }
)} diff --git a/packages/types/components/ProposalLine.ts b/packages/types/components/ProposalLine.ts index 098899a069..d41c4dcc92 100644 --- a/packages/types/components/ProposalLine.ts +++ b/packages/types/components/ProposalLine.ts @@ -5,6 +5,5 @@ export type StatefulProposalLineProps = { proposalId: string proposalViewUrl: string onClick?: () => void - isPreProposeProposal: boolean openInNewTab?: boolean } diff --git a/packages/types/contracts/CwProposalSingle.v1.ts b/packages/types/contracts/CwProposalSingle.v1.ts index 13a25c3953..22d251794e 100644 --- a/packages/types/contracts/CwProposalSingle.v1.ts +++ b/packages/types/contracts/CwProposalSingle.v1.ts @@ -4,6 +4,8 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ +import { ProposalModuleWithInfo } from './DaoDaoCore' + export type Addr = string export type Uint128 = string export type Duration = @@ -309,6 +311,7 @@ export interface ProposalResponse { hideFromSearch?: boolean dao?: string daoProposalId?: string + proposalModule?: ProposalModuleWithInfo createdAt?: string completedAt?: string executedAt?: string diff --git a/packages/types/contracts/DaoProposalMultiple.ts b/packages/types/contracts/DaoProposalMultiple.ts index d4aab6ed1b..648fb7159a 100644 --- a/packages/types/contracts/DaoProposalMultiple.ts +++ b/packages/types/contracts/DaoProposalMultiple.ts @@ -4,6 +4,8 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ +import { ProposalModuleWithInfo } from './DaoDaoCore' + export type Duration = | { height: number @@ -433,10 +435,12 @@ export interface ProposalListResponse { export interface ProposalResponse { id: number proposal: MultipleChoiceProposal + // Indexer may return these. hideFromSearch?: boolean dao?: string daoProposalId?: string + proposalModule?: ProposalModuleWithInfo createdAt?: string completedAt?: string executedAt?: string diff --git a/packages/types/contracts/DaoProposalSingle.v2.ts b/packages/types/contracts/DaoProposalSingle.v2.ts index 9f36761e8f..9ad0b7fe29 100644 --- a/packages/types/contracts/DaoProposalSingle.v2.ts +++ b/packages/types/contracts/DaoProposalSingle.v2.ts @@ -4,6 +4,8 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ +import { ProposalModuleWithInfo } from './DaoDaoCore' + export type Duration = | { height: number @@ -438,6 +440,7 @@ export interface ProposalResponse { hideFromSearch?: boolean dao?: string daoProposalId?: string + proposalModule?: ProposalModuleWithInfo createdAt?: string completedAt?: string executedAt?: string