diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 658b295e348..6dbc071de5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ env: PDF_SERVICE_URL: http://localhost:3002 API_KEY: dvl-1510egmf4a23d80342403fb599qd CI: true + DISABLE_MOCK_UPLOADS: 1 jobs: lint: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7bc5af0fb01..d8b9e16f113 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,6 +14,16 @@ env: API_KEY: dvl-1510egmf4a23d80342403fb599qd CI: true + AWS_KEY: user + AWS_SECRET: password + AWS_S3_BUCKET: opencollective-e2e + AWS_S3_REGION: us-east-1 + AWS_S3_API_VERSION: latest + AWS_S3_ENDPOINT: http://localhost:9000 + AWS_S3_SSL_ENABLED: false + AWS_S3_FORCE_PATH_STYLE: true + GRAPHQL_ERROR_DETAILED: true + DISABLE_MOCK_UPLOADS: 1 E2E_TEST: 1 PGHOST: localhost PGUSER: postgres @@ -31,8 +41,6 @@ env: TERM: xterm STRIPE_WEBHOOK_KEY: ${{ secrets.STRIPE_WEBHOOK_KEY }} STRIPE_WEBHOOK_SIGNING_SECRET: ${{ secrets.STRIPE_WEBHOOK_SIGNING_SECRET }} - AWS_KEY: ${{ secrets.AWS_KEY }} - AWS_SECRET: ${{ secrets.AWS_SECRET }} jobs: e2e: @@ -63,6 +71,14 @@ jobs: - 5432:5432 # needed because the postgres container does not provide a healthcheck options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + minio: + image: minio/minio:edge-cicd + ports: + - 9000:9000 + options: --name=minio --health-cmd "curl http://localhost:9000/minio/health/live" + env: + MINIO_ROOT_USER: user + MINIO_ROOT_PASSWORD: password steps: # man-db trigger on apt install is taking some time @@ -87,6 +103,13 @@ jobs: wget https://github.com/stripe/stripe-cli/releases/download/v1.13.9/stripe_1.13.9_linux_x86_64.tar.gz -O /tmp/stripe_1.13.9_linux_x86_64.tar.gz sudo tar xzvf /tmp/stripe_1.13.9_linux_x86_64.tar.gz -C /bin/ + - name: Setup minio bucket + run: | + wget https://dl.min.io/client/mc/release/linux-amd64/mc + chmod +x ./mc + ./mc alias set minio http://127.0.0.1:9000 user password + ./mc mb --ignore-existing minio/opencollective-e2e + - name: Checkout (frontend) uses: actions/checkout@v4 diff --git a/components/Dropzone.tsx b/components/Dropzone.tsx index 5d3febd7e30..82c7dc475ab 100644 --- a/components/Dropzone.tsx +++ b/components/Dropzone.tsx @@ -371,7 +371,7 @@ type DropzoneProps = React.HTMLAttributes & { } & ( | { /** Collect File only, do not upload files */ - collectFilesOnly: true; + collectFilesOnly?: boolean; /** Whether the dropzone should accept multiple files */ isMulti?: boolean; /** Called back with the uploaded files on success */ diff --git a/components/budget/ExpenseBudgetItem.js b/components/budget/ExpenseBudgetItem.js index ac1c223eef2..d4f83715a0d 100644 --- a/components/budget/ExpenseBudgetItem.js +++ b/components/budget/ExpenseBudgetItem.js @@ -117,14 +117,19 @@ const ExpenseBudgetItem = ({ }) => { const intl = useIntl(); const { LoggedInUser } = useLoggedInUser(); - const [showFilesViewerModal, setShowFilesViewerModal] = React.useState(false); + const [showFilesViewerModal, setShowFilesViewerModal] = React.useState(null); const featuredProfile = isInverted ? expense?.account : expense?.payee; const isAdminView = view === 'admin'; const isSubmitterView = view === 'submitter'; const isCharge = expense?.type === expenseTypes.CHARGE; const pendingReceipt = isCharge && expense?.items?.every(i => i.url === null); - const files = React.useMemo(() => getFilesFromExpense(expense, intl), [expense]); - const nbAttachedFiles = !isAdminView ? 0 : files.length; + const invoiceFile = expense?.invoiceFile; + const attachedFiles = React.useMemo( + () => getFilesFromExpense(expense, intl).filter(f => !invoiceFile || f.url !== invoiceFile.url), + [expense, intl, invoiceFile], + ); + const files = React.useMemo(() => getFilesFromExpense(expense, intl), [expense, intl]); + const nbAttachedFiles = !isAdminView ? 0 : attachedFiles.length; const isExpensePaidOrRejected = [ExpenseStatus.REJECTED, ExpenseStatus.PAID].includes(expense?.status); const shouldDisplayStatusTagActions = (isExpensePaidOrRejected || expense?.status === ExpenseStatus.APPROVED) && @@ -423,6 +428,40 @@ const ExpenseBudgetItem = ({ )} )} + {expense.invoiceFile && ( +
+ + + + {isLoading ? ( + + ) : ( + expandExpense(e, expense.invoiceFile.url) + : () => setShowFilesViewerModal([expense.invoiceFile]) + } + px={2} + ml={-2} + isBorderless + textAlign="left" + > + +    + + + )} +
+ )} {nbAttachedFiles > 0 && (
@@ -436,7 +475,11 @@ const ExpenseBudgetItem = ({ fontSize="11px" cursor="pointer" buttonSize="tiny" - onClick={useDrawer ? expandExpense : () => setShowFilesViewerModal(true)} + onClick={ + useDrawer + ? e => expandExpense(e, attachedFiles[0].url) + : () => setShowFilesViewerModal(attachedFiles) + } px={2} ml={-2} isBorderless @@ -453,6 +496,7 @@ const ExpenseBudgetItem = ({ )}
)} + {lastComment && (
@@ -519,7 +563,7 @@ const ExpenseBudgetItem = ({ }, { expenseId: expense.legacyId }, )} - onClose={() => setShowFilesViewerModal(false)} + onClose={() => setShowFilesViewerModal(null)} /> )} @@ -557,6 +601,7 @@ ExpenseBudgetItem.propTypes = { items: PropTypes.arrayOf(PropTypes.object), requiredLegalDocuments: PropTypes.arrayOf(PropTypes.string), attachedFiles: PropTypes.arrayOf(PropTypes.object), + invoiceFile: PropTypes.object, payee: PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, type: PropTypes.string.isRequired, diff --git a/components/dashboard/sections/expenses/HostDashboardExpenses.tsx b/components/dashboard/sections/expenses/HostDashboardExpenses.tsx index 2bbc19650f8..f10e9c4504c 100644 --- a/components/dashboard/sections/expenses/HostDashboardExpenses.tsx +++ b/components/dashboard/sections/expenses/HostDashboardExpenses.tsx @@ -307,11 +307,11 @@ const HostExpenses = ({ accountSlug: hostSlug }: DashboardSectionProps) => { }} useDrawer openExpenseLegacyId={Number(router.query.openExpenseId)} - setOpenExpenseLegacyId={legacyId => { + setOpenExpenseLegacyId={(legacyId, attachmentUrl) => { router.push( { pathname: pageRoute, - query: getQueryParams({ ...query, openExpenseId: legacyId }), + query: getQueryParams({ ...query, openExpenseId: legacyId, attachmentUrl }), }, undefined, { shallow: true }, diff --git a/components/dashboard/sections/tax-information/TaxInformationForm.tsx b/components/dashboard/sections/tax-information/TaxInformationForm.tsx index f4ef2bf84d6..c526f3cc8d2 100644 --- a/components/dashboard/sections/tax-information/TaxInformationForm.tsx +++ b/components/dashboard/sections/tax-information/TaxInformationForm.tsx @@ -125,7 +125,7 @@ export const TaxInformationForm = ({ } return ( - schema={schema} initialValues={initialValues} config={FORM_CONFIG} diff --git a/components/expenses/EditExpenseDialog.tsx b/components/expenses/EditExpenseDialog.tsx index c52ac0d804f..60286d14a88 100644 --- a/components/expenses/EditExpenseDialog.tsx +++ b/components/expenses/EditExpenseDialog.tsx @@ -17,6 +17,7 @@ import { FormField } from '../FormField'; import { FormikZod } from '../FormikZod'; import { AdditionalAttachments, ExpenseItemsForm } from '../submit-expense/form/ExpenseItemsSection'; import { PayoutMethodFormContent } from '../submit-expense/form/PayoutMethodSection'; +import { InvoiceFormOption } from '../submit-expense/form/TypeOfExpenseSection'; import { WhoIsGettingPaidForm } from '../submit-expense/form/WhoIsGettingPaidSection'; import { InviteeAccountType, useExpenseForm, YesNoOption } from '../submit-expense/useExpenseForm'; import { Button } from '../ui/Button'; @@ -48,6 +49,8 @@ const RenderFormFields = ({ field, onSubmit, expense }) => { return ; case 'attachments': return ; + case 'invoiceFile': + return ; } }; const EditPayee = ({ expense, onSubmit }) => { @@ -235,10 +238,12 @@ const EditAttachments = ({ expense, onSubmit }) => { pickSchemaFields: { expenseAttachedFiles: true }, }); const transformedOnSubmit = values => { + const attachedFiles = values.additionalAttachments.map(a => ({ + url: typeof a === 'string' ? a : a?.url, + })); + const editValues = { - attachedFiles: values.additionalAttachments.map(a => ({ - url: typeof a === 'string' ? a : a?.url, - })), + attachedFiles, }; return onSubmit(editValues); }; @@ -279,6 +284,66 @@ const EditAttachments = ({ expense, onSubmit }) => { ); }; + +const EditInvoiceFile = ({ expense, onSubmit }) => { + const formRef = React.useRef(); + const startOptions = React.useRef({ + expenseId: expense.legacyId, + isInlineEdit: true, + pickSchemaFields: { invoiceFile: true, invoiceNumber: true }, + }); + const transformedOnSubmit = values => { + let invoiceFile; + if (values.hasInvoiceOption === YesNoOption.YES && values.invoiceFile) { + invoiceFile = { url: typeof values.invoiceFile === 'string' ? values.invoiceFile : values.invoiceFile.url }; + } else if (values.hasInvoiceOption === YesNoOption.NO) { + invoiceFile = null; + } + + const editValues = { + invoiceFile, + reference: values.invoiceNumber, + }; + return onSubmit(editValues); + }; + + const expenseForm = useExpenseForm({ + formRef, + initialValues: { + inviteeAccountType: InviteeAccountType.INDIVIDUAL, + expenseItems: [ + { + amount: { + valueInCents: 0, + currency: 'USD', + }, + description: '', + }, + ], + additionalAttachments: [], + hasInvoiceOption: YesNoOption.YES, + inviteeNewIndividual: {}, + inviteeNewOrganization: { + organization: {}, + }, + newPayoutMethod: { + data: {}, + }, + }, + startOptions: startOptions.current, + onSubmit: transformedOnSubmit, + }); + + return ( + +
e.preventDefault()}> + + + +
+ ); +}; + const EditExpenseItems = ({ expense, onSubmit }) => { const formRef = React.useRef(); const startOptions = React.useRef({ @@ -395,7 +460,7 @@ export default function EditExpenseDialog({ goToLegacyEdit, }: { expense: Expense; - field: 'title' | 'expenseItems' | 'payoutMethod' | 'payee' | 'type' | 'attachments'; + field: 'title' | 'expenseItems' | 'payoutMethod' | 'payee' | 'type' | 'attachments' | 'invoiceFile'; title: string; description?: string; dialogContentClassName?: string; diff --git a/components/expenses/Expense.tsx b/components/expenses/Expense.tsx index 6349937ce0a..078d14481f4 100644 --- a/components/expenses/Expense.tsx +++ b/components/expenses/Expense.tsx @@ -146,7 +146,7 @@ function Expense(props) { createdUser: null, showFilesViewerModal: false, }); - const [openUrl, setOpenUrl] = useState(null); + const [openUrl, setOpenUrl] = useState(router.query.attachmentUrl as string); const [replyingToComment, setReplyingToComment] = useState(null); const [hasConfirmedOCR, setConfirmedOCR] = useState(false); const hasItemsWithOCR = Boolean(state.editedExpense?.items?.some(itemHasOCR)); @@ -430,7 +430,7 @@ function Expense(props) { setState({ ...state, showFilesViewerModal: true }); }; - const files = React.useMemo(() => getFilesFromExpense(expense, intl), [expense]); + const files = React.useMemo(() => getFilesFromExpense(expense, intl), [expense, intl]); useEffect(() => { const showFilesViewerModal = isDrawer && isDesktop && files?.length > 0; diff --git a/components/expenses/ExpenseAttachedFilesForm.tsx b/components/expenses/ExpenseAttachedFilesForm.tsx index 8d7708a670e..f212adb8250 100644 --- a/components/expenses/ExpenseAttachedFilesForm.tsx +++ b/components/expenses/ExpenseAttachedFilesForm.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { uniqBy } from 'lodash'; import { FormattedMessage } from 'react-intl'; -import type { Account } from '../../lib/graphql/types/v2/schema'; +import type { Account, UploadedFileKind } from '../../lib/graphql/types/v2/schema'; import { attachmentDropzoneParams } from './lib/attachments'; import Dropzone from '../Dropzone'; @@ -20,6 +20,9 @@ const ExpenseAttachedFilesForm = ({ title, description, onChange, + fieldName = 'attachedFiles', + isSingle = false, + kind, }: ExpenseAttachedFilesFormProps) => { const [files, setFiles] = React.useState(uniqBy(defaultValue, 'url')); @@ -42,7 +45,7 @@ const ExpenseAttachedFilesForm = ({ - {files?.length > 0 && ( + {files?.length > 0 && !isSingle && ( { @@ -69,14 +72,18 @@ const ExpenseAttachedFilesForm = ({ ) : ( { - setFiles(uploadedFiles); - onChange(uploadedFiles); + let value = uploadedFiles; + if (!Array.isArray(uploadedFiles)) { + value = [uploadedFiles]; + } + setFiles(value); + onChange(value); }} /> )} @@ -91,6 +98,9 @@ type ExpenseAttachedFilesFormProps = { disabled?: boolean; hasOCRFeature?: boolean; collective?: Account; + fieldName?: string; + isSingle?: boolean; + kind?: UploadedFileKind; onChange: (attachedFiles: Array<{ url: string }>) => void; }; diff --git a/components/expenses/ExpenseForm.js b/components/expenses/ExpenseForm.js index bb963585410..5bc0833755e 100644 --- a/components/expenses/ExpenseForm.js +++ b/components/expenses/ExpenseForm.js @@ -181,12 +181,18 @@ export const prepareExpenseForSubmit = expenseData => { payoutMethod.id = null; } + const attachedFiles = []; + if (keepAttachedFiles) { + attachedFiles.push(...(expenseData.attachedFiles || [])); + } + return { ...pick(expenseData, ['id', 'type', 'tags', 'currency']), payee, payeeLocation, payoutMethod, - attachedFiles: keepAttachedFiles ? expenseData.attachedFiles?.map(file => pick(file, ['id', 'url', 'name'])) : [], + attachedFiles: attachedFiles.map(file => pick(file, ['id', 'url', 'name'])), + invoiceFile: expenseData.invoiceFile ? pick(expenseData.invoiceFile, 'url') : null, tax: expenseData.taxes?.filter(tax => !tax.isDisabled).map(tax => pick(tax, ['type', 'rate', 'idNumber'])), items: expenseData.items.map(item => prepareExpenseItemForSubmit(expenseData, item)), accountingCategory: !expenseData.accountingCategory ? null : pick(expenseData.accountingCategory, ['id']), @@ -888,6 +894,7 @@ const ExpenseFormBody = ({
} description={ } + onChange={invoiceFile => + formik.setFieldValue('invoiceFile', invoiceFile?.length ? invoiceFile[0] : null) + } + form={formik} + isSingle + fieldName="invoiceFile" + defaultValue={values.invoiceFile ? [values.invoiceFile] : []} + /> +
+
+ } + description={ + + } onChange={attachedFiles => formik.setFieldValue('attachedFiles', attachedFiles)} form={formik} defaultValue={values.attachedFiles} @@ -1094,6 +1119,7 @@ const ExpenseForm = ({ initialValues.payoutMethod = expense.draft.payoutMethod || expense.payoutMethod; initialValues.payeeLocation = expense.draft.payeeLocation; initialValues.payee = expense.recurringExpense ? expense.payee : expense.draft.payee; + initialValues.invoiceFile = expense.draft.invoiceFile; } return ( diff --git a/components/expenses/ExpenseMoreActionsButton.js b/components/expenses/ExpenseMoreActionsButton.js index dff1d46ec9b..990aeb6ad1e 100644 --- a/components/expenses/ExpenseMoreActionsButton.js +++ b/components/expenses/ExpenseMoreActionsButton.js @@ -264,7 +264,8 @@ const ExpenseMoreActionsButton = ({ )} - {permissions.canSeeInvoiceInfo && + {!props.hasAttachedInvoiceFile && + permissions.canSeeInvoiceInfo && [expenseTypes.INVOICE, expenseTypes.SETTLEMENT].includes(expense?.type) && ( {({ isLoading, downloadInvoice }) => ( @@ -345,6 +346,7 @@ const ExpenseMoreActionsButton = ({ ExpenseMoreActionsButton.propTypes = { isDisabled: PropTypes.bool, + hasAttachedInvoiceFile: PropTypes.bool, expense: PropTypes.shape({ id: PropTypes.string.isRequired, legacyId: PropTypes.number.isRequired, diff --git a/components/expenses/ExpenseSummary.js b/components/expenses/ExpenseSummary.js index afcf27299f7..e5c867810b6 100644 --- a/components/expenses/ExpenseSummary.js +++ b/components/expenses/ExpenseSummary.js @@ -2,13 +2,14 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { themeGet } from '@styled-system/theme-get'; import { includes } from 'lodash'; -import { MessageSquare } from 'lucide-react'; +import { Download, MessageSquare } from 'lucide-react'; import { createPortal } from 'react-dom'; import { FormattedDate, FormattedMessage, useIntl } from 'react-intl'; import styled from 'styled-components'; import expenseTypes from '../../lib/constants/expenseTypes'; import { PayoutMethodType } from '../../lib/constants/payout-method'; +import { i18nGraphqlException } from '../../lib/errors'; import { ExpenseStatus, ExpenseType } from '../../lib/graphql/types/v2/graphql'; import useLoggedInUser from '../../lib/hooks/useLoggedInUser'; import { PREVIEW_FEATURE_KEYS } from '../../lib/preview-features'; @@ -32,12 +33,15 @@ import StyledHr from '../StyledHr'; import Tags from '../Tags'; import { H1, P, Span } from '../Text'; import TruncatedTextWithTooltip from '../TruncatedTextWithTooltip'; +import { Button } from '../ui/Button'; +import { useToast } from '../ui/useToast'; import UploadedFilePreview from '../UploadedFilePreview'; import EditExpenseDialog from './EditExpenseDialog'; import { ExpenseAccountingCategoryPill } from './ExpenseAccountingCategoryPill'; import ExpenseAmountBreakdown from './ExpenseAmountBreakdown'; import ExpenseAttachedFiles from './ExpenseAttachedFiles'; +import ExpenseInvoiceDownloadHelper from './ExpenseInvoiceDownloadHelper'; import ExpenseMoreActionsButton from './ExpenseMoreActionsButton'; import ExpenseStatusTag from './ExpenseStatusTag'; import ExpenseSummaryAdditionalInformation from './ExpenseSummaryAdditionalInformation'; @@ -105,6 +109,7 @@ const ExpenseSummary = ({ ...props }) => { const intl = useIntl(); + const { toast } = useToast(); const isReceipt = expense?.type === expenseTypes.RECEIPT; const isCreditCardCharge = expense?.type === expenseTypes.CHARGE; const isGrant = expense?.type === expenseTypes.GRANT; @@ -127,6 +132,15 @@ const ExpenseSummary = ({ expense?.type !== ExpenseType.GRANT && expense?.status !== ExpenseStatus.DRAFT; + const invoiceFile = React.useMemo( + () => expense?.invoiceFile || expense?.draft?.invoiceFile, + [expense?.invoiceFile, expense?.draft?.invoiceFile], + ); + const attachedFiles = React.useMemo( + () => (expense?.attachedFiles?.length ? expense.attachedFiles : (expense?.draft?.attachedFiles ?? [])), + [expense?.attachedFiles, expense?.draft?.attachedFiles], + ); + const processButtons = ( { onDelete?.(expense); onClose?.(); @@ -528,7 +543,55 @@ const ExpenseSummary = ({
)} - {expenseTypeSupportsAttachments(expense?.type) && expense?.attachedFiles?.length > 0 && ( + {expenseTypeSupportsAttachments(expense?.type) && (invoiceFile || useInlineExpenseEdit) && ( + + + {!expense && isLoading ? ( + + ) : ( + + + + )} + + {useInlineExpenseEdit && ( + + )} + + {invoiceFile ? ( + + ) : ( + + toast({ + variant: 'error', + message: i18nGraphqlException(intl, e), + }) + } + > + {({ isLoading, downloadInvoice }) => ( + + )} + + )} + + )} + {expenseTypeSupportsAttachments(expense?.type) && (attachedFiles.length > 0 || useInlineExpenseEdit) && ( {!expense && isLoading ? ( @@ -548,7 +611,7 @@ const ExpenseSummary = ({ /> )} - + )} @@ -640,6 +703,10 @@ ExpenseSummary.propTypes = { url: PropTypes.string.isRequired, }).isRequired, ), + invoiceFile: PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string.isRequired, + }), taxes: PropTypes.arrayOf( PropTypes.shape({ type: PropTypes.string, @@ -694,6 +761,16 @@ ExpenseSummary.propTypes = { url: PropTypes.string, }), ), + attachedFiles: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string.isRequired, + }).isRequired, + ), + invoiceFile: PropTypes.shape({ + id: PropTypes.string, + url: PropTypes.string.isRequired, + }), taxes: PropTypes.arrayOf( PropTypes.shape({ type: PropTypes.string, diff --git a/components/expenses/ExpensesList.js b/components/expenses/ExpensesList.js index 1fbf18c9df6..7b635aa31f1 100644 --- a/components/expenses/ExpensesList.js +++ b/components/expenses/ExpensesList.js @@ -182,9 +182,9 @@ const ExpensesList = ({ onDelete={onDelete} onProcess={onProcess} selected={!openExpenseLegacyId && selectedExpenseIndex === idx} - expandExpense={e => { + expandExpense={(e, attachmentUrl) => { e.preventDefault(); - setOpenExpenseLegacyId(expense.legacyId); + setOpenExpenseLegacyId(expense.legacyId, attachmentUrl); }} useDrawer={useDrawer} /> diff --git a/components/expenses/graphql/fragments.ts b/components/expenses/graphql/fragments.ts index 4b5eeef383b..ed001a25df8 100644 --- a/components/expenses/graphql/fragments.ts +++ b/components/expenses/graphql/fragments.ts @@ -345,6 +345,16 @@ export const expensePageExpenseFieldsFragment = gql` rate idNumber } + invoiceFile { + id + url + name + type + size + ... on ImageFileInfo { + width + } + } attachedFiles { id url @@ -842,6 +852,16 @@ export const expensesListAdminFieldsFragment = gql` type rate } + invoiceFile { + id + url + name + type + size + ... on ImageFileInfo { + width + } + } attachedFiles { id url diff --git a/components/submit-expense/SubmitExpenseFlow.tsx b/components/submit-expense/SubmitExpenseFlow.tsx index 7c8ae64028b..cd1c15e190d 100644 --- a/components/submit-expense/SubmitExpenseFlow.tsx +++ b/components/submit-expense/SubmitExpenseFlow.tsx @@ -169,12 +169,6 @@ export function SubmitExpenseFlow(props: SubmitExpenseFlowProps) { url: typeof a === 'string' ? a : a?.url, })); - if (values.hasInvoiceOption === YesNoOption.YES && values.expenseTypeOption === ExpenseType.INVOICE) { - attachedFiles.push({ - url: typeof values.invoiceFile === 'string' ? values.invoiceFile : values.invoiceFile?.url, - }); - } - const expenseInput: CreateExpenseFromDashboardMutationVariables['expenseCreateInput'] = { description: values.title, reference: @@ -201,6 +195,12 @@ export function SubmitExpenseFlow(props: SubmitExpenseFlowProps) { currency: formOptions.expenseCurrency, customData: null, invoiceInfo: null, + invoiceFile: + values.hasInvoiceOption === YesNoOption.NO + ? null + : values.invoiceFile + ? { url: typeof values.invoiceFile === 'string' ? values.invoiceFile : values.invoiceFile.url } + : undefined, items: values.expenseItems.map(ei => ({ description: ei.description, amountV2: { diff --git a/components/submit-expense/form/TypeOfExpenseSection.tsx b/components/submit-expense/form/TypeOfExpenseSection.tsx index 17bb649abf9..a24c9ce906e 100644 --- a/components/submit-expense/form/TypeOfExpenseSection.tsx +++ b/components/submit-expense/form/TypeOfExpenseSection.tsx @@ -1,4 +1,6 @@ +/* eslint-disable prefer-arrow-callback */ import React from 'react'; +import { useFormikContext } from 'formik'; import { pick } from 'lodash'; import { FormattedMessage, useIntl } from 'react-intl'; @@ -35,8 +37,6 @@ function getFormProps(form: ExpenseForm) { ...pick(form.options, ['isAdminOfPayee', 'account', 'host', 'payee', 'lockedFields']), ...pick(form.values, [ 'expenseTypeOption', - 'invoiceFile', - 'hasInvoiceOption', 'acknowledgedCollectiveReceiptExpensePolicy', 'acknowledgedCollectiveInvoiceExpensePolicy', 'acknowledgedHostInvoiceExpensePolicy', @@ -45,44 +45,12 @@ function getFormProps(form: ExpenseForm) { }; } -// eslint-disable-next-line prefer-arrow-callback export const TypeOfExpenseSection = memoWithGetFormProps(function TypeOfExpenseSection( props: TypeOfExpenseSectionProps, ) { - const intl = useIntl(); const expenseTypeOption = props.expenseTypeOption; - const { toast } = useToast(); const isTypeLocked = props.lockedFields?.includes?.(ExpenseLockableFields.TYPE); - const { setFieldValue } = props; - - const onGraphQLSuccess = React.useCallback( - uploadResults => { - setFieldValue('invoiceFile', uploadResults[0].file); - }, - [setFieldValue], - ); - - const onSuccess = React.useCallback( - files => { - setFieldValue('invoiceFile', files ? files[0] : null); - }, - [setFieldValue], - ); - - const onReject = React.useCallback( - msg => { - toast({ variant: 'error', message: msg }); - }, - [toast], - ); - - const attachInvoiceLabel = React.useMemo( - () => intl.formatMessage({ defaultMessage: 'Attach your invoice file', id: 'Oa/lhY' }), - [intl], - ); - - const { LoggedInUser } = useLoggedInUser(); return ( )} - {!props.initialLoading && expenseTypeOption === ExpenseType.INVOICE && ( -
-