Skip to content

Commit

Permalink
Add Reference field to Invoice (#10637)
Browse files Browse the repository at this point in the history
* refact: enable Wise transfer details for everyone

* refact: add reference input to Expense form

* refact: add reference input to new Expense flow

* refact: display invoice reference in Expense lists

* refact: automatically populate Wise transfer reference

* chore: update langs and schemas

* refact: automatically truncate reference

* fix: expense reference input height bug

* chore: update reference field description to fit in the field placeholder

* enhancement(ExpenseForm): add optional to reference label

* enhancement(ExpenseForm): better data sanitization

* enhancement(Expense): reveal full reference on hover

* update graphql schemas

---------

Co-authored-by: Benjamin Piouffle <[email protected]>
  • Loading branch information
kewitz and Betree committed Sep 10, 2024
1 parent bf3d097 commit 8920c01
Show file tree
Hide file tree
Showing 37 changed files with 920 additions and 635 deletions.
32 changes: 0 additions & 32 deletions components/TruncatedTextWithTooltip.js

This file was deleted.

41 changes: 41 additions & 0 deletions components/TruncatedTextWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { truncate } from 'lodash';

import { cn, truncateMiddle } from '../lib/utils';

import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';

/**
* A tooltip that truncates a value if it's longer than the
* provided length.
*/
const TruncatedTextWithTooltip = ({
value,
cursor = 'help',
truncatePosition = 'start',
length = 30,
}: {
value: string;
cursor?: React.CSSProperties['cursor'];
truncatePosition?: 'start' | 'middle' | 'end';
length?: number;
}) => {
if (value?.length <= length) {
return value;
} else {
return (
<Tooltip>
<TooltipTrigger className={cn(cursor && `cursor-${cursor}`)}>
{truncatePosition === 'start'
? truncate(value, { length })
: truncatePosition === 'middle'
? truncateMiddle(value, length)
: '...'}
</TooltipTrigger>
<TooltipContent>{value}</TooltipContent>
</Tooltip>
);
}
};

export default TruncatedTextWithTooltip;
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,17 @@
exports[`TruncatedTextWithTooltip component renders default options 1`] = `"A short string"`;

exports[`TruncatedTextWithTooltip component renders default options 2`] = `
.c0 {
display: inline-block;
cursor: help;
}
.c0 button:disabled {
pointer-events: none;
}
<div
className="c0"
cursor="help"
data-cy="tooltip-trigger"
display="inline-block"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
<button
className="cursor-help"
data-state="closed"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onPointerDown={[Function]}
onPointerLeave={[Function]}
onPointerMove={[Function]}
type="button"
>
a string that is more than ...
</div>
</button>
`;
20 changes: 18 additions & 2 deletions components/budget/ExpenseBudgetItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import StyledLink from '../StyledLink';
import Tags from '../Tags';
import { H3 } from '../Text';
import TransactionSign from '../TransactionSign';
import TruncatedTextWithTooltip from '../TruncatedTextWithTooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';

const DetailColumnHeader = styled.div`
Expand Down Expand Up @@ -401,14 +402,28 @@ const ExpenseBudgetItem = ({
<Box mt="6px">
<PayoutMethodTypeWithIcon
isLoading={isLoading}
type={expense?.payoutMethod?.type}
type={expense.payoutMethod?.type}
iconSize="10px"
fontSize="11px"
fontWeight="normal"
color="black.700"
/>
</Box>
</Box>
{Boolean(expense.reference) && (
<Box mr={[3, 4]}>
<DetailColumnHeader>
<FormattedMessage id="Expense.Reference" defaultMessage="Reference" />
</DetailColumnHeader>
{isLoading ? (
<LoadingPlaceholder height={15} width={90} />
) : (
<div className="mt-[4px] text-[11px]">
<TruncatedTextWithTooltip value={expense.reference} length={10} truncatePosition="middle" />
</div>
)}
</Box>
)}
{nbAttachedFiles > 0 && (
<Box mr={[3, 4]}>
<DetailColumnHeader>
Expand Down Expand Up @@ -438,7 +453,7 @@ const ExpenseBudgetItem = ({
)}
</Box>
)}
{Boolean(expense?.account?.hostAgreements?.totalCount) && (
{Boolean(expense.account?.hostAgreements?.totalCount) && (
<Box mr={[3, 4]}>
<DetailColumnHeader>
<FormattedMessage defaultMessage="Host Agreements" id="kq2gKV" />
Expand Down Expand Up @@ -528,6 +543,7 @@ ExpenseBudgetItem.propTypes = {
totalCount: PropTypes.number,
}),
type: PropTypes.string.isRequired,
reference: PropTypes.string,
description: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
Expand Down
84 changes: 61 additions & 23 deletions components/expenses/ExpenseForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Undo } from '@styled-icons/fa-solid/Undo';
import { Field, FieldArray, Form, Formik } from 'formik';
import { first, isEmpty, omit, pick } from 'lodash';
import { first, isEmpty, omit, pick, trimStart } from 'lodash';
import { useRouter } from 'next/router';
import { createPortal } from 'react-dom';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
Expand Down Expand Up @@ -46,6 +46,7 @@ import StyledButton from '../StyledButton';
import StyledCard from '../StyledCard';
import { StyledCurrencyPicker } from '../StyledCurrencyPicker';
import StyledHr from '../StyledHr';
import StyledInput from '../StyledInput';
import StyledInputFormikField from '../StyledInputFormikField';
import StyledTextarea from '../StyledTextarea';
import { Label, P, Span } from '../Text';
Expand Down Expand Up @@ -180,23 +181,19 @@ export const prepareExpenseForSubmit = expenseData => {
}

return {
...pick(expenseData, [
'id',
'description',
'longDescription',
'type',
'privateMessage',
'invoiceInfo',
'tags',
'currency',
]),
...pick(expenseData, ['id', 'type', 'tags', 'currency']),
payee,
payeeLocation,
payoutMethod,
attachedFiles: keepAttachedFiles ? expenseData.attachedFiles?.map(file => pick(file, ['id', 'url', 'name'])) : [],
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']),
description: expenseData.description?.trim(),
longDescription: expenseData.longDescription?.trim(),
privateMessage: expenseData.privateMessage?.trim(),
invoiceInfo: expenseData.invoiceInfo?.trim(),
reference: expenseData.reference?.trim(),
};
};

Expand Down Expand Up @@ -834,20 +831,60 @@ const ExpenseFormBody = ({
</div>
)}
{values.type === expenseTypes.INVOICE && (
<Box my={40}>
<ExpenseAttachedFilesForm
title={<FormattedMessage id="UploadInvoice" defaultMessage="Upload invoice" />}
description={
<React.Fragment>
<div className="mt-2">
<div className="text-lg font-normal text-muted-foreground">
<FormattedMessage
id="UploadInvoiceDescription"
defaultMessage="If you already have an invoice document, you can upload it here."
defaultMessage="{field} (optional)"
id="OptionalFieldLabel"
values={{
field: (
<span className="font-bold text-foreground">
<FormattedMessage id="InvoiceReference" defaultMessage="Invoice reference" />
</span>
),
}}
/>
}
onChange={attachedFiles => formik.setFieldValue('attachedFiles', attachedFiles)}
form={formik}
defaultValue={values.attachedFiles}
/>
</Box>
</div>
<p className="text-xs text-gray-500">
<FormattedMessage
id="InvoiceReferenceDescription"
defaultMessage="If the invoice being submitted has a reference number, add it here"
/>
</p>
<Field
as={StyledInput}
autoFocus={autoFocusTitle}
error={errors.reference}
fontSize="14px"
id="expense-reference"
mt={3}
name="reference"
px="12px"
py="8px"
width="100%"
maxLength={255}
onChange={e => {
e.target.value = trimStart(e.target.value).replace(/\s+/g, ' ');
handleChange(e);
}}
/>
</div>
<div className="mt-5">
<ExpenseAttachedFilesForm
title={<FormattedMessage id="UploadInvoice" defaultMessage="Upload invoice" />}
description={
<FormattedMessage
id="UploadInvoiceDescription"
defaultMessage="If you already have an invoice document, you can upload it here."
/>
}
onChange={attachedFiles => formik.setFieldValue('attachedFiles', attachedFiles)}
form={formik}
defaultValue={values.attachedFiles}
/>
</div>
</React.Fragment>
)}

<Flex alignItems="center" my={24}>
Expand Down Expand Up @@ -1028,6 +1065,7 @@ const ExpenseForm = ({
initialValues.items = expense.draft.items?.map(newExpenseItem) || [];
initialValues.taxes = expense.draft.taxes;
initialValues.attachedFiles = expense.draft.attachedFiles;
initialValues.reference = expense.draft.reference;
initialValues.payoutMethod = expense.draft.payoutMethod || expense.payoutMethod;
initialValues.payeeLocation = expense.draft.payeeLocation;
initialValues.payee = expense.recurringExpense ? expense.payee : expense.draft.payee;
Expand Down
32 changes: 24 additions & 8 deletions components/expenses/ExpenseSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import StyledCard from '../StyledCard';
import StyledHr from '../StyledHr';
import Tags from '../Tags';
import { H1, P, Span } from '../Text';
import TruncatedTextWithTooltip from '../TruncatedTextWithTooltip';
import { Separator } from '../ui/Separator';
import UploadedFilePreview from '../UploadedFilePreview';

Expand Down Expand Up @@ -299,24 +300,38 @@ const ExpenseSummary = ({
) : (
<P fontSize="14px" color="black.700" data-cy="expense-author">
<FormattedDate value={expense.createdAt} dateStyle="medium" />
{expense?.comments && (
{expense.merchantId && (
<React.Fragment>
<Spacer />
<MessageSquare size="16px" style={{ display: 'inline-block' }} />
&nbsp;
{expense.comments.totalCount}
<FormattedMessage
id="Expense.MerchantId"
defaultMessage="Merchant ID: {id}"
values={{ id: expense.merchantId }}
/>
</React.Fragment>
)}
{expense?.merchantId && (
{expense.reference && (
<React.Fragment>
<Spacer />
<FormattedMessage
id="Expense.MerchantId"
defaultMessage="Merchant ID: {id}"
values={{ id: expense.merchantId }}
id="ReferenceValue"
defaultMessage="Ref: {reference}"
values={{
reference: (
<TruncatedTextWithTooltip value={expense.reference} length={10} truncatePosition="middle" />
),
}}
/>
</React.Fragment>
)}
{expense.comments && (
<React.Fragment>
<Spacer />
<MessageSquare size="16px" style={{ display: 'inline-block' }} />
&nbsp;
{expense.comments.totalCount}
</React.Fragment>
)}
</P>
)}
</Flex>
Expand Down Expand Up @@ -535,6 +550,7 @@ ExpenseSummary.propTypes = {
legacyId: PropTypes.number,
accountingCategory: PropTypes.object,
description: PropTypes.string.isRequired,
reference: PropTypes.string,
longDescription: PropTypes.string,
amount: PropTypes.number.isRequired,
currency: PropTypes.string.isRequired,
Expand Down
Loading

0 comments on commit 8920c01

Please sign in to comment.