Migrer vers Workspace v2
Workspace v2 est un système de workspaces repensé qui introduit un modèle hiérarchique de groupes, fenêtres et workspaces. Il remplace le modèle plat de workspace v1 par une approche structurée offrant un meilleur support des workspaces enfants, un suivi correct des modifications non enregistrées et une gestion de cycle de vie scopée.
Ce guide explique comment migrer les workspaces de votre module frontend de l’API v1 vers v2.
Contexte
Le système workspace v1 avait plusieurs limitations :
- Structure plate — tous les workspaces partageaient un seul conteneur, rendant difficile la gestion de flux complexes en plusieurs étapes.
- Suivi manuel des modifications non enregistrées — les workspaces devaient appeler
promptBeforeClosing()de manière impérative. - Pas de support des workspaces enfants — les flux en plusieurs étapes (ex. panier de commandes → recherche de médicaments) nécessitaient des contournements ad-hoc.
- Gestion des titres — les workspaces devaient appeler
setTitle()pour mettre à jour leur titre.
Workspace v2 résout ces problèmes avec une hiérarchie à trois niveaux :
- Groupe de workspaces — un conteneur de niveau supérieur qui possède le menu d’actions (siderail). Les groupes définissent la portée, le comportement d’overlay et la persistance.
- Fenêtre de workspace — un panneau au sein d’un groupe qui peut être maximisé, minimisé ou masqué. Chaque fenêtre appartient à un groupe.
- Workspace — le contenu réel rendu à l’intérieur d’une fenêtre. Les workspaces peuvent lancer des workspaces enfants dans la même fenêtre.
Concepts clés
Avant de migrer, il est utile de comprendre comment les trois niveaux sont liés :
- Un groupe de workspaces est déclaré dans
workspaceGroups2. Il peut être ouvert explicitement aveclaunchWorkspaceGroup2, ou implicitement lorsquelaunchWorkspace2ouvre un workspace qui appartient au groupe. Si le groupe possède des fenêtres avec icônes, il rend le menu d’actions (siderail) tant que le groupe est ouvert. Un seul groupe peut être ouvert à la fois. - Une fenêtre de workspace est déclarée dans
workspaceWindows2et appartient à un groupe. Une fenêtre avec uneiconcorrespond à un bouton du menu d’actions. L’icône est enregistrée comme une extension dont l’ID d’extension doit correspondre au nom de la fenêtre. C’est ainsi queActionMenuButton2sait quelle fenêtre il contrôle. - Un workspace est déclaré dans
workspaces2et appartient à une fenêtre. Les workspaces sont les panneaux de contenu réels. Une fenêtre peut contenir une pile de workspaces (parent + enfants).
Les props de groupe fournissent un contexte partagé (ex. patient actuel, visite) à tous les workspaces du groupe. Les props de fenêtre et les props de workspace sont scopées à leurs niveaux respectifs. Lorsque l’une de ces props change de manière incompatible, le système invite l’utilisateur à fermer les workspaces avec des modifications non enregistrées.
Étapes de migration
Mettre à jour routes.json
Remplacez la section workspaces dans votre routes.json par les nouvelles sections workspaces2, workspaceWindows2 et workspaceGroups2.
Avant (v1) :
{
"workspaces": [
{
"name": "my-form-workspace",
"component": "myFormWorkspace",
"title": "myFormTitle",
"type": "form"
}
]
}Après (v2) :
{
"workspaces2": [
{
"name": "my-form-workspace",
"component": "myFormWorkspace",
"window": "my-form-window"
}
],
"workspaceWindows2": [
{
"name": "my-form-window",
"group": "my-workspace-group"
}
],
"workspaceGroups2": [
{
"name": "my-workspace-group"
}
]
}Différences clés :
- Les propriétés
titleettypesont supprimées des définitions de workspace. Les titres sont désormais définis par le composant wrapper<Workspace2>. - Chaque workspace doit spécifier la
windowà laquelle il appartient. - Chaque fenêtre doit spécifier le
groupauquel elle appartient. - Les groupes peuvent optionnellement configurer les propriétés
overlay,persistenceetscopePattern.
Si les workspaces de votre application appartiennent à un groupe possédé par une autre application (ex. le groupe patient-chart), vous n’avez besoin de déclarer que workspaces2 et workspaceWindows2. Vous n’avez pas besoin de déclarer workspaceGroups2 — c’est la responsabilité de l’application qui possède le groupe.
Options de fenêtre de workspace
Les fenêtres supportent les propriétés optionnelles suivantes :
| Propriété | Type | Description |
|---|---|---|
canMaximize | boolean | Si la fenêtre peut être maximisée |
group | string | Obligatoire. Le groupe auquel cette fenêtre appartient |
icon | string | Composant d’icône de menu d’actions exporté pour cette fenêtre |
order | number | Ordre d’affichage dans le menu d’actions |
width | "narrow" | "wider" | "extra-wide" | Largeur du panneau de workspace |
Options de groupe de workspaces
Les groupes supportent les propriétés optionnelles suivantes :
| Propriété | Type | Description |
|---|---|---|
overlay | boolean | Si les workspaces se rendent en overlay |
persistence | "app-wide" | "closable" | Mode de persistance du cycle de vie. Utilisez "closable" pour rendre le menu d’actions du groupe avec un bouton de fermeture. |
scopePattern | string | Modèle d’URL définissant où les workspaces persistent |
Si persistence est omis, le groupe se comporte comme un groupe app-wide : il peut garder plusieurs fenêtres ouvertes et ne se ferme que lors d’un changement d’app ou de scopePattern. Utilisez "closable" pour les flux ciblés où le menu d’actions doit inclure un bouton de fermeture. Dans un groupe closable, lancer une autre fenêtre vérifie les workspaces concernés pour détecter les changements non enregistrés puis ferme les autres fenêtres de ce groupe.
Initialiser le groupe de workspaces uniquement si nécessaire
Déclarer un groupe dans workspaceGroups2 ne signifie pas automatiquement que vous devez appeler launchWorkspaceGroup2. Un appel normal à launchWorkspace2 ouvre le groupe du workspace, la fenêtre cible et le workspace s’ils ne sont pas déjà ouverts.
Appelez launchWorkspaceGroup2 depuis l’application propriétaire lorsque le groupe doit être actif avant le premier lancement de workspace, ou lorsque le groupe a besoin de props de contexte partagées avant que les boutons du menu d’actions et les workspaces ne soient rendus. Patient-chart le fait pour que le contexte patient et visite soit disponible à tous les workspaces du groupe patient-chart.
import { launchWorkspaceGroup2 } from "@openmrs/esm-framework";
// Dans le composant racine de votre application :
useEffect(() => {
launchWorkspaceGroup2("patient-chart", {
patient,
patientUuid,
visitContext,
mutateVisitContext,
});
}, [patient, patientUuid, visitContext, mutateVisitContext]);Un seul groupe de workspaces peut être ouvert à la fois. Si launchWorkspaceGroup2 est appelé avec un nom de groupe différent (ou des props incompatibles), O3 vérifie les workspaces ouverts pour détecter des changements non enregistrés et demande confirmation si nécessaire avant d’ouvrir le nouveau groupe.
La plupart des modules n’ont pas besoin de cette étape. Les modules qui ajoutent des workspaces à un groupe existant, comme patient-chart, doivent ignorer entièrement l’initialisation du groupe. Les modules qui possèdent un groupe simple peuvent aussi s’appuyer sur launchWorkspace2, sauf si le groupe a besoin d’un contexte app-wide ou d’un menu d’actions avant l’ouverture d’un workspace.
Mettre à jour les signatures des composants workspace
Les composants workspace reçoivent des props différentes en v2. Le changement principal est que les props personnalisées sont désormais imbriquées sous workspaceProps au lieu d’être étalées en props de niveau supérieur, et le contexte partagé est disponible via groupProps.
Avant (v1) :
import { type DefaultWorkspaceProps } from "@openmrs/esm-framework";
interface MyFormProps extends DefaultWorkspaceProps {
patientUuid: string;
appointment?: Appointment;
}
const MyForm: React.FC<MyFormProps> = ({
patientUuid,
appointment,
closeWorkspace,
closeWorkspaceWithSavedChanges,
promptBeforeClosing,
setTitle,
}) => {
// ...
};Après (v2) :
import {
Workspace2,
type Workspace2DefinitionProps,
} from "@openmrs/esm-framework";
interface MyFormProps {
patientUuid: string;
appointment?: Appointment;
}
export default function MyForm({
workspaceProps,
groupProps,
closeWorkspace,
}: Workspace2DefinitionProps<MyFormProps>) {
const { patientUuid, appointment } = workspaceProps ?? {};
return (
<Workspace2 title={t("myForm", "Mon formulaire")} hasUnsavedChanges={isDirty}>
{/* contenu du workspace */}
</Workspace2>
);
}Changements clés :
- Remplacer
DefaultWorkspacePropsparWorkspace2DefinitionProps<VosProps>. Le générique accepte jusqu’à trois paramètres de type :<WorkspaceProps, WindowProps, GroupProps>. - Les props personnalisées sont accessibles via
workspacePropsau lieu d’être étalées au niveau supérieur. Notez queworkspaceProps,windowPropsetgroupPropspeuvent êtrenull. - Le contexte partagé (ex. patient actuel) est disponible via
groupPropsau lieu d’être passé à chaque workspace individuellement. promptBeforeClosingetsetTitlesont supprimés. À la place, passezhasUnsavedChangesettitlecomme props au composant wrapper<Workspace2>.closeWorkspaceWithSavedChangesest remplacé parcloseWorkspace({ discardUnsavedChanges: true }).closeWorkspaceaccepte également{ closeWindow: true }pour fermer la fenêtre entière (tous les workspaces qu’elle contient), pas seulement le workspace actuel.- Enveloppez le contenu de votre workspace dans
<Workspace2>, qui rend la barre d’en-tête du workspace avec le titre et les boutons maximiser/minimiser et fermer.
Mettre à jour les appels de lancement de workspace
Remplacez launchWorkspace par launchWorkspace2. La signature complète est :
launchWorkspace2<WorkspaceProps, WindowProps, GroupProps>(
workspaceName: string,
workspaceProps?: WorkspaceProps | null,
windowProps?: WindowProps | null,
groupProps?: GroupProps | null,
): Promise<boolean>L’option workspaceTitle n’est plus acceptée — les titres sont définis par le composant workspace lui-même.
Avant (v1) :
launchWorkspace("my-form-workspace", {
patientUuid,
appointment,
workspaceTitle: t("editAppointment", "Modifier le rendez-vous"),
});Après (v2) :
launchWorkspace2("my-form-workspace", {
patientUuid,
appointment,
});Pour lancer un workspace nécessitant des props de fenêtre ou de groupe, passez-les comme arguments supplémentaires :
launchWorkspace2(
"start-visit-workspace-form",
{ openedFrom: "patient-chart-start-visit" }, // props du workspace
{}, // props de la fenêtre
{ patient, patientUuid: patient.id, visitContext: null, mutateVisitContext }, // props du groupe
);Mettre à jour les boutons du menu d’actions
Remplacez ActionMenuButton par ActionMenuButton2. Le nouveau composant remplace handler et type par un objet workspaceToLaunch. Il gère également automatiquement la visibilité de la fenêtre — cliquer sur le bouton masquera, restaurera ou lancera le workspace selon l’état actuel.
Référencez le composant de bouton depuis la propriété icon de la fenêtre propriétaire dans workspaceWindows2. Le framework enregistre cette icône comme une extension dont l’ID est le nom de la fenêtre.
Avant (v1) :
<ActionMenuButton
getIcon={(props) => <EditIcon {...props} />}
label={t("edit", "Modifier")}
iconDescription={t("edit", "Modifier")}
handler={() => launchWorkspace("my-form-workspace", { patientUuid })}
type="form"
/>Après (v2) :
<ActionMenuButton2
icon={(props) => <EditIcon {...props} />}
label={t("edit", "Modifier")}
workspaceToLaunch={{
workspaceName: "my-form-workspace",
workspaceProps: { patientUuid },
}}
/>ActionMenuButton2 supporte également un callback optionnel onBeforeWorkspaceLaunch qui peut empêcher l’ouverture du workspace (par exemple pour inviter l’utilisateur à démarrer une visite d’abord) :
<ActionMenuButton2
icon={(props) => <DocumentIcon {...props} />}
label={t("clinicalForms", "Formulaires cliniques")}
workspaceToLaunch={{
workspaceName: "clinical-forms-workspace",
workspaceProps: {},
}}
onBeforeWorkspaceLaunch={startVisitIfNeeded}
/>Mettre à jour les patterns de workspaces enfants
Si votre workspace lance des sous-workspaces (ex. un panier de commandes qui ouvre une recherche de médicaments), utilisez la prop launchChildWorkspace au lieu d’appeler launchWorkspace directement.
Avant (v1) :
const OrderBasket: React.FC<DefaultWorkspaceProps> = ({
closeWorkspace,
}) => {
const handleAddDrug = () => {
launchWorkspace("drug-search-workspace", { onSelect: handleDrugSelect });
};
// ...
};Après (v2) :
function OrderBasket({
closeWorkspace,
launchChildWorkspace,
}: Workspace2DefinitionProps<OrderBasketProps>) {
const handleAddDrug = () => {
launchChildWorkspace("drug-search-workspace", {
onSelect: handleDrugSelect,
});
};
// ...
}Les workspaces enfants sont rendus dans la même fenêtre et sont automatiquement fermés lorsque leur workspace parent est fermé.
Mettre à jour les tests
Les tests doivent fournir la nouvelle structure de props lors du rendu des composants workspace.
Avant (v1) :
const defaultProps = {
patientUuid: mockPatient.id,
closeWorkspace: vi.fn(),
closeWorkspaceWithSavedChanges: vi.fn(),
promptBeforeClosing: vi.fn(),
setTitle: vi.fn(),
};
render(<MyForm {...defaultProps} />);Après (v2) :
const defaultProps: Workspace2DefinitionProps<MyFormProps> = {
workspaceProps: {
patientUuid: mockPatient.id,
},
closeWorkspace: vi.fn(),
launchChildWorkspace: vi.fn(),
groupProps: null,
windowProps: null,
workspaceName: "my-form-workspace",
windowName: "my-form-window",
isRootWorkspace: true,
showActionMenu: false,
};
render(<MyForm {...defaultProps} />);Exemples de migration
- Patient chart — l’exemple le plus complet, incluant les patterns de workspaces enfants pour les formulaires cliniques et le panier de commandes
- Application de rendez-vous
- Application de files d’attente
- Application de salle
- Application de gestion des lits
- Application de gestion des listes de patients
- Application de laboratoire