Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 1 addition & 69 deletions src/Utils/keyboardShortcutUtils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import { useMemo } from "react";

import shortcutsConfig from "@/config/keyboardShortcuts.json";

import { useIsMobile } from "@/hooks/use-mobile";
import { isAppleDevice } from "./utils";

/**
Expand Down Expand Up @@ -57,58 +52,6 @@ export function formatKeyboardShortcut(key: string): string {
}
}

type ShortcutContext = keyof typeof shortcutsConfig;

interface ShortcutConfig {
key: string;
action: string;
}

/**
* Generic hook to get shortcut display strings for any context
*
* @deprecated Use `useShortcutDisplay` from `@/context/ShortcutContext` instead.
* This hook is now integrated with the ShortcutContext and automatically uses the current context hierarchy.
*
* @param contexts Optional array of contexts to search for shortcuts. If not provided, searches all contexts.
* @param dynamicResolver Optional function to resolve dynamic shortcuts (e.g., questionnaires)
* @returns Function to get display string for an action ID
*/
export function useShortcutDisplays(
contexts?: ShortcutContext[],
dynamicResolver?: (actionId: string) => string | undefined,
) {
const isMobile = useIsMobile();

return useMemo(() => {
const getDisplay = (actionId: string): string | undefined => {
if (isMobile) {
return undefined;
}

const searchContexts =
contexts || (Object.keys(shortcutsConfig) as ShortcutContext[]);

for (const context of searchContexts) {
const shortcuts = shortcutsConfig[context] as ShortcutConfig[];
const shortcut = shortcuts.find((s) => s.action === actionId);

if (shortcut) {
return formatKeyboardShortcut(shortcut.key);
}
}

if (dynamicResolver) {
return dynamicResolver(actionId);
}

return undefined;
};

return getDisplay;
}, [contexts, dynamicResolver, isMobile]);
}

// Debounce map to prevent multiple rapid clicks
const clickDebounceMap = new Map<string, number>();

Expand All @@ -129,18 +72,7 @@ export function shortcutActionHandler(shortcutId: string) {
) as HTMLElement;

if (element) {
if (element.tagName === "A" && "href" in element) {
window.location.href = (element as HTMLAnchorElement).href;
} else {
element.click();
}
element.click();
}
};
}

