Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Search sorting #1374

Merged
merged 4 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/back/routes/proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import CoauthorModel from '../../entities/Coauthor/model'
import { CoauthorStatus } from '../../entities/Coauthor/types'
import isDAOCommittee from '../../entities/Committee/isDAOCommittee'
import { hasOpenSlots } from '../../entities/Committee/utils'
import { GrantRequest, getGrantRequestSchema } from '../../entities/Grant/types'
import { GrantRequest, getGrantRequestSchema, toGrantSubtype } from '../../entities/Grant/types'
import {
SUBMISSION_THRESHOLD_DRAFT,
SUBMISSION_THRESHOLD_GOVERNANCE,
Expand Down Expand Up @@ -49,6 +49,7 @@ import {
ProposalRequiredVP,
ProposalStatus,
ProposalType,
SortingOrder,
UpdateProposalStatusProposal,
newProposalBanNameScheme,
newProposalCatalystScheme,
Expand Down Expand Up @@ -76,6 +77,9 @@ import {
isValidName,
isValidPointOfInterest,
isValidUpdateProposalStatus,
toProposalStatus,
toProposalType,
toSortingOrder,
} from '../../entities/Proposal/utils'
import { SNAPSHOT_DURATION } from '../../entities/Snapshot/constants'
import { validateUniqueAddresses } from '../../entities/Transparency/utils'
Expand Down Expand Up @@ -118,15 +122,15 @@ export default routes((route) => {

export async function getProposals(req: WithAuth) {
const query = req.query
const type = query.type && String(query.type)
const subtype = query.subtype && String(query.subtype)
const status = query.status && String(query.status)
const type = toProposalType(String(query.type), () => undefined)
const subtype = toGrantSubtype(String(query.subtype), () => undefined)
const status = toProposalStatus(String(query.status), () => undefined)
const user = query.user && String(query.user)
const search = query.search && String(query.search)
const timeFrame = query.timeFrame && String(query.timeFrame)
const timeFrameKey = query.timeFrameKey && String(query.timeFrameKey)
const coauthor = (query.coauthor && Boolean(query.coauthor)) || false
const order = query.order && String(query.order) === 'ASC' ? 'ASC' : 'DESC'
const order = toSortingOrder(String(query.order), () => undefined)
const snapshotIds = query.snapshotIds && String(query.snapshotIds)
const subscribed = query.subscribed ? req.auth || '' : undefined
const offset = query.offset && Number.isFinite(Number(query.offset)) ? Number(query.offset) : MIN_PROPOSAL_OFFSET
Expand Down Expand Up @@ -436,7 +440,7 @@ export async function createProposalTender(req: WithAuth) {

const tenderProposals = await ProposalModel.getProposalList({
linkedProposalId: configuration.linked_proposal_id,
order: 'ASC',
order: SortingOrder.ASC,
})

if (hasTenderProcessFinished(tenderProposals)) {
Expand Down
18 changes: 3 additions & 15 deletions src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { BadgeCreationResult, RevokeOrReinstateResult, UserBadges } from '../ent
import { BidRequest, UnpublishedBidAttributes } from '../entities/Bid/types'
import { Budget, BudgetWithContestants, CategoryBudget } from '../entities/Budget/types'
import { CoauthorAttributes, CoauthorStatus } from '../entities/Coauthor/types'
import { GrantRequest, ProposalGrantCategory, SubtypeOptions } from '../entities/Grant/types'
import { GrantRequest, ProposalGrantCategory } from '../entities/Grant/types'
import {
CategorizedGrants,
FilterProposalList,
NewProposalBanName,
NewProposalCatalyst,
NewProposalDraft,
Expand All @@ -28,7 +29,6 @@ import {
ProposalAttributes,
ProposalCommentsInDiscourse,
ProposalStatus,
ProposalType,
} from '../entities/Proposal/types'
import { QuarterBudgetAttributes } from '../entities/QuarterBudget/types'
import { SubscriptionAttributes } from '../entities/Subscription/types'
Expand Down Expand Up @@ -66,21 +66,9 @@ type NewProposalMap = {
[`/proposals/hiring`]: NewProposalHiring
}

export type GetProposalsFilter = {
user: string
type: ProposalType
subtype?: SubtypeOptions
status: ProposalStatus
subscribed: boolean | string
coauthor: boolean
search?: string | null
timeFrame?: string | null
timeFrameKey?: string | null
order?: 'ASC' | 'DESC'
export type GetProposalsFilter = FilterProposalList & {
limit: number
offset: number
snapshotIds?: string
linkedProposalId?: string
}

const getGovernanceApiUrl = () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Modal/BidVotingModal/BidVotingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Close } from 'decentraland-ui/dist/components/Close/Close'
import { Header } from 'decentraland-ui/dist/components/Header/Header'
import { Modal, ModalProps } from 'decentraland-ui/dist/components/Modal/Modal'

import { ProposalAttributes, ProposalType } from '../../../entities/Proposal/types'
import { ProposalAttributes, ProposalType, SortingOrder } from '../../../entities/Proposal/types'
import { SelectedVoteChoice } from '../../../entities/Votes/types'
import useFormatMessage from '../../../hooks/useFormatMessage'
import useProposals from '../../../hooks/useProposals'
Expand All @@ -28,7 +28,7 @@ function BidVotingModal({ onCastVote, castingVote, linkedTenderId, proposalPageS
const { proposals } = useProposals({
type: ProposalType.Bid,
linkedProposalId: linkedTenderId,
order: 'ASC',
order: SortingOrder.ASC,
})
const { selectedChoice, showBidVotingModal, showVotingError, retryTimer, showSnapshotRedirect } = proposalPageState
const { snapshot_id: currentProposal } = proposal
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/CategoryFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default function CategoryFilter({
const t = useFormatMessage()
const params = useURLSearchParams()
const type = params.get(FILTER_KEY)
const currentSubtype = useMemo(() => toGrantSubtype(params.get('subtype')), [params])
const currentSubtype = useMemo(() => toGrantSubtype(params.get('subtype'), () => null), [params])

const areProposals = isEqual(filterType, ProposalType)
const filters = areProposals
Expand Down
17 changes: 13 additions & 4 deletions src/components/Search/SortingMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import classNames from 'classnames'
import { Dropdown } from 'decentraland-ui/dist/components/Dropdown/Dropdown'
import { useMobileMediaQuery } from 'decentraland-ui/dist/components/Media/Media'

import { SortingOrder } from '../../entities/Proposal/types'
import { toSortingOrder } from '../../entities/Proposal/utils'
import { getUrlFilters } from '../../helpers'
import useFormatMessage from '../../hooks/useFormatMessage'
import { navigate } from '../../utils/locations'
Expand All @@ -16,8 +18,9 @@ const SORT_KEY = 'order'
export default function SortingMenu() {
const location = useLocation()
const params = useMemo(() => new URLSearchParams(location.search), [location.search])
const order = useMemo(() => (params.get('order') === 'ASC' ? 'ASC' : 'DESC'), [params])
const arrowDirection = useMemo(() => (order === 'ASC' ? 'Upwards' : 'Downwards'), [order])
const isSearching = !!params.get('search')
const order = toSortingOrder(params.get('order'), () => (isSearching ? 'RELEVANCE' : SortingOrder.DESC))
const arrowDirection = order === SortingOrder.ASC ? 'Upwards' : 'Downwards'
const isMobile = useMobileMediaQuery()

const t = useFormatMessage()
Expand All @@ -29,13 +32,19 @@ export default function SortingMenu() {
text={t(`navigation.search.sorting.${order}`) || ''}
>
<Dropdown.Menu>
{isSearching && (
<Dropdown.Item
text={t('navigation.search.sorting.RELEVANCE')}
onClick={() => navigate(getUrlFilters(SORT_KEY, params, undefined))}
/>
)}
<Dropdown.Item
text={t('navigation.search.sorting.DESC')}
onClick={() => navigate(getUrlFilters(SORT_KEY, params, 'DESC'))}
onClick={() => navigate(getUrlFilters(SORT_KEY, params, SortingOrder.DESC))}
/>
<Dropdown.Item
text={t('navigation.search.sorting.ASC')}
onClick={() => navigate(getUrlFilters(SORT_KEY, params, 'ASC'))}
onClick={() => navigate(getUrlFilters(SORT_KEY, params, SortingOrder.ASC))}
/>
</Dropdown.Menu>
</Dropdown>
Expand Down
4 changes: 2 additions & 2 deletions src/entities/Grant/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export function isGrantSubtype(value: string | null | undefined) {
)
}

export function toGrantSubtype(value: string | null | undefined): SubtypeOptions | undefined {
return isGrantSubtype(value) ? (value as SubtypeOptions) : undefined
export function toGrantSubtype<OrElse>(value: string | null | undefined, orElse: () => OrElse) {
return isGrantSubtype(value) ? (value as SubtypeOptions) : orElse()
}

export const GrantRequestGeneralInfoSchema = {
Expand Down
34 changes: 13 additions & 21 deletions src/entities/Proposal/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,23 @@ import { OldGrantCategory, SubtypeAlternativeOptions, isGrantSubtype } from '../
import SubscriptionModel from '../Subscription/model'

import tsquery from './tsquery'
import { ProposalAttributes, ProposalStatus, ProposalType, isProposalType } from './types'
import {
FilterProposalList,
ProposalAttributes,
ProposalStatus,
ProposalType,
SortingOrder,
isProposalType,
} from './types'
import { SITEMAP_ITEMS_PER_PAGE, isProposalStatus } from './utils'

export type FilterProposalList = {
type: string
subtype?: string
user: string
status: string
subscribed: string
coauthor: boolean
search?: string
timeFrame?: string
timeFrameKey?: string
order?: 'ASC' | 'DESC'
snapshotIds?: string
linkedProposalId?: string
}

export type FilterPagination = {
limit: number
offset: number
}

const VALID_TIMEFRAME_KEYS = ['created_at', 'finish_at']
const VALID_ORDER_DIRECTION = ['ASC', 'DESC']
const VALID_ORDER_DIRECTION = Object.values(SortingOrder)

export default class ProposalModel extends Model<ProposalAttributes> {
static tableName = 'proposals'
Expand Down Expand Up @@ -245,7 +237,7 @@ export default class ProposalModel extends Model<ProposalAttributes> {
return 0
}

if (subscribed && !isEthereumAddress(subscribed)) {
if (subscribed && !isEthereumAddress(String(subscribed))) {
return 0
}

Expand Down Expand Up @@ -336,7 +328,7 @@ export default class ProposalModel extends Model<ProposalAttributes> {
return []
}

if (subscribed && !isEthereumAddress(subscribed)) {
if (subscribed && !isEthereumAddress(String(subscribed))) {
return []
}

Expand All @@ -358,13 +350,13 @@ export default class ProposalModel extends Model<ProposalAttributes> {

const timeFrame = this.parseTimeframe(filter.timeFrame)
const timeFrameKey = filter.timeFrameKey || 'created_at'
const orderDirection = order || 'DESC'
const orderDirection = !order ? SortingOrder.DESC : order
ncomerci marked this conversation as resolved.
Show resolved Hide resolved

if (!VALID_TIMEFRAME_KEYS.includes(timeFrameKey) || !VALID_ORDER_DIRECTION.includes(orderDirection)) {
return []
}

const orderBy = search ? '"rank"' : `p.${timeFrameKey}`
const orderBy = search && !order ? '"rank"' : `p.${timeFrameKey}`

const sqlSnapshotIds = snapshotIds?.split(',').map((id) => SQL`${id}`)
const sqlSnapshotIdsJoin = sqlSnapshotIds ? join(sqlSnapshotIds) : null
Expand Down
39 changes: 29 additions & 10 deletions src/entities/Proposal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
PaymentToken,
ProjectStatus,
ProposalGrantCategory,
SubtypeOptions,
VestingStartDate,
} from '../Grant/types'
import { IndexedUpdate } from '../Updates/types'
Expand Down Expand Up @@ -68,6 +69,21 @@ export type ProposalAttributes<C extends Record<string, unknown> = any> = {
textsearch: SQLStatement | string | null | undefined
}

export type FilterProposalList = {
ncomerci marked this conversation as resolved.
Show resolved Hide resolved
user: string
type: ProposalType
subtype?: SubtypeOptions
status: ProposalStatus
subscribed: boolean | string
coauthor: boolean
search?: string | null
timeFrame?: string | null
timeFrameKey?: string | null
order?: SortingOrder
snapshotIds?: string
linkedProposalId?: string
}

export enum ProposalStatus {
Pending = 'pending',
Active = 'active',
Expand All @@ -94,6 +110,11 @@ export enum ProposalType {
Bid = 'bid',
}

export enum SortingOrder {
ASC = 'ASC',
DESC = 'DESC',
}

export type GovernanceProcessType = ProposalType.Poll | ProposalType.Draft | ProposalType.Governance
export type BiddingProcessType = ProposalType.Pitch | ProposalType.Tender | ProposalType.Bid

Expand Down Expand Up @@ -140,16 +161,14 @@ export function isCatalystType(value: string | null | undefined): boolean {
}
}

export function toCatalystType(value: string | null | undefined): CatalystType | null {
return isCatalystType(value) ? (value as CatalystType) : null
}

export function toProposalType(value: string | null | undefined): ProposalType | null {
return isProposalType(value) ? (value as ProposalType) : null
}

export function toPoiType(value: string | null | undefined): PoiType | null {
return isPoiType(value) ? (value as PoiType) : null
export function isSortingOrder(value: string | null | undefined): boolean {
switch (value) {
case SortingOrder.ASC:
case SortingOrder.DESC:
return true
default:
return false
}
}

export function getPoiTypeAction(poiType: PoiType) {
Expand Down
41 changes: 38 additions & 3 deletions src/entities/Proposal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ import { UpdateAttributes } from '../Updates/types'
import { DISCOURSE_API } from '../User/utils'

import { MAX_NAME_SIZE, MIN_NAME_SIZE } from './constants'
import { ProposalAttributes, ProposalStatus, ProposalType } from './types'
import {
CatalystType,
PoiType,
ProposalAttributes,
ProposalStatus,
ProposalType,
SortingOrder,
isCatalystType,
isPoiType,
isProposalType,
isSortingOrder,
} from './types'

export const MIN_PROPOSAL_OFFSET = 0
export const MAX_PROPOSAL_LIMIT = 100
Expand Down Expand Up @@ -175,8 +186,32 @@ export function isProposalStatus(value: string | null | undefined): boolean {
}
}

export function toProposalStatus(value: string | null | undefined, orElse: () => any): ProposalStatus | any {
return isProposalStatus(value) ? (value as ProposalStatus) : orElse()
function toCustomType<FinalType, OrElse, ValueType>(
value: ValueType,
isType: (value: ValueType) => boolean,
orElse: () => OrElse
): FinalType | OrElse {
return isType(value) ? (value as unknown as FinalType) : orElse()
}

export function toProposalStatus<OrElse>(value: string | null | undefined, orElse: () => OrElse) {
return toCustomType<ProposalStatus, OrElse, typeof value>(value, isProposalStatus, orElse)
}

export function toCatalystType<OrElse>(value: string | null | undefined, orElse: () => OrElse) {
return toCustomType<CatalystType, OrElse, typeof value>(value, isCatalystType, orElse)
}

export function toProposalType<OrElse>(value: string | null | undefined, orElse: () => OrElse) {
return toCustomType<ProposalType, OrElse, typeof value>(value, isProposalType, orElse)
}

export function toPoiType<OrElse>(value: string | null | undefined, orElse: () => OrElse) {
return toCustomType<PoiType, OrElse, typeof value>(value, isPoiType, orElse)
}

export function toSortingOrder<OrElse>(value: string | null | undefined, orElse: () => OrElse) {
return toCustomType<SortingOrder, OrElse, typeof value>(value, isSortingOrder, orElse)
Copy link
Member

@andyesp andyesp Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the orElse callback as I find it confusing to see all those functions returning null/undefined everywhere. I'd change this to a default value argument. But we can discuss this later and change it in another PR.

}

export function isProposalDeletable(proposalStatus?: ProposalStatus) {
Expand Down
Loading
Loading