make Pageable filters more generic (not just statuses)
This commit is contained in:
parent
60575b4024
commit
239c7a3432
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => (
|
||||
{page.filters.map(fId => {
|
||||
const f = filters.getById(fId);
|
||||
return (
|
||||
<Tag
|
||||
key={sf}
|
||||
onClose={() => this.handleFilterClose(sf)}
|
||||
color={getStatusById(statuses, sf).tagColor}
|
||||
key={fId}
|
||||
onClose={() => this.handleFilterClose(fId)}
|
||||
color={f.color}
|
||||
closable
|
||||
>
|
||||
status: {sf}
|
||||
{f.display}
|
||||
</Tag>
|
||||
))}
|
||||
{filters.length > 1 && (
|
||||
);
|
||||
})}
|
||||
{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
|
||||
// filters
|
||||
if (parsed.filters) {
|
||||
handleResetQuery();
|
||||
handleChangeQuery({ filters: [`STATUS_${parsed.status}`] });
|
||||
}
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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 isn’t 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);
|
||||
|
|
Loading…
Reference in New Issue