Skip to content

Commit

Permalink
Display the votes on the network proposal details page
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed Feb 1, 2024
1 parent eca5f4c commit 2881f04
Show file tree
Hide file tree
Showing 14 changed files with 744 additions and 31 deletions.
1 change: 1 addition & 0 deletions .changelog/1214.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Display the votes on the network proposal details page
77 changes: 77 additions & 0 deletions src/app/components/Proposals/ProposalVoteIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { FC } from 'react'
import { TFunction } from 'i18next'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CancelIcon from '@mui/icons-material/Cancel'
import QuestionIcon from '@mui/icons-material/QuestionMark'
import { styled } from '@mui/material/styles'
import { COLORS } from '../../../styles/theme/colors'
import { ProposalVoteValue } from '../../../types/vote'

const StyledBox = styled(Box)(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 3,
flex: 1,
borderRadius: 10,
padding: theme.spacing(2, 2, 2, '10px'),
fontSize: '12px',
minWidth: '85px',
}))

const StyledIcon = styled(Box)({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '18px',
})

const getStatuses = (t: TFunction) => ({
[ProposalVoteValue.abstain]: {
backgroundColor: COLORS.lightSilver,
icon: QuestionIcon,
iconColor: COLORS.grayExtraDark,
label: t('networkProposal.vote.abstain'),
textColor: COLORS.grayExtraDark,
},
[ProposalVoteValue.yes]: {
backgroundColor: COLORS.honeydew,
icon: CheckCircleIcon,
iconColor: COLORS.eucalyptus,
label: t('networkProposal.vote.yes'),
textColor: COLORS.grayExtraDark,
},
[ProposalVoteValue.no]: {
backgroundColor: COLORS.linen,
icon: CancelIcon,
iconColor: COLORS.errorIndicatorBackground,
label: t('networkProposal.vote.no'),
textColor: COLORS.grayExtraDark,
},
})

type ProposalVoteIndicatorProps = {
vote: ProposalVoteValue
}

export const ProposalVoteIndicator: FC<ProposalVoteIndicatorProps> = ({ vote }) => {
const { t } = useTranslation()

if (!ProposalVoteValue[vote]) {
return null
}

const statusConfig = getStatuses(t)[vote]
const IconComponent = statusConfig.icon

return (
<StyledBox sx={{ backgroundColor: statusConfig.backgroundColor, color: statusConfig.textColor }}>
{statusConfig.label}
<StyledIcon sx={{ color: statusConfig.iconColor }}>
<IconComponent color="inherit" fontSize="inherit" />
</StyledIcon>
</StyledBox>
)
}
64 changes: 64 additions & 0 deletions src/app/components/Proposals/VoteTypePills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Chip from '@mui/material/Chip'
import Typography from '@mui/material/Typography'
import { COLORS } from '../../../styles/theme/colors'
import { ProposalVoteValue, VoteType } from '../../../types/vote'

type VoteTypePillsProps = {
handleChange: (voteType: VoteType) => void
value?: VoteType
}

export const VoteTypePills: FC<VoteTypePillsProps> = ({ handleChange, value }) => {
const { t } = useTranslation()
const options: { label: string; value: VoteType }[] = [
{
label: t('networkProposal.vote.all'),
value: 'any',
},
{
label: t('networkProposal.vote.yes'),
value: ProposalVoteValue.yes,
},
{
label: t('networkProposal.vote.abstain'),
value: ProposalVoteValue.abstain,
},
{
label: t('networkProposal.vote.no'),
value: ProposalVoteValue.no,
},
]

return (
<>
{options.map(option => {
const selected = option.value === value
return (
<Chip
key={option.value}
onClick={() => handleChange(option.value)}
clickable
color="secondary"
label={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography component="span" sx={{ fontSize: 16 }}>
{option.label}
</Typography>
</Box>
}
sx={{
mr: 3,
borderColor: COLORS.brandMedium,
backgroundColor: selected ? COLORS.brandMedium : COLORS.brandMedium15,
color: selected ? COLORS.white : COLORS.grayExtraDark,
}}
variant={selected ? 'outlined-selected' : 'outlined'}
/>
)
})}
</>
)
}
70 changes: 70 additions & 0 deletions src/app/components/Proposals/VoterSearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FC } from 'react'
import TextField from '@mui/material/TextField'
import SearchIcon from '@mui/icons-material/Search'
import HighlightOffIcon from '@mui/icons-material/HighlightOff'
import InputAdornment from '@mui/material/InputAdornment'
import { COLORS } from '../../../styles/theme/colors'
import { SearchVariant } from '../Search'
import { useTranslation } from 'react-i18next'
import IconButton from '@mui/material/IconButton'

export interface SearchBarProps {
variant: SearchVariant
value: string
onChange: (value: string) => void
}

export const VoterSearchBar: FC<SearchBarProps> = ({ variant, value, onChange }) => {
const { t } = useTranslation()
const startAdornment = variant === 'button' && (
<InputAdornment
position="start"
disablePointerEvents // Pass clicks through, so it focuses the input
>
<SearchIcon sx={{ color: COLORS.grayDark }} />
</InputAdornment>
)

const onClearValue = () => onChange('')

const endAdornment = (
<InputAdornment position="end">
<>
{value && (
<IconButton color="inherit" onClick={onClearValue}>
<HighlightOffIcon />
</IconButton>
)}
</>
</InputAdornment>
)

return (
<>
<TextField
value={value}
onChange={e => onChange(e.target.value)}
InputProps={{
inputProps: {
sx: {
p: 0,
marginRight: 2,
},
},
startAdornment,
endAdornment,
}}
placeholder={t('networkProposal.searchForVoters')}
// FormHelperTextProps={{
// component: 'div', // replace p with div tag
// sx: {
// marginTop: 0,
// marginBottom: 0,
// marginLeft: variant === 'button' ? '48px' : '17px',
// marginRight: variant === 'button' ? '48px' : '17px',
// },
// }}
/>
</>
)
}
35 changes: 31 additions & 4 deletions src/app/components/Table/PaginationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,42 @@ export interface SimplePaginationEngine {
linkToPage: (pageNumber: number) => To
}

