diff --git a/src/common/hooks/useAtomWithPrevent.ts b/src/common/hooks/useAtomWithPrevent.ts index 3effdd8499..72ff169461 100644 --- a/src/common/hooks/useAtomWithPrevent.ts +++ b/src/common/hooks/useAtomWithPrevent.ts @@ -17,14 +17,15 @@ import { } from 'jotai'; import { Invoice } from '../interfaces/invoice'; import { useEffect, useState } from 'react'; -import { cloneDeep, flatMapDeep, isEqual, isObject, keys } from 'lodash'; +import { cloneDeep, flatMapDeep, isEqual, isObject, keys, unset } from 'lodash'; import { preventLeavingPageAtom } from './useAddPreventNavigationEvents'; import { useParams } from 'react-router-dom'; import { diff } from 'deep-object-diff'; import { useDebounce } from 'react-use'; import { Quote } from '../interfaces/quote'; +import { PurchaseOrder } from '../interfaces/purchase-order'; -type Entity = Invoice | Quote; +type Entity = Invoice | Quote | PurchaseOrder; type SetAtom = (...args: Args) => Result; export const changesAtom = atom(null); @@ -81,18 +82,29 @@ export function useAtomWithPrevent( ) { const currentEntityPaths = generatePaths(entity as T); + /** + * Filters out: + * 1. Properties specified in EXCLUDING_PROPERTIES_KEYS (e.g. terms, footer etc.) + * 2. Line item _id properties (e.g. line_items.0._id which is path to the _id of the first line item) since new IDs are generated + * when joining the page + */ const currentPathsForExcluding = currentEntityPaths.filter((path) => - EXCLUDING_PROPERTIES_KEYS.some((excludingPropertyKey) => - path.includes(excludingPropertyKey) + EXCLUDING_PROPERTIES_KEYS.some( + (excludingPropertyKey) => + path?.includes(excludingPropertyKey) || + (path?.includes('line_items') && path?.split('.')?.[2] === '_id') ) ); const updatedEntity = cloneDeep(entity) as T; currentPathsForExcluding.forEach((path) => { - if (!path.includes('.')) { - delete updatedEntity[path as unknown as keyof Entity]; - delete currentInitialValue[path as unknown as keyof Entity]; + if ( + !path?.includes('.') || + (path?.includes('line_items') && path?.split('.')?.[2] === '_id') + ) { + unset(updatedEntity, path as unknown as keyof Entity); + unset(currentInitialValue, path as unknown as keyof Entity); } }); diff --git a/src/components/PreviousNextNavigation.tsx b/src/components/PreviousNextNavigation.tsx index 8256f9a859..aa783487a5 100644 --- a/src/components/PreviousNextNavigation.tsx +++ b/src/components/PreviousNextNavigation.tsx @@ -32,6 +32,7 @@ import { RecurringExpense } from '$app/common/interfaces/recurring-expense'; import { Transaction } from '$app/common/interfaces/transactions'; import { Tooltip } from './Tooltip'; import { useTranslation } from 'react-i18next'; +import { usePreventNavigation } from '$app/common/hooks/usePreventNavigation'; type Entity = | 'recurring_invoice' @@ -85,6 +86,7 @@ export function PreviousNextNavigation({ entity, entityEndpointName }: Props) { const [t] = useTranslation(); const navigate = useNavigate(); + const preventNavigation = usePreventNavigation(); const queryClient = useQueryClient(); @@ -118,6 +120,30 @@ export function PreviousNextNavigation({ entity, entityEndpointName }: Props) { return currentIndex + 1; }; + const navigateToPrevious = () => { + const previousIndex = getPreviousIndex(); + + if (previousIndex !== null) { + navigate( + route(`/${entity}s/:id/${isEditPage ? 'edit' : ''}`, { + id: currentData[previousIndex].id, + }) + ); + } + }; + + const navigateToNext = () => { + const nextIndex = getNextIndex(); + + if (nextIndex !== null) { + navigate( + route(`/${entity}s/:id/${isEditPage ? 'edit' : ''}`, { + id: currentData[nextIndex].id, + }) + ); + } + }; + useEffect(() => { const data = queryClient .getQueryCache() @@ -164,15 +190,9 @@ export function PreviousNextNavigation({ entity, entityEndpointName }: Props) { 'cursor-pointer': getPreviousIndex() !== null, })} onClick={() => { - const previousIndex = getPreviousIndex(); - - if (previousIndex !== null) { - navigate( - route(`/${entity}s/:id/${isEditPage ? 'edit' : ''}`, { - id: currentData[previousIndex].id, - }) - ); - } + preventNavigation({ + fn: () => navigateToPrevious(), + }); }} > @@ -192,15 +212,9 @@ export function PreviousNextNavigation({ entity, entityEndpointName }: Props) { 'cursor-pointer': getNextIndex() !== null, })} onClick={() => { - const nextIndex = getNextIndex(); - - if (nextIndex !== null) { - navigate( - route(`/${entity}s/:id/${isEditPage ? 'edit' : ''}`, { - id: currentData[nextIndex].id, - }) - ); - } + preventNavigation({ + fn: () => navigateToNext(), + }); }} > diff --git a/src/pages/credits/Credit.tsx b/src/pages/credits/Credit.tsx index c04a3fed1e..ebad28ae43 100644 --- a/src/pages/credits/Credit.tsx +++ b/src/pages/credits/Credit.tsx @@ -16,7 +16,7 @@ import { Page } from '$app/components/Breadcrumbs'; import { Default } from '$app/components/layouts/Default'; import { ResourceActions } from '$app/components/ResourceActions'; import { Spinner } from '$app/components/Spinner'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtomValue } from 'jotai'; import { cloneDeep } from 'lodash'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -42,6 +42,7 @@ import { } from '$app/common/queries/sockets'; import { CommonActions } from '../invoices/edit/components/CommonActions'; import { PreviousNextNavigation } from '$app/components/PreviousNextNavigation'; +import { useAtomWithPrevent } from '$app/common/hooks/useAtomWithPrevent'; export default function Credit() { const { documentTitle } = useTitle('edit_credit'); @@ -62,8 +63,8 @@ export default function Credit() { const { data } = useCreditQuery({ id: id! }); - const [credit, setQuote] = useAtom(creditAtom); const invoiceSum = useAtomValue(invoiceSumAtom); + const [credit, setCredit] = useAtomWithPrevent(creditAtom); const [client, setClient] = useState(); const [errors, setErrors] = useState(); @@ -89,7 +90,7 @@ export default function Credit() { _credit.line_items.map((item) => (item._id = v4())); - setQuote(_credit); + setCredit(_credit); if (_credit && _credit.client) { setClient(_credit.client); diff --git a/src/pages/credits/common/hooks.tsx b/src/pages/credits/common/hooks.tsx index 1e2b59cf88..3f79a94185 100644 --- a/src/pages/credits/common/hooks.tsx +++ b/src/pages/credits/common/hooks.tsx @@ -485,6 +485,7 @@ export function useActions(params?: Params) { isCommonActionSection={!dropdown} tooltipText={t('add_comment')} icon={MdComment} + disablePreventNavigation > {t('add_comment')} diff --git a/src/pages/credits/create/Create.tsx b/src/pages/credits/create/Create.tsx index 9326ef0e2a..4711be15d9 100644 --- a/src/pages/credits/create/Create.tsx +++ b/src/pages/credits/create/Create.tsx @@ -29,6 +29,7 @@ import { InvoiceSum } from '$app/common/helpers/invoices/invoice-sum'; import { InvoiceSumInclusive } from '$app/common/helpers/invoices/invoice-sum-inclusive'; import { Credit } from '$app/common/interfaces/credit'; import { Tab, Tabs } from '$app/components/Tabs'; +import { useAtomWithPrevent } from '$app/common/hooks/useAtomWithPrevent'; export interface CreditsContext { credit: Credit | undefined; @@ -73,7 +74,7 @@ export default function Create() { const [searchParams] = useSearchParams(); - const [credit, setCredit] = useAtom(creditAtom); + const [credit, setCredit] = useAtomWithPrevent(creditAtom); const [invoiceSum, setInvoiceSum] = useAtom(invoiceSumAtom); const [client, setClient] = useState(); diff --git a/src/pages/invoices/Invoice.tsx b/src/pages/invoices/Invoice.tsx index 84705a45be..ea9b787e38 100644 --- a/src/pages/invoices/Invoice.tsx +++ b/src/pages/invoices/Invoice.tsx @@ -33,7 +33,6 @@ import { Client } from '$app/common/interfaces/client'; import { useInvoiceUtilities } from './create/hooks/useInvoiceUtilities'; import { Spinner } from '$app/components/Spinner'; import { AddUninvoicedItemsButton } from './common/components/AddUninvoicedItemsButton'; -import { useAtom } from 'jotai'; import { EInvoiceComponent } from '../settings'; import { socketId, @@ -47,6 +46,7 @@ import { Invoice as InvoiceType } from '$app/common/interfaces/invoice'; import { useCheckEInvoiceValidation } from '../settings/e-invoice/common/hooks/useCheckEInvoiceValidation'; import { useCurrentCompany } from '$app/common/hooks/useCurrentCompany'; import { PreviousNextNavigation } from '$app/components/PreviousNextNavigation'; +import { useAtomWithPrevent } from '$app/common/hooks/useAtomWithPrevent'; dayjs.extend(utc); @@ -65,7 +65,7 @@ export default function Invoice() { const entityAssigned = useEntityAssigned(); const actions = useActions(); - const [invoice, setInvoice] = useAtom(invoiceAtom); + const [invoice, setInvoice] = useAtomWithPrevent(invoiceAtom); const [triggerValidationQuery, setTriggerValidationQuery] = useState(true); diff --git a/src/pages/invoices/create/Create.tsx b/src/pages/invoices/create/Create.tsx index 1acdea6bab..b83afd4aaa 100644 --- a/src/pages/invoices/create/Create.tsx +++ b/src/pages/invoices/create/Create.tsx @@ -32,6 +32,7 @@ import { Tab, Tabs } from '$app/components/Tabs'; import { InvoiceSum } from '$app/common/helpers/invoices/invoice-sum'; import { InvoiceSumInclusive } from '$app/common/helpers/invoices/invoice-sum-inclusive'; import { AddUninvoicedItemsButton } from '../common/components/AddUninvoicedItemsButton'; +import { useAtomWithPrevent } from '$app/common/hooks/useAtomWithPrevent'; export type ChangeHandler = ( property: T, @@ -54,7 +55,7 @@ export default function Create() { const { t } = useTranslation(); const { documentTitle } = useTitle('new_invoice'); - const [invoice, setInvoice] = useAtom(invoiceAtom); + const [invoice, setInvoice] = useAtomWithPrevent(invoiceAtom); const { data, isLoading } = useBlankInvoiceQuery({ enabled: typeof invoice === 'undefined', diff --git a/src/pages/invoices/edit/components/Actions.tsx b/src/pages/invoices/edit/components/Actions.tsx index fde7c63824..08b8b4b9f3 100644 --- a/src/pages/invoices/edit/components/Actions.tsx +++ b/src/pages/invoices/edit/components/Actions.tsx @@ -264,6 +264,7 @@ export function useActions(params?: Params) { isCommonActionSection={!dropdown} tooltipText={t('add_comment')} icon={MdComment} + disablePreventNavigation > {t('add_comment')} diff --git a/src/pages/purchase-orders/PurchaseOrder.tsx b/src/pages/purchase-orders/PurchaseOrder.tsx index e1f7274a48..616488401d 100644 --- a/src/pages/purchase-orders/PurchaseOrder.tsx +++ b/src/pages/purchase-orders/PurchaseOrder.tsx @@ -19,7 +19,7 @@ import { Spinner } from '$app/components/Spinner'; import { cloneDeep } from 'lodash'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useParams } from 'react-router-dom'; +import { Outlet, useParams, useSearchParams } from 'react-router-dom'; import { v4 } from 'uuid'; import { useActions } from './common/hooks'; import { useSave } from './edit/hooks/useSave'; @@ -37,11 +37,15 @@ import { InvoiceSumInclusive } from '$app/common/helpers/invoices/invoice-sum-in import { useCalculateInvoiceSum } from './edit/hooks/useCalculateInvoiceSum'; import { CommonActions } from '../invoices/edit/components/CommonActions'; import { PreviousNextNavigation } from '$app/components/PreviousNextNavigation'; +import { useAtomWithPrevent } from '$app/common/hooks/useAtomWithPrevent'; +import { purchaseOrderAtom } from './common/atoms'; export default function PurchaseOrder() { const { documentTitle } = useTitle('edit_purchase_order'); const [t] = useTranslation(); + const [searchParams] = useSearchParams(); + const { id } = useParams(); const { data } = usePurchaseOrderQuery({ id }); @@ -62,7 +66,8 @@ export default function PurchaseOrder() { >(); const [isDefaultTerms, setIsDefaultTerms] = useState(false); const [isDefaultFooter, setIsDefaultFooter] = useState(false); - const [purchaseOrder, setPurchaseOrder] = useState(); + const [purchaseOrder, setPurchaseOrder] = + useAtomWithPrevent(purchaseOrderAtom); const actions = useActions(); const tabs = useTabs({ purchaseOrder }); @@ -78,17 +83,22 @@ export default function PurchaseOrder() { } = useChangeTemplate(); useEffect(() => { - if (data) { - const po = cloneDeep(data); + const isAnyAction = searchParams.get('action'); + + const currentPurchaseOrder = + isAnyAction && purchaseOrder ? purchaseOrder : data; + + if (currentPurchaseOrder) { + const _purchaseOrder = cloneDeep(currentPurchaseOrder); - po.line_items.forEach((item) => (item._id = v4())); + _purchaseOrder.line_items.forEach((item) => (item._id = v4())); - po.invitations.forEach( + _purchaseOrder.invitations.forEach( (invitation) => (invitation['client_contact_id'] = invitation.client_contact_id || '') ); - setPurchaseOrder(po); + setPurchaseOrder(_purchaseOrder); } }, [data]); diff --git a/src/pages/purchase-orders/common/hooks.tsx b/src/pages/purchase-orders/common/hooks.tsx index b3c8359bb2..9744312b60 100644 --- a/src/pages/purchase-orders/common/hooks.tsx +++ b/src/pages/purchase-orders/common/hooks.tsx @@ -557,6 +557,7 @@ export function useActions(params: ActionsParams = {}) { isCommonActionSection={!dropdown} tooltipText={t('add_comment')} icon={MdComment} + disablePreventNavigation > {t('add_comment')} diff --git a/src/pages/purchase-orders/create/Create.tsx b/src/pages/purchase-orders/create/Create.tsx index 03406c4c5d..c71286f2ff 100644 --- a/src/pages/purchase-orders/create/Create.tsx +++ b/src/pages/purchase-orders/create/Create.tsx @@ -18,7 +18,6 @@ import { ValidationBag } from '$app/common/interfaces/validation-bag'; import { Page } from '$app/components/Breadcrumbs'; import { Default } from '$app/components/layouts/Default'; import { Spinner } from '$app/components/Spinner'; -import { useAtom } from 'jotai'; import { cloneDeep } from 'lodash'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,6 +32,7 @@ import { InvoiceSumInclusive } from '$app/common/helpers/invoices/invoice-sum-in import { useBlankPurchaseOrderQuery } from '$app/common/queries/purchase-orders'; import { Tab, Tabs } from '$app/components/Tabs'; import { useCalculateInvoiceSum } from '../edit/hooks/useCalculateInvoiceSum'; +import { useAtomWithPrevent } from '$app/common/hooks/useAtomWithPrevent'; export interface PurchaseOrderContext { purchaseOrder: PurchaseOrder | undefined; @@ -80,7 +80,8 @@ export default function Create() { }, ]; - const [purchaseOrder, setPurchaseOrder] = useAtom(purchaseOrderAtom); + const [purchaseOrder, setPurchaseOrder] = + useAtomWithPrevent(purchaseOrderAtom); const { data, isLoading } = useBlankPurchaseOrderQuery({ enabled: typeof purchaseOrder === 'undefined', diff --git a/src/pages/quotes/common/hooks.tsx b/src/pages/quotes/common/hooks.tsx index ba949c56d1..47ff802aec 100644 --- a/src/pages/quotes/common/hooks.tsx +++ b/src/pages/quotes/common/hooks.tsx @@ -507,6 +507,7 @@ export function useActions(params?: Params) { isCommonActionSection={!dropdown} tooltipText={t('add_comment')} icon={MdComment} + disablePreventNavigation > {t('add_comment')}