Skip to content

Commit

Permalink
Merge branch 'develop' into 5137.2-Requisition-line-edit-+-UI-updates…
Browse files Browse the repository at this point in the history
…-to-line-columns
  • Loading branch information
roxy-dao committed Nov 5, 2024
2 parents 957c5e2 + bdaeea7 commit 04cc6b1
Show file tree
Hide file tree
Showing 61 changed files with 1,676 additions and 206 deletions.
Original file line number Diff line number Diff line change
@@ -1,73 +1,51 @@
import { useContext, useEffect, useRef } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
import { useCallback, useContext, useEffect } from 'react';
import { useBeforeUnload, useBlocker } from 'react-router-dom';
import { useTranslation } from '@common/intl';
import { useToggle } from '../useToggle';
import { ConfirmationModalContext } from '@openmsupply-client/common';

const promptUser = (e: BeforeUnloadEvent) => {
// Cancel the event
e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
// Chrome requires returnValue to be set
e.returnValue = '';
};

// Ideally we'd use the `Prompt` component instead ( or usePrompt or useBlocker ) to prompt when navigating away using react-router
// however, these weren't implemented in react-router-dom v6 at the time of implementation
/** useConfirmOnLeaving is a hook that will prompt the user if they try to navigate away from,
* or refresh the page, when there are unsaved changes. Be careful when using within a tab component though
* these are unloaded, but the event handler is at the window level, and so doesn't care
* or refresh the page, when there are unsaved changes.
* */
export const useConfirmOnLeaving = (isUnsaved?: boolean) => {
const unblockRef = useRef<any>(null);
const { navigator } = useContext(NavigationContext);
const t = useTranslation();
const { isOn, toggle } = useToggle();
const showConfirmation = (onOk: () => void) => {
if (
confirm(
`${t('heading.are-you-sure')}\n${t('messages.confirm-cancel-generic')}`
)
) {
onOk();
}
const customConfirm = (onOk: () => void) => {
setOnConfirm(onOk);
showConfirmation();
};

useEffect(() => {
// note that multiple calls to addEventListener don't result in multiple event listeners being added
// since the method called is idempotent. However, I didn't want to rely on the implementation details
// so have the toggle state to ensure we only add/remove the event listener once
if (isUnsaved && !isOn) {
window.addEventListener('beforeunload', promptUser, { capture: true });
toggle();
const push = navigator.push;
const { setOpen, setMessage, setOnConfirm, setTitle } = useContext(
ConfirmationModalContext
);

navigator.push = (...args: Parameters<typeof push>) => {
showConfirmation(() => {
push(...args);
});
};
const showConfirmation = useCallback(() => {
setMessage(t('heading.are-you-sure'));
setTitle(t('messages.confirm-cancel-generic'));
setOpen(true);
}, [setMessage, setTitle, setOpen]);

return () => {
navigator.push = push;
};
}
if (!isUnsaved && isOn) {
window.removeEventListener('beforeunload', promptUser, { capture: true });
toggle();
unblockRef.current?.();
}
}, [isUnsaved]);
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
!!isUnsaved && currentLocation.pathname !== nextLocation.pathname
);

// always remove the event listener on unmount, and don't check the toggle
// which would be trapped in a stale closure
useEffect(
() => () => {
window.removeEventListener('beforeunload', promptUser, {
capture: true,
});
unblockRef.current?.();
},
[]
// handle page refresh events
useBeforeUnload(
useCallback(
event => {
// Cancel the refresh
if (isUnsaved) event.preventDefault();
},
[isUnsaved]
),
{ capture: true }
);

return { showConfirmation };
useEffect(() => {
if (blocker.state === 'blocked') {
setOnConfirm(blocker.proceed);
showConfirmation();
}
}, [blocker]);

return { showConfirmation: customConfirm };
};
4 changes: 3 additions & 1 deletion client/packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export {
Link,
useNavigate,
useParams,
BrowserRouter,
HashRouter,
Routes,
Route,
Navigate,
useMatch,
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
} from 'react-router-dom';

export * from './utils';
Expand Down
6 changes: 4 additions & 2 deletions client/packages/common/src/intl/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
"description.fc-sell-price": "Foreign currency sell price per pack",
"description.forecast-quantity": "Forecast Quantity to Reach Target",
"description.initial-stock-on-hand": "Stock on hand on the first day of the program period",
"description.available-stock": "The customer's initial stock on hand + incoming stock +/- inventory adjustments - outgoing stock",
"description.available-stock": "The customer's initial stock on hand + incoming stock +/- inventory adjustments - outgoing stock",
"description.invoice-number": "Shipment number",
"description.last-reading-datetime": "Date and time of the last reading",
"description.max-min-temperature": "Maximum or minimum temperature reading",
Expand Down Expand Up @@ -237,6 +237,8 @@
"error.failed-to-save-item-variant": "Failed to save item variant",
"error.failed-to-save-service-charges": "Failed to save service charges",
"error.failed-to-save-vaccination": "Failed to save vaccination!",
"error.failed-to-save-item-variant": "Failed to save item variant",
"error.duplicate-item-variant-name": "Item variant with the same name already exists for this item",
"error.failed-to-save-vaccine-course": "Failed to save vaccine course",
"error.fetch-notifications": "Unable to display cold chain notifications",
"error.field-must-be-specified": "{{field}} must be specified",
Expand Down Expand Up @@ -1642,4 +1644,4 @@
"warning.caps-lock": "Warning: Caps lock is on",
"warning.field-not-parsed": "{{field}} not parsed",
"warning.nothing-to-supply": "Nothing left to supply!"
}
}
14 changes: 11 additions & 3 deletions client/packages/common/src/intl/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"button.save-and-confirm-status": "Confirmer {{status}}",
"button.save-log": "Enregistrer le journal",
"button.scan": "Scanner",
"button.select-a-color": "Choisir une couleur",
"button.select-a-color": "Sélectionner une couleur",
"button.stop": "Stop",
"button.supply-to-approved": "Fournir Qtés Autorisées",
"button.supply-to-requested": "Quantité demandée",
Expand Down Expand Up @@ -155,7 +155,7 @@
"description.last-reading-datetime": "Date et heure du dernier relevé",
"description.max-min-temperature": "Relevé de temperature maximum ou minimum",
"description.months-of-stock": "Mois de stock disponible",
"description.notification-preferences": "Par défaut, le système vous dira si vous avez plus de 3 ou 6 mois de stock sur la page d'accueil.",
"description.notification-preferences": "Par défaut, le système vous dira si vous avez moin de 3 ou plus de 6 mois de stock sur la page d'accueil.",
"description.number-of-shipments": "Le nombre de bons de livraison créé depuis cette réquisition",
"description.our-soh": "Notre Stock",
"description.pack-quantity": "Quantité reçues en nombre de boites",
Expand Down Expand Up @@ -1535,5 +1535,13 @@
"warning.cannot-create-placeholder-units": "Il y a un total de {{allocatedQuantity}} unités disponibles. Impossible d'allouer toutes les {{requestedQuantity}} unités demandées.",
"warning.caps-lock": "Avertissement: Verrouillage majuscule activé",
"warning.field-not-parsed": "{{field}} non analysé",
"warning.nothing-to-supply": "La quantité demandée à été fournie !"
"warning.nothing-to-supply": "La quantité demandée à été fournie !",
"button.new-requisition": "Nouvelle Réquisition",
"button.update-status": "Mettre à jour le statut",
"button.view-prescription": "Consulter la prescription",
"button.replenishment-return-lines": "Retourner les Lignes Sélectionnées",
"description.initial-stock-on-hand": "Stock disponible au premier jour de la période du programme",
"description.available-stock": "Stock initial du client + stock entrant +/- ajustements d'inventaire - stock sortant",
"description.rnr-adjustments": "Ajustements effectués pour cet article durant cette période (via les prises d'inventaire ou ajustements d'inventaire)",
"description.rnr-losses": "Enregistrer manuellement les pertes de cet article durant la période"
}
9 changes: 3 additions & 6 deletions client/packages/common/src/intl/utils/DateUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { renderHookWithProvider } from '../../utils/testing';
import { useFormatDateTime } from './DateUtils';
import { DateUtils } from './DateUtils';

