From c9396e23204eb4b3e26b851c3173c6e5ccad2e1d Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 20 Jul 2022 17:52:27 -0300 Subject: [PATCH 01/30] feat(pi): fetch pi summaries on home page --- .../apps/politeia/src/pages/Details/index.js | 2 +- .../apps/politeia/src/pages/Home/index.js | 4 +++ .../apps/politeia/src/pi/summaries/effects.js | 25 ++++++++++++++++++- .../politeia/src/pi/summaries/services.js | 15 +++++++++-- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/plugins-structure/apps/politeia/src/pages/Details/index.js b/plugins-structure/apps/politeia/src/pages/Details/index.js index 560c303bf..2c642b63f 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/index.js +++ b/plugins-structure/apps/politeia/src/pages/Details/index.js @@ -29,7 +29,7 @@ export default App.createRoute({ listenerCreator: fetchDetailsListenerCreator, }, { - id: "pi/summaries", + id: "pi/summaries/single", listenerCreator: fetchDetailsListenerCreator, }, ], diff --git a/plugins-structure/apps/politeia/src/pages/Home/index.js b/plugins-structure/apps/politeia/src/pages/Home/index.js index e8009c4d1..b294f7ad1 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/index.js +++ b/plugins-structure/apps/politeia/src/pages/Home/index.js @@ -23,6 +23,10 @@ export default App.createRoute({ id: "ticketvote/summaries", listenerCreator: fetchNextBatchSummariesListenerCreator, }, + { + id: "pi/summaries", + listenerCreator: fetchNextBatchSummariesListenerCreator, + }, { id: "comments/count", listenerCreator: fetchNextBatchCountListenerCreator, diff --git a/plugins-structure/apps/politeia/src/pi/summaries/effects.js b/plugins-structure/apps/politeia/src/pi/summaries/effects.js index b4349a440..d281b5cab 100644 --- a/plugins-structure/apps/politeia/src/pi/summaries/effects.js +++ b/plugins-structure/apps/politeia/src/pi/summaries/effects.js @@ -1,6 +1,29 @@ import { piSummaries } from "./"; +import { getTokensToFetch } from "@politeiagui/core/records/utils"; +import isEmpty from "lodash/isEmpty"; -export async function fetchRecordPiSummaries(state, dispatch, { token }) { +export async function fetchSingleRecordPiSummaries(state, dispatch, { token }) { const hasPiSummaries = piSummaries.selectByToken(state, token); if (!hasPiSummaries) await dispatch(piSummaries.fetch({ tokens: [token] })); } + +export async function fetchRecordsPiSummaries( + state, + dispatch, + { inventoryList } +) { + const { + piSummaries: { byToken }, + piPolicy: { + policy: { summariespagesize }, + }, + } = state; + const piSummariesToFetch = getTokensToFetch({ + inventoryList, + lookupTable: byToken, + pageSize: summariespagesize, + }); + if (!isEmpty(piSummariesToFetch)) { + await dispatch(piSummaries.fetch({ tokens: piSummariesToFetch })); + } +} diff --git a/plugins-structure/apps/politeia/src/pi/summaries/services.js b/plugins-structure/apps/politeia/src/pi/summaries/services.js index f70f10015..6b502ee1f 100644 --- a/plugins-structure/apps/politeia/src/pi/summaries/services.js +++ b/plugins-structure/apps/politeia/src/pi/summaries/services.js @@ -1,15 +1,26 @@ import { store } from "@politeiagui/core"; import { fetchPolicyIfIdle } from "../utils"; -import { fetchRecordPiSummaries } from "./effects"; +import { + fetchRecordsPiSummaries, + fetchSingleRecordPiSummaries, +} from "./effects"; import { validatePiSummariesPageSize } from "../lib/validation"; export const services = [ + { + id: "pi/summaries/single", + action: async () => { + await fetchPolicyIfIdle(); + validatePiSummariesPageSize(store.getState()); + }, + effect: fetchSingleRecordPiSummaries, + }, { id: "pi/summaries", action: async () => { await fetchPolicyIfIdle(); validatePiSummariesPageSize(store.getState()); }, - effect: fetchRecordPiSummaries, + effect: fetchRecordsPiSummaries, }, ]; From 79876ef39475f25dfcc462ad0c101f6a09a62081 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 20 Jul 2022 18:01:15 -0300 Subject: [PATCH 02/30] feat(app/pi): status tag from proposals summaries --- .../politeia/src/components/Proposal/ProposalCard.js | 11 ++++++++--- .../apps/politeia/src/pages/Home/common/StatusList.js | 6 ++++-- .../apps/politeia/src/pages/Home/useStatusList.js | 7 +++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js index 0022c0c37..5d43d87cc 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js @@ -3,12 +3,17 @@ import { Button, StatusTag } from "pi-ui"; import { RecordCard } from "@politeiagui/common-ui"; import { CommentsCount } from "@politeiagui/comments/ui"; import { getShortToken } from "@politeiagui/core/records/utils"; -import { decodeProposalRecord, getLegacyProposalStatusTagProps } from "./utils"; +import { decodeProposalRecord, getProposalStatusTagProps } from "./utils"; import { ProposalStatusBar, ProposalSubtitle } from "./common"; -const ProposalCard = ({ record, voteSummary, commentsCount }) => { +const ProposalCard = ({ + record, + voteSummary, + commentsCount, + proposalSummary, +}) => { const proposal = decodeProposalRecord(record); - const statusTagProps = getLegacyProposalStatusTagProps(record, voteSummary); + const statusTagProps = getProposalStatusTagProps(proposalSummary); const proposalLink = `/record/${getShortToken(proposal.token)}`; return (
diff --git a/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js b/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js index 6ff8cf32b..83aaefe24 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js +++ b/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js @@ -33,7 +33,8 @@ function StatusList({ hasMoreInventory, homeStatus, countComments, - summaries, + voteSummaries, + proposalSummaries, fetchNextBatch, recordsInOrder, areAllInventoryEntriesFetched, @@ -82,7 +83,8 @@ function StatusList({ key={token} record={record} commentsCount={countComments?.[token]} - voteSummary={summaries?.[token]} + voteSummary={voteSummaries?.[token]} + proposalSummary={proposalSummaries?.[token]} /> ); })} diff --git a/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js b/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js index ab3516086..9b281ccb4 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js +++ b/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js @@ -5,6 +5,7 @@ import { records } from "@politeiagui/core/records"; import { recordsPolicy } from "@politeiagui/core/records/policy"; import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; import { commentsCount } from "@politeiagui/comments/count"; +import { piSummaries } from "../../pi/summaries"; function areAllEntriesFetched(inventoryList, records) { if (!inventoryList) return false; @@ -17,7 +18,8 @@ function areAllEntriesFetched(inventoryList, records) { function useStatusList({ inventory, inventoryStatus }) { const homeStatus = useSelector(selectHomeStatus); const countComments = useSelector(commentsCount.selectAll); - const summaries = useSelector(ticketvoteSummaries.selectAll); + const voteSummaries = useSelector(ticketvoteSummaries.selectAll); + const proposalSummaries = useSelector(piSummaries.selectAll); const recordsPageSize = useSelector((state) => recordsPolicy.selectRule(state, "recordspagesize") ); @@ -36,7 +38,8 @@ function useStatusList({ inventory, inventoryStatus }) { hasMoreRecords, homeStatus, countComments, - summaries, + voteSummaries, + proposalSummaries, fetchNextBatch, recordsInOrder, recordsPageSize, From 4fab4538f2f92334b3e1e0cb1f349d624d31d7bd Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 20 Jul 2022 18:04:12 -0300 Subject: [PATCH 03/30] style: improve readability --- .../politeia/src/components/Proposal/utils.js | 119 +++++++++--------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/utils.js b/plugins-structure/apps/politeia/src/components/Proposal/utils.js index 581bb9d32..9c1528ff3 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/utils.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/utils.js @@ -367,67 +367,66 @@ export function getLegacyProposalStatusTagProps(record, voteSummary) { return { type: "grayNegative", text: "missing" }; } -export const getProposalStatusTagProps = (proposalSummary, isDarkTheme) => { - if (proposalSummary) { - switch (proposalSummary.status) { - case PROPOSAL_SUMMARY_STATUS_UNVETTED: - return { - type: "yellowTime", - text: "Unvetted", - }; - case PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED: - case PROPOSAL_SUMMARY_STATUS_ABANDONED: - return { - type: isDarkTheme ? "blueNegative" : "grayNegative", - text: "Abandoned", - }; - - case PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED: - case PROPOSAL_SUMMARY_STATUS_CENSORED: - return { - type: "orangeNegativeCircled", - text: "Censored", - }; - - case PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW: - return { - type: isDarkTheme ? "blueTime" : "blackTime", - text: "Waiting for author to authorize voting", - }; - - case PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED: - return { - type: "yellowTime", - text: "Waiting for admin to start voting", - }; - - case PROPOSAL_SUMMARY_STATUS_VOTE_STARTED: - return { type: "bluePending", text: "Voting" }; - - case PROPOSAL_SUMMARY_STATUS_REJECTED: - return { - type: "orangeNegativeCircled", - text: "Rejected", - }; - - case PROPOSAL_SUMMARY_STATUS_ACTIVE: - return { type: "bluePending", text: "Active" }; - - case PROPOSAL_SUMMARY_STATUS_CLOSED: - return { type: "grayNegative", text: "Closed" }; - - case PROPOSAL_SUMMARY_STATUS_COMPLETED: - return { type: "greenCheck", text: "Completed" }; - - case PROPOSAL_SUMMARY_STATUS_APPROVED: - return { type: "greenCheck", text: "Approved" }; - - default: - break; - } +export const getProposalStatusTagProps = (proposalSummary) => { + if (!proposalSummary?.status) + return { type: "grayNegative", text: "missing" }; + + switch (proposalSummary.status) { + case PROPOSAL_SUMMARY_STATUS_UNVETTED: + return { + type: "yellowTime", + text: "Unvetted", + }; + case PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED: + case PROPOSAL_SUMMARY_STATUS_ABANDONED: + return { + type: "grayNegative", + text: "Abandoned", + }; + + case PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED: + case PROPOSAL_SUMMARY_STATUS_CENSORED: + return { + type: "orangeNegativeCircled", + text: "Censored", + }; + + case PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW: + return { + type: "blackTime", + text: "Waiting for author to authorize voting", + }; + + case PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED: + return { + type: "yellowTime", + text: "Waiting for admin to start voting", + }; + + case PROPOSAL_SUMMARY_STATUS_VOTE_STARTED: + return { type: "bluePending", text: "Voting" }; + + case PROPOSAL_SUMMARY_STATUS_REJECTED: + return { + type: "orangeNegativeCircled", + text: "Rejected", + }; + + case PROPOSAL_SUMMARY_STATUS_ACTIVE: + return { type: "bluePending", text: "Active" }; + + case PROPOSAL_SUMMARY_STATUS_CLOSED: + return { type: "grayNegative", text: "Closed" }; + + case PROPOSAL_SUMMARY_STATUS_COMPLETED: + return { type: "greenCheck", text: "Completed" }; + + case PROPOSAL_SUMMARY_STATUS_APPROVED: + return { type: "greenCheck", text: "Approved" }; + + default: + break; } - - return { type: "grayNegative", text: "missing" }; }; /** From 19f664dbd3e5c9c81f48ec3578d5391fa0e9144a Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 20 Jul 2022 18:06:36 -0300 Subject: [PATCH 04/30] style: improve naming --- .../apps/politeia/src/components/Proposal/utils.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/utils.js b/plugins-structure/apps/politeia/src/components/Proposal/utils.js index 9c1528ff3..a21762737 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/utils.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/utils.js @@ -309,13 +309,14 @@ function getProposalTimestamps(record) { } /** - * getProposalStatusTagProps returns the formatted `{ type, text }` - * props for StatusTag component for given record and ticketvote summary. + * getProposalStatusTagPropsFromVoteSummary returns the formatted + * `{ type, text }` props for StatusTag component for given record and + * ticketvote summary. * @param {Record} record record object * @param {VoteSummary} voteSummary ticketvote summary object * @returns {Object} `{ type, text }` StatusTag props */ -export function getLegacyProposalStatusTagProps(record, voteSummary) { +export function getProposalStatusTagPropsFromVoteSummary(record, voteSummary) { const voteMetadata = decodeVoteMetadataFile(record.files); const isRfpSubmission = voteMetadata?.linkto; if (record.status === RECORD_STATUS_PUBLIC && !!voteSummary) { From 0bb3770eb64037826365792ee269ff59250372d9 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 20 Jul 2022 18:26:17 -0300 Subject: [PATCH 05/30] wip(pi): add proposal billing status changes --- .../politeia/src/pi/billing/billingSlice.js | 45 +++++++++++++++++++ .../apps/politeia/src/pi/lib/api.js | 10 +++++ .../apps/politeia/src/pi/lib/constants.js | 1 + 3 files changed, 56 insertions(+) create mode 100644 plugins-structure/apps/politeia/src/pi/billing/billingSlice.js diff --git a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js new file mode 100644 index 000000000..9554f5d70 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js @@ -0,0 +1,45 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import * as api from "../lib/api"; +// TODO: validate billing status changes page size + +export const initialState = { + byToken: {}, + status: "idle", + error: null, +}; + +export const fetchBillingStatusChanges = createAsyncThunk( + "piBilling/fetchStatusChanges", + async ({ tokens }, { getState, rejectWithValue }) => { + try { + return await api.fetchBillingStatusChanges(getState(), { tokens }); + } catch (error) { + // TODO: user friendly error message + return rejectWithValue(error.message); + } + } +); + +const piBillingSlice = createSlice({ + name: "piBilling", + initialState, + extraReducers(builder) { + builder + .addCase(fetchBillingStatusChanges.pending, (state) => { + state.status = "loading"; + }) + .addCase(fetchBillingStatusChanges.fulfilled, (state, action) => { + state.status = "succeeded"; + state.byToken = { + ...state.byToken, + ...action.payload.billingstatuschanges, + }; + }) + .addCase(fetchBillingStatusChanges.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload; + }); + }, +}); + +export default piBillingSlice.reducer; diff --git a/plugins-structure/apps/politeia/src/pi/lib/api.js b/plugins-structure/apps/politeia/src/pi/lib/api.js index 07509013e..3e7df5501 100644 --- a/plugins-structure/apps/politeia/src/pi/lib/api.js +++ b/plugins-structure/apps/politeia/src/pi/lib/api.js @@ -1,6 +1,7 @@ import { fetchOptions, getCsrf, parseResponse } from "@politeiagui/core/client"; import { PI_API_ROUTE, + ROUTE_BILLING_STATUS_CHANGES, ROUTE_POLICY, ROUTE_SUMMARIES, VERSION, @@ -23,3 +24,12 @@ export async function fetchPolicy(state) { ); return await parseResponse(response); } + +export async function fetchBillingStatusChanges(state, { tokens }) { + const csrf = await getCsrf(state); + const response = await fetch( + `${PI_API_ROUTE}${VERSION}${ROUTE_BILLING_STATUS_CHANGES}`, + fetchOptions(csrf, { tokens }, "POST") + ); + return await parseResponse(response); +} diff --git a/plugins-structure/apps/politeia/src/pi/lib/constants.js b/plugins-structure/apps/politeia/src/pi/lib/constants.js index cb8561859..e159c0b18 100644 --- a/plugins-structure/apps/politeia/src/pi/lib/constants.js +++ b/plugins-structure/apps/politeia/src/pi/lib/constants.js @@ -1,6 +1,7 @@ export const PI_API_ROUTE = "/api/pi/"; export const VERSION = "v1"; +export const ROUTE_BILLING_STATUS_CHANGES = "/billingstatuschanges"; export const ROUTE_SUMMARIES = "/summaries"; export const ROUTE_POLICY = "/policy"; From bcdfbb08eeaf3c24bea94b89935b20af381ea51e Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 11:45:13 -0300 Subject: [PATCH 06/30] feat(pi): add billing slice --- .../apps/politeia/src/pi/billing/billing.test.js | 1 + .../apps/politeia/src/pi/billing/billingSlice.js | 5 +++++ .../apps/politeia/src/pi/billing/index.js | 13 +++++++++++++ plugins-structure/apps/politeia/src/pi/plugin.js | 5 +++++ 4 files changed, 24 insertions(+) create mode 100644 plugins-structure/apps/politeia/src/pi/billing/billing.test.js create mode 100644 plugins-structure/apps/politeia/src/pi/billing/index.js diff --git a/plugins-structure/apps/politeia/src/pi/billing/billing.test.js b/plugins-structure/apps/politeia/src/pi/billing/billing.test.js new file mode 100644 index 000000000..6559edcf2 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/billing/billing.test.js @@ -0,0 +1 @@ +// TODO: Write tests diff --git a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js index 9554f5d70..a7de9563a 100644 --- a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js +++ b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js @@ -42,4 +42,9 @@ const piBillingSlice = createSlice({ }, }); +export const selectPiBillingStatusChangesByToken = (state, token) => + state.piBilling?.byToken[token]; +export const selectPiBillingStatusChanges = (state) => state.piBilling?.byToken; +export const selectPiBillingStatus = (state) => state.piBilling?.status; + export default piBillingSlice.reducer; diff --git a/plugins-structure/apps/politeia/src/pi/billing/index.js b/plugins-structure/apps/politeia/src/pi/billing/index.js new file mode 100644 index 000000000..03d4c37f9 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/billing/index.js @@ -0,0 +1,13 @@ +import { + fetchBillingStatusChanges, + selectPiBillingStatus, + selectPiBillingStatusChanges, + selectPiBillingStatusChangesByToken, +} from "./billingSlice"; + +export const piBilling = { + fetch: fetchBillingStatusChanges, + selectStatus: selectPiBillingStatus, + selectAll: selectPiBillingStatusChanges, + selectByToken: selectPiBillingStatusChangesByToken, +}; diff --git a/plugins-structure/apps/politeia/src/pi/plugin.js b/plugins-structure/apps/politeia/src/pi/plugin.js index d7e809fdb..a9d3986ce 100644 --- a/plugins-structure/apps/politeia/src/pi/plugin.js +++ b/plugins-structure/apps/politeia/src/pi/plugin.js @@ -2,11 +2,16 @@ import { pluginSetup } from "@politeiagui/core"; import { services } from "./services"; import policyReducer from "./policy/policySlice"; import summariesReducer from "./summaries/summariesSlice"; +import billingReducer from "./billing/billingSlice"; // Declare pi plugin interface const PiPlugin = pluginSetup({ services, reducers: [ + { + key: "piBilling", + reducer: billingReducer, + }, { key: "piPolicy", reducer: policyReducer, From a37574561cd4e7bcebb0b6d4788a213062ca815a Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 12:13:10 -0300 Subject: [PATCH 07/30] feat(app/pi): billing status on approved tab --- .../apps/politeia/src/pages/Home/Approved/Approved.js | 2 +- .../apps/politeia/src/pages/Home/common/RecordsStatusList.js | 3 ++- .../apps/politeia/src/pages/Home/useStatusList.js | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins-structure/apps/politeia/src/pages/Home/Approved/Approved.js b/plugins-structure/apps/politeia/src/pages/Home/Approved/Approved.js index 780469c87..d33d6728a 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/Approved/Approved.js +++ b/plugins-structure/apps/politeia/src/pages/Home/Approved/Approved.js @@ -11,7 +11,7 @@ function Approved() { return isListEmpty ? ( ) : ( - + ); } diff --git a/plugins-structure/apps/politeia/src/pages/Home/common/RecordsStatusList.js b/plugins-structure/apps/politeia/src/pages/Home/common/RecordsStatusList.js index 7ebaee81e..f498b9356 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/common/RecordsStatusList.js +++ b/plugins-structure/apps/politeia/src/pages/Home/common/RecordsStatusList.js @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { ticketvoteInventory } from "@politeiagui/ticketvote/inventory"; import StatusList from "./StatusList"; -function RecordsStatusList({ status, onRenderNextStatus }) { +function RecordsStatusList({ status, onRenderNextStatus, hasBillingStatus }) { const [page, setPage] = useState(1); function handleFetchNextInventoryPage() { setPage(page + 1); @@ -20,6 +20,7 @@ function RecordsStatusList({ status, onRenderNextStatus }) { inventoryStatus={inventoryStatus} inventory={inventory} onRenderNextStatus={onRenderNextStatus} + hasBillingStatus={hasBillingStatus} /> ); } diff --git a/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js b/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js index 9b281ccb4..c198df6c3 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js +++ b/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js @@ -6,6 +6,7 @@ import { recordsPolicy } from "@politeiagui/core/records/policy"; import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; import { commentsCount } from "@politeiagui/comments/count"; import { piSummaries } from "../../pi/summaries"; +import { piBilling } from "../../pi/billing"; function areAllEntriesFetched(inventoryList, records) { if (!inventoryList) return false; @@ -27,6 +28,7 @@ function useStatusList({ inventory, inventoryStatus }) { records.selectByTokensBatch(state, inventory) ); const allRecords = useSelector(records.selectAll); + const billingStatusChanges = useSelector(piBilling.selectAll); const hasMoreRecords = recordsInOrder.length !== 0 && recordsInOrder.length < inventory.length; @@ -44,6 +46,7 @@ function useStatusList({ inventory, inventoryStatus }) { recordsInOrder, recordsPageSize, areAllInventoryEntriesFetched: areAllEntriesFetched(inventory, allRecords), + billingStatusChanges, }; } From b5a6c5a3fc110e2f3f1b2d3fb72455a98b62ce6e Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 16:04:00 -0300 Subject: [PATCH 08/30] feat(pi): add billing status changes service --- .../politeia/src/pi/billing/billingSlice.js | 8 ++++- .../apps/politeia/src/pi/billing/effects.js | 36 +++++++++++++++++++ .../apps/politeia/src/pi/billing/services.js | 22 ++++++++++++ .../apps/politeia/src/pi/lib/validation.js | 17 +++++++++ .../apps/politeia/src/pi/services.js | 2 ++ .../src/pi/summaries/summariesSlice.js | 1 + 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 plugins-structure/apps/politeia/src/pi/billing/effects.js create mode 100644 plugins-structure/apps/politeia/src/pi/billing/services.js diff --git a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js index a7de9563a..645f96062 100644 --- a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js +++ b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js @@ -1,6 +1,6 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import * as api from "../lib/api"; -// TODO: validate billing status changes page size +import { validatePiBillingStatusChangesPageSize } from "../lib/validation"; export const initialState = { byToken: {}, @@ -17,6 +17,12 @@ export const fetchBillingStatusChanges = createAsyncThunk( // TODO: user friendly error message return rejectWithValue(error.message); } + }, + { + condition: (body, { getState }) => + body && + !!body.tokens && + validatePiBillingStatusChangesPageSize(getState()), } ); diff --git a/plugins-structure/apps/politeia/src/pi/billing/effects.js b/plugins-structure/apps/politeia/src/pi/billing/effects.js new file mode 100644 index 000000000..9cbaca456 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/billing/effects.js @@ -0,0 +1,36 @@ +import { piBilling } from "./"; +import { getTokensToFetch } from "@politeiagui/core/records/utils"; +import isEmpty from "lodash/isEmpty"; + +export async function fetchSingleRecordBillingStatusChanges( + state, + dispatch, + { token } +) { + const hasBillingStatusChanges = piBilling.selectByToken(state, token); + if (!hasBillingStatusChanges) + await dispatch(piBilling.fetch({ tokens: [token] })); +} + +export async function fetchRecordsBillingStatusChanges( + state, + dispatch, + { inventoryList } +) { + const { + piBilling: { byToken }, + piPolicy: { + policy: { billingstatuschangespagesize }, + }, + } = state; + + const billingStatusesToFetch = getTokensToFetch({ + inventoryList, + lookupTable: byToken, + pageSize: billingstatuschangespagesize, + }); + + if (!isEmpty(billingStatusesToFetch)) { + await dispatch(piBilling.fetch({ tokens: billingStatusesToFetch })); + } +} diff --git a/plugins-structure/apps/politeia/src/pi/billing/services.js b/plugins-structure/apps/politeia/src/pi/billing/services.js new file mode 100644 index 000000000..40944ee36 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/billing/services.js @@ -0,0 +1,22 @@ +import { fetchPolicyIfIdle } from "../utils"; +import { + fetchRecordsBillingStatusChanges, + fetchSingleRecordBillingStatusChanges, +} from "./effects"; + +export const services = [ + { + id: "pi/billingStatusChanges/single", + action: async () => { + await fetchPolicyIfIdle(); + }, + effect: fetchSingleRecordBillingStatusChanges, + }, + { + id: "pi/billingStatusChanges", + action: async () => { + await fetchPolicyIfIdle(); + }, + effect: fetchRecordsBillingStatusChanges, + }, +]; diff --git a/plugins-structure/apps/politeia/src/pi/lib/validation.js b/plugins-structure/apps/politeia/src/pi/lib/validation.js index d7d13b1ff..c12d1bafa 100644 --- a/plugins-structure/apps/politeia/src/pi/lib/validation.js +++ b/plugins-structure/apps/politeia/src/pi/lib/validation.js @@ -19,3 +19,20 @@ export function validatePiSummariesPageSize(state) { } return true; } + +export function validatePiBillingStatusChangesPageSize(state) { + const pageSize = + state && + state.piPolicy && + state.piPolicy.policy && + state.piPolicy.policy.billingstatuschangespagesize; + + if (!pageSize) { + const error = Error( + "Pi policy should be loaded before fetching billing status changes" + ); + console.error(error); + throw error; + } + return true; +} diff --git a/plugins-structure/apps/politeia/src/pi/services.js b/plugins-structure/apps/politeia/src/pi/services.js index 45cc115da..a988fe366 100644 --- a/plugins-structure/apps/politeia/src/pi/services.js +++ b/plugins-structure/apps/politeia/src/pi/services.js @@ -1,7 +1,9 @@ import { fetchPolicyIfIdle } from "./utils"; import { services as summariesServices } from "./summaries/services"; +import { services as billingServices } from "./billing/services"; export const services = [ + ...billingServices, ...summariesServices, { id: "pi/new", diff --git a/plugins-structure/apps/politeia/src/pi/summaries/summariesSlice.js b/plugins-structure/apps/politeia/src/pi/summaries/summariesSlice.js index be68c16ed..430353939 100644 --- a/plugins-structure/apps/politeia/src/pi/summaries/summariesSlice.js +++ b/plugins-structure/apps/politeia/src/pi/summaries/summariesSlice.js @@ -14,6 +14,7 @@ export const fetchPiSummaries = createAsyncThunk( try { return await api.fetchSummaries(getState(), { tokens }); } catch (error) { + // TODO: user friendly error message return rejectWithValue(error.message); } }, From b50a3e60659806f452028ccb5736f223deecbf9d Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 16:04:41 -0300 Subject: [PATCH 09/30] feat(app/pi): load billing changes on approved tab --- .../apps/politeia/src/pages/Home/actions.js | 13 +++++++++---- .../apps/politeia/src/pages/Home/index.js | 5 +++++ .../apps/politeia/src/pages/Home/listeners.js | 11 +++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/plugins-structure/apps/politeia/src/pages/Home/actions.js b/plugins-structure/apps/politeia/src/pages/Home/actions.js index 0880dfe09..15ae63ff6 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/actions.js +++ b/plugins-structure/apps/politeia/src/pages/Home/actions.js @@ -5,11 +5,16 @@ export const fetchNextBatchSummaries = createAction( "home/fetchNextBatchSummaries" ); export const fetchNextBatchRecords = createAction("home/fetchNextBatchRecords"); +export const fetchNextBatchBillingStatuses = createAction( + "home/fetchNextBatchBillingStatuses" +); -export function fetchNextBatch(payload) { +export function fetchNextBatch(status) { + const hasBillingStatus = status === "approved"; return (dispatch) => { - dispatch(fetchNextBatchCount(payload)); - dispatch(fetchNextBatchSummaries(payload)); - dispatch(fetchNextBatchRecords(payload)); + dispatch(fetchNextBatchCount(status)); + dispatch(fetchNextBatchSummaries(status)); + dispatch(fetchNextBatchRecords(status)); + hasBillingStatus && dispatch(fetchNextBatchBillingStatuses(status)); }; } diff --git a/plugins-structure/apps/politeia/src/pages/Home/index.js b/plugins-structure/apps/politeia/src/pages/Home/index.js index b294f7ad1..dbeaa3389 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/index.js +++ b/plugins-structure/apps/politeia/src/pages/Home/index.js @@ -3,6 +3,7 @@ import { routeCleanup } from "../../utils/routeCleanup"; import { createRouteView } from "../../utils/createRouteView"; import Home from "./Home"; import { + fetchNextBatchBillingStatusesListenerCreator, fetchNextBatchCountListenerCreator, fetchNextBatchRecordsListenerCreator, fetchNextBatchSummariesListenerCreator, @@ -27,6 +28,10 @@ export default App.createRoute({ id: "pi/summaries", listenerCreator: fetchNextBatchSummariesListenerCreator, }, + { + id: "pi/billingStatusChanges", + listenerCreator: fetchNextBatchBillingStatusesListenerCreator, + }, { id: "comments/count", listenerCreator: fetchNextBatchCountListenerCreator, diff --git a/plugins-structure/apps/politeia/src/pages/Home/listeners.js b/plugins-structure/apps/politeia/src/pages/Home/listeners.js index 159d5ad8a..628c63b26 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/listeners.js +++ b/plugins-structure/apps/politeia/src/pages/Home/listeners.js @@ -1,5 +1,6 @@ import { fetchNextBatch, + fetchNextBatchBillingStatuses, fetchNextBatchCount, fetchNextBatchRecords, fetchNextBatchSummaries, @@ -44,12 +45,18 @@ export const fetchNextBatchRecordsListenerCreator = { injectEffect: injectRecordsBatchEffect, }; +export const fetchNextBatchBillingStatusesListenerCreator = { + actionCreator: fetchNextBatchBillingStatuses, + injectEffect, +}; + export const listeners = [ { type: "ticketvoteInventory/fetch/fulfilled", effect: ({ meta, payload }, listenerApi) => { - if (payload.inventory[meta.arg.status].length > 0) { - listenerApi.dispatch(fetchNextBatch(meta.arg.status)); + const { status } = meta.arg; + if (payload.inventory[status].length > 0) { + listenerApi.dispatch(fetchNextBatch(status)); } }, }, From b5c6e41c71591d8c82f5b4a56cb637bf6cf1f19a Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 16:22:38 -0300 Subject: [PATCH 10/30] test(pi): billing slice unit tests --- .../politeia/src/pi/billing/billing.test.js | 111 +++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/plugins-structure/apps/politeia/src/pi/billing/billing.test.js b/plugins-structure/apps/politeia/src/pi/billing/billing.test.js index 6559edcf2..5eb332eef 100644 --- a/plugins-structure/apps/politeia/src/pi/billing/billing.test.js +++ b/plugins-structure/apps/politeia/src/pi/billing/billing.test.js @@ -1 +1,110 @@ -// TODO: Write tests +import { configureStore } from "@reduxjs/toolkit"; +import * as api from "../lib/api"; +import reducer, { + fetchBillingStatusChanges, + initialState, +} from "./billingSlice"; +import policyReducer from "../policy/policySlice"; + +const mockBillingResponse = [ + { + token: "0b40715ced95bb13", + status: 3, + publickey: + "51a5877c828cbc61fa609e12167ec78ec506998928d787235961ab11f185a618", + signature: + "ac14aa4c1ceba895a320b964dde059869e17bd0af0f6f2ec50f5e325cc61e12085f10d72a16d45c0cdd9feae4d12fed3862e3fa214f022b09914e255b8e3ab08", + receipt: + "ceb536ed55db7c0ddbaa8b137f008be4220b15d6efe143cdcdaa60ce5a34bb8d29f2258d70a62d4eafef3db91bfe4cb101287c1c44583880a8059512f59b7a09", + timestamp: 1658415512, + }, +]; + +// Pi Billing +describe("Given fetchBillingStatusChanges from billing service", () => { + let store; + let fetchBillingStatusChangesSpy; + const params = { + tokens: ["fakeToken", "fakeToken2"], + }; + beforeEach(() => { + store = configureStore({ + reducer: { + piBilling: reducer, + piPolicy: policyReducer, + }, + preloadedState: { + piPolicy: { + policy: { + billingstatuschangespagesize: 5, + }, + }, + }, + }); + fetchBillingStatusChangesSpy = jest.spyOn(api, "fetchBillingStatusChanges"); + }); + afterEach(() => { + fetchBillingStatusChangesSpy.mockRestore(); + }); + describe("when given parameters are empty", () => { + it("should return the initial state", () => { + expect(reducer(undefined, {})).toEqual(initialState); + }); + }); + describe("when policy isn't loaded before dispatching fetchBillingStatusChanges", () => { + it("should update the status to failed and log error", async () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + + store = configureStore({ reducer: { piBilling: reducer } }); + await store.dispatch(fetchBillingStatusChanges(params)); + expect(fetchBillingStatusChangesSpy).not.toBeCalled(); + expect(consoleErrorSpy).toBeCalled(); + const state = store.getState().piBilling; + expect(state.byToken).toEqual({}); + expect(state.status).toEqual("failed"); + + consoleErrorSpy.mockRestore(); + }); + }); + describe("when fetchBillingStatusChanges dispatches with valid params", () => { + it("should update the status to loading", () => { + store.dispatch(fetchBillingStatusChanges(params)); + + expect(fetchBillingStatusChangesSpy).toBeCalled(); + const state = store.getState().piBilling; + expect(state.byToken).toEqual({}); + expect(state.status).toEqual("loading"); + }); + }); + describe("when fetchBillingStatusChanges succeeds", () => { + it("should update byToken and status", async () => { + const resValue = { + billingstatuschanges: { + fakeToken: mockBillingResponse, + fakeToken2: mockBillingResponse, + }, + }; + fetchBillingStatusChangesSpy.mockResolvedValueOnce(resValue); + + await store.dispatch(fetchBillingStatusChanges(params)); + + expect(fetchBillingStatusChangesSpy).toBeCalled(); + const state = store.getState().piBilling; + expect(state.byToken).toEqual(resValue.billingstatuschanges); + expect(state.status).toEqual("succeeded"); + }); + }); + describe("when fetchBillingStatusChanges fails", () => { + it("should dispatch failure and update the error", async () => { + const error = new Error("ERROR"); + fetchBillingStatusChangesSpy.mockRejectedValueOnce(error); + + await store.dispatch(fetchBillingStatusChanges(params)); + + expect(fetchBillingStatusChangesSpy).toBeCalled(); + const state = store.getState().piBilling; + expect(state.status).toEqual("failed"); + expect(state.error).toEqual("ERROR"); + }); + }); +}); From 2b0f3ceae55f3e9ad9cef603a2db8ff4c5301580 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 16:55:21 -0300 Subject: [PATCH 11/30] feat(app/pi): load billing changes on Details page --- .../apps/politeia/src/pages/Details/index.js | 5 ++++ .../politeia/src/pages/Details/listeners.js | 23 +++++++++++++++++++ .../apps/politeia/src/pi/utils.js | 11 +++++++++ 3 files changed, 39 insertions(+) diff --git a/plugins-structure/apps/politeia/src/pages/Details/index.js b/plugins-structure/apps/politeia/src/pages/Details/index.js index 2c642b63f..423f97572 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/index.js +++ b/plugins-structure/apps/politeia/src/pages/Details/index.js @@ -3,6 +3,7 @@ import { routeCleanup } from "../../utils/routeCleanup"; import { createRouteView } from "../../utils/createRouteView"; import { fetchDetailsListenerCreator, + fetchProposalSummaryListenerCreator, recordFetchDetailsListenerCreator, } from "./listeners"; import Details from "./Details"; @@ -32,6 +33,10 @@ export default App.createRoute({ id: "pi/summaries/single", listenerCreator: fetchDetailsListenerCreator, }, + { + id: "pi/billingStatusChanges/single", + listenerCreator: fetchProposalSummaryListenerCreator, + }, ], cleanup: routeCleanup, view: createRouteView(Details), diff --git a/plugins-structure/apps/politeia/src/pages/Details/listeners.js b/plugins-structure/apps/politeia/src/pages/Details/listeners.js index 6756f06d2..bf7743e16 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/listeners.js +++ b/plugins-structure/apps/politeia/src/pages/Details/listeners.js @@ -1,4 +1,5 @@ import { fetchProposalDetails } from "./actions"; +import { isProposalCompleteOrClosed } from "../../pi/utils"; function injectEffect(effect) { return async ({ payload }, { getState, dispatch }) => { @@ -21,11 +22,33 @@ function injectRecordDetailsEffect(effect) { }; } +function injectCompletedOrClosedProposalEffect(effect) { + return async ( + { payload, meta }, + { getState, dispatch, unsubscribe, subscribe } + ) => { + unsubscribe(); + const state = getState(); + const [token] = meta.arg.tokens; + const { status } = Object.values(payload.summaries)[0]; + + if (isProposalCompleteOrClosed(status)) { + await effect(state, dispatch, { token }); + } + subscribe(); + }; +} + export const fetchDetailsListenerCreator = { type: "records/fetchDetails/fulfilled", injectEffect, }; +export const fetchProposalSummaryListenerCreator = { + type: "piSummaries/fetch/fulfilled", + injectEffect: injectCompletedOrClosedProposalEffect, +}; + export const recordFetchDetailsListenerCreator = { actionCreator: fetchProposalDetails, injectEffect: injectRecordDetailsEffect, diff --git a/plugins-structure/apps/politeia/src/pi/utils.js b/plugins-structure/apps/politeia/src/pi/utils.js index ae2f54013..f667e3a0d 100644 --- a/plugins-structure/apps/politeia/src/pi/utils.js +++ b/plugins-structure/apps/politeia/src/pi/utils.js @@ -1,8 +1,19 @@ import { store } from "@politeiagui/core"; import { piPolicy } from "./policy"; +import { + PROPOSAL_SUMMARY_STATUS_CLOSED, + PROPOSAL_SUMMARY_STATUS_COMPLETED, +} from "./lib/constants"; export function fetchPolicyIfIdle() { if (piPolicy.selectStatus(store.getState()) === "idle") { return store.dispatch(piPolicy.fetch()); } } + +export function isProposalCompleteOrClosed(status) { + return [ + PROPOSAL_SUMMARY_STATUS_COMPLETED, + PROPOSAL_SUMMARY_STATUS_CLOSED, + ].includes(status); +} From 4dd44a83810b4eb57d280e10aadb9a21a06eee15 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 21 Jul 2022 17:29:00 -0300 Subject: [PATCH 12/30] feat(app/pi): billing change reason on Details pg --- .../src/components/Proposal/ProposalDetails.js | 10 ++++++++++ .../apps/politeia/src/pages/Details/Details.js | 2 ++ .../politeia/src/pages/Details/useProposalDetails.js | 6 +++++- plugins-structure/apps/politeia/src/pi/index.js | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js index 2cfbf52a6..f642305db 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js @@ -20,12 +20,14 @@ import { Button, ButtonIcon, Message } from "pi-ui"; import { getShortToken } from "@politeiagui/core/records/utils"; import styles from "./styles.module.css"; import ModalProposalDiff from "./ModalProposalDiff"; +import last from "lodash/last"; const ProposalDetails = ({ record, voteSummary, piSummary, onFetchRecordTimestamps, + billingStatusChanges, }) => { const [open] = useModal(); @@ -60,6 +62,8 @@ const ProposalDetails = ({ } const isAbandoned = proposalDetails.archived || proposalDetails.censored; + const billingSatusChangeReason = + billingStatusChanges && last(billingStatusChanges)?.reason; return (
@@ -68,6 +72,12 @@ const ProposalDetails = ({ Reason: {proposalDetails.abandonmentReason} )} + {billingSatusChangeReason && ( + + {/* TODO: format message and inform status */} + Reason: {billingSatusChangeReason} + + )} {comments && ( diff --git a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js index 870c9181b..6f0178510 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js +++ b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js @@ -6,7 +6,7 @@ import { selectDetailsStatus } from "./selectors"; import { records } from "@politeiagui/core/records"; import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; import { recordComments } from "@politeiagui/comments/comments"; -import { piSummaries } from "../../pi"; +import { piBilling, piSummaries } from "../../pi"; function useProposalDetails({ token }) { const dispatch = useDispatch(); @@ -27,6 +27,9 @@ function useProposalDetails({ token }) { const piSummary = useSelector((state) => piSummaries.selectByToken(state, fullToken) ); + const billingStatusChanges = useSelector((state) => + piBilling.selectByToken(state, fullToken) + ); const recordDetailsError = useSelector(records.selectError); const voteSummaryError = useSelector(ticketvoteSummaries.selectError); const commentsError = useSelector(recordComments.selectError); @@ -53,6 +56,7 @@ function useProposalDetails({ token }) { piSummary, record, voteSummary, + billingStatusChanges, }; } diff --git a/plugins-structure/apps/politeia/src/pi/index.js b/plugins-structure/apps/politeia/src/pi/index.js index cc98dab66..1b2525168 100644 --- a/plugins-structure/apps/politeia/src/pi/index.js +++ b/plugins-structure/apps/politeia/src/pi/index.js @@ -1,4 +1,5 @@ export * from "./lib/constants"; export { default } from "./plugin"; export { piPolicy } from "./policy"; +export { piBilling } from "./billing"; export { piSummaries } from "./summaries"; From 57c69516c1ad57b1d518df6c68a61ab18ad16274 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 22 Jul 2022 16:15:11 -0300 Subject: [PATCH 13/30] feat: show billing status changes reason --- .../src/components/Proposal/ProposalDetails.js | 11 ++++------- .../apps/politeia/src/pages/Details/Details.js | 4 ++-- .../politeia/src/pages/Details/useProposalDetails.js | 6 +++--- .../apps/politeia/src/pi/billing/billingSlice.js | 5 +++++ .../apps/politeia/src/pi/billing/index.js | 2 ++ 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js index f642305db..0f87642ff 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js @@ -20,14 +20,13 @@ import { Button, ButtonIcon, Message } from "pi-ui"; import { getShortToken } from "@politeiagui/core/records/utils"; import styles from "./styles.module.css"; import ModalProposalDiff from "./ModalProposalDiff"; -import last from "lodash/last"; const ProposalDetails = ({ record, voteSummary, piSummary, onFetchRecordTimestamps, - billingStatusChanges, + billingStatusChange, }) => { const [open] = useModal(); @@ -62,8 +61,6 @@ const ProposalDetails = ({ } const isAbandoned = proposalDetails.archived || proposalDetails.censored; - const billingSatusChangeReason = - billingStatusChanges && last(billingStatusChanges)?.reason; return (
@@ -72,10 +69,10 @@ const ProposalDetails = ({ Reason: {proposalDetails.abandonmentReason} )} - {billingSatusChangeReason && ( + {billingStatusChange?.reason && ( - {/* TODO: format message and inform status */} - Reason: {billingSatusChangeReason} +
Proposal is {piSummary.status}.
+
Reason: {billingStatusChange.reason}.
)} {comments && ( diff --git a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js index 6f0178510..a7c40b90a 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js +++ b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js @@ -27,8 +27,8 @@ function useProposalDetails({ token }) { const piSummary = useSelector((state) => piSummaries.selectByToken(state, fullToken) ); - const billingStatusChanges = useSelector((state) => - piBilling.selectByToken(state, fullToken) + const billingStatusChange = useSelector((state) => + piBilling.selectLastByToken(state, fullToken) ); const recordDetailsError = useSelector(records.selectError); const voteSummaryError = useSelector(ticketvoteSummaries.selectError); @@ -56,7 +56,7 @@ function useProposalDetails({ token }) { piSummary, record, voteSummary, - billingStatusChanges, + billingStatusChange, }; } diff --git a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js index 645f96062..8a759512a 100644 --- a/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js +++ b/plugins-structure/apps/politeia/src/pi/billing/billingSlice.js @@ -1,6 +1,7 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import * as api from "../lib/api"; import { validatePiBillingStatusChangesPageSize } from "../lib/validation"; +import last from "lodash/last"; export const initialState = { byToken: {}, @@ -50,6 +51,10 @@ const piBillingSlice = createSlice({ export const selectPiBillingStatusChangesByToken = (state, token) => state.piBilling?.byToken[token]; +export const selectPiBillingLastStatusChangeByToken = (state, token) => { + const statusChanges = selectPiBillingStatusChangesByToken(state, token); + return last(statusChanges); +}; export const selectPiBillingStatusChanges = (state) => state.piBilling?.byToken; export const selectPiBillingStatus = (state) => state.piBilling?.status; diff --git a/plugins-structure/apps/politeia/src/pi/billing/index.js b/plugins-structure/apps/politeia/src/pi/billing/index.js index 03d4c37f9..5d229c307 100644 --- a/plugins-structure/apps/politeia/src/pi/billing/index.js +++ b/plugins-structure/apps/politeia/src/pi/billing/index.js @@ -1,5 +1,6 @@ import { fetchBillingStatusChanges, + selectPiBillingLastStatusChangeByToken, selectPiBillingStatus, selectPiBillingStatusChanges, selectPiBillingStatusChangesByToken, @@ -10,4 +11,5 @@ export const piBilling = { selectStatus: selectPiBillingStatus, selectAll: selectPiBillingStatusChanges, selectByToken: selectPiBillingStatusChangesByToken, + selectLastByToken: selectPiBillingLastStatusChangeByToken, }; From 86b13c0f08d2b4e9ed41da7c60c4c2fed02f8efd Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 22 Jul 2022 16:29:10 -0300 Subject: [PATCH 14/30] feat(pi): proposals service initial scope --- .../politeia/src/pi/proposals/selectors.js | 0 .../apps/politeia/src/pi/proposals/utils.js | 484 ++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 plugins-structure/apps/politeia/src/pi/proposals/selectors.js create mode 100644 plugins-structure/apps/politeia/src/pi/proposals/utils.js diff --git a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js new file mode 100644 index 000000000..e69de29bb diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js new file mode 100644 index 000000000..3b154ce56 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -0,0 +1,484 @@ +import { + decodeRecordFile, + decodeRecordMetadata, + getShortToken, +} from "@politeiagui/core/records/utils"; +import { + RECORD_STATUS_ARCHIVED, + RECORD_STATUS_CENSORED, + RECORD_STATUS_PUBLIC, + RECORD_STATUS_UNREVIEWED, +} from "@politeiagui/core/records/constants"; +import { + TICKETVOTE_STATUS_APPROVED, + TICKETVOTE_STATUS_AUTHORIZED, + TICKETVOTE_STATUS_FINISHED, + TICKETVOTE_STATUS_REJECTED, + TICKETVOTE_STATUS_STARTED, + TICKETVOTE_STATUS_UNAUTHORIZED, +} from "@politeiagui/ticketvote/constants"; +import { + PROPOSAL_SUMMARY_STATUS_ABANDONED, + PROPOSAL_SUMMARY_STATUS_ACTIVE, + PROPOSAL_SUMMARY_STATUS_APPROVED, + PROPOSAL_SUMMARY_STATUS_CENSORED, + PROPOSAL_SUMMARY_STATUS_CLOSED, + PROPOSAL_SUMMARY_STATUS_COMPLETED, + PROPOSAL_SUMMARY_STATUS_REJECTED, + PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW, + PROPOSAL_SUMMARY_STATUS_UNVETTED, + PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED, + PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED, + PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED, + PROPOSAL_SUMMARY_STATUS_VOTE_STARTED, +} from "../lib/constants"; +import isArray from "lodash/fp/isArray"; + +const PROPOSAL_METADATA_FILENAME = "proposalmetadata.json"; +const PROPOSAL_INDEX_FILENAME = "index.md"; +const PROPOSAL_VOTE_METADATA_FILENAME = "votemetadata.json"; + +const MONTHS_LABELS = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +/** + * Record object + * @typedef {{ + * censorshiprecord: { token: String, merkle: String, signature: String }, + * files: Array, + * metadata: Array, + * state: Number, + * status: Number, + * timestamp: Number, + * username: String, + * version: Number + * }} Record + */ + +/** + * Ticketvote Vote Summary object + * @typedef {{ + * type: Number, + * status: Number, + * duration: Number, + * startblockheight: Number, + * startblockhash: String, + * endblockheight: Number, + * eligibletickets: Number, + * quorumpercentage: Number, + * passpercentage: Number, + * results: Array, + * bestblock: Number + * }} VoteSummary + */ + +/** + * Proposal object + * @typedef {{ + * name: String, + * token: String, + * recordState: Number, + * recordStatus: Number, + * version: Number, + * timestamps: { + * publishedat: Number, + * editedat: Number, + * censoredat: Number, + * abandonedat: Number, + * }, + * voteMetadata: { + * linkto: Number, + * linkby: Number, + * }, + * author: { + * username: String, + * userid: String, + * }, + * body: String, + * proposalMetadata: Object, + * censored: Bool, + * archived: Bool, + * abandonReason: String, + * attachments: Array + * }} Proposal + */ + +/** + * decodeProposalMetadataFile returns the decoded "proposalmetadata.json" file + * for given record's files array. + * @param {Array} files record's files + * @returns {Object} Proposal metadata object + */ +export function decodeProposalMetadataFile(files) { + const metadata = files.find((f) => f.name === PROPOSAL_METADATA_FILENAME); + return decodeRecordFile(metadata); +} + +/** + * decodeProposalMetadataFile returns the decoded "proposalmetadata.json" file + * for given record's files array. + * @param {Array} files record's files + * @returns {Object} Proposal metadata object + */ +export function decodeProposalBodyFile(files) { + const body = files.find((f) => f.name === PROPOSAL_INDEX_FILENAME); + return body && decodeURIComponent(escape(window.atob(body.payload))); +} + +/** + * decodeVoteMetadataFile accepts a proposal files array parses it's vote + * metadata and returns it as object of the form { linkto, linkby }. + * @param {Array} files Record Files array + * @returns {Object} `{linkto, linkby}` decoded vote metadata + */ +export function decodeVoteMetadataFile(files) { + const metadata = + files && files.find((f) => f.name === PROPOSAL_VOTE_METADATA_FILENAME); + return decodeRecordFile(metadata); +} + +/** + * decodeProposalUserMetadata filters all "usermd" metadata streams and decodes + * their payloads. Returns decoded and filtered metadata streams. + * @param {Array} metadataStreams record's metadata streams + * @returns {Array} Decoded "usermd" metadata streams + */ +export function decodeProposalUserMetadata(metadataStreams) { + const userMd = metadataStreams + ? metadataStreams + .filter((md) => md.pluginid === "usermd") + .map((md) => decodeRecordMetadata(md)) + : []; + return userMd; +} + +export function decodeProposalAttachments(files) { + return files.filter( + (f) => + f.name !== PROPOSAL_INDEX_FILENAME && + f.name !== PROPOSAL_VOTE_METADATA_FILENAME && + f.name !== PROPOSAL_METADATA_FILENAME + ); +} + +/** + * decodeProposalRecord returns a formatted proposal object for given record. + * It decodes all proposal-related data from records and converts it into a + * readable proposal object. + * @param {Record} record record object + * @returns {Proposal} formatted proposal object + */ +export function decodeProposalRecord(record) { + if (!record) return; + const { name, ...proposalMetadata } = decodeProposalMetadataFile( + record.files + ); + const userMetadata = decodeProposalUserMetadata(record.metadata); + const voteMetadata = decodeVoteMetadataFile(record.files); + const body = decodeProposalBodyFile(record.files); + const attachments = decodeProposalAttachments(record.files); + const useridMd = userMetadata.find((md) => md && md.payload.userid); + const { token } = record.censorshiprecord; + return { + name: name ? name : getShortToken(token), + token, + recordState: record.state, + recordStatus: record.status, + version: record.version, + timestamps: getProposalTimestamps(record), + voteMetadata, + author: { + username: record.username, + userid: useridMd?.payload?.userid, + }, + body, + proposalMetadata, + archived: record.status === RECORD_STATUS_ARCHIVED, + censored: record.status === RECORD_STATUS_CENSORED, + // TODO: remove abandonmentReason + abandonmentReason: getAbandonmentReason(userMetadata), + attachments, + userMetadata, + }; +} + +function findStatusMetadataFromPayloads(mdPayloads, status) { + if (!mdPayloads) return {}; + const publicMd = mdPayloads.find((p) => p.status === status); + if (!publicMd) { + // traverse payload arrays + const arrayPayloads = mdPayloads.find((p) => isArray(p)); + return findStatusMetadataFromPayloads(arrayPayloads, status); + } + return publicMd; +} + +function getAbandonmentReason(userMetadata) { + if (!userMetadata) return null; + const payloads = userMetadata.map((md) => md.payload); + for (const status of [RECORD_STATUS_ARCHIVED, RECORD_STATUS_CENSORED]) { + const { reason } = findStatusMetadataFromPayloads(payloads, status); + if (reason) { + return reason; + } + } +} + +/** + * formatDateToInternationalString accepts an object of day, month and year. + * It returns a string of human viewable international date from the result + * of DatePicker or BackEnd and supposes they are correct. + * String format: 08 Sep 2021 + * @param {object} { day, month, year } + * @returns {string} + */ +export function formatDateToInternationalString({ day, month, year }) { + const monthLabel = MONTHS_LABELS[month - 1]; + if (monthLabel === undefined) { + return "Invalid Date"; + } + const dayView = `0${day}`.slice(-2); + return `${dayView} ${MONTHS_LABELS[month - 1]} ${year}`; +} + +/** + * getPublicStatusChangeMetadata returns the metadata stream that describes when + * the record has been made `public`, i.e. status === 2. + * @param {Array} userMetadata record's "usermd" metadata stream + * @returns {Object} Public status change metadata stream + */ +export function getPublicStatusChangeMetadata(userMetadata) { + if (!userMetadata) return {}; + const payloads = userMetadata.map((md) => md.payload); + return findStatusMetadataFromPayloads(payloads, RECORD_STATUS_PUBLIC); +} + +/** + * getProposalTimestamps returns published, censored, edited and abandoned + * timestamps for given record. Default timestamps values are 0. + * @param {Record} record record object + * @returns {Object} `{publishedat: number, editedat: number, censoredat: number, + * abandonedat: number}` - Object with publishedat, censoredat, abandonedat + */ +function getProposalTimestamps(record) { + if (!record) + return { publishedat: 0, editedat: 0, censoredat: 0, abandonedat: 0 }; + + let publishedat = 0, + censoredat = 0, + abandonedat = 0, + editedat = 0; + const { status, timestamp, version, metadata } = record; + const userMetadata = decodeProposalUserMetadata(metadata); + // unreviewed + if (status === RECORD_STATUS_UNREVIEWED) { + publishedat = timestamp; + } + // publlished but not edited + if (status === RECORD_STATUS_PUBLIC && version <= 1) { + publishedat = timestamp; + } + // edited, have to grab published timestamp from metadata + if (status === RECORD_STATUS_PUBLIC && version > 1) { + const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); + publishedat = publishedMetadata.timestamp; + editedat = timestamp; + } + if (status === RECORD_STATUS_CENSORED) { + const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); + publishedat = publishedMetadata.timestamp; + censoredat = timestamp; + } + if (status === RECORD_STATUS_ARCHIVED) { + const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); + publishedat = publishedMetadata.timestamp; + abandonedat = timestamp; + } + + return { publishedat, editedat, censoredat, abandonedat }; +} + +/** + * getProposalStatusTagPropsFromVoteSummary returns the formatted + * `{ type, text }` props for StatusTag component for given record and + * ticketvote summary. + * @param {Record} record record object + * @param {VoteSummary} voteSummary ticketvote summary object + * @returns {Object} `{ type, text }` StatusTag props + */ +export function getProposalStatusTagPropsFromVoteSummary(record, voteSummary) { + const voteMetadata = decodeVoteMetadataFile(record.files); + const isRfpSubmission = voteMetadata?.linkto; + if (record.status === RECORD_STATUS_PUBLIC && !!voteSummary) { + switch (voteSummary.status) { + case TICKETVOTE_STATUS_UNAUTHORIZED: + return { + type: "blackTime", + text: isRfpSubmission + ? "Waiting for runoff vote to start" + : "Waiting for author to authorize voting", + }; + case TICKETVOTE_STATUS_AUTHORIZED: + return { + type: "yellowTime", + text: "Waiting for admin to start voting", + }; + case TICKETVOTE_STATUS_STARTED: + return { type: "bluePending", text: "Active" }; + case TICKETVOTE_STATUS_FINISHED: + return { + type: "grayNegative", + text: "Finished", + }; + case TICKETVOTE_STATUS_REJECTED: + return { + type: "orangeNegativeCircled", + text: "Rejected", + }; + case TICKETVOTE_STATUS_APPROVED: + return { type: "greenCheck", text: "Approved" }; + default: + break; + } + } + + if (record.status === RECORD_STATUS_ARCHIVED) { + return { + type: "grayNegative", + text: "Abandoned", + }; + } + if (record.status === RECORD_STATUS_CENSORED) { + return { + type: "orangeNegativeCircled", + text: "Censored", + }; + } + + return { type: "grayNegative", text: "missing" }; +} + +export const getProposalStatusTagProps = (proposalSummary) => { + if (!proposalSummary?.status) + return { type: "grayNegative", text: "missing" }; + + switch (proposalSummary.status) { + case PROPOSAL_SUMMARY_STATUS_UNVETTED: + return { + type: "yellowTime", + text: "Unvetted", + }; + case PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED: + case PROPOSAL_SUMMARY_STATUS_ABANDONED: + return { + type: "grayNegative", + text: "Abandoned", + }; + + case PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED: + case PROPOSAL_SUMMARY_STATUS_CENSORED: + return { + type: "orangeNegativeCircled", + text: "Censored", + }; + + case PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW: + return { + type: "blackTime", + text: "Waiting for author to authorize voting", + }; + + case PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED: + return { + type: "yellowTime", + text: "Waiting for admin to start voting", + }; + + case PROPOSAL_SUMMARY_STATUS_VOTE_STARTED: + return { type: "bluePending", text: "Voting" }; + + case PROPOSAL_SUMMARY_STATUS_REJECTED: + return { + type: "orangeNegativeCircled", + text: "Rejected", + }; + + case PROPOSAL_SUMMARY_STATUS_ACTIVE: + return { type: "bluePending", text: "Active" }; + + case PROPOSAL_SUMMARY_STATUS_CLOSED: + return { type: "grayNegative", text: "Closed" }; + + case PROPOSAL_SUMMARY_STATUS_COMPLETED: + return { type: "greenCheck", text: "Completed" }; + + case PROPOSAL_SUMMARY_STATUS_APPROVED: + return { type: "greenCheck", text: "Approved" }; + + default: + break; + } +}; + +/** + * showVoteStatusBar returns if vote has started, finished, approved or + * rejected, which indicates whether the StatusBar should be displayed or not. + * @param {VoteSummary} voteSummary ticketvote summary + */ +export function showVoteStatusBar(voteSummary) { + if (!voteSummary) return false; + return [ + TICKETVOTE_STATUS_STARTED, + TICKETVOTE_STATUS_FINISHED, + TICKETVOTE_STATUS_APPROVED, + TICKETVOTE_STATUS_REJECTED, + ].includes(voteSummary.status); +} + +/** + * getFilesDiff returns an array of files with a `added` or `removed` key for + * added or removed files. Unchanged files aren't tagged and composes the last + * elements of the diff array. + * + * @param {Array} newFiles new record files + * @param {Array} oldFiles old record files + */ +export function getFilesDiff(newFiles, oldFiles) { + const filesDiffFunc = (arr) => (elem) => + !arr.some( + (arrelem) => + arrelem.name === elem.name && arrelem.payload === elem.payload + ); + const filesEqFunc = (arr) => (elem) => !filesDiffFunc(arr)(elem); + return { + added: newFiles.filter(filesDiffFunc(oldFiles)), + removed: oldFiles.filter(filesDiffFunc(newFiles)), + unchanged: newFiles.filter(filesEqFunc(oldFiles)), + }; +} + +export function getImagesByDigest(text, files) { + if (!text) return {}; + const markdownImageRegexParser = + /!\[[^\]]*\]\((?.*?)(?="|\))(?".*")?\)/g; + const inlineImagesMatches = text.matchAll(markdownImageRegexParser); + let imagesByDigest = {}; + for (const match of inlineImagesMatches) { + const { digest } = match.groups; + const file = files.find((f) => f.digest === digest); + imagesByDigest[digest] = file; + } + return imagesByDigest; +} From a55d30aae05676eb6496cacf7c9d823447ec62c8 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 22 Jul 2022 16:34:29 -0300 Subject: [PATCH 15/30] feat(app/pi): use utils from pi proposals service --- .../components/Proposal/ModalProposalDiff.js | 6 +- .../src/components/Proposal/ProposalCard.js | 5 +- .../components/Proposal/ProposalDetails.js | 5 +- .../Proposal/common/ProposalStatusBar.js | 2 +- .../Proposal/common/ProposalStatusTag.js | 2 +- .../politeia/src/components/Proposal/utils.js | 482 ------------------ .../Proposal => pi/proposals}/utils.test.js | 0 .../apps/politeia/src/routes/routes.js | 2 +- 8 files changed, 16 insertions(+), 488 deletions(-) delete mode 100644 plugins-structure/apps/politeia/src/components/Proposal/utils.js rename plugins-structure/apps/politeia/src/{components/Proposal => pi/proposals}/utils.test.js (100%) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js b/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js index daceb8a97..3f5e2228a 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js @@ -10,7 +10,11 @@ import { RecordCard, ThumbnailGrid, } from "@politeiagui/common-ui"; -import { decodeProposalRecord, getFilesDiff, getImagesByDigest } from "./utils"; +import { + decodeProposalRecord, + getFilesDiff, + getImagesByDigest, +} from "../../pi/proposals/utils"; import { ProposalDownloads } from "./common"; import styles from "./ModalProposalDiff.module.css"; import range from "lodash/range"; diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js index 5d43d87cc..051074eae 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js @@ -3,7 +3,10 @@ import { Button, StatusTag } from "pi-ui"; import { RecordCard } from "@politeiagui/common-ui"; import { CommentsCount } from "@politeiagui/comments/ui"; import { getShortToken } from "@politeiagui/core/records/utils"; -import { decodeProposalRecord, getProposalStatusTagProps } from "./utils"; +import { + decodeProposalRecord, + getProposalStatusTagProps, +} from "../../pi/proposals/utils"; import { ProposalStatusBar, ProposalSubtitle } from "./common"; const ProposalCard = ({ diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js index 0f87642ff..c9e0a7970 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js @@ -8,7 +8,10 @@ import { ThumbnailGrid, useModal, } from "@politeiagui/common-ui"; -import { decodeProposalRecord, getImagesByDigest } from "./utils"; +import { + decodeProposalRecord, + getImagesByDigest, +} from "../../pi/proposals/utils"; import { ProposalDownloads, ProposalMetadata, diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusBar.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusBar.js index c3c1f29e2..21b65363b 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusBar.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusBar.js @@ -1,6 +1,6 @@ import React from "react"; import { TicketvoteRecordVoteStatusBar } from "@politeiagui/ticketvote/ui"; -import { showVoteStatusBar } from "../utils"; +import { showVoteStatusBar } from "../../../pi/proposals/utils"; function ProposalStatusBar({ voteSummary }) { return ( diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js index e80769cdf..497256225 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js @@ -1,6 +1,6 @@ import React from "react"; import { StatusTag } from "pi-ui"; -import { getProposalStatusTagProps } from "../utils"; +import { getProposalStatusTagProps } from "../../../pi/proposals/utils"; function ProposalStatusTag({ piSummary }) { const statusTagProps = getProposalStatusTagProps(piSummary); diff --git a/plugins-structure/apps/politeia/src/components/Proposal/utils.js b/plugins-structure/apps/politeia/src/components/Proposal/utils.js deleted file mode 100644 index a21762737..000000000 --- a/plugins-structure/apps/politeia/src/components/Proposal/utils.js +++ /dev/null @@ -1,482 +0,0 @@ -import { - decodeRecordFile, - decodeRecordMetadata, - getShortToken, -} from "@politeiagui/core/records/utils"; -import { - RECORD_STATUS_ARCHIVED, - RECORD_STATUS_CENSORED, - RECORD_STATUS_PUBLIC, - RECORD_STATUS_UNREVIEWED, -} from "@politeiagui/core/records/constants"; -import { - TICKETVOTE_STATUS_APPROVED, - TICKETVOTE_STATUS_AUTHORIZED, - TICKETVOTE_STATUS_FINISHED, - TICKETVOTE_STATUS_REJECTED, - TICKETVOTE_STATUS_STARTED, - TICKETVOTE_STATUS_UNAUTHORIZED, -} from "@politeiagui/ticketvote/constants"; -import { - PROPOSAL_SUMMARY_STATUS_ABANDONED, - PROPOSAL_SUMMARY_STATUS_ACTIVE, - PROPOSAL_SUMMARY_STATUS_APPROVED, - PROPOSAL_SUMMARY_STATUS_CENSORED, - PROPOSAL_SUMMARY_STATUS_CLOSED, - PROPOSAL_SUMMARY_STATUS_COMPLETED, - PROPOSAL_SUMMARY_STATUS_REJECTED, - PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW, - PROPOSAL_SUMMARY_STATUS_UNVETTED, - PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED, - PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED, - PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED, - PROPOSAL_SUMMARY_STATUS_VOTE_STARTED, -} from "../../pi/lib/constants"; -import isArray from "lodash/fp/isArray"; - -const PROPOSAL_METADATA_FILENAME = "proposalmetadata.json"; -const PROPOSAL_INDEX_FILENAME = "index.md"; -const PROPOSAL_VOTE_METADATA_FILENAME = "votemetadata.json"; - -const MONTHS_LABELS = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", -]; - -/** - * Record object - * @typedef {{ - * censorshiprecord: { token: String, merkle: String, signature: String }, - * files: Array, - * metadata: Array, - * state: Number, - * status: Number, - * timestamp: Number, - * username: String, - * version: Number - * }} Record - */ - -/** - * Ticketvote Vote Summary object - * @typedef {{ - * type: Number, - * status: Number, - * duration: Number, - * startblockheight: Number, - * startblockhash: String, - * endblockheight: Number, - * eligibletickets: Number, - * quorumpercentage: Number, - * passpercentage: Number, - * results: Array, - * bestblock: Number - * }} VoteSummary - */ - -/** - * Proposal object - * @typedef {{ - * name: String, - * token: String, - * recordState: Number, - * recordStatus: Number, - * version: Number, - * timestamps: { - * publishedat: Number, - * editedat: Number, - * censoredat: Number, - * abandonedat: Number, - * }, - * voteMetadata: { - * linkto: Number, - * linkby: Number, - * }, - * author: { - * username: String, - * userid: String, - * }, - * body: String, - * proposalMetadata: Object, - * censored: Bool, - * archived: Bool, - * abandonReason: String, - * attachments: Array - * }} Proposal - */ - -/** - * decodeProposalMetadataFile returns the decoded "proposalmetadata.json" file - * for given record's files array. - * @param {Array} files record's files - * @returns {Object} Proposal metadata object - */ -export function decodeProposalMetadataFile(files) { - const metadata = files.find((f) => f.name === PROPOSAL_METADATA_FILENAME); - return decodeRecordFile(metadata); -} - -/** - * decodeProposalMetadataFile returns the decoded "proposalmetadata.json" file - * for given record's files array. - * @param {Array} files record's files - * @returns {Object} Proposal metadata object - */ -export function decodeProposalBodyFile(files) { - const body = files.find((f) => f.name === PROPOSAL_INDEX_FILENAME); - return body && decodeURIComponent(escape(window.atob(body.payload))); -} - -/** - * decodeVoteMetadataFile accepts a proposal files array parses it's vote - * metadata and returns it as object of the form { linkto, linkby }. - * @param {Array} files Record Files array - * @returns {Object} `{linkto, linkby}` decoded vote metadata - */ -export function decodeVoteMetadataFile(files) { - const metadata = - files && files.find((f) => f.name === PROPOSAL_VOTE_METADATA_FILENAME); - return decodeRecordFile(metadata); -} - -/** - * decodeProposalUserMetadata filters all "usermd" metadata streams and decodes - * their payloads. Returns decoded and filtered metadata streams. - * @param {Array} metadataStreams record's metadata streams - * @returns {Array} Decoded "usermd" metadata streams - */ -export function decodeProposalUserMetadata(metadataStreams) { - const userMd = metadataStreams - ? metadataStreams - .filter((md) => md.pluginid === "usermd") - .map((md) => decodeRecordMetadata(md)) - : []; - return userMd; -} - -export function decodeProposalAttachments(files) { - return files.filter( - (f) => - f.name !== PROPOSAL_INDEX_FILENAME && - f.name !== PROPOSAL_VOTE_METADATA_FILENAME && - f.name !== PROPOSAL_METADATA_FILENAME - ); -} - -/** - * decodeProposalRecord returns a formatted proposal object for given record. - * It decodes all proposal-related data from records and converts it into a - * readable proposal object. - * @param {Record} record record object - * @returns {Proposal} formatted proposal object - */ -export function decodeProposalRecord(record) { - if (!record) return; - const { name, ...proposalMetadata } = decodeProposalMetadataFile( - record.files - ); - const userMetadata = decodeProposalUserMetadata(record.metadata); - const voteMetadata = decodeVoteMetadataFile(record.files); - const body = decodeProposalBodyFile(record.files); - const attachments = decodeProposalAttachments(record.files); - const useridMd = userMetadata.find((md) => md && md.payload.userid); - const { token } = record.censorshiprecord; - return { - name: name ? name : getShortToken(token), - token, - recordState: record.state, - recordStatus: record.status, - version: record.version, - timestamps: getProposalTimestamps(record), - voteMetadata, - author: { - username: record.username, - userid: useridMd?.payload?.userid, - }, - body, - proposalMetadata, - archived: record.status === RECORD_STATUS_ARCHIVED, - censored: record.status === RECORD_STATUS_CENSORED, - abandonmentReason: getAbandonmentReason(userMetadata), - attachments, - }; -} - -function findStatusMetadataFromPayloads(mdPayloads, status) { - if (!mdPayloads) return {}; - const publicMd = mdPayloads.find((p) => p.status === status); - if (!publicMd) { - // traverse payload arrays - const arrayPayloads = mdPayloads.find((p) => isArray(p)); - return findStatusMetadataFromPayloads(arrayPayloads, status); - } - return publicMd; -} - -function getAbandonmentReason(userMetadata) { - if (!userMetadata) return null; - const payloads = userMetadata.map((md) => md.payload); - for (const status of [RECORD_STATUS_ARCHIVED, RECORD_STATUS_CENSORED]) { - const { reason } = findStatusMetadataFromPayloads(payloads, status); - if (reason) { - return reason; - } - } -} - -/** - * formatDateToInternationalString accepts an object of day, month and year. - * It returns a string of human viewable international date from the result - * of DatePicker or BackEnd and supposes they are correct. - * String format: 08 Sep 2021 - * @param {object} { day, month, year } - * @returns {string} - */ -export function formatDateToInternationalString({ day, month, year }) { - const monthLabel = MONTHS_LABELS[month - 1]; - if (monthLabel === undefined) { - return "Invalid Date"; - } - const dayView = `0${day}`.slice(-2); - return `${dayView} ${MONTHS_LABELS[month - 1]} ${year}`; -} - -/** - * getPublicStatusChangeMetadata returns the metadata stream that describes when - * the record has been made `public`, i.e. status === 2. - * @param {Array} userMetadata record's "usermd" metadata stream - * @returns {Object} Public status change metadata stream - */ -export function getPublicStatusChangeMetadata(userMetadata) { - if (!userMetadata) return {}; - const payloads = userMetadata.map((md) => md.payload); - return findStatusMetadataFromPayloads(payloads, RECORD_STATUS_PUBLIC); -} - -/** - * getProposalTimestamps returns published, censored, edited and abandoned - * timestamps for given record. Default timestamps values are 0. - * @param {Record} record record object - * @returns {Object} `{publishedat: number, editedat: number, censoredat: number, - * abandonedat: number}` - Object with publishedat, censoredat, abandonedat - */ -function getProposalTimestamps(record) { - if (!record) - return { publishedat: 0, editedat: 0, censoredat: 0, abandonedat: 0 }; - - let publishedat = 0, - censoredat = 0, - abandonedat = 0, - editedat = 0; - const { status, timestamp, version, metadata } = record; - const userMetadata = decodeProposalUserMetadata(metadata); - // unreviewed - if (status === RECORD_STATUS_UNREVIEWED) { - publishedat = timestamp; - } - // publlished but not edited - if (status === RECORD_STATUS_PUBLIC && version <= 1) { - publishedat = timestamp; - } - // edited, have to grab published timestamp from metadata - if (status === RECORD_STATUS_PUBLIC && version > 1) { - const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); - publishedat = publishedMetadata.timestamp; - editedat = timestamp; - } - if (status === RECORD_STATUS_CENSORED) { - const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); - publishedat = publishedMetadata.timestamp; - censoredat = timestamp; - } - if (status === RECORD_STATUS_ARCHIVED) { - const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); - publishedat = publishedMetadata.timestamp; - abandonedat = timestamp; - } - - return { publishedat, editedat, censoredat, abandonedat }; -} - -/** - * getProposalStatusTagPropsFromVoteSummary returns the formatted - * `{ type, text }` props for StatusTag component for given record and - * ticketvote summary. - * @param {Record} record record object - * @param {VoteSummary} voteSummary ticketvote summary object - * @returns {Object} `{ type, text }` StatusTag props - */ -export function getProposalStatusTagPropsFromVoteSummary(record, voteSummary) { - const voteMetadata = decodeVoteMetadataFile(record.files); - const isRfpSubmission = voteMetadata?.linkto; - if (record.status === RECORD_STATUS_PUBLIC && !!voteSummary) { - switch (voteSummary.status) { - case TICKETVOTE_STATUS_UNAUTHORIZED: - return { - type: "blackTime", - text: isRfpSubmission - ? "Waiting for runoff vote to start" - : "Waiting for author to authorize voting", - }; - case TICKETVOTE_STATUS_AUTHORIZED: - return { - type: "yellowTime", - text: "Waiting for admin to start voting", - }; - case TICKETVOTE_STATUS_STARTED: - return { type: "bluePending", text: "Active" }; - case TICKETVOTE_STATUS_FINISHED: - return { - type: "grayNegative", - text: "Finished", - }; - case TICKETVOTE_STATUS_REJECTED: - return { - type: "orangeNegativeCircled", - text: "Rejected", - }; - case TICKETVOTE_STATUS_APPROVED: - return { type: "greenCheck", text: "Approved" }; - default: - break; - } - } - - if (record.status === RECORD_STATUS_ARCHIVED) { - return { - type: "grayNegative", - text: "Abandoned", - }; - } - if (record.status === RECORD_STATUS_CENSORED) { - return { - type: "orangeNegativeCircled", - text: "Censored", - }; - } - - return { type: "grayNegative", text: "missing" }; -} - -export const getProposalStatusTagProps = (proposalSummary) => { - if (!proposalSummary?.status) - return { type: "grayNegative", text: "missing" }; - - switch (proposalSummary.status) { - case PROPOSAL_SUMMARY_STATUS_UNVETTED: - return { - type: "yellowTime", - text: "Unvetted", - }; - case PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED: - case PROPOSAL_SUMMARY_STATUS_ABANDONED: - return { - type: "grayNegative", - text: "Abandoned", - }; - - case PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED: - case PROPOSAL_SUMMARY_STATUS_CENSORED: - return { - type: "orangeNegativeCircled", - text: "Censored", - }; - - case PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW: - return { - type: "blackTime", - text: "Waiting for author to authorize voting", - }; - - case PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED: - return { - type: "yellowTime", - text: "Waiting for admin to start voting", - }; - - case PROPOSAL_SUMMARY_STATUS_VOTE_STARTED: - return { type: "bluePending", text: "Voting" }; - - case PROPOSAL_SUMMARY_STATUS_REJECTED: - return { - type: "orangeNegativeCircled", - text: "Rejected", - }; - - case PROPOSAL_SUMMARY_STATUS_ACTIVE: - return { type: "bluePending", text: "Active" }; - - case PROPOSAL_SUMMARY_STATUS_CLOSED: - return { type: "grayNegative", text: "Closed" }; - - case PROPOSAL_SUMMARY_STATUS_COMPLETED: - return { type: "greenCheck", text: "Completed" }; - - case PROPOSAL_SUMMARY_STATUS_APPROVED: - return { type: "greenCheck", text: "Approved" }; - - default: - break; - } -}; - -/** - * showVoteStatusBar returns if vote has started, finished, approved or - * rejected, which indicates whether the StatusBar should be displayed or not. - * @param {VoteSummary} voteSummary ticketvote summary - */ -export function showVoteStatusBar(voteSummary) { - if (!voteSummary) return false; - return [ - TICKETVOTE_STATUS_STARTED, - TICKETVOTE_STATUS_FINISHED, - TICKETVOTE_STATUS_APPROVED, - TICKETVOTE_STATUS_REJECTED, - ].includes(voteSummary.status); -} - -/** - * getFilesDiff returns an array of files with a `added` or `removed` key for - * added or removed files. Unchanged files aren't tagged and composes the last - * elements of the diff array. - * - * @param {Array} newFiles new record files - * @param {Array} oldFiles old record files - */ -export function getFilesDiff(newFiles, oldFiles) { - const filesDiffFunc = (arr) => (elem) => - !arr.some( - (arrelem) => - arrelem.name === elem.name && arrelem.payload === elem.payload - ); - const filesEqFunc = (arr) => (elem) => !filesDiffFunc(arr)(elem); - return { - added: newFiles.filter(filesDiffFunc(oldFiles)), - removed: oldFiles.filter(filesDiffFunc(newFiles)), - unchanged: newFiles.filter(filesEqFunc(oldFiles)), - }; -} - -export function getImagesByDigest(text, files) { - if (!text) return {}; - const markdownImageRegexParser = - /!\[[^\]]*\]\((?.*?)(?="|\))(?".*")?\)/g; - const inlineImagesMatches = text.matchAll(markdownImageRegexParser); - let imagesByDigest = {}; - for (const match of inlineImagesMatches) { - const { digest } = match.groups; - const file = files.find((f) => f.digest === digest); - imagesByDigest[digest] = file; - } - return imagesByDigest; -} diff --git a/plugins-structure/apps/politeia/src/components/Proposal/utils.test.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.test.js similarity index 100% rename from plugins-structure/apps/politeia/src/components/Proposal/utils.test.js rename to plugins-structure/apps/politeia/src/pi/proposals/utils.test.js diff --git a/plugins-structure/apps/politeia/src/routes/routes.js b/plugins-structure/apps/politeia/src/routes/routes.js index 13c9ccbfe..e554d0035 100644 --- a/plugins-structure/apps/politeia/src/routes/routes.js +++ b/plugins-structure/apps/politeia/src/routes/routes.js @@ -1,7 +1,7 @@ import { store } from "@politeiagui/core"; import { records } from "@politeiagui/core/records"; import { DetailsRoute, HomeRoute, NewProposalRoute } from "../pages"; -import { decodeProposalRecord } from "../components/Proposal/utils"; +import { decodeProposalRecord } from "../pi/proposals/utils"; import { routeCleanup } from "../utils/routeCleanup"; export const routes = [ From 8484426af4ceb64619885f97784c232df6e83e1c Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Mon, 25 Jul 2022 15:03:48 -0300 Subject: [PATCH 16/30] wip: proposal status change selector --- .../src/pages/Details/useProposalDetails.js | 11 +++++++- .../apps/politeia/src/pi/index.js | 1 + .../apps/politeia/src/pi/proposals/index.js | 5 ++++ .../politeia/src/pi/proposals/selectors.js | 25 ++++++++++++++++++ .../apps/politeia/src/pi/proposals/utils.js | 13 ++++++++-- .../packages/ticketvote/src/lib/utils.js | 26 +++++++++++++++++++ 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 plugins-structure/apps/politeia/src/pi/proposals/index.js diff --git a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js index a7c40b90a..e3eef9971 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js +++ b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js @@ -6,7 +6,7 @@ import { selectDetailsStatus } from "./selectors"; import { records } from "@politeiagui/core/records"; import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; import { recordComments } from "@politeiagui/comments/comments"; -import { piBilling, piSummaries } from "../../pi"; +import { piBilling, piSummaries, proposals } from "../../pi"; function useProposalDetails({ token }) { const dispatch = useDispatch(); @@ -30,6 +30,15 @@ function useProposalDetails({ token }) { const billingStatusChange = useSelector((state) => piBilling.selectLastByToken(state, fullToken) ); + + // test: + const proposalStatusChange = useSelector((state) => + proposals.selectStatusChangeByToken(state, { + status: piSummary?.status, + token: fullToken, + }) + ); + const recordDetailsError = useSelector(records.selectError); const voteSummaryError = useSelector(ticketvoteSummaries.selectError); const commentsError = useSelector(recordComments.selectError); diff --git a/plugins-structure/apps/politeia/src/pi/index.js b/plugins-structure/apps/politeia/src/pi/index.js index 1b2525168..91729adf8 100644 --- a/plugins-structure/apps/politeia/src/pi/index.js +++ b/plugins-structure/apps/politeia/src/pi/index.js @@ -3,3 +3,4 @@ export { default } from "./plugin"; export { piPolicy } from "./policy"; export { piBilling } from "./billing"; export { piSummaries } from "./summaries"; +export { proposals } from "./proposals"; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/index.js b/plugins-structure/apps/politeia/src/pi/proposals/index.js new file mode 100644 index 000000000..e35fee910 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/proposals/index.js @@ -0,0 +1,5 @@ +import { selectProposalStatusChangeByToken } from "./selectors"; + +export const proposals = { + selectStatusChangeByToken: selectProposalStatusChangeByToken, +}; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js index e69de29bb..1506729fb 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js @@ -0,0 +1,25 @@ +import { records } from "@politeiagui/core/records"; +import { + decodeProposalUserMetadata, + getRecordStatusChanges, + getPublicStatusChangeMetadata, +} from "./utils"; +import * as ctes from "../lib/constants"; + +// under-review/authorized: status change to record public (getPublicStatusChangeMetadata) +// voting: vote summary start block height +// active/rejected: vote summary end block height +// completed/closed: pi billing status changes + +export function selectProposalStatusChangeByToken(state, { status, token }) { + if (!status || !token) return null; + const record = records.selectByToken(state, token); + if (!record) return null; + + const userMetadata = decodeProposalUserMetadata(record.metadata); + + let statusChange; + // if (isUnderReviewOrAuthorized(status)) { + // statusChange = getPublicStatusChangeMetadata(userMetadata); + // } else if (isVoting(status)) {} +} diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index 3b154ce56..abf37f449 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -17,6 +17,7 @@ import { TICKETVOTE_STATUS_STARTED, TICKETVOTE_STATUS_UNAUTHORIZED, } from "@politeiagui/ticketvote/constants"; +import { getTimestampFromBlocks } from "@politeiagui/ticketvote/utils"; import { PROPOSAL_SUMMARY_STATUS_ABANDONED, PROPOSAL_SUMMARY_STATUS_ACTIVE, @@ -210,11 +211,10 @@ export function decodeProposalRecord(record) { // TODO: remove abandonmentReason abandonmentReason: getAbandonmentReason(userMetadata), attachments, - userMetadata, }; } -function findStatusMetadataFromPayloads(mdPayloads, status) { +export function findStatusMetadataFromPayloads(mdPayloads, status) { if (!mdPayloads) return {}; const publicMd = mdPayloads.find((p) => p.status === status); if (!publicMd) { @@ -225,6 +225,13 @@ function findStatusMetadataFromPayloads(mdPayloads, status) { return publicMd; } +export function getRecordStatusChanges(userMetadata) { + if (!userMetadata) return null; + const payloads = userMetadata.map((md) => md.payload); + return payloads.filter((p) => p?.status); +} + +// TODO: kill this function getAbandonmentReason(userMetadata) { if (!userMetadata) return null; const payloads = userMetadata.map((md) => md.payload); @@ -265,6 +272,8 @@ export function getPublicStatusChangeMetadata(userMetadata) { return findStatusMetadataFromPayloads(payloads, RECORD_STATUS_PUBLIC); } +export function getVotingTimestamps(voteSummary) {} + /** * getProposalTimestamps returns published, censored, edited and abandoned * timestamps for given record. Default timestamps values are 0. diff --git a/plugins-structure/packages/ticketvote/src/lib/utils.js b/plugins-structure/packages/ticketvote/src/lib/utils.js index 216d0ed9d..f7fd881ca 100644 --- a/plugins-structure/packages/ticketvote/src/lib/utils.js +++ b/plugins-structure/packages/ticketvote/src/lib/utils.js @@ -78,3 +78,29 @@ export const validTicketvoteStatuses = [ ...validStringTicketvoteStatuses, ...validNumberTicketvoteStatuses, ]; + +/** + * Returns the amount of blocks left to the currentHeight + * @param {Number} block + * @param {Number} currentHeight + * @returns {Number} number of blocks left + */ +export function getVoteBlocksDiff(block, currentHeight) { + if (!block || !currentHeight) return 0; + return +block - currentHeight; +} + +/** + * Returns the blocks difference from current block height in milliseconds + * @param {Number} block + * @param {Number} currentHeight + * @param {Number} blockDuration + * @returns {Number} + */ +export function getTimestampFromBlocks(block, currentHeight, blockDuration) { + const blocksDiff = getVoteBlocksDiff(block, currentHeight); + const blockTimeMinutes = blocksDiff * blockDuration; + const mili = blockTimeMinutes * 60000; + const dateMs = new Date(mili + Date.now()); + return Math.round(dateMs / 1000); // returns unix timestamp +} From 41665292b2a82acd757448af5b00fa3e3c68e157 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Tue, 26 Jul 2022 18:38:58 -0300 Subject: [PATCH 17/30] wip: Proposal Status changes --- .../src/pages/Details/useProposalDetails.js | 13 +- .../apps/politeia/src/pi/lib/constants.js | 34 +++-- .../apps/politeia/src/pi/proposals/index.js | 4 +- .../politeia/src/pi/proposals/selectors.js | 61 +++++--- .../apps/politeia/src/pi/proposals/utils.js | 131 +++++++++++++----- .../apps/politeia/src/pi/utils.js | 9 +- .../packages/ticketvote/src/lib/utils.js | 12 +- .../src/ticketvote/summaries/index.js | 3 + .../ticketvote/summaries/summariesSlice.js | 35 ++++- 9 files changed, 219 insertions(+), 83 deletions(-) diff --git a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js index e3eef9971..698558f57 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js +++ b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js @@ -31,18 +31,14 @@ function useProposalDetails({ token }) { piBilling.selectLastByToken(state, fullToken) ); - // test: - const proposalStatusChange = useSelector((state) => - proposals.selectStatusChangeByToken(state, { - status: piSummary?.status, - token: fullToken, - }) - ); - const recordDetailsError = useSelector(records.selectError); const voteSummaryError = useSelector(ticketvoteSummaries.selectError); const commentsError = useSelector(recordComments.selectError); + const recordStatusChanges = useSelector((state) => + proposals.selectStatusChangesByToken(state, fullToken) + ); + async function onFetchRecordTimestamps({ token, version }) { const res = await dispatch(recordsTimestamps.fetch({ token, version })); return res.payload; @@ -66,6 +62,7 @@ function useProposalDetails({ token }) { record, voteSummary, billingStatusChange, + recordStatusChanges, }; } diff --git a/plugins-structure/apps/politeia/src/pi/lib/constants.js b/plugins-structure/apps/politeia/src/pi/lib/constants.js index e159c0b18..7b6d533e8 100644 --- a/plugins-structure/apps/politeia/src/pi/lib/constants.js +++ b/plugins-structure/apps/politeia/src/pi/lib/constants.js @@ -5,19 +5,27 @@ export const ROUTE_BILLING_STATUS_CHANGES = "/billingstatuschanges"; export const ROUTE_SUMMARIES = "/summaries"; export const ROUTE_POLICY = "/policy"; -export const PROPOSAL_SUMMARY_STATUS_UNVETTED = "unvetted"; -export const PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED = "unvetted-abandoned"; -export const PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED = "unvetted-censored"; -export const PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW = "under-review"; -export const PROPOSAL_SUMMARY_STATUS_ABANDONED = "abandoned"; -export const PROPOSAL_SUMMARY_STATUS_CENSORED = "censored"; -export const PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED = "vote-authorized"; -export const PROPOSAL_SUMMARY_STATUS_VOTE_STARTED = "vote-started"; -export const PROPOSAL_SUMMARY_STATUS_REJECTED = "rejected"; -export const PROPOSAL_SUMMARY_STATUS_ACTIVE = "active"; -export const PROPOSAL_SUMMARY_STATUS_COMPLETED = "completed"; -export const PROPOSAL_SUMMARY_STATUS_APPROVED = "approved"; -export const PROPOSAL_SUMMARY_STATUS_CLOSED = "closed"; +// Proposal Statuses +export const PROPOSAL_STATUS_UNVETTED = "unvetted"; +export const PROPOSAL_STATUS_UNVETTED_ABANDONED = "unvetted-abandoned"; +export const PROPOSAL_STATUS_UNVETTED_CENSORED = "unvetted-censored"; +export const PROPOSAL_STATUS_UNDER_REVIEW = "under-review"; +export const PROPOSAL_STATUS_ABANDONED = "abandoned"; +export const PROPOSAL_STATUS_CENSORED = "censored"; +export const PROPOSAL_STATUS_VOTE_AUTHORIZED = "vote-authorized"; +export const PROPOSAL_STATUS_VOTE_STARTED = "vote-started"; +export const PROPOSAL_STATUS_REJECTED = "rejected"; +export const PROPOSAL_STATUS_ACTIVE = "active"; +export const PROPOSAL_STATUS_COMPLETED = "completed"; +export const PROPOSAL_STATUS_APPROVED = "approved"; +export const PROPOSAL_STATUS_CLOSED = "closed"; +// UI only +export const PROPOSAL_STATUS_VOTE_ENDED = "vote-ended"; + +// Billing statuses +export const PROPOSAL_BILLING_STATUS_ACTIVE = 1; +export const PROPOSAL_BILLING_STATUS_CLOSED = 2; +export const PROPOSAL_BILLING_STATUS_COMPLETED = 3; export const PROPOSAL_TYPE_REGULAR = 1; export const PROPOSAL_TYPE_RFP = 2; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/index.js b/plugins-structure/apps/politeia/src/pi/proposals/index.js index e35fee910..6300f300e 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/index.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/index.js @@ -1,5 +1,5 @@ -import { selectProposalStatusChangeByToken } from "./selectors"; +import { selectProposalStatusChangesByToken } from "./selectors"; export const proposals = { - selectStatusChangeByToken: selectProposalStatusChangeByToken, + selectStatusChangesByToken: selectProposalStatusChangesByToken, }; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js index 1506729fb..f6c6fe778 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js @@ -1,25 +1,52 @@ import { records } from "@politeiagui/core/records"; +import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; +import { piBilling } from "../billing"; +import { piSummaries } from "../summaries"; import { - decodeProposalUserMetadata, - getRecordStatusChanges, - getPublicStatusChangeMetadata, + convertBillingStatusToProposalStatus, + convertRecordStatusToProposalStatus, + convertVoteStatusToProposalStatus, + getRecordStatusChangesMetadata, } from "./utils"; -import * as ctes from "../lib/constants"; -// under-review/authorized: status change to record public (getPublicStatusChangeMetadata) -// voting: vote summary start block height -// active/rejected: vote summary end block height -// completed/closed: pi billing status changes - -export function selectProposalStatusChangeByToken(state, { status, token }) { - if (!status || !token) return null; +export function selectProposalStatusChangesByToken(state, token) { + // Get Record and its Proposal Summary const record = records.selectByToken(state, token); - if (!record) return null; + const piSummary = piSummaries.selectByToken(state, token); + if (!record || !piSummary) return; + // Status changes + // TODO: create a core/records selector for record status changes + const recordStatusChanges = getRecordStatusChangesMetadata(record); + const billingStatusChanges = piBilling.selectByToken(state, token) || []; + const voteStatusChanges = + ticketvoteSummaries.selectStatusChangesByToken(state, token) || []; + + // convert status from status change to pi-summary + const proposalVoteStatusChanges = voteStatusChanges.map( + ({ status, ...rest }) => ({ + ...rest, + status: convertVoteStatusToProposalStatus(status, record.status), + }) + ); + const proposalRecordStatusChanges = recordStatusChanges.map( + ({ status, ...rest }) => ({ + ...rest, + status: convertRecordStatusToProposalStatus(status, record.state), + }) + ); + const proposalBillingStatusChanges = billingStatusChanges.map( + ({ status, ...rest }) => ({ + ...rest, + status: convertBillingStatusToProposalStatus(status), + }) + ); - const userMetadata = decodeProposalUserMetadata(record.metadata); + // Order by most recent status change + const statusChangesOrdered = [ + ...proposalBillingStatusChanges, + ...proposalRecordStatusChanges, + ...proposalVoteStatusChanges, + ].sort((a, b) => b.timestamp - a.timestamp); - let statusChange; - // if (isUnderReviewOrAuthorized(status)) { - // statusChange = getPublicStatusChangeMetadata(userMetadata); - // } else if (isVoting(status)) {} + return statusChangesOrdered; } diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index abf37f449..2522e442d 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -4,6 +4,8 @@ import { getShortToken, } from "@politeiagui/core/records/utils"; import { + RECORD_STATE_UNVETTED, + RECORD_STATE_VETTED, RECORD_STATUS_ARCHIVED, RECORD_STATUS_CENSORED, RECORD_STATUS_PUBLIC, @@ -13,27 +15,32 @@ import { TICKETVOTE_STATUS_APPROVED, TICKETVOTE_STATUS_AUTHORIZED, TICKETVOTE_STATUS_FINISHED, + TICKETVOTE_STATUS_INELIGIBLE, TICKETVOTE_STATUS_REJECTED, TICKETVOTE_STATUS_STARTED, TICKETVOTE_STATUS_UNAUTHORIZED, } from "@politeiagui/ticketvote/constants"; -import { getTimestampFromBlocks } from "@politeiagui/ticketvote/utils"; import { - PROPOSAL_SUMMARY_STATUS_ABANDONED, - PROPOSAL_SUMMARY_STATUS_ACTIVE, - PROPOSAL_SUMMARY_STATUS_APPROVED, - PROPOSAL_SUMMARY_STATUS_CENSORED, - PROPOSAL_SUMMARY_STATUS_CLOSED, - PROPOSAL_SUMMARY_STATUS_COMPLETED, - PROPOSAL_SUMMARY_STATUS_REJECTED, - PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW, - PROPOSAL_SUMMARY_STATUS_UNVETTED, - PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED, - PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED, - PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED, - PROPOSAL_SUMMARY_STATUS_VOTE_STARTED, + PROPOSAL_BILLING_STATUS_ACTIVE, + PROPOSAL_BILLING_STATUS_CLOSED, + PROPOSAL_BILLING_STATUS_COMPLETED, + PROPOSAL_STATUS_ABANDONED, + PROPOSAL_STATUS_ACTIVE, + PROPOSAL_STATUS_APPROVED, + PROPOSAL_STATUS_CENSORED, + PROPOSAL_STATUS_CLOSED, + PROPOSAL_STATUS_COMPLETED, + PROPOSAL_STATUS_REJECTED, + PROPOSAL_STATUS_UNDER_REVIEW, + PROPOSAL_STATUS_UNVETTED, + PROPOSAL_STATUS_UNVETTED_ABANDONED, + PROPOSAL_STATUS_UNVETTED_CENSORED, + PROPOSAL_STATUS_VOTE_AUTHORIZED, + PROPOSAL_STATUS_VOTE_ENDED, + PROPOSAL_STATUS_VOTE_STARTED, } from "../lib/constants"; import isArray from "lodash/fp/isArray"; +import isEmpty from "lodash/isEmpty"; const PROPOSAL_METADATA_FILENAME = "proposalmetadata.json"; const PROPOSAL_INDEX_FILENAME = "index.md"; @@ -211,6 +218,7 @@ export function decodeProposalRecord(record) { // TODO: remove abandonmentReason abandonmentReason: getAbandonmentReason(userMetadata), attachments, + userMetadata, }; } @@ -225,10 +233,23 @@ export function findStatusMetadataFromPayloads(mdPayloads, status) { return publicMd; } -export function getRecordStatusChanges(userMetadata) { - if (!userMetadata) return null; +// TODO: Docs +function getStatusMetadataPayloads(payloads) { + if (!payloads) return {}; + const statusChangePayloads = payloads.filter((p) => p?.status); + if (isEmpty(statusChangePayloads)) { + // traverse payload arrays + return payloads.find((p) => isArray(p)); + } + return statusChangePayloads; +} + +// TODO: Docs +export function getRecordStatusChangesMetadata(record) { + if (!record?.metadata) return; + const userMetadata = decodeProposalUserMetadata(record.metadata); const payloads = userMetadata.map((md) => md.payload); - return payloads.filter((p) => p?.status); + return getStatusMetadataPayloads(payloads); } // TODO: kill this @@ -272,8 +293,6 @@ export function getPublicStatusChangeMetadata(userMetadata) { return findStatusMetadataFromPayloads(payloads, RECORD_STATUS_PUBLIC); } -export function getVotingTimestamps(voteSummary) {} - /** * getProposalTimestamps returns published, censored, edited and abandoned * timestamps for given record. Default timestamps values are 0. @@ -384,56 +403,56 @@ export const getProposalStatusTagProps = (proposalSummary) => { return { type: "grayNegative", text: "missing" }; switch (proposalSummary.status) { - case PROPOSAL_SUMMARY_STATUS_UNVETTED: + case PROPOSAL_STATUS_UNVETTED: return { type: "yellowTime", text: "Unvetted", }; - case PROPOSAL_SUMMARY_STATUS_UNVETTED_ABANDONED: - case PROPOSAL_SUMMARY_STATUS_ABANDONED: + case PROPOSAL_STATUS_UNVETTED_ABANDONED: + case PROPOSAL_STATUS_ABANDONED: return { type: "grayNegative", text: "Abandoned", }; - case PROPOSAL_SUMMARY_STATUS_UNVETTED_CENSORED: - case PROPOSAL_SUMMARY_STATUS_CENSORED: + case PROPOSAL_STATUS_UNVETTED_CENSORED: + case PROPOSAL_STATUS_CENSORED: return { type: "orangeNegativeCircled", text: "Censored", }; - case PROPOSAL_SUMMARY_STATUS_UNDER_REVIEW: + case PROPOSAL_STATUS_UNDER_REVIEW: return { type: "blackTime", text: "Waiting for author to authorize voting", }; - case PROPOSAL_SUMMARY_STATUS_VOTE_AUTHORIZED: + case PROPOSAL_STATUS_VOTE_AUTHORIZED: return { type: "yellowTime", text: "Waiting for admin to start voting", }; - case PROPOSAL_SUMMARY_STATUS_VOTE_STARTED: + case PROPOSAL_STATUS_VOTE_STARTED: return { type: "bluePending", text: "Voting" }; - case PROPOSAL_SUMMARY_STATUS_REJECTED: + case PROPOSAL_STATUS_REJECTED: return { type: "orangeNegativeCircled", text: "Rejected", }; - case PROPOSAL_SUMMARY_STATUS_ACTIVE: + case PROPOSAL_STATUS_ACTIVE: return { type: "bluePending", text: "Active" }; - case PROPOSAL_SUMMARY_STATUS_CLOSED: + case PROPOSAL_STATUS_CLOSED: return { type: "grayNegative", text: "Closed" }; - case PROPOSAL_SUMMARY_STATUS_COMPLETED: + case PROPOSAL_STATUS_COMPLETED: return { type: "greenCheck", text: "Completed" }; - case PROPOSAL_SUMMARY_STATUS_APPROVED: + case PROPOSAL_STATUS_APPROVED: return { type: "greenCheck", text: "Approved" }; default: @@ -491,3 +510,51 @@ export function getImagesByDigest(text, files) { } return imagesByDigest; } + +// TODO: Docs +export function convertVoteStatusToProposalStatus(voteStatus, recordStatus) { + const voteStatusToProposalStatus = { + [TICKETVOTE_STATUS_UNAUTHORIZED]: PROPOSAL_STATUS_UNDER_REVIEW, + [TICKETVOTE_STATUS_AUTHORIZED]: PROPOSAL_STATUS_VOTE_AUTHORIZED, + [TICKETVOTE_STATUS_STARTED]: PROPOSAL_STATUS_VOTE_STARTED, + [TICKETVOTE_STATUS_APPROVED]: PROPOSAL_STATUS_APPROVED, + [TICKETVOTE_STATUS_REJECTED]: PROPOSAL_STATUS_REJECTED, + [TICKETVOTE_STATUS_FINISHED]: PROPOSAL_STATUS_VOTE_ENDED, + [TICKETVOTE_STATUS_INELIGIBLE]: + recordStatus === RECORD_STATUS_CENSORED + ? PROPOSAL_STATUS_CENSORED + : PROPOSAL_STATUS_ABANDONED, + }; + return voteStatusToProposalStatus[voteStatus]; +} + +// TODO: Docs +export function convertRecordStatusToProposalStatus( + recordStatus, + recordState = RECORD_STATE_VETTED +) { + if (!recordStatus) return; + const mapStateAndStatusToProposalStatus = { + [RECORD_STATE_UNVETTED]: { + [RECORD_STATUS_UNREVIEWED]: PROPOSAL_STATUS_UNVETTED, + [RECORD_STATUS_ARCHIVED]: PROPOSAL_STATUS_UNVETTED_ABANDONED, + [RECORD_STATUS_CENSORED]: PROPOSAL_STATUS_UNVETTED_CENSORED, + }, + [RECORD_STATE_VETTED]: { + [RECORD_STATUS_PUBLIC]: PROPOSAL_STATUS_UNDER_REVIEW, + [RECORD_STATUS_ARCHIVED]: PROPOSAL_STATUS_ABANDONED, + [RECORD_STATUS_CENSORED]: PROPOSAL_STATUS_CENSORED, + }, + }; + return mapStateAndStatusToProposalStatus[recordState][recordStatus]; +} + +// TODO: Docs +export function convertBillingStatusToProposalStatus(billingStatus) { + const mapBillingStatusToProposalStatus = { + [PROPOSAL_BILLING_STATUS_ACTIVE]: PROPOSAL_STATUS_ACTIVE, + [PROPOSAL_BILLING_STATUS_CLOSED]: PROPOSAL_STATUS_CLOSED, + [PROPOSAL_BILLING_STATUS_COMPLETED]: PROPOSAL_STATUS_COMPLETED, + }; + return mapBillingStatusToProposalStatus[billingStatus]; +} diff --git a/plugins-structure/apps/politeia/src/pi/utils.js b/plugins-structure/apps/politeia/src/pi/utils.js index f667e3a0d..81989a94d 100644 --- a/plugins-structure/apps/politeia/src/pi/utils.js +++ b/plugins-structure/apps/politeia/src/pi/utils.js @@ -1,8 +1,8 @@ import { store } from "@politeiagui/core"; import { piPolicy } from "./policy"; import { - PROPOSAL_SUMMARY_STATUS_CLOSED, - PROPOSAL_SUMMARY_STATUS_COMPLETED, + PROPOSAL_STATUS_CLOSED, + PROPOSAL_STATUS_COMPLETED, } from "./lib/constants"; export function fetchPolicyIfIdle() { @@ -12,8 +12,5 @@ export function fetchPolicyIfIdle() { } export function isProposalCompleteOrClosed(status) { - return [ - PROPOSAL_SUMMARY_STATUS_COMPLETED, - PROPOSAL_SUMMARY_STATUS_CLOSED, - ].includes(status); + return [PROPOSAL_STATUS_COMPLETED, PROPOSAL_STATUS_CLOSED].includes(status); } diff --git a/plugins-structure/packages/ticketvote/src/lib/utils.js b/plugins-structure/packages/ticketvote/src/lib/utils.js index f7fd881ca..a829209fa 100644 --- a/plugins-structure/packages/ticketvote/src/lib/utils.js +++ b/plugins-structure/packages/ticketvote/src/lib/utils.js @@ -94,12 +94,16 @@ export function getVoteBlocksDiff(block, currentHeight) { * Returns the blocks difference from current block height in milliseconds * @param {Number} block * @param {Number} currentHeight - * @param {Number} blockDuration + * @param {Number} blockDurationMinutes Block duration in minutes * @returns {Number} */ -export function getTimestampFromBlocks(block, currentHeight, blockDuration) { - const blocksDiff = getVoteBlocksDiff(block, currentHeight); - const blockTimeMinutes = blocksDiff * blockDuration; +export function getTimestampFromBlocks( + currentBlockHeight, + bestBlock, + blockDurationMinutes = 2 // TODO: SUPPORT MAINNET AND TESTNET. +) { + const blocksDiff = getVoteBlocksDiff(currentBlockHeight, bestBlock); + const blockTimeMinutes = blocksDiff * blockDurationMinutes; const mili = blockTimeMinutes * 60000; const dateMs = new Date(mili + Date.now()); return Math.round(dateMs / 1000); // returns unix timestamp diff --git a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js index 35e0f574a..1d80a1005 100644 --- a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js +++ b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js @@ -7,6 +7,7 @@ import { selectTicketvoteSummariesError, selectTicketvoteSummariesFetchedTokens, selectTicketvoteSummariesStatus, + selectTicketvoteSummariesStatusChangesByRecordToken, } from "./summariesSlice"; import { useTicketvoteSummaries } from "./useSummaries"; @@ -20,5 +21,7 @@ export const ticketvoteSummaries = { selectByStatus: selectTicketvoteSummariesByStatus, selectError: selectTicketvoteSummariesError, selectFetchedTokens: selectTicketvoteSummariesFetchedTokens, + selectStatusChangesByToken: + selectTicketvoteSummariesStatusChangesByRecordToken, useFetch: useTicketvoteSummaries, }; diff --git a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js index 2b395aa0c..54566b851 100644 --- a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js +++ b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js @@ -1,6 +1,15 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import * as api from "../../lib/api"; -import { getTicketvoteStatusCode } from "../../lib/utils"; +import { + getTicketvoteStatusCode, + getTimestampFromBlocks, +} from "../../lib/utils"; +import { + TICKETVOTE_STATUS_APPROVED, + TICKETVOTE_STATUS_FINISHED, + TICKETVOTE_STATUS_REJECTED, + TICKETVOTE_STATUS_STARTED, +} from "../../lib/constants"; import { getTicketvoteError } from "../../lib/errors"; import { validateTicketvoteStatus, @@ -101,6 +110,30 @@ export const selectTicketvoteSummariesFetchedTokens = (state, tokens) => { return Object.keys(summariesByTokens); }; +export const selectTicketvoteSummariesStatusChangesByRecordToken = ( + state, + token +) => { + const voteSummary = selectTicketvoteSummariesByRecordToken(state, token); + if (!voteSummary) return; + const { bestblock, endblockheight, startblockheight, status } = voteSummary; + if (!endblockheight || !startblockheight) return; + // TODO: Support both mainnet and testnet block params + const start = { + timestamp: getTimestampFromBlocks(startblockheight, bestblock), + status: TICKETVOTE_STATUS_STARTED, + }; + const end = { + timestamp: getTimestampFromBlocks(endblockheight, bestblock), + status: + status === TICKETVOTE_STATUS_APPROVED || + status === TICKETVOTE_STATUS_REJECTED + ? status + : TICKETVOTE_STATUS_FINISHED, + }; + return [start, end]; +}; + // Error export const selectTicketvoteSummariesError = (state) => state.ticketvoteSummaries?.error; From b9ad534a71bc78eade2066aa64a17869244e6c77 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 27 Jul 2022 16:50:36 -0300 Subject: [PATCH 18/30] fix: remove abandonment reason from proposal utils --- .../apps/politeia/src/pi/proposals/utils.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index 2522e442d..b5cd320fd 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -215,8 +215,6 @@ export function decodeProposalRecord(record) { proposalMetadata, archived: record.status === RECORD_STATUS_ARCHIVED, censored: record.status === RECORD_STATUS_CENSORED, - // TODO: remove abandonmentReason - abandonmentReason: getAbandonmentReason(userMetadata), attachments, userMetadata, }; @@ -252,18 +250,6 @@ export function getRecordStatusChangesMetadata(record) { return getStatusMetadataPayloads(payloads); } -// TODO: kill this -function getAbandonmentReason(userMetadata) { - if (!userMetadata) return null; - const payloads = userMetadata.map((md) => md.payload); - for (const status of [RECORD_STATUS_ARCHIVED, RECORD_STATUS_CENSORED]) { - const { reason } = findStatusMetadataFromPayloads(payloads, status); - if (reason) { - return reason; - } - } -} - /** * formatDateToInternationalString accepts an object of day, month and year. * It returns a string of human viewable international date from the result From f86a622014f063c3a44b20304fb3b3a9cb37e6f0 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 27 Jul 2022 16:52:44 -0300 Subject: [PATCH 19/30] fix: get proposal status changes from selector --- .../src/components/Proposal/ProposalDetails.js | 17 ++++++++--------- .../apps/politeia/src/pages/Details/Details.js | 2 ++ .../src/pages/Details/useProposalDetails.js | 4 ++-- .../apps/politeia/src/pi/proposals/selectors.js | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js index c9e0a7970..76b1acb06 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js @@ -29,7 +29,7 @@ const ProposalDetails = ({ voteSummary, piSummary, onFetchRecordTimestamps, - billingStatusChange, + proposalStatusChanges, }) => { const [open] = useModal(); @@ -65,17 +65,16 @@ const ProposalDetails = ({ const isAbandoned = proposalDetails.archived || proposalDetails.censored; + const currentStatusChange = proposalStatusChanges.find( + (s) => s.status === piSummary.status + ); + return (
- {isAbandoned && ( - - Reason: {proposalDetails.abandonmentReason} - - )} - {billingStatusChange?.reason && ( + {currentStatusChange?.reason && ( -
Proposal is {piSummary.status}.
-
Reason: {billingStatusChange.reason}.
+
Proposal is {currentStatusChange.status}.
+
Reason: {currentStatusChange.reason}
)} {comments && ( diff --git a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js index 698558f57..3147f09ab 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js +++ b/plugins-structure/apps/politeia/src/pages/Details/useProposalDetails.js @@ -35,7 +35,7 @@ function useProposalDetails({ token }) { const voteSummaryError = useSelector(ticketvoteSummaries.selectError); const commentsError = useSelector(recordComments.selectError); - const recordStatusChanges = useSelector((state) => + const proposalStatusChanges = useSelector((state) => proposals.selectStatusChangesByToken(state, fullToken) ); @@ -62,7 +62,7 @@ function useProposalDetails({ token }) { record, voteSummary, billingStatusChange, - recordStatusChanges, + proposalStatusChanges, }; } diff --git a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js index f6c6fe778..b9856988d 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js @@ -21,7 +21,7 @@ export function selectProposalStatusChangesByToken(state, token) { const voteStatusChanges = ticketvoteSummaries.selectStatusChangesByToken(state, token) || []; - // convert status from status change to pi-summary + // convert plugins status to proposal status const proposalVoteStatusChanges = voteStatusChanges.map( ({ status, ...rest }) => ({ ...rest, From 789492f29288204d34f106a1a4198bd82e40cdb5 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 27 Jul 2022 16:53:13 -0300 Subject: [PATCH 20/30] style(ticketvote): improve naming on utils --- .../packages/ticketvote/src/lib/utils.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins-structure/packages/ticketvote/src/lib/utils.js b/plugins-structure/packages/ticketvote/src/lib/utils.js index a829209fa..c14f8b702 100644 --- a/plugins-structure/packages/ticketvote/src/lib/utils.js +++ b/plugins-structure/packages/ticketvote/src/lib/utils.js @@ -80,14 +80,14 @@ export const validTicketvoteStatuses = [ ]; /** - * Returns the amount of blocks left to the currentHeight + * Returns the amount of blocks from the bestBlock * @param {Number} block - * @param {Number} currentHeight + * @param {Number} bestBlock * @returns {Number} number of blocks left */ -export function getVoteBlocksDiff(block, currentHeight) { - if (!block || !currentHeight) return 0; - return +block - currentHeight; +export function getVoteBlocksDiff(block, bestBlock) { + if (!block || !bestBlock) return 0; + return +block - bestBlock; } /** @@ -103,8 +103,7 @@ export function getTimestampFromBlocks( blockDurationMinutes = 2 // TODO: SUPPORT MAINNET AND TESTNET. ) { const blocksDiff = getVoteBlocksDiff(currentBlockHeight, bestBlock); - const blockTimeMinutes = blocksDiff * blockDurationMinutes; - const mili = blockTimeMinutes * 60000; - const dateMs = new Date(mili + Date.now()); + const blocksDiffMs = blocksDiff * blockDurationMinutes * 60 * 1000; + const dateMs = blocksDiffMs + Date.now(); return Math.round(dateMs / 1000); // returns unix timestamp } From 7e440b4f297bc305bc6f71caf4bc6107b3cf1bee Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Wed, 27 Jul 2022 17:15:45 -0300 Subject: [PATCH 21/30] style: cleanup unused code --- .../apps/politeia/src/pi/proposals/utils.js | 64 ++----------------- 1 file changed, 5 insertions(+), 59 deletions(-) diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index b5cd320fd..788f6c0a0 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -325,66 +325,12 @@ function getProposalTimestamps(record) { } /** - * getProposalStatusTagPropsFromVoteSummary returns the formatted - * `{ type, text }` props for StatusTag component for given record and - * ticketvote summary. - * @param {Record} record record object - * @param {VoteSummary} voteSummary ticketvote summary object + * getProposalStatusTagProps returns the formatted `{ type, text }` props for + * StatusTag component for given proposal summary. + * @param {Object} proposalSummary record object * @returns {Object} `{ type, text }` StatusTag props */ -export function getProposalStatusTagPropsFromVoteSummary(record, voteSummary) { - const voteMetadata = decodeVoteMetadataFile(record.files); - const isRfpSubmission = voteMetadata?.linkto; - if (record.status === RECORD_STATUS_PUBLIC && !!voteSummary) { - switch (voteSummary.status) { - case TICKETVOTE_STATUS_UNAUTHORIZED: - return { - type: "blackTime", - text: isRfpSubmission - ? "Waiting for runoff vote to start" - : "Waiting for author to authorize voting", - }; - case TICKETVOTE_STATUS_AUTHORIZED: - return { - type: "yellowTime", - text: "Waiting for admin to start voting", - }; - case TICKETVOTE_STATUS_STARTED: - return { type: "bluePending", text: "Active" }; - case TICKETVOTE_STATUS_FINISHED: - return { - type: "grayNegative", - text: "Finished", - }; - case TICKETVOTE_STATUS_REJECTED: - return { - type: "orangeNegativeCircled", - text: "Rejected", - }; - case TICKETVOTE_STATUS_APPROVED: - return { type: "greenCheck", text: "Approved" }; - default: - break; - } - } - - if (record.status === RECORD_STATUS_ARCHIVED) { - return { - type: "grayNegative", - text: "Abandoned", - }; - } - if (record.status === RECORD_STATUS_CENSORED) { - return { - type: "orangeNegativeCircled", - text: "Censored", - }; - } - - return { type: "grayNegative", text: "missing" }; -} - -export const getProposalStatusTagProps = (proposalSummary) => { +export function getProposalStatusTagProps(proposalSummary) { if (!proposalSummary?.status) return { type: "grayNegative", text: "missing" }; @@ -444,7 +390,7 @@ export const getProposalStatusTagProps = (proposalSummary) => { default: break; } -}; +} /** * showVoteStatusBar returns if vote has started, finished, approved or From a0ff4e4bfcfc7844b96e91488e24dd10ebfac28b Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 28 Jul 2022 18:05:12 -0300 Subject: [PATCH 22/30] fix(ticketvote): vote status changes cleanup --- .../packages/ticketvote/src/lib/utils.js | 56 ++++++++++++++++++- .../src/ticketvote/summaries/index.js | 3 - .../ticketvote/summaries/summariesSlice.js | 35 +----------- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/plugins-structure/packages/ticketvote/src/lib/utils.js b/plugins-structure/packages/ticketvote/src/lib/utils.js index c14f8b702..d48e6622c 100644 --- a/plugins-structure/packages/ticketvote/src/lib/utils.js +++ b/plugins-structure/packages/ticketvote/src/lib/utils.js @@ -100,10 +100,64 @@ export function getVoteBlocksDiff(block, bestBlock) { export function getTimestampFromBlocks( currentBlockHeight, bestBlock, - blockDurationMinutes = 2 // TODO: SUPPORT MAINNET AND TESTNET. + blockDurationMinutes ) { const blocksDiff = getVoteBlocksDiff(currentBlockHeight, bestBlock); const blocksDiffMs = blocksDiff * blockDurationMinutes * 60 * 1000; const dateMs = blocksDiffMs + Date.now(); return Math.round(dateMs / 1000); // returns unix timestamp } + +function getTicketvoteSummaryStatusChanges( + voteSummary, + blockDurationMinutes = 2 +) { + if (!voteSummary) return; + const { bestblock, endblockheight, startblockheight, status } = voteSummary; + if (!endblockheight || !startblockheight) return; + const start = { + timestamp: getTimestampFromBlocks( + startblockheight, + bestblock, + blockDurationMinutes + ), + status: TICKETVOTE_STATUS_STARTED, + }; + const end = { + timestamp: getTimestampFromBlocks( + endblockheight, + bestblock, + blockDurationMinutes + ), + status: + status === TICKETVOTE_STATUS_APPROVED || + status === TICKETVOTE_STATUS_REJECTED + ? status + : TICKETVOTE_STATUS_FINISHED, + }; + return [start, end]; +} + +/** + * getTicketvoteSummariesStatusChanges returns the status changes timestamps + * for each summary from `summaries`, using given `blockDurationMinutes` param. + * @param {Object} summaries Vote Summaries + * @param {Number} blockDurationMinutes Block duration in minutes + */ +export function getTicketvoteSummariesStatusChanges( + summaries, + blockDurationMinutes +) { + if (!summaries) return; + return Object.keys(summaries).reduce((statusChanges, token) => { + const statusChange = getTicketvoteSummaryStatusChanges( + summaries[token], + blockDurationMinutes + ); + if (!statusChange) return statusChanges; + return { + ...statusChanges, + [token]: statusChange, + }; + }, {}); +} diff --git a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js index 1d80a1005..35e0f574a 100644 --- a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js +++ b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/index.js @@ -7,7 +7,6 @@ import { selectTicketvoteSummariesError, selectTicketvoteSummariesFetchedTokens, selectTicketvoteSummariesStatus, - selectTicketvoteSummariesStatusChangesByRecordToken, } from "./summariesSlice"; import { useTicketvoteSummaries } from "./useSummaries"; @@ -21,7 +20,5 @@ export const ticketvoteSummaries = { selectByStatus: selectTicketvoteSummariesByStatus, selectError: selectTicketvoteSummariesError, selectFetchedTokens: selectTicketvoteSummariesFetchedTokens, - selectStatusChangesByToken: - selectTicketvoteSummariesStatusChangesByRecordToken, useFetch: useTicketvoteSummaries, }; diff --git a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js index 54566b851..2b395aa0c 100644 --- a/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js +++ b/plugins-structure/packages/ticketvote/src/ticketvote/summaries/summariesSlice.js @@ -1,15 +1,6 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import * as api from "../../lib/api"; -import { - getTicketvoteStatusCode, - getTimestampFromBlocks, -} from "../../lib/utils"; -import { - TICKETVOTE_STATUS_APPROVED, - TICKETVOTE_STATUS_FINISHED, - TICKETVOTE_STATUS_REJECTED, - TICKETVOTE_STATUS_STARTED, -} from "../../lib/constants"; +import { getTicketvoteStatusCode } from "../../lib/utils"; import { getTicketvoteError } from "../../lib/errors"; import { validateTicketvoteStatus, @@ -110,30 +101,6 @@ export const selectTicketvoteSummariesFetchedTokens = (state, tokens) => { return Object.keys(summariesByTokens); }; -export const selectTicketvoteSummariesStatusChangesByRecordToken = ( - state, - token -) => { - const voteSummary = selectTicketvoteSummariesByRecordToken(state, token); - if (!voteSummary) return; - const { bestblock, endblockheight, startblockheight, status } = voteSummary; - if (!endblockheight || !startblockheight) return; - // TODO: Support both mainnet and testnet block params - const start = { - timestamp: getTimestampFromBlocks(startblockheight, bestblock), - status: TICKETVOTE_STATUS_STARTED, - }; - const end = { - timestamp: getTimestampFromBlocks(endblockheight, bestblock), - status: - status === TICKETVOTE_STATUS_APPROVED || - status === TICKETVOTE_STATUS_REJECTED - ? status - : TICKETVOTE_STATUS_FINISHED, - }; - return [start, end]; -}; - // Error export const selectTicketvoteSummariesError = (state) => state.ticketvoteSummaries?.error; From 917e50a02c11b619fbd40085fabab778cb3fc408 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 28 Jul 2022 18:05:37 -0300 Subject: [PATCH 23/30] feat: proposal status changes service --- .../components/Proposal/ProposalDetails.js | 3 +- .../apps/politeia/src/pages/Details/index.js | 15 +++ .../politeia/src/pages/Details/listeners.js | 22 ++++ .../apps/politeia/src/pi/plugin.js | 5 + .../apps/politeia/src/pi/proposals/effects.js | 43 +++++++ .../apps/politeia/src/pi/proposals/index.js | 12 +- .../src/pi/proposals/proposalsSlice.js | 115 ++++++++++++++++++ .../politeia/src/pi/proposals/selectors.js | 52 -------- .../politeia/src/pi/proposals/services.js | 20 +++ .../apps/politeia/src/pi/services.js | 2 + 10 files changed, 235 insertions(+), 54 deletions(-) create mode 100644 plugins-structure/apps/politeia/src/pi/proposals/effects.js create mode 100644 plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js delete mode 100644 plugins-structure/apps/politeia/src/pi/proposals/selectors.js create mode 100644 plugins-structure/apps/politeia/src/pi/proposals/services.js diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js index 76b1acb06..9741bf483 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js @@ -63,9 +63,10 @@ const ProposalDetails = ({ open(ModalImages, { images, activeIndex: index }); } + // TODO: get pi status from status changes const isAbandoned = proposalDetails.archived || proposalDetails.censored; - const currentStatusChange = proposalStatusChanges.find( + const currentStatusChange = proposalStatusChanges?.find( (s) => s.status === piSummary.status ); diff --git a/plugins-structure/apps/politeia/src/pages/Details/index.js b/plugins-structure/apps/politeia/src/pages/Details/index.js index 423f97572..5d8c1017b 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/index.js +++ b/plugins-structure/apps/politeia/src/pages/Details/index.js @@ -2,8 +2,10 @@ import App from "../../app"; import { routeCleanup } from "../../utils/routeCleanup"; import { createRouteView } from "../../utils/createRouteView"; import { + fetchBillingStatusChangesListenerCreator, fetchDetailsListenerCreator, fetchProposalSummaryListenerCreator, + fetchVoteSummaryListenerCreator, recordFetchDetailsListenerCreator, } from "./listeners"; import Details from "./Details"; @@ -37,6 +39,19 @@ export default App.createRoute({ id: "pi/billingStatusChanges/single", listenerCreator: fetchProposalSummaryListenerCreator, }, + // Proposal status changes services + { + id: "pi/proposals/voteStatusChanges", + listenerCreator: fetchVoteSummaryListenerCreator, + }, + { + id: "pi/proposals/recordStatusChanges", + listenerCreator: fetchDetailsListenerCreator, + }, + { + id: "pi/proposals/billingStatusChanges", + listenerCreator: fetchBillingStatusChangesListenerCreator, + }, ], cleanup: routeCleanup, view: createRouteView(Details), diff --git a/plugins-structure/apps/politeia/src/pages/Details/listeners.js b/plugins-structure/apps/politeia/src/pages/Details/listeners.js index bf7743e16..e6e1d9a03 100644 --- a/plugins-structure/apps/politeia/src/pages/Details/listeners.js +++ b/plugins-structure/apps/politeia/src/pages/Details/listeners.js @@ -39,6 +39,18 @@ function injectCompletedOrClosedProposalEffect(effect) { }; } +function injectPayloadEffect(effect) { + return async ( + { payload }, + { getState, dispatch, unsubscribe, subscribe } + ) => { + unsubscribe(); + const state = getState(); + await effect(state, dispatch, payload); + subscribe(); + }; +} + export const fetchDetailsListenerCreator = { type: "records/fetchDetails/fulfilled", injectEffect, @@ -53,3 +65,13 @@ export const recordFetchDetailsListenerCreator = { actionCreator: fetchProposalDetails, injectEffect: injectRecordDetailsEffect, }; + +export const fetchVoteSummaryListenerCreator = { + type: "ticketvoteSummaries/fetch/fulfilled", + injectEffect: injectPayloadEffect, +}; + +export const fetchBillingStatusChangesListenerCreator = { + type: "piBilling/fetchStatusChanges/fulfilled", + injectEffect: injectPayloadEffect, +}; diff --git a/plugins-structure/apps/politeia/src/pi/plugin.js b/plugins-structure/apps/politeia/src/pi/plugin.js index a9d3986ce..40859895e 100644 --- a/plugins-structure/apps/politeia/src/pi/plugin.js +++ b/plugins-structure/apps/politeia/src/pi/plugin.js @@ -3,6 +3,7 @@ import { services } from "./services"; import policyReducer from "./policy/policySlice"; import summariesReducer from "./summaries/summariesSlice"; import billingReducer from "./billing/billingSlice"; +import proposalsReducer from "./proposals/proposalsSlice"; // Declare pi plugin interface const PiPlugin = pluginSetup({ @@ -20,6 +21,10 @@ const PiPlugin = pluginSetup({ key: "piSummaries", reducer: summariesReducer, }, + { + key: "piProposals", + reducer: proposalsReducer, + }, ], name: "pi", }); diff --git a/plugins-structure/apps/politeia/src/pi/proposals/effects.js b/plugins-structure/apps/politeia/src/pi/proposals/effects.js new file mode 100644 index 000000000..3d3fb9a5d --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/proposals/effects.js @@ -0,0 +1,43 @@ +import isArray from "lodash/isArray"; +import pick from "lodash/pick"; +import { proposals } from "./"; + +export async function setProposalsVoteStatusChangesEffect( + state, + dispatch, + summaries +) { + const { + records: { records }, + } = state; + await dispatch(proposals.setVoteStatusChanges({ summaries, records })); +} + +// Works for both single token and tokens array +export async function setProposalsRecordsStatusChangesEffect( + state, + dispatch, + payload +) { + let tokens = []; + if (!payload.token && payload.tokens && isArray(payload.tokens)) { + tokens = payload.tokens; + } else if (payload.token && !payload.tokens) { + tokens = [payload.token]; + } + const { + records: { records }, + } = state; + const fetchedRecords = pick(records, tokens); + await dispatch(proposals.setRecordStatusChanges({ records: fetchedRecords })); +} + +export async function setProposalsBillingStatusChangesEffect( + state, + dispatch, + { billingstatuschanges } +) { + await dispatch( + proposals.setBillingStatusChanges({ billings: billingstatuschanges }) + ); +} diff --git a/plugins-structure/apps/politeia/src/pi/proposals/index.js b/plugins-structure/apps/politeia/src/pi/proposals/index.js index 6300f300e..69fa22340 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/index.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/index.js @@ -1,5 +1,15 @@ -import { selectProposalStatusChangesByToken } from "./selectors"; +import { + selectProposalStatusChangesByToken, + selectProposalsStatusChanges, + setBillingStatusChanges, + setRecordStatusChanges, + setVoteStatusChanges, +} from "./proposalsSlice"; export const proposals = { + setBillingStatusChanges, + setRecordStatusChanges, + setVoteStatusChanges, + selectAllStatusChanges: selectProposalsStatusChanges, selectStatusChangesByToken: selectProposalStatusChangesByToken, }; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js b/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js new file mode 100644 index 000000000..edd0571f7 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js @@ -0,0 +1,115 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { + convertBillingStatusToProposalStatus, + convertRecordStatusToProposalStatus, + convertVoteStatusToProposalStatus, + getRecordStatusChangesMetadata, +} from "./utils"; +import { getTicketvoteSummariesStatusChanges } from "@politeiagui/ticketvote/utils"; + +export const initialState = { + statusChangesByToken: {}, +}; + +function mergeStatusChanges(statusChanges, newStatusChanges) { + return Object.keys(newStatusChanges).reduce((scs, token) => { + const scFromState = statusChanges[token] || []; + const newSc = newStatusChanges[token]; + return { + ...scs, + [token]: [...scFromState, ...newSc], + }; + }, {}); +} + +// TODO: Get parameter from correct env. +const blockTimeMinutes = 2; + +const proposalsSlice = createSlice({ + name: "piProposals", + initialState, + reducers: { + setVoteStatusChanges(state, action) { + const { summaries: voteSummariesByToken, records } = action.payload; + if (!voteSummariesByToken) return; + const voteStatusChangesByToken = getTicketvoteSummariesStatusChanges( + voteSummariesByToken, + blockTimeMinutes + ); + + const proposalsStatusChangesByToken = Object.keys( + voteStatusChangesByToken + ).reduce((proposalStatusChanges, token) => { + const record = records[token]; + return { + ...proposalStatusChanges, + [token]: voteStatusChangesByToken[token].map((vsc) => ({ + ...vsc, + status: convertVoteStatusToProposalStatus( + vsc.status, + record?.status + ), + })), + }; + }, {}); + state.statusChangesByToken = mergeStatusChanges( + state.statusChangesByToken, + proposalsStatusChangesByToken + ); + }, + setRecordStatusChanges(state, action) { + const { records } = action.payload; + const proposalsStatusChangesByToken = Object.keys(records).reduce( + (statusChanges, token) => { + const record = records[token]; + const recordStatusChanges = getRecordStatusChangesMetadata(record); + if (!recordStatusChanges) return statusChanges; + const proposalStatusChanges = recordStatusChanges.map((rsc) => ({ + ...rsc, + status: convertRecordStatusToProposalStatus( + rsc.status, + record.state + ), + })); + return { ...statusChanges, [token]: proposalStatusChanges }; + }, + {} + ); + state.statusChangesByToken = mergeStatusChanges( + state.statusChangesByToken, + proposalsStatusChangesByToken + ); + }, + setBillingStatusChanges(state, action) { + const { billings } = action.payload; + const proposalsStatusChangesByToken = Object.keys(billings).reduce( + (statusChanges, token) => ({ + ...statusChanges, + [token]: billings[token].map((bsc) => ({ + ...bsc, + status: convertBillingStatusToProposalStatus(bsc.status), + })), + }), + {} + ); + state.statusChangesByToken = mergeStatusChanges( + state.statusChangesByToken, + proposalsStatusChangesByToken + ); + }, + }, +}); + +export const { + setVoteStatusChanges, + setRecordStatusChanges, + setBillingStatusChanges, +} = proposalsSlice.actions; + +// Selectors +export const selectProposalStatusChangesByToken = (state, token) => + state.piProposals?.statusChangesByToken[token]; +export const selectProposalsStatusChanges = (state) => + state.piProposals?.statusChangesByToken; + +export default proposalsSlice.reducer; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js b/plugins-structure/apps/politeia/src/pi/proposals/selectors.js deleted file mode 100644 index b9856988d..000000000 --- a/plugins-structure/apps/politeia/src/pi/proposals/selectors.js +++ /dev/null @@ -1,52 +0,0 @@ -import { records } from "@politeiagui/core/records"; -import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; -import { piBilling } from "../billing"; -import { piSummaries } from "../summaries"; -import { - convertBillingStatusToProposalStatus, - convertRecordStatusToProposalStatus, - convertVoteStatusToProposalStatus, - getRecordStatusChangesMetadata, -} from "./utils"; - -export function selectProposalStatusChangesByToken(state, token) { - // Get Record and its Proposal Summary - const record = records.selectByToken(state, token); - const piSummary = piSummaries.selectByToken(state, token); - if (!record || !piSummary) return; - // Status changes - // TODO: create a core/records selector for record status changes - const recordStatusChanges = getRecordStatusChangesMetadata(record); - const billingStatusChanges = piBilling.selectByToken(state, token) || []; - const voteStatusChanges = - ticketvoteSummaries.selectStatusChangesByToken(state, token) || []; - - // convert plugins status to proposal status - const proposalVoteStatusChanges = voteStatusChanges.map( - ({ status, ...rest }) => ({ - ...rest, - status: convertVoteStatusToProposalStatus(status, record.status), - }) - ); - const proposalRecordStatusChanges = recordStatusChanges.map( - ({ status, ...rest }) => ({ - ...rest, - status: convertRecordStatusToProposalStatus(status, record.state), - }) - ); - const proposalBillingStatusChanges = billingStatusChanges.map( - ({ status, ...rest }) => ({ - ...rest, - status: convertBillingStatusToProposalStatus(status), - }) - ); - - // Order by most recent status change - const statusChangesOrdered = [ - ...proposalBillingStatusChanges, - ...proposalRecordStatusChanges, - ...proposalVoteStatusChanges, - ].sort((a, b) => b.timestamp - a.timestamp); - - return statusChangesOrdered; -} diff --git a/plugins-structure/apps/politeia/src/pi/proposals/services.js b/plugins-structure/apps/politeia/src/pi/proposals/services.js new file mode 100644 index 000000000..ad6ec1576 --- /dev/null +++ b/plugins-structure/apps/politeia/src/pi/proposals/services.js @@ -0,0 +1,20 @@ +import { + setProposalsBillingStatusChangesEffect, + setProposalsRecordsStatusChangesEffect, + setProposalsVoteStatusChangesEffect, +} from "./effects"; + +export const services = [ + { + id: "pi/proposals/voteStatusChanges", + effect: setProposalsVoteStatusChangesEffect, + }, + { + id: "pi/proposals/recordStatusChanges", + effect: setProposalsRecordsStatusChangesEffect, + }, + { + id: "pi/proposals/billingStatusChanges", + effect: setProposalsBillingStatusChangesEffect, + }, +]; diff --git a/plugins-structure/apps/politeia/src/pi/services.js b/plugins-structure/apps/politeia/src/pi/services.js index a988fe366..2388679f0 100644 --- a/plugins-structure/apps/politeia/src/pi/services.js +++ b/plugins-structure/apps/politeia/src/pi/services.js @@ -1,10 +1,12 @@ import { fetchPolicyIfIdle } from "./utils"; import { services as summariesServices } from "./summaries/services"; import { services as billingServices } from "./billing/services"; +import { services as proposalsServices } from "./proposals/services"; export const services = [ ...billingServices, ...summariesServices, + ...proposalsServices, { id: "pi/new", action: async () => { From 9298bd7a30c071a601d64dc4ea50062e32754f47 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 29 Jul 2022 18:31:04 -0300 Subject: [PATCH 24/30] feat(common-ui): improve RecordCard grid --- .../src/components/RecordCard/RecordCard.js | 45 +++++++------- .../components/RecordCard/styles.module.css | 59 +++++++++++++++---- 2 files changed, 69 insertions(+), 35 deletions(-) diff --git a/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js b/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js index c509dbf5b..43247c1ce 100644 --- a/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js +++ b/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js @@ -1,12 +1,24 @@ import React from "react"; -import { Card, Column, H2, Row, classNames } from "pi-ui"; +import { Card, H2, classNames } from "pi-ui"; import styles from "./styles.module.css"; +const TitleWrapper = ({ titleLink, children }) => + !titleLink ? ( +

{children}

+ ) : ( +

+ + {children} + +

+ ); + export function RecordCard({ title, titleLink, subtitle, rightHeader, + rightHeaderSubtitle, secondRow, thirdRow, fourthRow, @@ -23,27 +35,16 @@ export function RecordCard({ className )} > - - - {!titleLink ? ( -

{title}

- ) : ( -

- - {title} - -

- )} -
- - {rightHeader} - - -
{subtitle}
-
-
- {secondRow && {secondRow}} - {thirdRow && {thirdRow}} +
+

+ {title} +

+
{rightHeader}
+
{subtitle}
+
{rightHeaderSubtitle}
+
+ {secondRow &&
{secondRow}
} + {thirdRow &&
{thirdRow}
} {fourthRow &&
{fourthRow}
}
{footer}
diff --git a/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css b/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css index b50c98ad5..4ebc4b026 100644 --- a/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css +++ b/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css @@ -15,14 +15,6 @@ margin-top: var(--spacing-medium); } -.subtitle { - margin-top: var(--spacing-small); -} - -.rightHeader { - justify-self: end; -} - .footer { display: flex; width: 100%; @@ -41,6 +33,37 @@ border-color: var(--separator-color); } +.headerWrapper { + align-items: center; + display: grid; + grid-template-columns: 2fr 2fr 2fr 1fr; + grid-template-rows: 1fr 1fr; + gap: var(--spacing-small) 0em; + grid-template-areas: + "title title title rightHeader" + "subtitle subtitle subtitle rightHeaderSubtitle"; +} + +.title { + grid-area: title; +} + +.rightHeader { + justify-self: end; + grid-area: rightHeader; + text-align: end; +} + +.rightHeaderSubtitle { + justify-self: end; + grid-area: rightHeaderSubtitle; + text-align: end; +} + +.subtitle { + grid-area: subtitle; +} + @media screen and (max-width: 768px) { .firstRow { flex-direction: column-reverse; @@ -48,12 +71,22 @@ } .rightHeader { - margin-bottom: 1rem; justify-self: start; - grid-row: 1; - width: 100%; + text-align: start; + } + .rightHeaderSubtitle { + justify-self: start; + text-align: start; } - .header { - margin-top: var(--spacing-small); + + .headerWrapper { + justify-content: start; + justify-self: start; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto; + grid-template-areas: + "rightHeader rightHeaderSubtitle" + "title title" + "subtitle subtitle"; } } From 85c02795163cba03e7266f16fbdf6cbe4a1b5eb8 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 29 Jul 2022 18:33:58 -0300 Subject: [PATCH 25/30] fix(common-ui): Event prop 'event' node --- .../packages/common-ui/src/components/Event/Event.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins-structure/packages/common-ui/src/components/Event/Event.js b/plugins-structure/packages/common-ui/src/components/Event/Event.js index 62de1c42e..12f1f65e2 100644 --- a/plugins-structure/packages/common-ui/src/components/Event/Event.js +++ b/plugins-structure/packages/common-ui/src/components/Event/Event.js @@ -12,13 +12,15 @@ const Event = ({ event, timestamp, className, size }) => ( className={classNames(styles.eventTooltip, className)} truncate size={size} - >{`${event} ${timeAgo}`} + > + {event} {timeAgo} + )} ); Event.propTypes = { - event: PropTypes.string, + event: PropTypes.node, timestamp: PropTypes.number, show: PropTypes.bool, }; From 7c37b780b56de978c0353659bc9f1c7ccc455973 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 29 Jul 2022 18:36:52 -0300 Subject: [PATCH 26/30] feat(ticketvote): improve summary status changes --- plugins-structure/packages/ticketvote/src/lib/utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins-structure/packages/ticketvote/src/lib/utils.js b/plugins-structure/packages/ticketvote/src/lib/utils.js index d48e6622c..fe1d3a1e9 100644 --- a/plugins-structure/packages/ticketvote/src/lib/utils.js +++ b/plugins-structure/packages/ticketvote/src/lib/utils.js @@ -114,13 +114,16 @@ function getTicketvoteSummaryStatusChanges( ) { if (!voteSummary) return; const { bestblock, endblockheight, startblockheight, status } = voteSummary; - if (!endblockheight || !startblockheight) return; + if (status === TICKETVOTE_STATUS_UNAUTHORIZED) return; + if (status === TICKETVOTE_STATUS_AUTHORIZED) + return [{ timestamp: 0, status }]; const start = { timestamp: getTimestampFromBlocks( startblockheight, bestblock, blockDurationMinutes ), + blocksCount: getVoteBlocksDiff(startblockheight, bestblock), status: TICKETVOTE_STATUS_STARTED, }; const end = { @@ -129,6 +132,7 @@ function getTicketvoteSummaryStatusChanges( bestblock, blockDurationMinutes ), + blocksCount: getVoteBlocksDiff(endblockheight, bestblock), status: status === TICKETVOTE_STATUS_APPROVED || status === TICKETVOTE_STATUS_REJECTED From 2796a7988b0e8cb345dc21181777d6c93447d863 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Fri, 29 Jul 2022 20:11:35 -0300 Subject: [PATCH 27/30] feat: Add proposal status changes timestamps --- .../src/components/Proposal/ProposalCard.js | 21 ++++--- .../components/Proposal/ProposalDetails.js | 14 +++-- .../Proposal/common/ProposalStatusLabel.js | 26 ++++++++ .../Proposal/common/ProposalStatusTag.js | 6 +- .../Proposal/common/ProposalSubtitle.js | 4 +- .../src/components/Proposal/common/index.js | 1 + .../Proposal/common/styles.module.css | 10 +++ .../politeia/src/pages/Details/Details.js | 4 +- .../apps/politeia/src/pages/Details/index.js | 3 +- .../politeia/src/pages/Details/listeners.js | 5 ++ .../src/pages/Details/useProposalDetails.js | 4 +- .../src/pages/Home/common/StatusList.js | 2 + .../apps/politeia/src/pages/Home/index.js | 16 +++++ .../apps/politeia/src/pages/Home/listeners.js | 27 ++++++++ .../politeia/src/pages/Home/useStatusList.js | 4 ++ .../apps/politeia/src/pi/proposals/effects.js | 20 ++---- .../src/pi/proposals/proposalsSlice.js | 18 +++--- .../apps/politeia/src/pi/proposals/utils.js | 62 +++++++++++-------- .../src/components/RecordCard/RecordCard.js | 16 ++--- .../components/RecordCard/styles.module.css | 28 +++------ 20 files changed, 192 insertions(+), 99 deletions(-) create mode 100644 plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js index 051074eae..9ffe8d04b 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalCard.js @@ -1,23 +1,27 @@ import React from "react"; -import { Button, StatusTag } from "pi-ui"; +import { Button } from "pi-ui"; import { RecordCard } from "@politeiagui/common-ui"; import { CommentsCount } from "@politeiagui/comments/ui"; import { getShortToken } from "@politeiagui/core/records/utils"; +import { decodeProposalRecord } from "../../pi/proposals/utils"; import { - decodeProposalRecord, - getProposalStatusTagProps, -} from "../../pi/proposals/utils"; -import { ProposalStatusBar, ProposalSubtitle } from "./common"; + ProposalStatusBar, + ProposalStatusLabel, + ProposalStatusTag, + ProposalSubtitle, +} from "./common"; const ProposalCard = ({ record, voteSummary, commentsCount, proposalSummary, + proposalStatusChanges, }) => { const proposal = decodeProposalRecord(record); - const statusTagProps = getProposalStatusTagProps(proposalSummary); const proposalLink = `/record/${getShortToken(proposal.token)}`; + const currentStatusChange = + proposalSummary && proposalStatusChanges?.[proposalSummary.status]; return (
} - rightHeader={} + rightHeader={} + rightHeaderSubtitle={ + + } secondRow={} footer={ <> diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js index 9741bf483..5b07f6db4 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ProposalDetails.js @@ -16,6 +16,7 @@ import { ProposalDownloads, ProposalMetadata, ProposalStatusBar, + ProposalStatusLabel, ProposalStatusTag, ProposalSubtitle, } from "./common"; @@ -27,7 +28,7 @@ import ModalProposalDiff from "./ModalProposalDiff"; const ProposalDetails = ({ record, voteSummary, - piSummary, + proposalSummary, onFetchRecordTimestamps, proposalStatusChanges, }) => { @@ -63,12 +64,10 @@ const ProposalDetails = ({ open(ModalImages, { images, activeIndex: index }); } - // TODO: get pi status from status changes const isAbandoned = proposalDetails.archived || proposalDetails.censored; - const currentStatusChange = proposalStatusChanges?.find( - (s) => s.status === piSummary.status - ); + const currentStatusChange = + proposalSummary && proposalStatusChanges?.[proposalSummary.status]; return (
@@ -92,7 +91,10 @@ const ProposalDetails = ({ onChangeVersion={handleChangeVersion} /> } - rightHeader={} + rightHeader={} + rightHeaderSubtitle={ + + } secondRow={
diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js new file mode 100644 index 000000000..26a8d356a --- /dev/null +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js @@ -0,0 +1,26 @@ +import React from "react"; +import { Event } from "@politeiagui/common-ui"; +import styles from "./styles.module.css"; +import { getProposalStatusEvent } from "../../../pi/proposals/utils"; +import { PROPOSAL_STATUS_VOTE_STARTED } from "../../../pi/lib/constants"; + +function ProposalStatusLabel({ statusChange }) { + const event = getProposalStatusEvent(statusChange); + return statusChange?.timestamp && event ? ( +
+ + {statusChange.blocksCount && + statusChange.status === PROPOSAL_STATUS_VOTE_STARTED && ( +
+ {-statusChange.blocksCount} blocks left +
+ )} +
+ ) : null; +} + +export default ProposalStatusLabel; diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js index 497256225..9c53d4b5e 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusTag.js @@ -2,9 +2,9 @@ import React from "react"; import { StatusTag } from "pi-ui"; import { getProposalStatusTagProps } from "../../../pi/proposals/utils"; -function ProposalStatusTag({ piSummary }) { - const statusTagProps = getProposalStatusTagProps(piSummary); - return ; +function ProposalStatusTag({ proposalSummary }) { + const { text, type } = getProposalStatusTagProps(proposalSummary); + return ; } export default ProposalStatusTag; diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalSubtitle.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalSubtitle.js index 59bc6d76d..53a4961f0 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalSubtitle.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalSubtitle.js @@ -12,14 +12,12 @@ function ProposalSubtitle({ timestamps = {}, onChangeVersion, }) { - const { publishedat, editedat, abandonedat, censoredat } = timestamps; + const { publishedat, editedat } = timestamps; return ( {username} {publishedat && } {editedat && } - {abandonedat && } - {censoredat && } {version > 1 && (onChangeVersion ? ( recordComments.selectByToken(state, fullToken) ); - const piSummary = useSelector((state) => + const proposalSummary = useSelector((state) => piSummaries.selectByToken(state, fullToken) ); const billingStatusChange = useSelector((state) => @@ -58,7 +58,7 @@ function useProposalDetails({ token }) { detailsStatus, fullToken, onFetchRecordTimestamps, - piSummary, + proposalSummary, record, voteSummary, billingStatusChange, diff --git a/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js b/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js index 83aaefe24..643535f3d 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js +++ b/plugins-structure/apps/politeia/src/pages/Home/common/StatusList.js @@ -38,6 +38,7 @@ function StatusList({ fetchNextBatch, recordsInOrder, areAllInventoryEntriesFetched, + proposalsStatusChanges, } = useStatusList({ inventory, inventoryStatus, status }); function handleFetchMore() { @@ -85,6 +86,7 @@ function StatusList({ commentsCount={countComments?.[token]} voteSummary={voteSummaries?.[token]} proposalSummary={proposalSummaries?.[token]} + proposalStatusChanges={proposalsStatusChanges?.[token]} /> ); })} diff --git a/plugins-structure/apps/politeia/src/pages/Home/index.js b/plugins-structure/apps/politeia/src/pages/Home/index.js index dbeaa3389..f2a05c568 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/index.js +++ b/plugins-structure/apps/politeia/src/pages/Home/index.js @@ -3,10 +3,13 @@ import { routeCleanup } from "../../utils/routeCleanup"; import { createRouteView } from "../../utils/createRouteView"; import Home from "./Home"; import { + fetchBillingStatusChangesListenerCreator, fetchNextBatchBillingStatusesListenerCreator, fetchNextBatchCountListenerCreator, fetchNextBatchRecordsListenerCreator, fetchNextBatchSummariesListenerCreator, + fetchRecordsListenerCreator, + fetchVoteSummariesListenerCreator, listeners, } from "./listeners"; @@ -36,6 +39,19 @@ export default App.createRoute({ id: "comments/count", listenerCreator: fetchNextBatchCountListenerCreator, }, + // Proposals Status changes + { + id: "pi/proposals/voteStatusChanges", + listenerCreator: fetchVoteSummariesListenerCreator, + }, + { + id: "pi/proposals/recordStatusChanges", + listenerCreator: fetchRecordsListenerCreator, + }, + { + id: "pi/proposals/billingStatusChanges", + listenerCreator: fetchBillingStatusChangesListenerCreator, + }, ], listeners, cleanup: routeCleanup, diff --git a/plugins-structure/apps/politeia/src/pages/Home/listeners.js b/plugins-structure/apps/politeia/src/pages/Home/listeners.js index 628c63b26..0b7bce620 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/listeners.js +++ b/plugins-structure/apps/politeia/src/pages/Home/listeners.js @@ -30,6 +30,18 @@ function injectRecordsBatchEffect(effect) { }; } +function injectPayloadEffect(effect) { + return async ( + { payload }, + { getState, dispatch, unsubscribe, subscribe } + ) => { + unsubscribe(); + const state = getState(); + await effect(state, dispatch, payload); + subscribe(); + }; +} + export const fetchNextBatchCountListenerCreator = { actionCreator: fetchNextBatchCount, injectEffect, @@ -50,6 +62,21 @@ export const fetchNextBatchBillingStatusesListenerCreator = { injectEffect, }; +export const fetchVoteSummariesListenerCreator = { + type: "ticketvoteSummaries/fetch/fulfilled", + injectEffect: injectPayloadEffect, +}; + +export const fetchRecordsListenerCreator = { + type: "records/fetch/fulfilled", + injectEffect: injectPayloadEffect, +}; + +export const fetchBillingStatusChangesListenerCreator = { + type: "piBilling/fetchStatusChanges/fulfilled", + injectEffect: injectPayloadEffect, +}; + export const listeners = [ { type: "ticketvoteInventory/fetch/fulfilled", diff --git a/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js b/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js index c198df6c3..f9904f573 100644 --- a/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js +++ b/plugins-structure/apps/politeia/src/pages/Home/useStatusList.js @@ -7,6 +7,7 @@ import { ticketvoteSummaries } from "@politeiagui/ticketvote/summaries"; import { commentsCount } from "@politeiagui/comments/count"; import { piSummaries } from "../../pi/summaries"; import { piBilling } from "../../pi/billing"; +import { proposals } from "../../pi/proposals"; function areAllEntriesFetched(inventoryList, records) { if (!inventoryList) return false; @@ -30,6 +31,8 @@ function useStatusList({ inventory, inventoryStatus }) { const allRecords = useSelector(records.selectAll); const billingStatusChanges = useSelector(piBilling.selectAll); + const proposalsStatusChanges = useSelector(proposals.selectAllStatusChanges); + const hasMoreRecords = recordsInOrder.length !== 0 && recordsInOrder.length < inventory.length; @@ -47,6 +50,7 @@ function useStatusList({ inventory, inventoryStatus }) { recordsPageSize, areAllInventoryEntriesFetched: areAllEntriesFetched(inventory, allRecords), billingStatusChanges, + proposalsStatusChanges, }; } diff --git a/plugins-structure/apps/politeia/src/pi/proposals/effects.js b/plugins-structure/apps/politeia/src/pi/proposals/effects.js index 3d3fb9a5d..ee0378391 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/effects.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/effects.js @@ -1,5 +1,3 @@ -import isArray from "lodash/isArray"; -import pick from "lodash/pick"; import { proposals } from "./"; export async function setProposalsVoteStatusChangesEffect( @@ -13,23 +11,17 @@ export async function setProposalsVoteStatusChangesEffect( await dispatch(proposals.setVoteStatusChanges({ summaries, records })); } -// Works for both single token and tokens array +// Works for both single record or records batch export async function setProposalsRecordsStatusChangesEffect( state, dispatch, payload ) { - let tokens = []; - if (!payload.token && payload.tokens && isArray(payload.tokens)) { - tokens = payload.tokens; - } else if (payload.token && !payload.tokens) { - tokens = [payload.token]; - } - const { - records: { records }, - } = state; - const fetchedRecords = pick(records, tokens); - await dispatch(proposals.setRecordStatusChanges({ records: fetchedRecords })); + const records = + payload.state && payload.status + ? { [payload.censorshiprecord.token]: payload } + : payload; + await dispatch(proposals.setRecordStatusChanges({ records })); } export async function setProposalsBillingStatusChangesEffect( diff --git a/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js b/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js index edd0571f7..0cecb66de 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js @@ -6,20 +6,25 @@ import { getRecordStatusChangesMetadata, } from "./utils"; import { getTicketvoteSummariesStatusChanges } from "@politeiagui/ticketvote/utils"; +import keyBy from "lodash/keyBy"; export const initialState = { statusChangesByToken: {}, }; function mergeStatusChanges(statusChanges, newStatusChanges) { - return Object.keys(newStatusChanges).reduce((scs, token) => { - const scFromState = statusChanges[token] || []; + const conflicting = Object.keys(newStatusChanges).reduce((scs, token) => { + const scFromState = statusChanges[token] || {}; const newSc = newStatusChanges[token]; return { ...scs, - [token]: [...scFromState, ...newSc], + [token]: { ...scFromState, ...keyBy(newSc, "status") }, }; }, {}); + return { + ...statusChanges, + ...conflicting, + }; } // TODO: Get parameter from correct env. @@ -29,14 +34,13 @@ const proposalsSlice = createSlice({ name: "piProposals", initialState, reducers: { - setVoteStatusChanges(state, action) { + setVoteStatusChanges: (state, action) => { const { summaries: voteSummariesByToken, records } = action.payload; if (!voteSummariesByToken) return; const voteStatusChangesByToken = getTicketvoteSummariesStatusChanges( voteSummariesByToken, blockTimeMinutes ); - const proposalsStatusChangesByToken = Object.keys( voteStatusChangesByToken ).reduce((proposalStatusChanges, token) => { @@ -57,7 +61,7 @@ const proposalsSlice = createSlice({ proposalsStatusChangesByToken ); }, - setRecordStatusChanges(state, action) { + setRecordStatusChanges: (state, action) => { const { records } = action.payload; const proposalsStatusChangesByToken = Object.keys(records).reduce( (statusChanges, token) => { @@ -80,7 +84,7 @@ const proposalsSlice = createSlice({ proposalsStatusChangesByToken ); }, - setBillingStatusChanges(state, action) { + setBillingStatusChanges: (state, action) => { const { billings } = action.payload; const proposalsStatusChangesByToken = Object.keys(billings).reduce( (statusChanges, token) => ({ diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index 788f6c0a0..ecc130a98 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -283,51 +283,33 @@ export function getPublicStatusChangeMetadata(userMetadata) { * getProposalTimestamps returns published, censored, edited and abandoned * timestamps for given record. Default timestamps values are 0. * @param {Record} record record object - * @returns {Object} `{publishedat: number, editedat: number, censoredat: number, - * abandonedat: number}` - Object with publishedat, censoredat, abandonedat + * @returns {Object} `{publishedat: number, editedat: number}` */ function getProposalTimestamps(record) { - if (!record) - return { publishedat: 0, editedat: 0, censoredat: 0, abandonedat: 0 }; + if (!record) return { publishedat: 0, editedat: 0 }; let publishedat = 0, - censoredat = 0, - abandonedat = 0, editedat = 0; const { status, timestamp, version, metadata } = record; const userMetadata = decodeProposalUserMetadata(metadata); - // unreviewed - if (status === RECORD_STATUS_UNREVIEWED) { - publishedat = timestamp; - } - // publlished but not edited - if (status === RECORD_STATUS_PUBLIC && version <= 1) { + + if (status === RECORD_STATUS_UNREVIEWED || version <= 1) { publishedat = timestamp; } // edited, have to grab published timestamp from metadata - if (status === RECORD_STATUS_PUBLIC && version > 1) { + if (version > 1) { const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); publishedat = publishedMetadata.timestamp; editedat = timestamp; } - if (status === RECORD_STATUS_CENSORED) { - const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); - publishedat = publishedMetadata.timestamp; - censoredat = timestamp; - } - if (status === RECORD_STATUS_ARCHIVED) { - const publishedMetadata = getPublicStatusChangeMetadata(userMetadata); - publishedat = publishedMetadata.timestamp; - abandonedat = timestamp; - } - return { publishedat, editedat, censoredat, abandonedat }; + return { publishedat, editedat }; } /** * getProposalStatusTagProps returns the formatted `{ type, text }` props for * StatusTag component for given proposal summary. - * @param {Object} proposalSummary record object + * @param {Object} proposalSummary * @returns {Object} `{ type, text }` StatusTag props */ export function getProposalStatusTagProps(proposalSummary) { @@ -391,6 +373,34 @@ export function getProposalStatusTagProps(proposalSummary) { break; } } +/** + * getProposalStatusEvent returns the formatted status change event description. + * @param {Object} statusChange proposal status change + * @returns {String} event description + */ +export function getProposalStatusEvent(statusChange) { + if (!statusChange?.status) return null; + switch (statusChange.status) { + case PROPOSAL_STATUS_VOTE_STARTED: + return "vote ends"; + case PROPOSAL_STATUS_ACTIVE: + case PROPOSAL_STATUS_APPROVED: + case PROPOSAL_STATUS_REJECTED: + return "vote ended"; + case PROPOSAL_STATUS_CLOSED: + return "billing closed"; + case PROPOSAL_STATUS_COMPLETED: + return "billing completed"; + case PROPOSAL_STATUS_UNVETTED_ABANDONED: + case PROPOSAL_STATUS_ABANDONED: + return "abandoned"; + case PROPOSAL_STATUS_UNVETTED_CENSORED: + case PROPOSAL_STATUS_CENSORED: + return "censored"; + default: + return null; + } +} /** * showVoteStatusBar returns if vote has started, finished, approved or @@ -449,7 +459,7 @@ export function convertVoteStatusToProposalStatus(voteStatus, recordStatus) { [TICKETVOTE_STATUS_UNAUTHORIZED]: PROPOSAL_STATUS_UNDER_REVIEW, [TICKETVOTE_STATUS_AUTHORIZED]: PROPOSAL_STATUS_VOTE_AUTHORIZED, [TICKETVOTE_STATUS_STARTED]: PROPOSAL_STATUS_VOTE_STARTED, - [TICKETVOTE_STATUS_APPROVED]: PROPOSAL_STATUS_APPROVED, + [TICKETVOTE_STATUS_APPROVED]: PROPOSAL_STATUS_ACTIVE, [TICKETVOTE_STATUS_REJECTED]: PROPOSAL_STATUS_REJECTED, [TICKETVOTE_STATUS_FINISHED]: PROPOSAL_STATUS_VOTE_ENDED, [TICKETVOTE_STATUS_INELIGIBLE]: diff --git a/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js b/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js index 43247c1ce..896bbd7ea 100644 --- a/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js +++ b/plugins-structure/packages/common-ui/src/components/RecordCard/RecordCard.js @@ -4,13 +4,11 @@ import styles from "./styles.module.css"; const TitleWrapper = ({ titleLink, children }) => !titleLink ? ( -

{children}

+ children ) : ( -

- - {children} - -

+ + {children} + ); export function RecordCard({ @@ -39,9 +37,11 @@ export function RecordCard({

{title}

-
{rightHeader}
+
+ {rightHeader} + {rightHeaderSubtitle} +
{subtitle}
-
{rightHeaderSubtitle}
{secondRow &&
{secondRow}
} {thirdRow &&
{thirdRow}
} diff --git a/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css b/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css index 4ebc4b026..cf76d76be 100644 --- a/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css +++ b/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css @@ -36,30 +36,27 @@ .headerWrapper { align-items: center; display: grid; - grid-template-columns: 2fr 2fr 2fr 1fr; + grid-template-columns: 6fr 1fr; grid-template-rows: 1fr 1fr; gap: var(--spacing-small) 0em; grid-template-areas: - "title title title rightHeader" - "subtitle subtitle subtitle rightHeaderSubtitle"; + "title rightHeader" + "subtitle rightHeader"; } .title { grid-area: title; + align-self: flex-start; } .rightHeader { + align-self: flex-start; + align-items: center; justify-self: end; grid-area: rightHeader; text-align: end; } -.rightHeaderSubtitle { - justify-self: end; - grid-area: rightHeaderSubtitle; - text-align: end; -} - .subtitle { grid-area: subtitle; } @@ -74,19 +71,10 @@ justify-self: start; text-align: start; } - .rightHeaderSubtitle { - justify-self: start; - text-align: start; - } .headerWrapper { - justify-content: start; - justify-self: start; - grid-template-columns: 1fr 1fr; + grid-template-columns: auto; grid-template-rows: auto; - grid-template-areas: - "rightHeader rightHeaderSubtitle" - "title title" - "subtitle subtitle"; + grid-template-areas: "rightHeader" "title" "subtitle "; } } From 19b05a335807c036f99370b34e8475378d07f107 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Tue, 2 Aug 2022 12:19:10 -0300 Subject: [PATCH 28/30] fix: record card styles for status tag description --- .../components/Proposal/ModalProposalDiff.js | 4 +-- .../Proposal/ModalProposalDiff.module.css | 2 ++ .../Proposal/common/ProposalStatusLabel.js | 28 +++++++++++-------- .../Proposal/common/styles.module.css | 19 +++++++++++-- .../apps/politeia/src/pi/proposals/utils.js | 18 ++++++------ .../components/RecordCard/styles.module.css | 5 ++-- 6 files changed, 49 insertions(+), 27 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js b/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js index 3f5e2228a..a920ff30a 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/ModalProposalDiff.js @@ -58,7 +58,7 @@ function AttachmentsDiff({ newFiles, oldFiles }) { function VersionSelector({ maxVersion, onChange, current, minVersion = 1 }) { const options = range(maxVersion, minVersion - 1, -1).map((v) => ({ - label: `version ${v}`, + label: `v${v}`, value: v, })); function getValueOption(value) { @@ -254,7 +254,7 @@ function ProposalDiff({ setDiffView={setIsMarkdownView} /> } - subtitle={ + secondRow={ *:last-child { diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js index 26a8d356a..6bb2d8f2f 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js @@ -1,26 +1,32 @@ import React from "react"; import { Event } from "@politeiagui/common-ui"; import styles from "./styles.module.css"; -import { getProposalStatusEvent } from "../../../pi/proposals/utils"; +import { getProposalStatusDescription } from "../../../pi/proposals/utils"; import { PROPOSAL_STATUS_VOTE_STARTED } from "../../../pi/lib/constants"; function ProposalStatusLabel({ statusChange }) { - const event = getProposalStatusEvent(statusChange); - return statusChange?.timestamp && event ? ( -
- + if (!statusChange) return null; + const { event, description } = getProposalStatusDescription(statusChange); + return ( +
+ {event && ( + + )} + {description && ( +
{description}
+ )} {statusChange.blocksCount && statusChange.status === PROPOSAL_STATUS_VOTE_STARTED && ( -
+
{-statusChange.blocksCount} blocks left
)}
- ) : null; + ); } export default ProposalStatusLabel; diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css b/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css index 65fd5f4b6..50ec99e21 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css @@ -2,12 +2,27 @@ color: var(--text-secondary-color); } +.statusLabelWrapper { + position: absolute; + right: var(--container-padding-right); +} + .statusLabel { - margin-top: 0.5rem; font-size: var(--font-size-small); } -.statusLabelBlocks { +.statusLabelText { font-size: var(--font-size-small); color: var(--text-secondary-color); } + +@media screen and (max-width: 768px) { + .statusLabelWrapper { + top: 2.3rem; + display: flex; + } + .statusLabelText::before { + content: "•"; + padding: 0 0.5rem; + } +} diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index ecc130a98..8ae1c4f8a 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -374,31 +374,31 @@ export function getProposalStatusTagProps(proposalSummary) { } } /** - * getProposalStatusEvent returns the formatted status change event description. + * getProposalStatusDescription returns the formatted status change event description. * @param {Object} statusChange proposal status change * @returns {String} event description */ -export function getProposalStatusEvent(statusChange) { +export function getProposalStatusDescription(statusChange) { if (!statusChange?.status) return null; switch (statusChange.status) { case PROPOSAL_STATUS_VOTE_STARTED: - return "vote ends"; + return { event: "vote ends" }; case PROPOSAL_STATUS_ACTIVE: case PROPOSAL_STATUS_APPROVED: case PROPOSAL_STATUS_REJECTED: - return "vote ended"; + return { event: "vote ended" }; case PROPOSAL_STATUS_CLOSED: - return "billing closed"; + return { event: "billing closed" }; case PROPOSAL_STATUS_COMPLETED: - return "billing completed"; + return { event: "billing completed" }; case PROPOSAL_STATUS_UNVETTED_ABANDONED: case PROPOSAL_STATUS_ABANDONED: - return "abandoned"; + return { event: "abandoned" }; case PROPOSAL_STATUS_UNVETTED_CENSORED: case PROPOSAL_STATUS_CENSORED: - return "censored"; + return { event: "censored" }; default: - return null; + return {}; } } diff --git a/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css b/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css index cf76d76be..5d7a238f2 100644 --- a/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css +++ b/plugins-structure/packages/common-ui/src/components/RecordCard/styles.module.css @@ -36,9 +36,8 @@ .headerWrapper { align-items: center; display: grid; - grid-template-columns: 6fr 1fr; - grid-template-rows: 1fr 1fr; - gap: var(--spacing-small) 0em; + grid-template-columns: 5fr 2fr; + gap: var(--spacing-small); grid-template-areas: "title rightHeader" "subtitle rightHeader"; From 98550e2557429c43ca04c8e8dc8a034ea015ea2d Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Tue, 2 Aug 2022 15:38:27 -0300 Subject: [PATCH 29/30] fix(ticketvote): use correct vote status changes --- .../packages/ticketvote/src/lib/utils.js | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/plugins-structure/packages/ticketvote/src/lib/utils.js b/plugins-structure/packages/ticketvote/src/lib/utils.js index fe1d3a1e9..92f9ef280 100644 --- a/plugins-structure/packages/ticketvote/src/lib/utils.js +++ b/plugins-structure/packages/ticketvote/src/lib/utils.js @@ -114,32 +114,26 @@ function getTicketvoteSummaryStatusChanges( ) { if (!voteSummary) return; const { bestblock, endblockheight, startblockheight, status } = voteSummary; - if (status === TICKETVOTE_STATUS_UNAUTHORIZED) return; - if (status === TICKETVOTE_STATUS_AUTHORIZED) - return [{ timestamp: 0, status }]; - const start = { - timestamp: getTimestampFromBlocks( - startblockheight, - bestblock, - blockDurationMinutes - ), - blocksCount: getVoteBlocksDiff(startblockheight, bestblock), - status: TICKETVOTE_STATUS_STARTED, - }; - const end = { - timestamp: getTimestampFromBlocks( - endblockheight, - bestblock, - blockDurationMinutes - ), - blocksCount: getVoteBlocksDiff(endblockheight, bestblock), - status: - status === TICKETVOTE_STATUS_APPROVED || - status === TICKETVOTE_STATUS_REJECTED - ? status - : TICKETVOTE_STATUS_FINISHED, - }; - return [start, end]; + switch (status) { + case TICKETVOTE_STATUS_AUTHORIZED: + return { status }; + case TICKETVOTE_STATUS_STARTED: + case TICKETVOTE_STATUS_REJECTED: + case TICKETVOTE_STATUS_APPROVED: + return { + endblockheight, + startblockheight, + timestamp: getTimestampFromBlocks( + endblockheight, + bestblock, + blockDurationMinutes + ), + blocksCount: getVoteBlocksDiff(endblockheight, bestblock), + status, + }; + default: + return; + } } /** From 19dd4e9959c864b1dd3587737ab06811ea754889 Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Tue, 2 Aug 2022 15:38:42 -0300 Subject: [PATCH 30/30] fix: use correct timestamps for status changes --- .../Proposal/common/ProposalStatusLabel.js | 18 +++++++++++------- .../Proposal/common/styles.module.css | 8 +++++++- .../src/pi/proposals/proposalsSlice.js | 18 +++++++++++------- .../apps/politeia/src/pi/proposals/utils.js | 1 + 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js index 6bb2d8f2f..d8ffe810f 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/ProposalStatusLabel.js @@ -1,8 +1,8 @@ import React from "react"; import { Event } from "@politeiagui/common-ui"; +import { Tooltip } from "pi-ui"; import styles from "./styles.module.css"; import { getProposalStatusDescription } from "../../../pi/proposals/utils"; -import { PROPOSAL_STATUS_VOTE_STARTED } from "../../../pi/lib/constants"; function ProposalStatusLabel({ statusChange }) { if (!statusChange) return null; @@ -19,12 +19,16 @@ function ProposalStatusLabel({ statusChange }) { {description && (
{description}
)} - {statusChange.blocksCount && - statusChange.status === PROPOSAL_STATUS_VOTE_STARTED && ( -
- {-statusChange.blocksCount} blocks left -
- )} + {statusChange.blocksCount > 0 && ( + + {statusChange.blocksCount} blocks left + + )}
); } diff --git a/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css b/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css index 50ec99e21..eb1b45d0e 100644 --- a/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css +++ b/plugins-structure/apps/politeia/src/components/Proposal/common/styles.module.css @@ -5,12 +5,18 @@ .statusLabelWrapper { position: absolute; right: var(--container-padding-right); + display: flex; + flex-flow: column; } .statusLabel { font-size: var(--font-size-small); } +.statusLabelTooltip { + width: max-content; +} + .statusLabelText { font-size: var(--font-size-small); color: var(--text-secondary-color); @@ -19,7 +25,7 @@ @media screen and (max-width: 768px) { .statusLabelWrapper { top: 2.3rem; - display: flex; + flex-flow: row; } .statusLabelText::before { content: "•"; diff --git a/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js b/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js index 0cecb66de..cb8c4737e 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/proposalsSlice.js @@ -45,15 +45,19 @@ const proposalsSlice = createSlice({ voteStatusChangesByToken ).reduce((proposalStatusChanges, token) => { const record = records[token]; + const vsc = voteStatusChangesByToken[token]; + if (!vsc) return proposalStatusChanges; return { ...proposalStatusChanges, - [token]: voteStatusChangesByToken[token].map((vsc) => ({ - ...vsc, - status: convertVoteStatusToProposalStatus( - vsc.status, - record?.status - ), - })), + [token]: [ + { + ...vsc, + status: convertVoteStatusToProposalStatus( + vsc.status, + record?.status + ), + }, + ], }; }, {}); state.statusChangesByToken = mergeStatusChanges( diff --git a/plugins-structure/apps/politeia/src/pi/proposals/utils.js b/plugins-structure/apps/politeia/src/pi/proposals/utils.js index 8ae1c4f8a..1fb16e126 100644 --- a/plugins-structure/apps/politeia/src/pi/proposals/utils.js +++ b/plugins-structure/apps/politeia/src/pi/proposals/utils.js @@ -384,6 +384,7 @@ export function getProposalStatusDescription(statusChange) { case PROPOSAL_STATUS_VOTE_STARTED: return { event: "vote ends" }; case PROPOSAL_STATUS_ACTIVE: + case PROPOSAL_STATUS_VOTE_ENDED: case PROPOSAL_STATUS_APPROVED: case PROPOSAL_STATUS_REJECTED: return { event: "vote ended" };