Skip to content

Commit

Permalink
Merge branch '6305-add-next-of-kin-to-patient-schema' into 6306-add-n…
Browse files Browse the repository at this point in the history
…ext-of-kin-to-patient-edit
  • Loading branch information
lache-melvin committed Jan 29, 2025
2 parents 21f6b01 + d70b815 commit 49ab2dc
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,9 @@ export const EquipmentDetailView = () => {
const t = useTranslation();
const { setCustomBreadcrumbs } = useBreadcrumbs();
const [draft, setDraft] = useState<DraftAsset>();
const [isDirty, setIsDirty] = useState(false);
const { error, success } = useNotification();

useConfirmOnLeaving(isDirty);
const { isDirty, setIsDirty } = useConfirmOnLeaving('equipment-detail-view');

const save = async () => {
if (!draft) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import React, { useRef } from 'react';
import {
AppBarButtonsPortal,
LoadingButton,
Expand All @@ -22,7 +22,6 @@ export const AppBarButtons = () => {
const t = useTranslation();
const hiddenFileInput = useRef<HTMLInputElement>(null);
const { storeId } = useAuthContext();
const [isUploadingFridgeTag, setIsUploadingFridgeTag] = useState(false);
const { success, error } = useNotification();
const queryClient = useQueryClient();
const sensorApi = useSensor.utils.api();
Expand All @@ -34,7 +33,8 @@ export const AppBarButtons = () => {
title: t('title.new-sensor'),
});
// prevent a user reloading the page while uploading
useConfirmOnLeaving(isUploadingFridgeTag);
const { isDirty: isUploadingFridgeTag, setIsDirty: setIsUploadingFridgeTag } =
useConfirmOnLeaving('upload-fridge-tag');

const onUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e?.target?.files?.[0];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Grid } from '@mui/material';
import { StoryFn } from '@storybook/react';
import { useConfirmOnLeaving } from './useConfirmOnLeaving';
Expand All @@ -10,14 +10,13 @@ export default {
};

const Template: StoryFn = () => {
const [isUnsaved, setIsUnsaved] = useState(false);
useConfirmOnLeaving(isUnsaved);
const { isDirty, setIsDirty } = useConfirmOnLeaving('storybook');

return (
<Grid>
<ToggleButton
selected={isUnsaved}
onClick={() => setIsUnsaved(!isUnsaved)}
selected={isDirty}
onClick={() => setIsDirty(!isDirty)}
label="Prompt if leaving this page"
value="dirty"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useContext, useEffect } from 'react';
import { useBeforeUnload, useBlocker } from 'react-router-dom';
import { create } from 'zustand';
import { useTranslation } from '@common/intl';
import { ConfirmationModalContext, Location } from '@openmsupply-client/common';

Expand All @@ -8,38 +9,64 @@ import { ConfirmationModalContext, Location } from '@openmsupply-client/common';
* navigate away from, or refresh the page, when there are unsaved changes.
*/
export const useConfirmOnLeaving = (
isUnsaved?: boolean,
key: string,
customCheck?: (currentLocation: Location, nextLocation: Location) => boolean
) => {
const { blocking, setBlocking, clearKey } = useBlockNavigationState();

// Register the key for blocking navigation
useEffect(() => {
setBlocking(key, false, customCheck);

// Cleanup
return () => clearKey(key);
}, []);

const isDirty = blocking.get(key)?.shouldBlock ?? false;
const setIsDirty = (dirty: boolean) => setBlocking(key, dirty, customCheck);

return { isDirty, setIsDirty };
};

/**
* useBlocker only allows one blocker to be active at a time, despite the fact that
* we might want to block from multiple sources.
*
* So we render this hook in `Site`, at the root of the app, and handle the different
* blocking conditions with BlockNavigation zustand state
*/
export const useBlockNavigation = () => {
const t = useTranslation();
const customConfirm = (onOk: () => void) => {
setOnConfirm(onOk);
showConfirmation();
};
const { blocking } = useBlockNavigationState();

const { setOpen, setMessage, setOnConfirm, setTitle } = useContext(
ConfirmationModalContext
);

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

const blockers: BlockingState[] = Array.from(blocking.values());
const shouldBlock = blockers.some(b => b.shouldBlock);

const blocker = useBlocker(({ currentLocation, nextLocation }) => {
if (customCheck) return customCheck(currentLocation, nextLocation);
return !!isUnsaved && currentLocation.pathname !== nextLocation.pathname;
for (const b of blockers) {
if (b.customCheck) return b.customCheck(currentLocation, nextLocation);
}
return !!shouldBlock && currentLocation.pathname !== nextLocation.pathname;
});

// handle page refresh events
useBeforeUnload(
useCallback(
event => {
// Cancel the refresh
if (isUnsaved) event.preventDefault();
if (shouldBlock) event.preventDefault();
},
[isUnsaved]
[shouldBlock]
),
{ capture: true }
);
Expand All @@ -50,6 +77,48 @@ export const useConfirmOnLeaving = (
showConfirmation();
}
}, [blocker]);

return { showConfirmation: customConfirm };
};

interface BlockingState {
shouldBlock: boolean;
customCheck?: (currentLocation: Location, nextLocation: Location) => boolean;
}
interface BlockNavigationControl {
blocking: Map<string, BlockingState>;
setBlocking: (
key: string,
blocking: boolean,
/**
* Only one registered customCheck will be used at a time, the first one,
* even if multiple blockers are registered
*/
customCheck?: (currentLocation: Location, nextLocation: Location) => boolean
) => void;
clearKey: (key: string) => void;
}

const useBlockNavigationState = create<BlockNavigationControl>(set => {
return {
blocking: new Map(),
setBlocking: (key, blocking, customCheck) => {
set(state => {
const blockingState = new Map(state.blocking);
blockingState.set(key, { shouldBlock: blocking, customCheck });
return {
...state,
blocking: blockingState,
};
});
},
clearKey: key => {
set(state => {
const blockingState = new Map(state.blocking);
blockingState.delete(key);
return {
...state,
blocking: blockingState,
};
});
},
};
});
3 changes: 2 additions & 1 deletion client/packages/common/src/intl/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,7 @@
"messages.no-locations": "No locations available",
"messages.no-log-entries": "No log entries available",
"messages.no-master-lists": "No master lists available",
"messages.no-matching-patients": "No matching patients! 😁",
"messages.no-matching-patients": "No matching patients",
"messages.no-matching-patients-for-contact-trace": "No matching patients",
"messages.no-patient-record": "Can't access detailed information for this patient.",
"messages.no-programs": "This patient is not enrolled in any programs",
Expand Down Expand Up @@ -1551,6 +1551,7 @@
"messages.template-download-text": "Not sure how to start? ",
"messages.total-breaches": "Total unacknowledged: {{count}}",
"messages.total-excursions": "Total excursions: {{count}}",
"messages.type-to-search": "Start typing to search",
"messages.unassign-min-mos": "This will remove the stock reorder threshold. Reorder threshold will now default to be the same as target months of stock.",
"messages.unknown-error": "Unknown error",
"messages.unlocked-description": "This will re-enable changes to the stocktake.",
Expand Down
88 changes: 85 additions & 3 deletions client/packages/common/src/intl/locales/pt/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
"description.rnr-amc": "O consumo mensal médio é calculado utilizando o valor de consumo Ajustado, utilizando este formulário de R&R e os anteriores, se estiverem disponíveis",
"description.rnr-approved-quantity": "A quantidade aprovada para ser ordenada. Populado depois do formulário R&R ser finalizado e autorizado.",
"description.rnr-consumed": "A quantidade deste item distribuída no período anterior (Através de Saídas e Prescrições)",
"description.rnr-consumed-adjusted": "Mostra o consumo ajustado para levar em consideração os dias sem stock. = (consumido x 30)/(30 - duração sem stock)",
"description.rnr-consumed-adjusted": "Mostra o consumo ajustado para levar em consideração os dias sem stock. = (consumido x dias no período)/(dias no período - duração sem stock)",
"description.rnr-final-balance": "Nível de inventário ao final do período. =Saldo inicial + Recebido - Consumido ± Ajustes",
"description.rnr-initial-balance": "Stock disponível ao início do período. Proveniente do formulário R&R anterior, se estiver disponível, ou se calcula com o stock disponível no início do período.",
"description.rnr-losses": "Registrar manualmente as perdas deste artigo durante o período",
Expand Down Expand Up @@ -843,7 +843,7 @@
"label.soh": "Estoque Disponível (ED)",
"label.start-datetime": "Data/hora de início",
"label.status": "Estado",
"label.status-given": "Dado",
"label.status-given": "Administrado",
"label.statushistory": "Histórico de Estados",
"label.stock-level": "Nível de estoque",
"label.stock-on-hand": "Estoque Disponível",
Expand Down Expand Up @@ -1291,5 +1291,87 @@
"label.initialise-store-properties": "Inicializar propiedades lo local para GAPS",
"label.diastolic": "Diastólica",
"label.directions": "Instruções",
"label.incoming": "Entrada"
"label.incoming": "Entrada",
"label.unit-sell-price": "Preço de Venda da Unidade",
"label.reason-adjustment": "Razão de Ajuste",
"label.program-enrolment": "Inscrição no Programa",
"label.patients": "Pacientes",
"label.record-stock-transaction": "Registrar transação de stock para vacinação anterior",
"label.recorded-contact-differs": "Contacto Registrado: {{recordedName}}",
"error.something-wrong-info-icon": "Oops! Algo de errado ocorreu. Clique no ícone de informação para ver os detalhes do erro.",
"label.needs-replacement": "Precisa de substituição",
"label.other-facility": "Outro local",
"label.upload-files": "Carregar ficheiros",
"label.units-issued": "Unidades Fornecidas",
"label.vaccinations": "Vacinações",
"label.vaccine-item": "Artigo de Vacina",
"label.observations": "Observações",
"label.cold-consecutive": "Cadeia de Frio Consecutiva",
"label.max-months-of-stock": "MOS alvo",
"label.hot-cumulative": "Periodo acumulado de calor",
"label.new-patient": "Novo Paciente",
"label.user-guide": "Ver a versão online (requer conexão com a internet)",
"label.cant-change-location": "Apenas pode-se trocar a localização de linhas quando o estado é Novo",
"label.cant-delete-disabled-internal-order": "Apenas pode-se excluir as linhas quando o pedido interno está em Rascunho",
"label.requested": "Solicitado",
"error.no-create-outbound-shipment-permission": "O utilizador não possui permissão para criar um Envio a partir de uma Requisição",
"label.expiring-item-period": "Período de validade",
"label.monthly-consumption-look-back-period": "Histórico de consumo mensal",
"label.more": "Mais...",
"label.use-catalogue": "Usar catálogo de ativos",
"label.vaccination": "Vacinação",
"label.note": "Nota",
"label.modified-datetime-UTC": "Data/hora de modificação (UTC)",
"label.refused": "Recusado",
"label.report-filters": "Filtros de Relatórios",
"label.search-results": "Resultados da busca",
"label.secondary": "Secundário",
"label.shipment-created": "Envio criado",
"label.systolic": "Sistólica",
"label.threshold-for-understock": "Nivel mínimo de stock",
"label.threshold-for-overstock": "Nível máximo de stock",
"label.new-item": "Novo Artigo",
"label.support": "Suporte",
"label.unacknowledged": "Não reconhecido",
"label.supply": "Fornecer",
"label.mark-as-status": "Marque como {{status}}",
"label.pack-cost-price": "Preço de custo por pacote",
"label.packs-of-1": "{{count}} {{unitWord}} em pacotes de 1",
"label.job-title": "Título do cargo",
"label.new-functional-status": "Novo estado funcional",
"label.our-stock": "Nosso stock",
"label.outgoing": "De saída",
"label.reason-for-contacting": "Motivo para contactarnos",
"label.prescription-date": "Data da prescrição",
"label.short-expiry": "Validade Curta",
"label.tertiary": "Terciário",
"label.this-store": "Este local",
"label.previous-encounters": "Encontros anteriores",
"label.primary": "Primário",
"label.out-of-stock": "Fora de stock",
"label.outbound-shipment": "Saídas",
"label.no-reason": "Sem razão",
"label.patient-details": "Detalhes do Paciente",
"label.packs-of-size": "{{packs}} pacotes de {{size}} {{unitWord}} ({{count}} unidades)",
"label.vaccine-given": "Vacina administrada",
"label.min-months-of-stock": "Ponto de reordenamento MOS",
"label.pack-sell-price": "Preço de venda por pacote",
"label.update-transactions": "Actualizar transações de stock",
"label.rtmd": "mSupply",
"label.months-abbreviation": "M",
"label.new-contact-trace": "Adicionar Contacto",
"label.new-encounter": "Novo Encontro",
"label.new-internal-order": "Nova Ordem",
"label.ratio": "Proporção",
"label.status-late": "Vencido",
"label.status-not-given": "Não dado",
"label.status-pending": "Pendente",
"error.unable-to-create-requisition": "Não foi possível criar a requisição",
"error.max-orders-reached-for-period": "Máximo de requisições para este período já criado",
"label.cold-cumulative": "Cadeia de Frio Acumulada",
"label.revert-existing-transaction": "Reverter transação de stock existente",
"label.select-batch": "Selecionar lote",
"label.stocktake-frequency": "Frequência de tomada de inventário",
"label.storage-type": "Tipo de armazenamento",
"label.total-before-tax": "Total antes de impostos"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { FC, ReactNode, useState, useEffect, useCallback } from 'react';
import TabContext from '@mui/lab/TabContext';
import { Box } from '@mui/material';
import {
useConfirmOnLeaving,
UrlQueryObject,
UrlQuerySort,
useDetailPanelStore,
Expand All @@ -13,6 +12,7 @@ import { AppBarTabsPortal } from '../../portals';
import { DetailTab } from './DetailTab';
import { ShortTabList, Tab } from './Tabs';
import { useUrlQuery } from '@common/hooks';
import { useConfirmationModal } from '../../modals';

export type TabDefinition = {
Component: ReactNode;
Expand Down Expand Up @@ -44,8 +44,12 @@ export const DetailTabs: FC<DetailTabsProps> = ({
const currentUrlTab = urlQuery['tab'] as string | undefined;
const currentTab = isValidTab(currentUrlTab)
? currentUrlTab
: tabs[0]?.value ?? '';
const { showConfirmation } = useConfirmOnLeaving(false);
: (tabs[0]?.value ?? '');

const showConfirmation = useConfirmationModal({
title: t('heading.are-you-sure'),
message: t('messages.confirm-cancel-generic'),
});

// Inelegant hack to force the "Underline" indicator for the currently active
// tab to re-render in the correct position when one of the side "drawers" is
Expand Down Expand Up @@ -76,11 +80,11 @@ export const DetailTabs: FC<DetailTabsProps> = ({

// restore the query params for the tab
const query: UrlQueryObject = restoreTabQuery
? tabQueryParams[tab] ?? getDefaultTabQueryParams(tab)
? (tabQueryParams[tab] ?? getDefaultTabQueryParams(tab))
: { tab };

if (!!tabConfirm?.confirmOnLeaving && requiresConfirmation(currentTab)) {
showConfirmation(() => updateQuery(query, overwriteQuery));
showConfirmation({ onConfirm: () => updateQuery(query, overwriteQuery) });
} else {
updateQuery(query, overwriteQuery);
}
Expand Down
3 changes: 3 additions & 0 deletions client/packages/host/src/Site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SnackbarProvider,
BarcodeScannerProvider,
DetailLoadingSkeleton,
useBlockNavigation,
} from '@openmsupply-client/common';
import { AppDrawer, AppBar, Footer, NotFound } from './components';
import { CommandK } from './CommandK';
Expand Down Expand Up @@ -69,6 +70,8 @@ export const Site: FC = () => {
setPageTitle(pageTitle);
}, [location, pageTitle, setPageTitle]);

useBlockNavigation();

return (
<RequireAuthentication>
<EasterEggModalProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
useDialog,
useNotification,
ModalMode,
useDirtyCheck,
useConfirmOnLeaving,
TableProvider,
createTableStore,
Expand Down Expand Up @@ -36,8 +35,9 @@ const useDraftInboundLines = (item: InboundLineItem | null) => {
const { id } = useInbound.document.fields('id');
const { mutateAsync, isLoading } = useInbound.lines.save();
const [draftLines, setDraftLines] = useState<DraftInboundLine[]>([]);
const { isDirty, setIsDirty } = useDirtyCheck();
useConfirmOnLeaving(isDirty);
const { isDirty, setIsDirty } = useConfirmOnLeaving(
'inbound-shipment-line-edit'
);

const defaultPackSize = item?.defaultPackSize || 1;

Expand Down
Loading

0 comments on commit 49ab2dc

Please sign in to comment.