describe('useFormatDateTime', () => {
it('getLocalDateTime returns start of day for local timezone regardless of time zone', () => {
const { result } = renderHookWithProvider(useFormatDateTime);
it('getNaiveDate returns start of day for local timezone regardless of time zone', () => {
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
const date = '2024-02-07';

expect(
result.current
.getLocalDate(date, undefined, undefined, timeZone)
DateUtils.getNaiveDate(date, undefined, undefined, timeZone)
?.toString()
.slice(0, 24)
).toBe('Wed Feb 07 2024 00:00:00');
Expand Down
60 changes: 29 additions & 31 deletions client/packages/common/src/intl/utils/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,35 @@ export const DateUtils = {
: new Date(date);
return isValid(maybeDate) ? maybeDate : null;
},
/**
* While getDateOrNull is naive to the timezone, the timezone will still
* change. When converting from the assumed naive zone of GMT to the local
* timezone, the dateTime will be wrong if the timezone is behind GMT.
* For example: for a user in -10 timezone, a date of 24-02-2024 will become
* 2024-02-23T13:00:00.000Z when rendered for mui datepicker.
* This function acts in the same way as getDateOrNull, but will create a
* datetime of start of day local time rather than start of day GMT by
* subtracting the local timezone offset.
* You can use this function anytime you need a datetime for mui date picker
* to be created from a date only string. This includes date of birth, date of
* death or any other date which is time and timezone agnostic.
*/
getNaiveDate: (
date?: Date | string | null,
format?: string,
options?: ParseOptions,
timeZone?: string
): Date | null => {
// tz passed as props options for testing purposes
const tz = timeZone ?? new Intl.DateTimeFormat().resolvedOptions().timeZone;
const UTCDateWithoutTime = DateUtils.getDateOrNull(date, format, options);
const offset = UTCDateWithoutTime
? getTimezoneOffset(tz, UTCDateWithoutTime)
: 0;
return UTCDateWithoutTime
? addMilliseconds(UTCDateWithoutTime, -offset)
: null;
},
minDate: (...dates: (Date | null)[]) => {
const maybeDate = fromUnixTime(
Math.min(
Expand Down Expand Up @@ -221,36 +250,6 @@ export const useFormatDateTime = () => {
: '';
};

/**
* While getDateOrNull is naive to the timezone, the timezone will still change.
* When converting from the assumed naive zone of GMT to the local timezone, the
* dateTime will be wrong if the timezone is behind GMT.
* For example: for a user in -10 timezone, a date of 24-02-2024 will become
* 2024-02-23T13:00:00.000Z when rendered for mui datepicker.
* This function acts in the same way as getDateOrNull, but will create a datetime
* of start of day local time rather than start of day GMT by subtracting the local
* timezone offset.
* You can use this function anytime you need a datetime for mui date picker to
* be created from a date only string. This includes date of birth, date of death
* or any other date which is time and timezone agnostic.
*/
const getLocalDate = (
date?: Date | string | null,
format?: string,
options?: ParseOptions,
timeZone?: string
): Date | null => {
// tz passed as props options for testing purposes
const tz = timeZone ?? new Intl.DateTimeFormat().resolvedOptions().timeZone;
const UTCDateWithoutTime = DateUtils.getDateOrNull(date, format, options);
const offset = UTCDateWithoutTime
? getTimezoneOffset(tz, UTCDateWithoutTime)
: 0;
return UTCDateWithoutTime
? addMilliseconds(UTCDateWithoutTime, -offset)
: null;
};

return {
urlQueryDate,
urlQueryDateTime,
Expand All @@ -263,6 +262,5 @@ export const useFormatDateTime = () => {
localisedDistanceToNow,
localisedTime,
relativeDateTime,
getLocalDate,
};
};
2 changes: 1 addition & 1 deletion client/packages/common/src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7490,7 +7490,7 @@ export enum UniqueValueKey {
Serial = 'serial'
}

export type UniqueValueViolation = InsertAssetCatalogueItemErrorInterface & InsertAssetErrorInterface & InsertAssetLogErrorInterface & InsertAssetLogReasonErrorInterface & InsertDemographicIndicatorErrorInterface & InsertDemographicProjectionErrorInterface & InsertLocationErrorInterface & UpdateAssetErrorInterface & UpdateDemographicIndicatorErrorInterface & UpdateDemographicProjectionErrorInterface & UpdateLocationErrorInterface & UpdateSensorErrorInterface & {
export type UniqueValueViolation = InsertAssetCatalogueItemErrorInterface & InsertAssetErrorInterface & InsertAssetLogErrorInterface & InsertAssetLogReasonErrorInterface & InsertDemographicIndicatorErrorInterface & InsertDemographicProjectionErrorInterface & InsertLocationErrorInterface & UpdateAssetErrorInterface & UpdateDemographicIndicatorErrorInterface & UpdateDemographicProjectionErrorInterface & UpdateLocationErrorInterface & UpdateSensorErrorInterface & UpsertItemVariantErrorInterface & {
__typename: 'UniqueValueViolation';
description: Scalars['String']['output'];
field: UniqueValueKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,25 @@ export const DateFilter: FC<{ filterDefinition: DateFilterDefinition }> = ({

const getDateFromUrl = (query: string, range: RangeOption | undefined) => {
const value = typeof query !== 'object' || !range ? query : query[range];
return DateUtils.getDateOrNull(value);
return DateUtils.getNaiveDate(value);
};

const getRangeBoundary = (
query: string | RangeObject<string>,
range: RangeOption | undefined,
limit: Date | string | undefined
) => {
const limitDate = DateUtils.getDateOrNull(limit);
const limitDate = DateUtils.getNaiveDate(limit);
if (typeof query !== 'object' || !range) return limitDate || undefined;
const { from, to } = query as RangeObject<string>;

if (range === 'from')
return to
? DateUtils.minDate(DateUtils.getDateOrNull(to), limitDate) ?? undefined
: limitDate ?? undefined;
? (DateUtils.minDate(DateUtils.getNaiveDate(to), limitDate) ?? undefined)
: (limitDate ?? undefined);
else
return from
? DateUtils.maxDate(DateUtils.getDateOrNull(from), limitDate) ?? undefined
: limitDate ?? undefined;
? (DateUtils.maxDate(DateUtils.getNaiveDate(from), limitDate) ??
undefined)
: (limitDate ?? undefined);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ export const NumberInputCell = <T extends RecordWithId>({
TextInputProps,
width,
endAdornment,
error,
}: CellProps<T> &
NumericInputProps & {
id?: string;
TextInputProps?: StandardTextFieldProps;
endAdornment?: string;
error?: boolean;
}): React.ReactElement<CellProps<T>> => {
const [buffer, setBuffer] = useBufferState(column.accessor({ rowData }));
const updater = useDebounceCallback(column.setter, [column.setter], 250);
Expand Down Expand Up @@ -64,6 +66,7 @@ export const NumberInputCell = <T extends RecordWithId>({
value={buffer as number | undefined}
width={width}
endAdornment={endAdornment}
error={error}
/>
);
};
Loading

0 comments on commit 04cc6b1

Please sign in to comment.