export function shortcutActionHandlers(shortcutIds: string[]) {
return shortcutIds.map((id) => ({
id,
handler: shortcutActionHandler(id),
}));
}
124 changes: 92 additions & 32 deletions src/components/Encounter/EncounterCommandDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import {
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import {
useEncounterShortcutDisplays,
useEncounterShortcuts,
} from "@/hooks/useEncounterShortcuts";
import { useShortcutDisplay } from "@/context/ShortcutContext";
import {
ArrowBigRight,
Building2,
Expand All @@ -37,6 +34,7 @@ import { useEncounter } from "@/pages/Encounters/utils/EncounterProvider";
import { EncounterRead } from "@/types/emr/encounter/encounter";
import questionnaireApi from "@/types/questionnaire/questionnaireApi";
import { useQuery } from "@tanstack/react-query";
import { useNavigate } from "raviger";
import { useTranslation } from "react-i18next";

interface ActionItem {
Expand Down Expand Up @@ -65,10 +63,16 @@ export function EncounterCommandDialog({
onOpenChange,
trigger,
}: EncounterCommandDialogProps) {
const { canWriteSelectedEncounter, canRestartSelectedEncounter } =
useEncounter();
const {
canWriteSelectedEncounter,
canRestartSelectedEncounter,
actions,
selectedEncounterId,
primaryEncounterId,
} = useEncounter();
const { t } = useTranslation();
const [search, setSearch] = useState("");
const navigate = useNavigate();

const questionnaireOptions = useQuestionnaireOptions("encounter_actions");

Expand All @@ -90,8 +94,7 @@ export function EncounterCommandDialog({
}
}, [open]);

const getShortcutDisplay = useEncounterShortcutDisplays();
const { handleAction } = useEncounterShortcuts();
const getShortcutDisplay = useShortcutDisplay();

const STORAGE_KEY = "encounter-command-dialog-recent-actions";
const MAX_RECENT_ACTIONS = 4;
Expand All @@ -108,24 +111,86 @@ export function EncounterCommandDialog({
const [recentActionsState, setRecentActionsState] =
useState<string[]>(getRecentActions);

// Handle keyboard shortcut to open command dialog
useEffect(() => {
const handleOpenCommandDialog = () => {
onOpenChange(true);
};

document.addEventListener(
"open-encounter-command-dialog",
handleOpenCommandDialog,
);
// Build encounter URL helper
const buildEncounterUrl = useCallback(
(path: string) => {
const currentEncounterIdToUse = primaryEncounterId || encounter.id;
const baseUrl = `/facility/${encounter.facility.id}/patient/${encounter.patient.id}/encounter/${currentEncounterIdToUse}${path}`;

// Add selectedEncounter parameter if we're viewing a different encounter
if (
selectedEncounterId &&
primaryEncounterId &&
selectedEncounterId !== primaryEncounterId
) {
const separator = path.includes("?") ? "&" : "?";
return `${baseUrl}${separator}selectedEncounter=${selectedEncounterId}`;
}

return baseUrl;
},
[encounter, selectedEncounterId, primaryEncounterId],
);

return () => {
document.removeEventListener(
"open-encounter-command-dialog",
handleOpenCommandDialog,
);
};
}, [onOpenChange]);
// Handle action execution
const handleAction = useCallback(
(actionId: string) => {
const actionHandlers: Record<string, () => void> = {
"add-service-request": () =>
navigate(buildEncounterUrl("/questionnaire/service_request")),
"add-medication-request": () =>
navigate(buildEncounterUrl("/questionnaire/medication_request")),
"add-allergy": () =>
navigate(buildEncounterUrl("/questionnaire/allergy_intolerance")),
"add-symptoms": () =>
navigate(buildEncounterUrl("/questionnaire/symptom")),
"add-diagnosis": () =>
navigate(buildEncounterUrl("/questionnaire/diagnosis")),
"update-encounter": () =>
navigate(buildEncounterUrl("/questionnaire/encounter")),
"service-requests": () =>
navigate(buildEncounterUrl("/service_requests")),
"diagnostic-reports": () =>
navigate(buildEncounterUrl("/diagnostic_reports")),
"clinical-history": () =>
navigate(
`/facility/${encounter.facility.id}/patient/${encounter.patient.id}/history/responses?sourceUrl=${encodeURIComponent(
buildEncounterUrl("/updates"),
)}`,
),
"treatment-summary": () =>
navigate(buildEncounterUrl("/treatment_summary")),
"encounter-overview": () => navigate(buildEncounterUrl("/updates")),
plots: () => navigate(buildEncounterUrl("/plots")),
observations: () => navigate(buildEncounterUrl("/observations")),
medicines: () => navigate(buildEncounterUrl("/medicines")),
files: () => navigate(buildEncounterUrl("/files")),
notes: () => navigate(buildEncounterUrl("/notes")),
devices: () => navigate(buildEncounterUrl("/devices")),
consents: () => navigate(buildEncounterUrl("/consents")),
"mark-as-completed": () => actions.markAsCompleted(),
"assign-location": () => actions.assignLocation(),
"view-location-history": () => actions.viewLocationHistory(),
"manage-care-team": () => actions.manageCareTeam(),
"manage-departments": () => actions.manageDepartments(),
dispense: () => actions.dispense(),
"restart-encounter": () => actions.restartEncounter(),
};

const handler = actionHandlers[actionId];
if (handler) {
handler();
return;
}

// Handle dynamic questionnaire actions
if (actionId.startsWith("questionnaire-")) {
const slug = actionId.replace("questionnaire-", "");
navigate(buildEncounterUrl(`/questionnaire/${slug}`));
}
},
[navigate, buildEncounterUrl, actions, encounter],
);

const addRecentAction = useCallback(
(actionId: string): void => {
Expand Down Expand Up @@ -170,12 +235,6 @@ export function EncounterCommandDialog({
shortcut: getShortcutDisplay("add-diagnosis"),
icon: <Plus />,
},
{
id: "add-questionnaire",
label: t("add_form"),
shortcut: getShortcutDisplay("add-questionnaire"),
icon: <Plus />,
},
{
id: "add-service-request",
label: t("service_request"),
Expand Down Expand Up @@ -368,9 +427,10 @@ export function EncounterCommandDialog({
questionnaires,
search,
getShortcutDisplay,
isLoading,
canWriteSelectedEncounter,
canRestartSelectedEncounter,
encounter.encounter_class,
encounter?.status,
]);

const findRecentActions = useCallback(
Expand Down
45 changes: 18 additions & 27 deletions src/config/keyboardShortcuts.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,35 +131,26 @@
],

"encounter": [
{ "key": "a", "action": "add-allergy", "description": "Add Allergy", "when": "canEdit" },
{ "key": "s", "action": "add-symptoms", "description": "Add Symptoms", "when": "canEdit" },
{ "key": "d", "action": "add-diagnosis", "description": "Add Diagnosis", "when": "canEdit" },
{ "key": "f", "action": "add-questionnaire", "description": "New Questionnaire/Form", "when": "canEdit && questionnairesEnabled" },
{ "key": "u e", "action": "update-encounter", "description": "Update Encounter", "when": "canEdit" },
{ "key": "a", "action": "add-allergy", "description": "Add Allergy", "when": "always" },
{ "key": "s", "action": "add-symptoms", "description": "Add Symptoms", "when": "always" },
{ "key": "d", "action": "add-diagnosis", "description": "Add Diagnosis", "when": "always" },
{ "key": "r", "action": "add-service-request", "description": "Add Service Request", "when": "always" },
{ "key": "m", "action": "add-medication-request", "description": "Add Medication", "when": "always" },
{ "key": "f", "action": "add-questionnaire", "description": "New Questionnaire/Form", "when": "always" },
{ "key": "shift+u", "action": "update-encounter", "description": "Update Encounter", "when": "always" },
{ "key": "h", "action": "clinical-history", "description": "Clinical History", "when": "always" },
{ "key": "g g", "action": "encounter-overview", "description": "Go to Overview", "when": "always" },
{ "key": "g p", "action": "plots", "description": "Go to Plots", "when": "always" },
{ "key": "g o", "action": "observations", "description": "Go to Observations", "when": "always" },
{ "key": "g m", "action": "medicines", "description": "Go to Medicines", "when": "always" },
{ "key": "g f", "action": "files", "description": "Go to Files", "when": "always" },
{ "key": "g n", "action": "notes", "description": "Go to Notes", "when": "always" },
{ "key": "g d", "action": "devices", "description": "Go to Devices", "when": "always" },
{ "key": "g c", "action": "consents", "description": "Go to Consents", "when": "always" },
{ "key": "g s", "action": "service-requests", "description": "Go to Service Requests", "when": "always" },
{ "key": "g r", "action": "diagnostic-reports", "description": "Go to Diagnostic Reports", "when": "always" },
{ "key": "cmd+e", "action": "open-command-dialog", "description": "Open Command Dialog", "when": "always" },
{ "key": "ctrl+e", "action": "open-command-dialog", "description": "Open Command Dialog", "when": "always" },
{ "key": "q 1", "action": "questionnaire-1", "description": "Questionnaire 1", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 2", "action": "questionnaire-2", "description": "Questionnaire 2", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 3", "action": "questionnaire-3", "description": "Questionnaire 3", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 4", "action": "questionnaire-4", "description": "Questionnaire 4", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 5", "action": "questionnaire-5", "description": "Questionnaire 5", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 6", "action": "questionnaire-6", "description": "Questionnaire 6", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 7", "action": "questionnaire-7", "description": "Questionnaire 7", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 8", "action": "questionnaire-8", "description": "Questionnaire 8", "when": "canEdit && questionnairesEnabled" },
{ "key": "q 9", "action": "questionnaire-9", "description": "Questionnaire 9", "when": "canEdit && questionnairesEnabled" },
{ "key": "m c", "action": "mark-as-completed", "description": "Mark as Completed", "when": "canEdit" },
{ "key": "l", "action": "assign-location", "description": "Assign Location", "when": "canEdit" },
{ "key": "shift+d", "action": "dispense", "description": "Dispense Medication", "when": "canEdit" }
{ "key": "q 1", "action": "questionnaire-1", "description": "Questionnaire 1", "when": "always" },
{ "key": "q 2", "action": "questionnaire-2", "description": "Questionnaire 2", "when": "always" },
{ "key": "q 3", "action": "questionnaire-3", "description": "Questionnaire 3", "when": "always" },
{ "key": "q 4", "action": "questionnaire-4", "description": "Questionnaire 4", "when": "always" },
{ "key": "q 5", "action": "questionnaire-5", "description": "Questionnaire 5", "when": "always" },
{ "key": "q 6", "action": "questionnaire-6", "description": "Questionnaire 6", "when": "always" },
{ "key": "q 7", "action": "questionnaire-7", "description": "Questionnaire 7", "when": "always" },
{ "key": "q 8", "action": "questionnaire-8", "description": "Questionnaire 8", "when": "always" },
{ "key": "q 9", "action": "questionnaire-9", "description": "Questionnaire 9", "when": "always" },
{ "key": "shift+c", "action": "mark-as-completed", "description": "Mark as Completed", "when": "always" },
{ "key": "shift+d", "action": "dispense", "description": "Dispense Medication", "when": "always" }
]
}
Loading
Loading