diff --git a/src/components/ModalDiff/DiffProposal.jsx b/src/components/ModalDiff/DiffProposal.jsx index bf811c707..4fb3b8e41 100644 --- a/src/components/ModalDiff/DiffProposal.jsx +++ b/src/components/ModalDiff/DiffProposal.jsx @@ -18,21 +18,15 @@ const DiffProposal = ({ latest, initVersion, token, ...props }) => { const { baseVersion, compareVersion, - baseLoading, - compareLoading, changedVersion, baseProposal, - compareProposal + compareProposal, + loading } = useCompareVersionSelector(initVersion, token); const isDiffAvailable = useMemo(() => { - return ( - !!baseProposal.details && - !!compareProposal.details && - !baseLoading && - !compareLoading - ); - }, [baseProposal, compareProposal, baseLoading, compareLoading]); + return !!baseProposal.details && !!compareProposal.details && !loading; + }, [baseProposal, compareProposal, loading]); const [activeTabIndex, setActiveTabIndex] = useState(0); useEffect(() => { @@ -54,8 +48,6 @@ const DiffProposal = ({ latest, initVersion, token, ...props }) => { onChange={changedVersion} base={baseVersion} compare={compareVersion} - baseLoading={baseLoading} - compareLoading={compareLoading} /> {isDiffAvailable ? ( <> diff --git a/src/components/ModalDiff/hooks.js b/src/components/ModalDiff/hooks.js index 725f98be8..a76708359 100644 --- a/src/components/ModalDiff/hooks.js +++ b/src/components/ModalDiff/hooks.js @@ -1,46 +1,97 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useMemo } from "react"; import { COMPARE, BASE } from "./constants"; import * as act from "src/actions"; import { useAction } from "src/redux"; +import drop from "lodash/drop"; import { getAttachmentsFiles, parseRawProposal } from "src/helpers"; +import useFetchMachine, { + FETCH, + REJECT, + RESOLVE, + START +} from "src/hooks/utils/useFetchMachine"; + +const parseProposal = (proposal) => { + if (!proposal) { + // Return empty data for case: proposal = undefined. + return { + details: {}, + files: [], + text: "", + title: "" + }; + } + // Parse current version. + const { description, name } = parseRawProposal(proposal); + return { + details: proposal, + files: getAttachmentsFiles(proposal.files), + text: description, + title: name + }; +}; export function useCompareVersionSelector(initVersion, token) { + const [versionsQueue, setVersionsQueue] = useState([ + initVersion, + initVersion - 1 + ]); + const [fetchedProposals, setFetchedProposals] = useState({}); const [baseVersion, setBaseVersion] = useState(initVersion - 1); const [compareVersion, setCompareVersion] = useState(initVersion); - const [baseLoading, setBaseLoading] = useState(false); - const [compareLoading, setCompareLoading] = useState(false); - const [compareProposal, setCompareProposal] = useState({}); - const [baseProposal, setBaseProposal] = useState({}); - const [error, setError] = useState(); - + const baseProposal = useMemo(() => { + const proposal = fetchedProposals[baseVersion]; + return parseProposal(proposal); + }, [baseVersion, fetchedProposals]); + const compareProposal = useMemo(() => { + const proposal = fetchedProposals[compareVersion]; + return parseProposal(proposal); + }, [compareVersion, fetchedProposals]); const onFetchProposalDetailsWithoutState = useAction( act.onFetchProposalDetailsWithoutState ); - const fetchProposalVersions = useCallback( - async (token, version) => { - if (!version) { - // Return empty data for case: version = 0. - return { - details: {}, - files: [], - text: "", - title: "" - }; - } - // Fetch provided version. - const proposal = await onFetchProposalDetailsWithoutState(token, version); - // Parse current version. - const { description, name } = parseRawProposal(proposal); - return { - details: proposal, - files: getAttachmentsFiles(proposal.files), - text: description, - title: name - }; + const [state, send] = useFetchMachine({ + actions: { + initial: () => { + if (versionsQueue.length) { + return send(START); + } + return send(RESOLVE); + }, + start: () => { + if (!versionsQueue.length) { + return send(RESOLVE); + } + const version = versionsQueue[0]; + if (version <= 0) { + setVersionsQueue(drop(versionsQueue)); + return send(START); + } + onFetchProposalDetailsWithoutState(token, version) + .then((proposal) => { + setFetchedProposals({ + ...fetchedProposals, + [version]: proposal + }); + setVersionsQueue(drop(versionsQueue)); + if (setVersionsQueue.length) { + return send(START); + } + return send(RESOLVE); + }) + .catch((e) => send(REJECT, e)); + + return send(FETCH); + }, + done: () => {} }, - [onFetchProposalDetailsWithoutState] - ); + initialValues: { + status: "idle", + loading: true, + verifying: true + } + }); const changedVersion = (versionType, v) => { if (versionType === COMPARE) { @@ -49,44 +100,19 @@ export function useCompareVersionSelector(initVersion, token) { if (versionType === BASE) { setBaseVersion(v); } + if (!fetchedProposals[v]) { + setVersionsQueue([v, ...versionsQueue]); + send(START); + } }; - - useEffect(() => { - setBaseLoading(true); - setError(null); - fetchProposalVersions(token, baseVersion) - .then((proposal) => { - setBaseLoading(false); - setBaseProposal(proposal); - }) - .catch((e) => { - setError(e); - setBaseLoading(false); - }); - }, [token, baseVersion, fetchProposalVersions]); - - useEffect(() => { - setCompareLoading(true); - setError(null); - fetchProposalVersions(token, compareVersion) - .then((proposal) => { - setCompareLoading(false); - setCompareProposal(proposal); - }) - .catch((e) => { - setError(e); - setCompareLoading(false); - }); - }, [token, compareVersion, fetchProposalVersions]); + const fetchingNotDone = state.loading || state.status !== "success"; return { baseVersion, compareVersion, - baseLoading, - compareLoading, changedVersion, baseProposal, compareProposal, - error + loading: fetchingNotDone }; } diff --git a/src/components/Proposal/Proposal.jsx b/src/components/Proposal/Proposal.jsx index 2c3e19922..73a40870e 100644 --- a/src/components/Proposal/Proposal.jsx +++ b/src/components/Proposal/Proposal.jsx @@ -19,7 +19,11 @@ import { getLegacyProposalStatusTagProps, getStatusBarData } from "./helpers"; -import { PROPOSAL_TYPE_RFP, PROPOSAL_TYPE_RFP_SUBMISSION } from "src/constants"; +import { + PROPOSAL_TYPE_RFP, + PROPOSAL_TYPE_RFP_SUBMISSION, + PROPOSAL_STATE_VETTED +} from "src/constants"; import { getMarkdownContent, getVotesReceived, @@ -56,6 +60,8 @@ import useModalContext from "src/hooks/utils/useModalContext"; import { useRouter } from "src/components/Router"; import { shortRecordToken, isEmpty, getKeyByValue } from "src/helpers"; import { usdFormatter } from "src/utils"; +import * as sel from "src/selectors"; +import { useSelector } from "../../redux"; /** * replaceImgDigestWithPayload uses a regex to parse images @@ -144,6 +150,8 @@ const Proposal = React.memo(function Proposal({ endDate, billingStatusChangeMetadata } = proposal; + const isAdmin = useSelector(sel.currentUserIsAdmin); + const isVetted = state === PROPOSAL_STATE_VETTED; const isRfp = !!linkby || type === PROPOSAL_TYPE_RFP; const isRfpSubmission = !!linkto || type === PROPOSAL_TYPE_RFP_SUBMISSION; const isRfpActive = isRfp && isActiveRfp(linkby); @@ -190,7 +198,8 @@ const Proposal = React.memo(function Proposal({ const mobile = useMediaQuery("(max-width: 560px)"); const showEditedDate = version > 1 && timestamp !== publishedat && !mobile; const showPublishedDate = publishedat && !mobile && !showEditedDate; - const showExtendedVersionPicker = extended && version > 1; + const showExtendedVersionPicker = + extended && version > 1 && !isCensored && (isVetted || isAuthor || isAdmin); const showVersionAsText = !extended && !mobile; const showVoteEnd = (isVoteActive || isVotingFinished) && !isAbandoned && !isCensored; diff --git a/src/components/RecordWrapper/RecordWrapper.jsx b/src/components/RecordWrapper/RecordWrapper.jsx index d28f6b099..66b1c839c 100644 --- a/src/components/RecordWrapper/RecordWrapper.jsx +++ b/src/components/RecordWrapper/RecordWrapper.jsx @@ -112,7 +112,7 @@ export const Subtitle = ({ children, separatorSymbol = "•" }) => ( export const JoinTitle = ({ children, className, separatorSymbol = "•" }) => ( ( {separatorSymbol} diff --git a/src/components/RecordWrapper/RecordWrapper.module.css b/src/components/RecordWrapper/RecordWrapper.module.css index 9f85fbe5c..8ccf3f7a8 100644 --- a/src/components/RecordWrapper/RecordWrapper.module.css +++ b/src/components/RecordWrapper/RecordWrapper.module.css @@ -19,6 +19,10 @@ flex-wrap: wrap; } +.flexWrap { + flex-wrap: wrap; +} + .eventTooltip { color: var(--text-secondary-color) !important; }