zcash-grant-system/admin/src/store.ts

676 lines
15 KiB
TypeScript
Raw Normal View History

2019-02-05 19:30:31 -08:00
import { pick } from 'lodash';
import { store } from 'react-easy-state';
import axios, { AxiosError } from 'axios';
2019-02-05 19:30:31 -08:00
import {
User,
Proposal,
Contribution,
ContributionArgs,
2019-02-05 19:30:31 -08:00
RFP,
RFPArgs,
EmailExample,
PageQuery,
PageData,
2019-02-05 19:30:31 -08:00
} from './types';
// API
const api = axios.create({
baseURL: process.env.BACKEND_URL + '/api/v1',
withCredentials: true,
});
async function login(username: string, password: string) {
const { data } = await api.post('/admin/login', {
username,
password,
});
return data.isLoggedIn;
}
async function logout() {
const { data } = await api.get('/admin/logout');
return data.isLoggedIn;
}
async function checkLogin() {
const { data } = await api.get('/admin/checklogin');
return data.isLoggedIn;
}
async function fetchStats() {
const { data } = await api.get('/admin/stats');
return data;
}
async function fetchUsers(params: Partial<PageQuery>) {
const { data } = await api.get('/admin/users', { params });
return data;
}
async function fetchUserDetail(id: number) {
const { data } = await api.get(`/admin/users/${id}`);
return data;
}
async function editUser(id: number, args: Partial<User>) {
const { data } = await api.put(`/admin/users/${id}`, args);
return data;
}
2019-02-04 13:18:50 -08:00
async function deleteUser(id: number) {
const { data } = await api.delete('/admin/users/' + id);
return data;
}
2019-02-06 10:38:07 -08:00
async function fetchArbiters(search: string) {
const { data } = await api.get(`/admin/arbiters`, { params: { search } });
return data;
}
async function setArbiter(proposalId: number, userId: number) {
const { data } = await api.put(`/admin/arbiters`, { proposalId, userId });
return data;
}
async function fetchProposals(params: Partial<PageQuery>) {
const { data } = await api.get('/admin/proposals', { params });
return data;
}
async function fetchProposalDetail(id: number) {
const { data } = await api.get(`/admin/proposals/${id}`);
return data;
}
async function updateProposal(p: Partial<Proposal>) {
const { data } = await api.put('/admin/proposals/' + p.proposalId, p);
return data;
}
async function deleteProposal(id: number) {
const { data } = await api.delete('/admin/proposals/' + id);
return data;
}
async function approveProposal(id: number, isApprove: boolean, rejectReason?: string) {
const { data } = await api.put(`/admin/proposals/${id}/approve`, {
isApprove,
rejectReason,
});
return data;
}
2019-02-13 08:54:46 -08:00
async function markMilestonePaid(proposalId: number, milestoneId: number, txId: string) {
const { data } = await api.put(
`/admin/proposals/${proposalId}/milestone/${milestoneId}/paid`,
{ txId },
);
return data;
}
async function getEmailExample(type: string) {
const { data } = await api.get(`/admin/email/example/${type}`);
return data;
}
async function getRFPs() {
const { data } = await api.get(`/admin/rfps`);
return data;
}
async function createRFP(args: RFPArgs) {
const { data } = await api.post('/admin/rfps', args);
return data;
}
async function editRFP(id: number, args: RFPArgs) {
const { data } = await api.put(`/admin/rfps/${id}`, args);
return data;
}
async function deleteRFP(id: number) {
await api.delete(`/admin/rfps/${id}`);
}
async function getContributions(params: PageQuery) {
const { data } = await api.get('/admin/contributions', { params });
return data;
}
async function getContribution(id: number) {
const { data } = await api.get(`/admin/contributions/${id}`);
return data;
}
async function createContribution(args: ContributionArgs) {
const { data } = await api.post('/admin/contributions', args);
return data;
}
async function editContribution(id: number, args: ContributionArgs) {
const { data } = await api.put(`/admin/contributions/${id}`, args);
return data;
}
// STORE
const app = store({
/*** DATA ***/
hasCheckedLogin: false,
isLoggedIn: false,
loginError: '',
generalError: [] as string[],
statsFetched: false,
statsFetching: false,
stats: {
userCount: 0,
proposalCount: 0,
proposalPendingCount: 0,
proposalNoArbiterCount: 0,
2019-02-13 08:54:46 -08:00
proposalMilestonePayoutsCount: 0,
},
users: {
page: createDefaultPageData<User>('EMAIL:DESC'),
},
userSaving: false,
userSaved: false,
userDetailFetching: false,
userDetail: null as null | User,
2019-02-04 13:18:50 -08:00
userDeleting: false,
userDeleted: false,
2019-02-06 10:38:07 -08:00
arbitersSearch: {
search: '',
results: [] as User[],
fetching: false,
error: null as string | null,
},
proposals: {
page: createDefaultPageData<Proposal>('CREATED:DESC'),
},
proposalDetail: null as null | Proposal,
proposalDetailFetching: false,
proposalDetailApproving: false,
2019-02-13 08:54:46 -08:00
proposalDetailMarkingMilestonePaid: false,
rfps: [] as RFP[],
rfpsFetching: false,
rfpsFetched: false,
rfpSaving: false,
rfpSaved: false,
rfpDeleting: false,
rfpDeleted: false,
contributions: {
page: createDefaultPageData<Contribution>('CREATED:DESC'),
},
contributionDetail: null as null | Contribution,
contributionDetailFetching: false,
contributionSaving: false,
contributionSaved: false,
emailExamples: {} as { [type: string]: EmailExample },
/*** ACTIONS ***/
removeGeneralError(i: number) {
app.generalError.splice(i, 1);
},
updateProposalInStore(p: Proposal) {
const index = app.proposals.page.items.findIndex(x => x.proposalId === p.proposalId);
if (index > -1) {
app.proposals.page.items[index] = p;
}
if (app.proposalDetail && app.proposalDetail.proposalId === p.proposalId) {
app.proposalDetail = p;
}
},
2019-02-06 10:38:07 -08:00
updateUserInStore(u: User) {
const index = app.users.page.items.findIndex(x => x.userid === u.userid);
2019-02-06 10:38:07 -08:00
if (index > -1) {
app.users.page.items[index] = u;
2019-02-06 10:38:07 -08:00
}
if (app.userDetail && app.userDetail.userid === u.userid) {
app.userDetail = {
...app.userDetail,
...u,
};
2019-02-06 10:38:07 -08:00
}
},
// Auth
async checkLogin() {
app.isLoggedIn = await checkLogin();
app.hasCheckedLogin = true;
},
async login(username: string, password: string) {
try {
app.isLoggedIn = await login(username, password);
} catch (e) {
app.loginError = e.response.data.message;
}
},
async logout() {
try {
app.isLoggedIn = await logout();
} catch (e) {
app.generalError.push(e.toString());
}
},
async fetchStats() {
app.statsFetching = true;
try {
app.stats = await fetchStats();
app.statsFetched = true;
} catch (e) {
handleApiError(e);
}
app.statsFetching = false;
},
// Users
async fetchUsers() {
app.users.page.fetching = true;
try {
const page = await fetchUsers(app.getUserPageQuery());
app.users.page = {
...app.users.page,
...page,
fetched: true,
};
} catch (e) {
handleApiError(e);
}
app.users.page.fetching = false;
},
getUserPageQuery() {
return pick(app.users.page, ['page', 'search', 'filters', 'sort']) as PageQuery;
},
setUserPageQuery(query: Partial<PageQuery>) {
// sometimes we need to reset page to 1
if (query.filters || query.search) {
query.page = 1;
}
app.users.page = {
...app.users.page,
...query,
};
},
resetUserPageQuery() {
app.users.page.page = 1;
app.users.page.search = '';
app.users.page.sort = 'CREATED:DESC';
app.users.page.filters = [];
},
async fetchUserDetail(id: number) {
app.userDetailFetching = true;
try {
app.userDetail = await fetchUserDetail(id);
} catch (e) {
handleApiError(e);
}
app.userDetailFetching = false;
},
async editUser(id: number, args: Partial<User>) {
app.userSaving = true;
app.userSaved = false;
try {
const user = await editUser(id, args);
app.updateUserInStore(user);
app.userSaved = true;
} catch (e) {
handleApiError(e);
}
app.userSaving = false;
},
2019-02-04 13:18:50 -08:00
async deleteUser(id: number) {
app.userDeleting = false;
app.userDeleted = false;
try {
await deleteUser(id);
app.users.page.items = app.users.page.items.filter(u => u.userid !== id);
2019-02-04 13:18:50 -08:00
app.userDeleted = true;
app.userDetail = null;
} catch (e) {
handleApiError(e);
}
2019-02-04 13:18:50 -08:00
app.userDeleting = false;
},
// Arbiters
2019-02-06 10:38:07 -08:00
async searchArbiters(search: string) {
app.arbitersSearch = {
...app.arbitersSearch,
search,
fetching: true,
};
try {
const data = await fetchArbiters(search);
app.arbitersSearch = {
...app.arbitersSearch,
...data,
};
} catch (e) {
handleApiError(e);
}
app.arbitersSearch.fetching = false;
},
async searchArbitersClear() {
app.arbitersSearch = {
search: '',
results: [] as User[],
fetching: false,
error: null,
};
},
async setArbiter(proposalId: number, userId: number) {
// let component handle errors for this one
const { proposal, user } = await setArbiter(proposalId, userId);
this.updateProposalInStore(proposal);
this.updateUserInStore(user);
},
// Proposals
async fetchProposals() {
app.proposals.page.fetching = true;
try {
const page = await fetchProposals(app.getProposalPageQuery());
app.proposals.page = {
...app.proposals.page,
...page,
fetched: true,
};
} catch (e) {
handleApiError(e);
}
app.proposals.page.fetching = false;
},
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,
};
},
getProposalPageQuery() {
return pick(app.proposals.page, ['page', 'search', 'filters', 'sort']) as PageQuery;
},
resetProposalPageQuery() {
app.proposals.page.page = 1;
app.proposals.page.search = '';
app.proposals.page.sort = 'CREATED:DESC';
app.proposals.page.filters = [];
},
async fetchProposalDetail(id: number) {
app.proposalDetailFetching = true;
try {
app.proposalDetail = await fetchProposalDetail(id);
} catch (e) {
handleApiError(e);
}
app.proposalDetailFetching = false;
},
async updateProposalDetail(updates: Partial<Proposal>) {
if (!app.proposalDetail) {
return;
}
try {
const res = await updateProposal({
...updates,
proposalId: app.proposalDetail.proposalId,
});
app.updateProposalInStore(res);
} catch (e) {
handleApiError(e);
}
},
async deleteProposal(id: number) {
try {
await deleteProposal(id);
app.proposals.page.items = app.proposals.page.items.filter(
p => p.proposalId === id,
);
} catch (e) {
handleApiError(e);
}
},
async approveProposal(isApprove: boolean, rejectReason?: string) {
if (!app.proposalDetail) {
const m = 'store.approveProposal(): Expected proposalDetail to be populated!';
app.generalError.push(m);
console.error(m);
return;
}
app.proposalDetailApproving = true;
try {
const { proposalId } = app.proposalDetail;
const res = await approveProposal(proposalId, isApprove, rejectReason);
app.updateProposalInStore(res);
} catch (e) {
handleApiError(e);
}
app.proposalDetailApproving = false;
},
2019-01-09 13:57:15 -08:00
2019-02-13 08:54:46 -08:00
async markMilestonePaid(proposalId: number, milestoneId: number, txId: string) {
app.proposalDetailMarkingMilestonePaid = true;
try {
const res = await markMilestonePaid(proposalId, milestoneId, txId);
app.updateProposalInStore(res);
} catch (e) {
handleApiError(e);
}
app.proposalDetailMarkingMilestonePaid = false;
},
// Email
async getEmailExample(type: string) {
try {
const example = await getEmailExample(type);
app.emailExamples = {
...app.emailExamples,
[type]: example,
};
} catch (e) {
handleApiError(e);
}
},
// RFPs
async fetchRFPs() {
app.rfpsFetching = true;
try {
app.rfps = await getRFPs();
app.rfpsFetched = true;
} catch (e) {
handleApiError(e);
}
app.rfpsFetching = false;
},
async createRFP(args: RFPArgs) {
app.rfpSaving = true;
try {
const data = await createRFP(args);
app.rfps = [data, ...app.rfps];
app.rfpSaved = true;
} catch (e) {
handleApiError(e);
}
app.rfpSaving = false;
},
async editRFP(id: number, args: RFPArgs) {
app.rfpSaving = true;
app.rfpSaved = false;
try {
await editRFP(id, args);
app.rfpSaved = true;
await app.fetchRFPs();
} catch (e) {
handleApiError(e);
}
app.rfpSaving = false;
},
async deleteRFP(id: number) {
app.rfpDeleting = true;
app.rfpDeleted = false;
try {
await deleteRFP(id);
app.rfps = app.rfps.filter(rfp => rfp.id !== id);
app.rfpDeleted = true;
} catch (e) {
handleApiError(e);
}
app.rfpDeleting = false;
},
// Contributions
async fetchContributions() {
app.contributions.page.fetching = true;
try {
const page = await getContributions(app.getContributionPageQuery());
app.contributions.page = {
...app.contributions.page,
...page,
fetched: true,
};
} catch (e) {
handleApiError(e);
}
app.contributions.page.fetching = false;
},
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,
};
},
getContributionPageQuery() {
return pick(app.contributions.page, [
'page',
'search',
'filters',
'sort',
]) as PageQuery;
},
resetContributionPageQuery() {
app.contributions.page.page = 1;
app.contributions.page.search = '';
app.contributions.page.sort = 'CREATED:DESC';
app.contributions.page.filters = [];
},
async fetchContributionDetail(id: number) {
app.contributionDetailFetching = true;
try {
app.contributionDetail = await getContribution(id);
} catch (e) {
handleApiError(e);
}
app.contributionDetailFetching = false;
},
async editContribution(id: number, args: ContributionArgs) {
app.contributionSaving = true;
app.contributionSaved = false;
try {
await editContribution(id, args);
app.contributionSaved = true;
} catch (e) {
handleApiError(e);
}
app.contributionSaving = false;
},
async createContribution(args: ContributionArgs) {
app.contributionSaving = true;
app.contributionSaved = false;
try {
await createContribution(args);
app.contributionSaved = true;
} catch (e) {
handleApiError(e);
}
app.contributionSaving = false;
},
});
// Utils
function handleApiError(e: AxiosError) {
if (e.response && e.response.data!.message) {
app.generalError.push(e.response!.data.message);
} else if (e.response && e.response.data!.data!) {
app.generalError.push(e.response!.data.data);
} else {
app.generalError.push(e.toString());
}
}
function createDefaultPageData<T>(sort: string): PageData<T> {
return {
sort,
page: 1,
search: '',
filters: [] as string[],
pageSize: 0,
total: 0,
items: [] as T[],
fetching: false,
fetched: false,
};
}
// Attach to window for inspection
(window as any).appStore = app;
// check login status periodically
app.checkLogin();
window.setInterval(app.checkLogin, 10000);
export type TApp = typeof app;
export default app;