Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Adding "Leave Page" Functionality To Invoices, Credits and Purchase Orders #2317

Merged
merged 8 commits into from
Jan 27, 2025
Merged
26 changes: 19 additions & 7 deletions src/common/hooks/useAtomWithPrevent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends any[], Result> = (...args: Args) => Result;

export const changesAtom = atom<any | null>(null);
Expand Down Expand Up @@ -81,18 +82,29 @@ export function useAtomWithPrevent<T extends Entity>(
) {
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')
Comment on lines +103 to +104
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, please explain change on this and logic behind it, seems weird.

Also, please add it as comment for future reference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beganovich Sure, the comment has been added. This logic aims to exclude unnecessary properties from tracking.

The _id property for each line item is newly generated whenever we load the edit page, eliminating the need to track them at all. Paths are generated conventionally - properties on the first level of an object use just the property name, while nested properties use dots between levels. For example, a path like 'line_items.0._id' indicates that if the path includes 'line_items' and the property is '_id', we exclude that path from tracking. This logic will only exclude _id properties from line items since it specifically targets these paths. All other line item properties will be tracked normally.

) {
unset(updatedEntity, path as unknown as keyof Entity);
unset(currentInitialValue, path as unknown as keyof Entity);
}
});

Expand Down
50 changes: 32 additions & 18 deletions src/components/PreviousNextNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -85,6 +86,7 @@ export function PreviousNextNavigation({ entity, entityEndpointName }: Props) {

const [t] = useTranslation();
const navigate = useNavigate();
const preventNavigation = usePreventNavigation();

const queryClient = useQueryClient();

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(),
});
}}
>
<Icon element={MdKeyboardArrowLeft} size={29} />
Expand All @@ -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(),
});
}}
>
<Icon element={MdKeyboardArrowRight} size={29} />
Expand Down
7 changes: 4 additions & 3 deletions src/pages/credits/Credit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand All @@ -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<Client>();
const [errors, setErrors] = useState<ValidationBag>();
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/pages/credits/common/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ export function useActions(params?: Params) {
isCommonActionSection={!dropdown}
tooltipText={t('add_comment')}
icon={MdComment}
disablePreventNavigation
>
{t('add_comment')}
</EntityActionElement>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/credits/create/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Client>();
Expand Down
4 changes: 2 additions & 2 deletions src/pages/invoices/Invoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -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<boolean>(true);
Expand Down
3 changes: 2 additions & 1 deletion src/pages/invoices/create/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T extends keyof Invoice>(
property: T,
Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/pages/invoices/edit/components/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export function useActions(params?: Params) {
isCommonActionSection={!dropdown}
tooltipText={t('add_comment')}
icon={MdComment}
disablePreventNavigation
>
{t('add_comment')}
</EntityActionElement>
Expand Down
24 changes: 17 additions & 7 deletions src/pages/purchase-orders/PurchaseOrder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 });

Expand All @@ -62,7 +66,8 @@ export default function PurchaseOrder() {
>();
const [isDefaultTerms, setIsDefaultTerms] = useState<boolean>(false);
const [isDefaultFooter, setIsDefaultFooter] = useState<boolean>(false);
const [purchaseOrder, setPurchaseOrder] = useState<PurchaseOrderType>();
const [purchaseOrder, setPurchaseOrder] =
useAtomWithPrevent(purchaseOrderAtom);

const actions = useActions();
const tabs = useTabs({ purchaseOrder });
Expand All @@ -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]);

Expand Down
1 change: 1 addition & 0 deletions src/pages/purchase-orders/common/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ export function useActions(params: ActionsParams = {}) {
isCommonActionSection={!dropdown}
tooltipText={t('add_comment')}
icon={MdComment}
disablePreventNavigation
>
{t('add_comment')}
</EntityActionElement>
Expand Down
5 changes: 3 additions & 2 deletions src/pages/purchase-orders/create/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/pages/quotes/common/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ export function useActions(params?: Params) {
isCommonActionSection={!dropdown}
tooltipText={t('add_comment')}
icon={MdComment}
disablePreventNavigation
>
{t('add_comment')}
</EntityActionElement>
Expand Down
Loading