zcash-grant-system/frontend/client/modules/proposals/reducers.ts

430 lines
11 KiB
TypeScript
Raw Normal View History

2018-09-10 09:55:26 -07:00
import types from './types';
import { cloneDeep } from 'lodash';
import { pendingMoreablePage, fulfilledMoreablePage } from 'utils/helpers';
import {
Proposal,
ProposalUpdates,
Comment,
ProposalContributions,
LoadableProposalPage,
Moreable,
} from 'types';
import { PROPOSAL_SORT } from 'api/constants';
2018-09-10 09:55:26 -07:00
export interface ProposalDetail extends Proposal {
isRequestingPayout: boolean;
requestPayoutError: string;
isRejectingPayout: boolean;
rejectPayoutError: string;
isAcceptingPayout: boolean;
acceptPayoutError: string;
}
2018-09-10 09:55:26 -07:00
export interface ProposalState {
page: LoadableProposalPage;
2019-02-11 21:42:21 -08:00
detail: null | ProposalDetail;
isFetchingDetail: boolean;
detailError: null | string;
2018-09-10 09:55:26 -07:00
detailComments: Moreable<Comment>;
2018-09-10 09:55:26 -07:00
proposalUpdates: { [id: string]: ProposalUpdates };
updatesError: null | string;
isFetchingUpdates: boolean;
2019-01-09 12:48:41 -08:00
proposalContributions: { [id: string]: ProposalContributions };
fetchContributionsError: null | string;
isFetchingContributions: boolean;
isPostCommentPending: boolean;
postCommentError: null | string;
2019-01-09 13:32:51 -08:00
isDeletingContribution: boolean;
deleteContributionError: null | string;
2018-09-10 09:55:26 -07:00
}
2019-02-11 21:42:21 -08:00
export const PROPOSAL_DETAIL_INITIAL_STATE = {
isRequestingPayout: false,
requestPayoutError: '',
isRejectingPayout: false,
rejectPayoutError: '',
isAcceptingPayout: false,
acceptPayoutError: '',
};
2018-09-10 09:55:26 -07:00
export const INITIAL_STATE: ProposalState = {
page: {
page: 1,
pageSize: 0,
total: 0,
search: '',
sort: PROPOSAL_SORT.NEWEST,
filters: {
category: [],
stage: [],
},
items: [],
hasFetched: false,
isFetching: false,
fetchError: null,
fetchTime: 0,
},
detail: null,
isFetchingDetail: false,
detailError: null,
2018-09-10 09:55:26 -07:00
detailComments: {
pages: [],
hasMore: false,
page: 1,
pageSize: 0,
total: 0,
search: '',
sort: '',
filters: [],
isFetching: false,
hasFetched: false,
fetchError: '',
fetchTime: 0,
parentId: null,
},
2018-09-10 09:55:26 -07:00
proposalUpdates: {},
updatesError: null,
isFetchingUpdates: false,
2019-01-09 12:48:41 -08:00
proposalContributions: {},
fetchContributionsError: null,
isFetchingContributions: false,
isPostCommentPending: false,
postCommentError: null,
2019-01-09 13:32:51 -08:00
isDeletingContribution: false,
deleteContributionError: null,
2018-09-10 09:55:26 -07:00
};
function addUpdates(state: ProposalState, payload: ProposalUpdates) {
2018-09-10 09:55:26 -07:00
return {
...state,
proposalUpdates: {
...state.proposalUpdates,
[payload.proposalId]: payload,
2018-09-10 09:55:26 -07:00
},
isFetchingUpdates: false,
};
}
2019-01-09 12:48:41 -08:00
function addContributions(state: ProposalState, payload: ProposalContributions) {
return {
...state,
proposalContributions: {
...state.proposalContributions,
[payload.proposalId]: payload,
},
isFetchingContributions: false,
};
}
interface PostCommentPayload {
proposalId: Proposal['proposalId'];
comment: Comment;
parentCommentId?: Comment['id'];
}
function addPostedComment(state: ProposalState, payload: PostCommentPayload) {
const { comment, parentCommentId } = payload;
// clone so we can mutate with great abandon!
const pages = cloneDeep(state.detailComments.pages);
if (!parentCommentId) {
// its a new comment, pop it into the very first position
if (pages[0]) {
pages[0].unshift(comment);
} else {
pages[0] = [comment];
}
} else {
// recursive populate replies for nested comment
const f = (id: number, p: Comment) => {
if (p.id === id) {
p.replies.unshift(comment);
return;
} else {
p.replies.forEach(x => f(id, x));
}
};
// pages > page > comments
pages.forEach(p =>
p.forEach(c => {
f(parentCommentId, c);
}),
);
}
return {
...state,
isPostCommentPending: false,
detailComments: {
...state.detailComments,
pages,
total: state.detailComments.total + 1,
},
};
}
2019-02-18 14:35:21 -08:00
function updateCommentInStore(
state: ProposalState,
commentId: Comment['id'],
update: Partial<Comment>,
) {
// clone so we can mutate with great abandon!
const pages = cloneDeep(state.detailComments.pages);
// recursive populate replies for nested comment
const f = (id: number, p: Comment) => {
if (p.id === id) {
Object.entries(update).forEach(([k, v]) => {
(p as any)[k] = v;
});
return;
} else {
p.replies.forEach(x => f(id, x));
}
};
// pages > page > comments
pages.forEach(p =>
p.forEach(c => {
f(commentId, c);
}),
);
return {
...state,
isPostCommentPending: false,
detailComments: {
...state.detailComments,
pages,
total: state.detailComments.total + 1,
},
};
}
2018-09-10 09:55:26 -07:00
export default (state = INITIAL_STATE, action: any) => {
const { payload } = action;
switch (action.type) {
case types.SET_PROPOSAL_PAGE:
return {
...state,
page: {
...state.page,
...payload,
page: payload.page || 1, // reset page to 1 for non-page changes
},
};
2018-09-10 09:55:26 -07:00
case types.PROPOSALS_DATA_PENDING:
return {
...state,
page: {
...state.page,
isFetching: true,
fetchError: null,
},
2018-09-10 09:55:26 -07:00
};
case types.PROPOSALS_DATA_FULFILLED:
return {
...state,
page: {
...payload,
isFetching: false,
hasFetched: true,
fetchTime: Date.now(),
},
};
2018-09-10 09:55:26 -07:00
case types.PROPOSALS_DATA_REJECTED:
return {
...state,
page: {
...state.page,
isFetching: false,
hasFetched: false,
fetchError: (payload && payload.message) || payload.toString(),
},
2018-09-10 09:55:26 -07:00
};
case types.PROPOSAL_DATA_PENDING:
// check if this proposal is in the page list, and optimistically set it
const loadedInPage = state.page.items.find(
p => p.proposalId === payload.proposalId,
);
return {
...state,
detail:
// if requesting same proposal, leave the detail object
state.detail && state.detail.proposalId === payload.proposalId
? state.detail
: { ...loadedInPage, ...PROPOSAL_DETAIL_INITIAL_STATE } || null,
isFetchingDetail: true,
detailError: null,
};
2018-09-10 09:55:26 -07:00
case types.PROPOSAL_DATA_FULFILLED:
return {
...state,
detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
isFetchingDetail: false,
};
case types.PROPOSAL_DATA_REJECTED:
return {
...state,
detail: null,
isFetchingDetail: false,
detailError: (payload && payload.message) || payload.toString(),
};
2018-09-10 09:55:26 -07:00
case types.PROPOSAL_PAYOUT_REQUEST_PENDING:
return {
...state,
detail: {
...state.detail,
isRequestingPayout: true,
requestPayoutError: '',
},
};
case types.PROPOSAL_PAYOUT_REQUEST_FULFILLED:
return {
...state,
detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
};
case types.PROPOSAL_PAYOUT_REQUEST_REJECTED:
return {
...state,
detail: {
...state.detail,
isRequestingPayout: false,
requestPayoutError: (payload && payload.message) || payload.toString(),
},
};
case types.PROPOSAL_PAYOUT_REJECT_PENDING:
return {
...state,
detail: {
...state.detail,
isRejectingPayout: true,
rejectPayoutError: '',
},
};
case types.PROPOSAL_PAYOUT_REJECT_FULFILLED:
return {
...state,
detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
};
case types.PROPOSAL_PAYOUT_REJECT_REJECTED:
return {
...state,
detail: {
...state.detail,
isRejectingPayout: false,
rejectPayoutError: (payload && payload.message) || payload.toString(),
},
};
case types.PROPOSAL_PAYOUT_ACCEPT_PENDING:
return {
...state,
detail: {
...state.detail,
isAcceptingPayout: true,
acceptPayoutError: '',
},
};
case types.PROPOSAL_PAYOUT_ACCEPT_FULFILLED:
return {
...state,
detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
};
case types.PROPOSAL_PAYOUT_ACCEPT_REJECTED:
return {
...state,
detail: {
...state.detail,
isAcceptingPayout: false,
acceptPayoutError: (payload && payload.message) || payload.toString(),
},
};
2018-09-10 09:55:26 -07:00
case types.PROPOSAL_COMMENTS_PENDING:
return {
...state,
detailComments: pendingMoreablePage(state.detailComments, payload),
2018-09-10 09:55:26 -07:00
};
case types.PROPOSAL_COMMENTS_FULFILLED:
return {
...state,
detailComments: fulfilledMoreablePage(state.detailComments, payload),
};
2018-09-10 09:55:26 -07:00
case types.PROPOSAL_COMMENTS_REJECTED:
return {
...state,
detailComments: {
...state.detailComments,
hasFetched: true,
isFetching: false,
fetchError: (payload && payload.message) || payload.toString(),
},
2018-09-10 09:55:26 -07:00
};
2019-02-18 14:35:21 -08:00
case types.REPORT_PROPOSAL_COMMENT_FULFILLED:
return updateCommentInStore(state, payload.commentId, { reported: true });
case types.UPDATE_PROPOSAL_COMMENT:
return updateCommentInStore(state, payload.commentId, payload.commentUpdate);
2018-09-10 09:55:26 -07:00
case types.PROPOSAL_UPDATES_PENDING:
return {
...state,
updatesError: null,
isFetchingUpdates: true,
};
case types.PROPOSAL_UPDATES_FULFILLED:
return addUpdates(state, payload);
case types.PROPOSAL_UPDATES_REJECTED:
return {
...state,
updatesError: (payload && payload.message) || payload.toString(),
2018-09-10 09:55:26 -07:00
isFetchingUpdates: false,
};
2019-01-09 12:48:41 -08:00
case types.PROPOSAL_CONTRIBUTIONS_PENDING:
return {
...state,
fetchContributionsError: null,
isFetchingContributions: true,
};
case types.PROPOSAL_CONTRIBUTIONS_FULFILLED:
return addContributions(state, payload);
case types.PROPOSAL_CONTRIBUTIONS_REJECTED:
return {
...state,
fetchContributionsError: (payload && payload.message) || payload.toString(),
2019-01-09 12:48:41 -08:00
isFetchingContributions: false,
};
case types.POST_PROPOSAL_COMMENT_PENDING:
return {
...state,
isPostCommentPending: true,
postCommentError: null,
};
case types.POST_PROPOSAL_COMMENT_FULFILLED:
return addPostedComment(state, payload);
case types.POST_PROPOSAL_COMMENT_REJECTED:
return {
...state,
isPostCommentPending: false,
postCommentError: payload,
};
2018-09-10 09:55:26 -07:00
default:
return state;
}
};