Skip to Content
DocsMigration guidesMigrate to Workspace v2

Migrating to Workspace v2

Workspace v2 is a redesigned workspace system that introduces a hierarchical model of groups, windows, and workspaces. It replaces the flat workspace model from v1 with a structured approach that provides better support for child workspaces, proper unsaved changes tracking, and scoped workspace lifecycle management.

This guide covers how to migrate your frontend module’s workspaces from the v1 API to v2.

Background

The v1 workspace system had several limitations:

  • Flat structure — all workspaces shared a single container, making it difficult to manage complex multi-step flows.
  • Manual unsaved changes tracking — workspaces had to call promptBeforeClosing() imperatively.
  • No child workspace support — multi-step flows (e.g. order basket → drug search) required ad-hoc workarounds.
  • Title management — workspaces had to call setTitle() to update their title.

Workspace v2 addresses these with a three-level hierarchy:

  • Workspace Group — a top-level container that owns the action menu (siderail). Groups define scope, overlay behavior, and persistence.
  • Workspace Window — a panel within a group that can be maximized, minimized, or hidden. Each window belongs to a group.
  • Workspace — the actual content rendered inside a window. Workspaces can launch child workspaces within the same window.

Key concepts

Before migrating, it helps to understand how the three levels relate:

  1. A workspace group is declared in workspaceGroups2. It can be opened explicitly with launchWorkspaceGroup2, or implicitly when launchWorkspace2 opens a workspace that belongs to the group. If the group has windows with icons, it renders the action menu (siderail) while the group is open. Only one group can be open at a time.
  2. A workspace window is declared in workspaceWindows2 and belongs to a group. A window with an icon corresponds to an action menu button. The icon is registered as an extension whose extension ID must match the window name. This is how ActionMenuButton2 knows which window it controls.
  3. A workspace is declared in workspaces2 and belongs to a window. Workspaces are the actual content panels. A window can contain a stack of workspaces (parent + children).

Group props provide shared context (e.g. current patient, visit) to all workspaces in the group. Window props and workspace props are scoped to their respective levels. When any of these props change in an incompatible way, the system prompts the user to close workspaces with unsaved changes.

Migration steps

Update routes.json

Replace the workspaces section in your routes.json with the new workspaces2, workspaceWindows2, and workspaceGroups2 sections.

Before (v1):

{ "workspaces": [ { "name": "my-form-workspace", "component": "myFormWorkspace", "title": "myFormTitle", "type": "form" } ] }

