make Pageable filters more generic (not just statuses)

This commit is contained in:
Aaron 2019-02-07 11:51:16 -06:00
parent 60575b4024
commit 239c7a3432
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
7 changed files with 127 additions and 86 deletions

View File

@ -6,7 +6,7 @@ import store from 'src/store';
import Pageable from 'components/Pageable';
import ContributionItem from './ContributionItem';
import { Contribution } from 'src/types';
import { PROPOSAL_STATUSES } from 'util/statuses';
import { contributionFilters } from 'util/filters';
class Contributions extends React.Component<{}> {
render() {
@ -16,7 +16,7 @@ class Contributions extends React.Component<{}> {
return (
<Pageable
page={page}
statuses={PROPOSAL_STATUSES}
filters={contributionFilters}
sorts={sorts}
searchPlaceholder="Search amount or txid"
controlsExtra={
@ -24,9 +24,7 @@ class Contributions extends React.Component<{}> {
<Button icon="plus">Create a contribution</Button>
</Link>
}
renderItem={(c: Contribution) =>
<ContributionItem key={c.id} contribution={c} />
}
renderItem={(c: Contribution) => <ContributionItem key={c.id} contribution={c} />}
handleSearch={store.fetchContributions}
handleChangeQuery={store.setContributionPageQuery}
handleResetQuery={store.resetContributionPageQuery}

View File

@ -23,14 +23,17 @@ class Home extends React.Component {
<div>
<Icon type="exclamation-circle" /> There are <b>{proposalPendingCount}</b>{' '}
proposals <b>waiting for review</b>.{' '}
<Link to="/proposals?status=PENDING">Click here</Link> to view them.
<Link to="/proposals?filters[]=STATUS_PENDING">Click here</Link> to view them.
</div>
),
!!proposalNoArbiterCount && (
<div>
<Icon type="exclamation-circle" /> There are <b>{proposalNoArbiterCount}</b>{' '}
live proposals <b>without an arbiter</b>.{' '}
<Link to="/proposals?status=LIVE&other=ARBITER">Click here</Link> to view them.
<Link to="/proposals?filters[]=STATUS_LIVE&filters[]=OTHER_ARBITER">
Click here
</Link>{' '}
to view them.
</div>
),
].filter(Boolean);

View File

@ -5,12 +5,12 @@ import { Icon, Button, Dropdown, Menu, Tag, List, Input, Pagination } from 'antd
import { ClickParam } from 'antd/lib/menu';
import { RouteComponentProps, withRouter } from 'react-router';
import { PageData, PageQuery } from 'src/types';
import { StatusSoT, getStatusById } from 'util/statuses';
import { Filters } from 'util/filters';
import './index.less';
interface OwnProps<T> {
page: PageData<T>;
statuses: Array<StatusSoT<any>>;
filters: Filters;
sorts: string[];
searchPlaceholder?: string;
controlsExtra?: React.ReactNode;
@ -29,16 +29,20 @@ class Pageable<T> extends React.Component<Props<T>, {}> {
}
render() {
const { page, statuses, sorts, renderItem, searchPlaceholder, controlsExtra } = this.props;
const {
page,
filters,
sorts,
renderItem,
searchPlaceholder,
controlsExtra,
} = this.props;
const loading = !page.fetched || page.fetching;
const filters = page.filters
.filter(f => f.startsWith('STATUS_'))
.map(f => f.replace('STATUS_', ''));
const statusFilterMenu = (
<Menu onClick={this.handleFilterClick}>
{statuses.map(s => (
<Menu.Item key={s.id}>{s.filterDisplay}</Menu.Item>
{filters.list.map(f => (
<Menu.Item key={f.id}>{f.display}</Menu.Item>
))}
</Menu>
);
@ -72,9 +76,7 @@ class Pageable<T> extends React.Component<Props<T>, {}> {
<Button title="refresh" icon="reload" onClick={this.props.handleSearch} />
{controlsExtra && (
<div className="Pageable-controls-extra">
{controlsExtra}
</div>
<div className="Pageable-controls-extra">{controlsExtra}</div>
)}
</div>
@ -87,17 +89,20 @@ class Pageable<T> extends React.Component<Props<T>, {}> {
{!!page.filters.length && (
<div className="Pageable-filters">
Filters:{' '}
{filters.map(sf => (
<Tag
key={sf}
onClose={() => this.handleFilterClose(sf)}
color={getStatusById(statuses, sf).tagColor}
closable
>
status: {sf}
</Tag>
))}
{filters.length > 1 && (
{page.filters.map(fId => {
const f = filters.getById(fId);
return (
<Tag
key={fId}
onClose={() => this.handleFilterClose(fId)}
color={f.color}
closable
>
{f.display}
</Tag>
);
})}
{page.filters.length > 1 && (
<Tag key="clear" onClick={this.handleFilterClear}>
clear
</Tag>
@ -127,17 +132,13 @@ class Pageable<T> extends React.Component<Props<T>, {}> {
}
private setQueryFromUrl = () => {
const { history, statuses, match, handleResetQuery, handleChangeQuery } = this.props;
const parsed = qs.parse(history.location.search);
const { history, match, handleResetQuery, handleChangeQuery } = this.props;
const parsed = qs.parse(history.location.search, { arrayFormat: 'bracket' });
// status filter
if (parsed.status) {
if (getStatusById(statuses, parsed.status)) {
// here we reset to normal page query params, we might want
// to do this every time we load or leave the component
handleResetQuery();
handleChangeQuery({ filters: [`STATUS_${parsed.status}`] });
}
// filters
if (parsed.filters) {
handleResetQuery();
handleChangeQuery({ filters: parsed.filters });
history.replace(match.url); // remove qs
}
};
@ -150,14 +151,14 @@ class Pageable<T> extends React.Component<Props<T>, {}> {
private handleFilterClick = (e: ClickParam) => {
const { page, handleChangeQuery, handleSearch } = this.props;
handleChangeQuery({
filters: uniq([`STATUS_${e.key}`, ...page.filters])
filters: uniq([e.key, ...page.filters]),
});
handleSearch();
};
private handleFilterClose = (filter: string) => {
const { page, handleChangeQuery, handleSearch } = this.props;
handleChangeQuery({ filters: without(page.filters, `STATUS_${filter}`) });
handleChangeQuery({ filters: without(page.filters, filter) });
handleSearch();
};

View File

@ -4,7 +4,7 @@ import store from 'src/store';
import ProposalItem from './ProposalItem';
import Pageable from 'components/Pageable';
import { Proposal } from 'src/types';
import { PROPOSAL_STATUSES } from 'util/statuses';
import { proposalFilters } from 'util/filters';
class Proposals extends React.Component<{}> {
render() {
@ -14,7 +14,7 @@ class Proposals extends React.Component<{}> {
return (
<Pageable
page={page}
statuses={PROPOSAL_STATUSES}
filters={proposalFilters}
sorts={sorts}
searchPlaceholder="Search proposal titles"
renderItem={(p: Proposal) => <ProposalItem key={p.proposalId} {...p} />}

View File

@ -10,7 +10,6 @@ import {
RFPArgs,
EmailExample,
PageQuery,
PROPOSAL_STATUS,
PageData,
} from './types';
@ -143,6 +142,7 @@ async function editContribution(id: number, args: ContributionArgs) {
// STORE
const app = store({
/*** DATA ***/
hasCheckedLogin: false,
isLoggedIn: false,
loginError: '',
@ -337,16 +337,9 @@ const app = store({
app.proposals.page.fetching = true;
try {
const page = await fetchProposals(app.getProposalPageQuery());
// filter strings with prefix p, and remove the prefix
const swp = (p: string, a: string[]) =>
a.filter((s: string) => s.startsWith(p)).map(x => x.replace(p, ''));
app.proposals.page = {
...app.proposals.page,
...page,
filters: {
status: swp('STATUS_', page.filters),
other: swp('OTHER_', page.filters),
},
fetched: true,
};
} catch (e) {
@ -356,6 +349,10 @@ const app = store({
},
setProposalPageQuery(query: Partial<PageQuery>) {
// sometimes we need to reset page to 1
if (query.filters || query.search) {
query.page = 1;
}
app.proposals.page = {
...app.proposals.page,
...query,
@ -363,13 +360,7 @@ const app = store({
},
getProposalPageQuery() {
const pq = pick(app.proposals.page, ['page', 'search', 'filters', 'sort']) as any;
const pfx = (p: string) => (s: string) => p + s;
pq.filters = [
...pq.filters.status.map(pfx('STATUS_')),
...pq.filters.other.map(pfx('OTHER_')),
];
return pq as PageQuery;
return pick(app.proposals.page, ['page', 'search', 'filters', 'sort']) as PageQuery;
},
resetProposalPageQuery() {
@ -516,6 +507,10 @@ const app = store({
},
setContributionPageQuery(query: Partial<PageQuery>) {
// sometimes we need to reset page to 1
if (query.filters || query.search) {
query.page = 1;
}
app.contributions.page = {
...app.contributions.page,
...query,

View File

@ -1,14 +1,72 @@
export const PROPOSAL_OTHER_FILTERS = [
{
id: 'ARBITER',
filterDisplay: 'Arbiter: missing',
tagColor: '#cf00d5',
},
];
export function getProposalOtherFilterById(id: string) {
const res = PROPOSAL_OTHER_FILTERS.find(x => x.id === id);
if (!res) {
throw Error(`getOtherProposalFilterById: could not find other filter for '${id}'`);
}
return res;
import { PROPOSAL_STATUSES, RFP_STATUSES, CONTRIBUTION_STATUSES } from './statuses';
export interface Filter {
id: string;
display: string;
color: string;
group: string;
}
export interface Filters {
list: Filter[];
getById: (id: string) => Filter;
}
const getFilterById = (from: Filter[]) => (id: string) => {
const search = from.find(x => x.id === id);
if (!search) {
throw Error(`filter.getById: could not find filter for '${id}'`);
}
return search;
};
// Proposal
const PROPOSAL_FILTERS = PROPOSAL_STATUSES.map(s => ({
id: `STATUS_${s.id}`,
display: `Status: ${s.tagDisplay}`,
color: s.tagColor,
group: 'Status',
}))
// proposal has extra filters
.concat([
{
id: `OTHER_ARBITER`,
display: `Other: Arbiter`,
color: '#cf00d5',
group: 'Other',
},
]);
export const proposalFilters: Filters = {
list: PROPOSAL_FILTERS,
getById: getFilterById(PROPOSAL_FILTERS),
};
// RFP
const RFP_FILTERS = RFP_STATUSES.map(s => ({
id: `STATUS_${s.id}`,
display: `Status: ${s.tagDisplay}`,
color: s.tagColor,
group: 'Status',
}));
export const rfpFilters: Filters = {
list: RFP_FILTERS,
getById: getFilterById(RFP_FILTERS),
};
// Contribution
const CONTRIBUTION_FILTERS = CONTRIBUTION_STATUSES.map(s => ({
id: `STATUS_${s.id}`,
display: `Status: ${s.tagDisplay}`,
color: s.tagColor,
group: 'Status',
}));
export const contributionFilters: Filters = {
list: CONTRIBUTION_FILTERS,
getById: getFilterById(CONTRIBUTION_FILTERS),
};

View File

@ -2,7 +2,6 @@ import { PROPOSAL_STATUS, RFP_STATUS, CONTRIBUTION_STATUS } from 'src/types';
export interface StatusSoT<E> {
id: E;
filterDisplay: string;
tagDisplay: string;
tagColor: string;
hint: string;
@ -11,42 +10,36 @@ export interface StatusSoT<E> {
export const PROPOSAL_STATUSES: Array<StatusSoT<PROPOSAL_STATUS>> = [
{
id: PROPOSAL_STATUS.APPROVED,
filterDisplay: 'Status: approved',
tagDisplay: 'Approved',
tagColor: '#afd500',
hint: 'Proposal has been approved and is awaiting being published by user.',
},
{
id: PROPOSAL_STATUS.DELETED,
filterDisplay: 'Status: deleted',
tagDisplay: 'Deleted',
tagColor: '#bebebe',
hint: 'Proposal has been deleted and is not visible on the platform.',
},
{
id: PROPOSAL_STATUS.DRAFT,
filterDisplay: 'Status: draft',
tagDisplay: 'Draft',
tagColor: '#8d8d8d',
hint: 'Proposal is being created by the user.',
},
{
id: PROPOSAL_STATUS.LIVE,
filterDisplay: 'Status: live',
tagDisplay: 'Live',
tagColor: '#108ee9',
hint: 'Proposal is live on the platform.',
},
{
id: PROPOSAL_STATUS.PENDING,
filterDisplay: 'Status: pending',
tagDisplay: 'Awaiting Approval',
tagColor: '#ffaa00',
hint: 'User is waiting for admin to approve or reject this Proposal.',
},
{
id: PROPOSAL_STATUS.REJECTED,
filterDisplay: 'Status: rejected',
tagDisplay: 'Approval Rejected',
tagColor: '#eb4118',
hint:
@ -54,7 +47,6 @@ export const PROPOSAL_STATUSES: Array<StatusSoT<PROPOSAL_STATUS>> = [
},
{
id: PROPOSAL_STATUS.STAKING,
filterDisplay: 'Status: staking',
tagDisplay: 'Staking',
tagColor: '#722ed1',
hint: 'This proposal is awaiting a staking contribution.',
@ -64,21 +56,18 @@ export const PROPOSAL_STATUSES: Array<StatusSoT<PROPOSAL_STATUS>> = [
export const RFP_STATUSES: Array<StatusSoT<RFP_STATUS>> = [
{
id: RFP_STATUS.DRAFT,
filterDisplay: 'Status: draft',
tagDisplay: 'Draft',
tagColor: '#ffaa00',
hint: 'RFP is currently being edited by admins and isnt visible to users.',
},
{
id: RFP_STATUS.LIVE,
filterDisplay: 'Status: live',
tagDisplay: 'Live',
tagColor: '#108ee9',
hint: 'RFP is live and users can submit proposals for it.',
},
{
id: RFP_STATUS.CLOSED,
filterDisplay: 'Status: closed',
tagDisplay: 'Closed',
tagColor: '#eb4118',
hint:
@ -89,26 +78,23 @@ export const RFP_STATUSES: Array<StatusSoT<RFP_STATUS>> = [
export const CONTRIBUTION_STATUSES: Array<StatusSoT<CONTRIBUTION_STATUS>> = [
{
id: CONTRIBUTION_STATUS.PENDING,
filterDisplay: 'Status: pending',
tagDisplay: 'Pending',
tagColor: '#ffaa00',
hint: 'Contribution is currently waiting to be sent and confirmed on chain',
},
{
id: CONTRIBUTION_STATUS.CONFIRMED,
filterDisplay: 'Status: confirmed',
tagDisplay: 'Confirmed',
tagColor: '#108ee9',
hint: 'Contribution was confirmed on chain with multiple block confirmations',
},
{
id: CONTRIBUTION_STATUS.DELETED,
filterDisplay: 'Status: deleted',
tagDisplay: 'Closed',
tagColor: '#eb4118',
hint: 'User deleted the contribution before it was sent or confirmed',
},
]
];
export function getStatusById<E>(statuses: Array<StatusSoT<E>>, id: E) {
const result = statuses.find(s => s.id === id);