From a689f164745696edf579b627d17b809726d8cfbe Mon Sep 17 00:00:00 2001 From: Victor Guedes Date: Thu, 2 Jul 2020 18:33:58 -0300 Subject: [PATCH 1/4] [cms] feat: change proposaltoken to proposalname on invoice lineitem This diff now allows users to select proposals by its name, instead of its token. Since getting proposals names require a batching request, and those requests are bounded by 20 proposals for each request, those requests have to be done more than once in case the token from the invoice (details and diff), or the proposal (new and edit actions) are not on the dropdown list. --- src/actions/api.js | 6 +- src/components/Diff/Diff.jsx | 4 +- src/components/Diff/DiffLineitems.jsx | 23 ++++-- src/components/Invoice/Invoice.jsx | 7 +- .../InvoiceDatasheet/InvoiceDatasheet.jsx | 19 ++++- .../components/LazySelector.jsx | 37 ++++++++++ src/components/InvoiceDatasheet/helpers.js | 18 +++-- src/components/InvoiceDatasheet/wrappers.jsx | 74 ++++++++++++++++++- src/components/InvoiceForm/InvoiceForm.jsx | 10 +-- src/components/ModalDiff/ModalDiffInvoice.jsx | 33 ++++++++- src/containers/Invoice/Detail/Detail.jsx | 44 ++++++++++- src/containers/Invoice/Edit/Edit.jsx | 6 +- src/containers/Invoice/New/New.jsx | 11 ++- src/containers/Invoice/New/hooks.js | 16 +--- src/containers/Proposal/Public/Public.jsx | 2 +- src/containers/Proposal/Unvetted/Unvetted.jsx | 2 +- src/containers/Proposal/hooks/index.js | 1 - src/hooks/api/useApprovedProposals.js | 41 ++++++++++ src/hooks/api/useProposalBatchWithoutRedux.js | 8 +- .../hooks => hooks/api}/useProposalsBatch.js | 0 20 files changed, 293 insertions(+), 69 deletions(-) create mode 100644 src/components/InvoiceDatasheet/components/LazySelector.jsx create mode 100644 src/hooks/api/useApprovedProposals.js rename src/{containers/Proposal/hooks => hooks/api}/useProposalsBatch.js (100%) diff --git a/src/actions/api.js b/src/actions/api.js index aa805c461..c1dcfad94 100644 --- a/src/actions/api.js +++ b/src/actions/api.js @@ -344,16 +344,16 @@ export const onFetchAdminInvoices = () => export const onFetchProposalsBatchWithoutState = ( tokens, - fetchPropsoals = true, + fetchProposals = true, fetchVoteSummary = true ) => withCsrf(async (_, __, csrf) => { const res = await Promise.all([ - fetchPropsoals && api.proposalsBatch(csrf, tokens), + fetchProposals && api.proposalsBatch(csrf, tokens), fetchVoteSummary && api.proposalsBatchVoteSummary(csrf, tokens) ]); const proposals = - fetchPropsoals && res.find((res) => res && res.proposals).proposals; + fetchProposals && res.find((res) => res && res.proposals).proposals; const summaries = fetchVoteSummary && res.find((res) => res && res.summaries).summaries; return [proposals, summaries]; diff --git a/src/components/Diff/Diff.jsx b/src/components/Diff/Diff.jsx index cd6fbca08..be253e0b7 100644 --- a/src/components/Diff/Diff.jsx +++ b/src/components/Diff/Diff.jsx @@ -157,7 +157,7 @@ export const DiffText = ({ newText, oldText }) => { )); }; -export const DiffInvoices = ({ oldData, newData, className }) => { +export const DiffInvoices = ({ oldData, newData, className, proposals }) => { const newLineitems = setLineitemParams(newData.lineitems, { rate: newData.contractorrate }); @@ -169,7 +169,7 @@ export const DiffInvoices = ({ oldData, newData, className }) => { return ( - + ); }; diff --git a/src/components/Diff/DiffLineitems.jsx b/src/components/Diff/DiffLineitems.jsx index 410d2fe79..8523f69d5 100644 --- a/src/components/Diff/DiffLineitems.jsx +++ b/src/components/Diff/DiffLineitems.jsx @@ -1,10 +1,11 @@ -import React, { useMemo } from "react"; +import React, { useMemo, useCallback } from "react"; import PropTypes from "prop-types"; import { classNames, useTheme } from "pi-ui"; import { fromUSDCentsToUSDUnits, fromMinutesToHours } from "src/helpers"; import styles from "./Diff.module.css"; import { TableRow } from "src/components/InvoiceDatasheet/InvoiceDatasheet"; import { getTotalsLine } from "src/components/InvoiceDatasheet/helpers"; +import get from "lodash/get"; const renderGrid = (lineItems) => lineItems.reduce( @@ -15,7 +16,7 @@ const renderGrid = (lineItems) => domain, subdomain, description, - proposaltoken, + proposalname, subuserid, subrate, labor, @@ -40,7 +41,7 @@ const renderGrid = (lineItems) => value: description, multiline: true }, - { value: proposaltoken }, + { value: proposalname }, { value: subuserid }, { value: subRate }, { value: laborHours }, @@ -57,15 +58,23 @@ const renderGrid = (lineItems) => { grid: [], expenseTotal: 0, laborTotal: 0, total: 0 } ); -const DiffLineitems = ({ lineItems }) => { +const DiffLineitems = ({ lineItems, proposals }) => { const { themeName } = useTheme(); const isDarkTheme = themeName === "dark"; - const { grid, expenseTotal, laborTotal, total } = useMemo( - () => renderGrid(lineItems), - [lineItems] + const getProposalName = useCallback( + (token) => get(proposals, [token, "name"]), + [proposals] ); + const { grid, expenseTotal, laborTotal, total } = useMemo(() => { + const newLineItems = lineItems.map((item) => ({ + ...item, + proposalname: getProposalName(item.proposaltoken) + })); + return renderGrid(newLineItems); + }, [lineItems, getProposalName]); + const numberOfCols = grid.length && grid[0] && grid[0].items.length; const newTotals = [ diff --git a/src/components/Invoice/Invoice.jsx b/src/components/Invoice/Invoice.jsx index 096f21ca4..6a49cb6dd 100644 --- a/src/components/Invoice/Invoice.jsx +++ b/src/components/Invoice/Invoice.jsx @@ -23,7 +23,7 @@ import ThumbnailGrid from "src/components/Files"; import { useLoaderContext } from "src/containers/Loader"; import VersionPicker from "src/components/VersionPicker"; -const Invoice = ({ invoice, extended, approvedProposalsTokens }) => { +const Invoice = ({ invoice, extended, approvedProposals }) => { const { censorshiprecord, file, @@ -113,6 +113,7 @@ const Invoice = ({ invoice, extended, approvedProposalsTokens }) => { )} version={version} token={invoiceToken} + proposals={approvedProposals} /> )} @@ -189,7 +190,7 @@ const Invoice = ({ invoice, extended, approvedProposalsTokens }) => { value={invoice && invoice.input.lineitems} readOnly userRate={invContractorRate / 100} - proposalsTokens={approvedProposalsTokens || []} + proposals={approvedProposals || []} /> )} @@ -203,7 +204,7 @@ const Invoice = ({ invoice, extended, approvedProposalsTokens }) => { Invoice.propTypes = { invoice: PropTypes.object.isRequired, - approvedProposalsTokens: PropTypes.array + approvedProposals: PropTypes.array }; export default Invoice; diff --git a/src/components/InvoiceDatasheet/InvoiceDatasheet.jsx b/src/components/InvoiceDatasheet/InvoiceDatasheet.jsx index 99bde0e52..ca52e0534 100644 --- a/src/components/InvoiceDatasheet/InvoiceDatasheet.jsx +++ b/src/components/InvoiceDatasheet/InvoiceDatasheet.jsx @@ -11,6 +11,7 @@ import CellRenderer from "./components/CellRenderer"; import TableButton from "./components/TableButton"; import usePolicy from "src/hooks/api/usePolicy"; import useSubContractors from "src/hooks/api/useSubContractors"; + import { processCellsChange, convertLineItemsToGrid, @@ -50,13 +51,23 @@ const InvoiceDatasheet = React.memo(function InvoiceDatasheet({ readOnly, userRate, errors, - proposalsTokens + proposals }) { const { policy } = usePolicy(); const { subContractors } = useSubContractors(); const [grid, setGrid] = useState([]); const [currentRate, setCurrentRate] = useState(userRate || 0); + const proposalsOptions = useMemo( + () => + proposals && + proposals.map((p) => ({ + label: p.name, + value: p.censorshiprecord.token + })), + [proposals] + ); + const handleCellsChange = useCallback( (changes) => { const { grid: newGrid } = processCellsChange(grid, changes, userRate); @@ -93,7 +104,7 @@ const InvoiceDatasheet = React.memo(function InvoiceDatasheet({ errors, currentRate, policy, - proposalsTokens, + proposalsOptions, subContractors ); setGrid(grid); @@ -104,7 +115,7 @@ const InvoiceDatasheet = React.memo(function InvoiceDatasheet({ errors, currentRate, policy, - proposalsTokens, + proposalsOptions, subContractors ] ); @@ -242,7 +253,7 @@ InvoiceDatasheet.propTypes = { value: PropTypes.array.isRequired, readOnly: PropTypes.bool.isRequired, onChange: PropTypes.func, - proposalsTokens: PropTypes.array.isRequired + proposals: PropTypes.array.isRequired }; InvoiceDatasheet.defaultProps = { diff --git a/src/components/InvoiceDatasheet/components/LazySelector.jsx b/src/components/InvoiceDatasheet/components/LazySelector.jsx new file mode 100644 index 000000000..739d26808 --- /dev/null +++ b/src/components/InvoiceDatasheet/components/LazySelector.jsx @@ -0,0 +1,37 @@ +import React, { useState, useCallback, useMemo } from "react"; +import { Select } from "pi-ui"; + +const fetchingOption = { + isFetchingOption: true, + label: "Fetch more...", + value: "" +}; + +const LazySelector = ({ options, onFetch, needsFetch, onChange, onCommit }) => { + const [selected, setSelected] = useState(); + const getValueObj = useCallback( + (value) => options.find((op) => op.value === value), + [options] + ); + + const handleChange = useCallback( + ({ isFetchingOption = false, value }) => { + if (isFetchingOption) { + onFetch(); + } else { + setSelected(getValueObj(value)); + onChange(value); + onCommit(value); + } + }, + [onFetch, setSelected, onChange, onCommit, getValueObj] + ); + + const ops = useMemo( + () => (needsFetch ? [...options, fetchingOption] : options), + [options, needsFetch] + ); + return ; + + useEffect( + function onOptionsChangeOrError() { + return () => { + setLoading(false); + }; + }, + [options, error] + ); + + return loading ? ( +
+ +
+ ) : ( +