/**
* The data returned by a comprehensive pagination engine to the data consumer component
*/
export interface PaginatedResults<Item> {
/**
* Control interface that can be plugged to a Table's `pagination` prop
*/
tablePaginationProps: TablePaginationProps

/**
* The data provided to the data consumer in the current window
*/
data: Item[] | undefined
}

/**
* A Comprehensive PaginationEngine sits between the server and the consumer of the data and does transformations
*
* Specifically, the interface for loading the data and the one for the data consumers are decoupled.
*/
export interface ComprehensivePaginationEngine<Item, QueryResult extends List> {
selectedPage: number
offsetForQuery: number
limitForQuery: number
paramsForQuery: { offset: number; limit: number }
/**
* The currently selected page from the data consumer's POV
*/
selectedPageForClient: number

/**
* Parameters for data to be loaded from the server
*/
paramsForServer: { offset: number; limit: number }

/**
* Get the current data/state info for the data consumer component.
*
* @param queryResult the data coming in the server, requested according to this engine's specs, including metadata
* @param key The field where the actual records can be found within queryResults
*/
getResults: (queryResult: QueryResult | undefined, key?: keyof QueryResult) => PaginatedResults<Item>
}
52 changes: 38 additions & 14 deletions src/app/components/Table/useClientSidePagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,27 @@ import { List } from '../../../oasis-nexus/api'
import { TablePaginationProps } from './TablePagination'

type ClientSizePaginationParams<Item> = {
/**
* How should we call the query parameter (in the URL)?
*/
paramName: string

/**
* The pagination page size from the POV of the data consumer component
*/
clientPageSize: number

/**
* The pagination page size used for actually loading the data from the server.
*
* Please note that currently this engine doesn't handle when the data consumer requires data which is not
* part of the initial window on the server side.
*/
serverPageSize: number

/**
* Filtering to be applied after loading the data from the server, before presenting it to the data consumer component
*/
filter?: (item: Item) => boolean
}

Expand All @@ -27,13 +45,15 @@ function findListIn<T extends List, Item>(data: T): Item[] {
}
}

/**
* The ClientSidePagination engine loads the data from the server with a big window in one go, for in-memory pagination
*/
export function useClientSidePagination<Item, QueryResult extends List>({
paramName,
clientPageSize,
serverPageSize,
filter,
}: ClientSizePaginationParams<Item>): ComprehensivePaginationEngine<Item, QueryResult> {
const selectedServerPage = 1
const [searchParams] = useSearchParams()
const selectedClientPageString = searchParams.get(paramName)
const selectedClientPage = parseInt(selectedClientPageString ?? '1', 10)
Expand All @@ -57,30 +77,35 @@ export function useClientSidePagination<Item, QueryResult extends List>({
return { search: newSearchParams.toString() }
}

const limit = serverPageSize
const offset = (selectedServerPage - 1) * clientPageSize
// From the server, we always want to load the first batch of data, with the provided (big) window.
// In theory, we could move this window as required, but currently this is not implemented.
const selectedServerPage = 1

// The query parameters that should be used for loading the data from the server
const paramsForQuery = {
offset,
limit,
offset: (selectedServerPage - 1) * serverPageSize,
limit: serverPageSize,
}

return {
selectedPage: selectedClientPage,
offsetForQuery: offset,
limitForQuery: limit,
paramsForQuery,
selectedPageForClient: selectedClientPage,
paramsForServer: paramsForQuery,
getResults: (queryResult, key) => {
const data = queryResult
? key
? (queryResult[key] as Item[])
: findListIn<QueryResult, Item>(queryResult)
const data = queryResult // we want to get list of items out from the incoming results
? key // do we know where (in which field) to look?
? (queryResult[key] as Item[]) // If yes, just get out the data
: findListIn<QueryResult, Item>(queryResult) // If no, we will try to guess
: undefined

// Apply the specified client-side filtering
const filteredData = !!data && !!filter ? data.filter(filter) : data

// The data window from the POV of the data consumer component
const offset = (selectedClientPage - 1) * clientPageSize
const limit = clientPageSize
const dataWindow = filteredData ? filteredData.slice(offset, offset + limit) : undefined

// The control interface for the data consumer component (i.e. Table)
const tableProps: TablePaginationProps = {
selectedPage: selectedClientPage,
linkToPage,
Expand All @@ -98,6 +123,5 @@ export function useClientSidePagination<Item, QueryResult extends List>({
data: dataWindow,
}
},
// tableProps,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,8 @@ export function useComprehensiveSearchParamsPagination<Item, QueryResult extends
}

return {
selectedPage,
offsetForQuery: offset,
limitForQuery: limit,
paramsForQuery,
selectedPageForClient: selectedPage,
paramsForServer: paramsForQuery,
getResults: (queryResult, key) => {
const data = queryResult
? key
Expand Down
Loading

0 comments on commit 2881f04

Please sign in to comment.