After (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" } ] }

Key differences:

  • The title and type properties are removed from workspace definitions. Titles are now set by the <Workspace2> wrapper component.
  • Each workspace must specify a window it belongs to.
  • Each window must specify a group it belongs to.
  • Groups can optionally configure overlay, persistence, and scopePattern properties.

If your app’s workspaces belong to a group owned by another app (e.g. the patient-chart group), you only need to declare workspaces2 and workspaceWindows2. You do not need to declare workspaceGroups2 — that’s the responsibility of the app that owns the group.

Workspace window options

Windows support the following optional properties:

PropertyTypeDescription
canMaximizebooleanWhether the window can be maximized
groupstringRequired. The group this window belongs to
iconstringExported action-menu icon component for this window
ordernumberDisplay order in the action menu
width"narrow" | "wider" | "extra-wide"Width of the workspace panel

Workspace group options

Groups support the following optional properties:

PropertyTypeDescription
overlaybooleanWhether workspaces render as overlays
persistence"app-wide" | "closable"Lifecycle persistence mode. Use "closable" to render the group action menu with a close button.
scopePatternstringURL pattern defining where workspaces persist

If persistence is omitted, the group behaves like an app-wide group: it can keep multiple windows open and closes only on app changes or scopePattern changes. Use "closable" for focused workflows where the action menu should include a close button. In a closable group, launching a different window checks affected workspaces for unsaved changes and then closes the other windows in that group.

Initialize the workspace group only when needed

Declaring a group in workspaceGroups2 does not automatically mean you must call launchWorkspaceGroup2. A normal launchWorkspace2 call opens the workspace’s group, target window, and workspace if they are not already open.

Call launchWorkspaceGroup2 from the owning app when the group needs to be active before the first workspace launch, or when the group needs shared context props before action-menu buttons and workspaces render. Patient-chart does this so patient and visit context are available to all workspaces in the patient-chart group.

import { launchWorkspaceGroup2 } from "@openmrs/esm-framework"; // In your app's root component: useEffect(() => { launchWorkspaceGroup2("patient-chart", { patient, patientUuid, visitContext, mutateVisitContext, }); }, [patient, patientUuid, visitContext, mutateVisitContext]);

Only one workspace group can be open at a time. If launchWorkspaceGroup2 is called with a different group name (or incompatible props), O3 checks the open workspaces for unsaved changes and prompts when needed before the new group is opened.

Most modules do not need this step. Modules that add workspaces to an existing group, such as patient-chart, should skip group initialization entirely. Modules that own a simple group can also rely on launchWorkspace2 unless the group needs app-wide context or an action menu before a workspace is opened.

Update workspace component signatures

Workspace components receive different props in v2. The main change is that custom props are now nested under workspaceProps instead of being spread as top-level props, and shared context is available via groupProps.

Before (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, }) => { // ... };

After (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", "My form")} hasUnsavedChanges={isDirty}> {/* workspace content */} </Workspace2> ); }

Key changes:

  • Replace DefaultWorkspaceProps with Workspace2DefinitionProps<YourProps>. The generic takes up to three type params: <WorkspaceProps, WindowProps, GroupProps>.
  • Custom props are accessed via workspaceProps instead of being spread at the top level. Note that workspaceProps, windowProps, and groupProps can be null.
  • Shared context (e.g. current patient) is available via groupProps instead of being passed to each workspace individually.
  • promptBeforeClosing and setTitle are removed. Instead, pass hasUnsavedChanges and title as props to the <Workspace2> wrapper component.
  • closeWorkspaceWithSavedChanges is replaced by closeWorkspace({ discardUnsavedChanges: true }).
  • closeWorkspace also accepts { closeWindow: true } to close the entire window (all workspaces in it), not just the current workspace.
  • Wrap your workspace content in <Workspace2>, which renders the workspace header bar with the title, maximize/minimize, and close buttons.

Update workspace launch calls

Replace launchWorkspace with launchWorkspace2. The full signature is:

launchWorkspace2<WorkspaceProps, WindowProps, GroupProps>( workspaceName: string, workspaceProps?: WorkspaceProps | null, windowProps?: WindowProps | null, groupProps?: GroupProps | null, ): Promise<boolean>

The workspaceTitle option is no longer accepted — titles are set by the workspace component itself.

Before (v1):

launchWorkspace("my-form-workspace", { patientUuid, appointment, workspaceTitle: t("editAppointment", "Edit appointment"), });

After (v2):

launchWorkspace2("my-form-workspace", { patientUuid, appointment, });

When launching a workspace that needs window or group props, pass them as additional arguments:

launchWorkspace2( "start-visit-workspace-form", { openedFrom: "patient-chart-start-visit" }, // workspace props {}, // window props { patient, patientUuid: patient.id, visitContext: null, mutateVisitContext }, // group props );

Update action menu buttons

Replace ActionMenuButton with ActionMenuButton2. The new component replaces handler and type with a workspaceToLaunch object. It also handles toggling window visibility automatically — clicking the button will hide, restore, or launch the workspace depending on the current state.

Reference the button component from the owning window’s icon property in workspaceWindows2. The framework registers that icon as an extension whose ID is the window name.

Before (v1):

<ActionMenuButton getIcon={(props) => <EditIcon {...props} />} label={t("edit", "Edit")} iconDescription={t("edit", "Edit")} handler={() => launchWorkspace("my-form-workspace", { patientUuid })} type="form" />

After (v2):

<ActionMenuButton2 icon={(props) => <EditIcon {...props} />} label={t("edit", "Edit")} workspaceToLaunch={{ workspaceName: "my-form-workspace", workspaceProps: { patientUuid }, }} />

ActionMenuButton2 also supports an optional onBeforeWorkspaceLaunch callback that can prevent the workspace from opening (e.g. to prompt the user to start a visit first):

<ActionMenuButton2 icon={(props) => <DocumentIcon {...props} />} label={t("clinicalForms", "Clinical forms")} workspaceToLaunch={{ workspaceName: "clinical-forms-workspace", workspaceProps: {}, }} onBeforeWorkspaceLaunch={startVisitIfNeeded} />

Update child workspace patterns

If your workspace launches sub-workspaces (e.g. an order basket that opens a drug search), use the launchChildWorkspace prop instead of calling launchWorkspace directly.

Before (v1):

const OrderBasket: React.FC<DefaultWorkspaceProps> = ({ closeWorkspace, }) => { const handleAddDrug = () => { launchWorkspace("drug-search-workspace", { onSelect: handleDrugSelect }); }; // ... };

After (v2):

function OrderBasket({ closeWorkspace, launchChildWorkspace, }: Workspace2DefinitionProps<OrderBasketProps>) { const handleAddDrug = () => { launchChildWorkspace("drug-search-workspace", { onSelect: handleDrugSelect, }); }; // ... }

Child workspaces are rendered within the same window and are automatically closed when their parent workspace is closed.

Update tests

Tests need to provide the new prop structure when rendering workspace components.

Before (v1):

const defaultProps = { patientUuid: mockPatient.id, closeWorkspace: vi.fn(), closeWorkspaceWithSavedChanges: vi.fn(), promptBeforeClosing: vi.fn(), setTitle: vi.fn(), }; render(<MyForm {...defaultProps} />);

After (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} />);

Migration examples

Last updated on