Workspace v2 Migration Guide

Workspace v2 Migration Guide

This guide documents the major changes in workspace v2, and how to migrate from the old workspace system.

Summary of changes:

  • Defining workspaces, workspace windows, and workspace groups, and how they interact together.

  • Support of (React-like) props at each level (workspace / window / group)

  • Support for child workspaces

  • Proper handling of unsaved changes in workspaces

  • Removal of <WorkspaceContainer>

Details:

Workspaces often work and share data together, and the new system requires those relationships to be clearly defined.

A workspace is a "page" that gets rendered in a workspace window. A workspace must belong to a workspace window.

A workspace window is the UI element that actually renders a workspace. (It's the thing that slides from the right in desktop mode.) When a workspace window is opened, it can only render one workspace at any given time. It is possible, however, for multiple workspaces to belong to the same workspace window. A workspace window must belong to a workspace group. Note that multiple windows in the same workspace group can be opened at the same time.

Examples:

  • "Add patient to queue" feature in the service queues app involves 2 workspaces: the patient search workspace, and the subsequent workspace to add a selected patient to a queue. This is an example of a workspace launching another (child) workspace. Both of them are rendered in the same window.

  • The order basket in patient chart. The main order basket workspace, the drug order form workspace, and the lab order form workspace all belong to the same Order Basket workspace window. (Note that there need not be parent-child workspace relationships for 2 workspaces to belong to the same window. For example, the drug order form can be opened standalone when we modify an existing drug order.)

Note that a workspace window can never be launched directly (same with the old system.) Instead, when we call launchWorkspace2, not only do we launch the specified workspace, but we also implicitly open its workspace window (if not already open) and its workspace group (if not already open).

A workspace window can optionally have an action menu icon associated with it. For example, the Order Basket window has a shopping cart icon.

A workspace and its workspace window are related, but they should not be conflated. This is true even for windows with just one workspace.

A workspace window also supports the following optional properties:

Property

Type

Description

 

 

Property

Type

Description

 

 

canMaximize

boolean

Whether the window can be maximized to fill the viewport

 

 

width

'narrow'

'wider'

'extra-wide'

The width of the window. Defaults to 'narrow'

order

number

Controls the ordering of the window's icon in the action menu

 

 

A workspace group is a set of workspace windows. When a workspace group is opened, and at least one of its workspace windows has an icon, the action menu (right nav in desktop mode) is rendered. Only one workspace group can be opened at any given time.

A workspace group supports the following optional properties:

Property

Type

Description

 

Property

Type

Description

 

persistence

'app-wide'

'closable'

Controls the close behavior of the group. 'app-wide' (default): The action menu renders without a close button; the group stays open for the entire duration of the app. 'closable': The action menu renders with a close button; the user may explicitly close the group. In this mode, only one workspace window is shown at a time.

overlay

boolean

If true, the workspace windows render as an overlay on top of the page content

 

scopePattern

string (regex)

Defines the URL scope where workspaces in this group should persist. If not defined, workspaces close only when navigating to a different app. If defined without capture groups (e.g. "^/home/appointments"), workspaces close when the URL doesn't match the pattern. If defined with capture groups (e.g. "^/patient/([^/]+)/chart"), workspaces close when the captured values change (e.g. navigating to a different patient).

 

Examples:

  • In the patient chart, the "Patient Chart" workspace group contains many workspace windows, 4 of which have icons: The order basket window, the visit note window, the clinical forms window, and the patient list window. Note that it has many other windows that do not have icons, such as the start visit window and the vitals window.

Context and props

Each level of grouping (of workspaces into windows, and windows into groups) has a notion of context. For example, in the patient chart:

  • The "Patient Chart" workspace group has a context that is the patient + their visit. (It would be jarring if the order basket window and clinical form window are both opened, but operating on a different patient, or operating on the same patient but different visits.)

  • Each workspace window also has its own context. For example, the Clinical Forms window, the order basket window and the visit note window can all be opened for viewing / editing different encounters from the past (provided that the encounters are from the same visit.) In this case, the context of each window is the encounter. If we attempt to open any of those workspaces to view another encounter, its context changes.

The props that we pass in at the workspace / window / group level define the context at that level. If we attempt to switch context at any level, all affected opened workspaces MUST first close. The system prompts the user, using a modal, for discarding unsaved changes before proceeding.

Null props and compatibility: When calling launchWorkspace2, passing null for window props or group props means the caller explicitly does not care about prop compatibility at that level. Two sets of props are considered compatible if either one is null, or if they are shallow equal. This is useful for workspaces that don't have ties to the window or group context. For example, a "patient search" workspace in the queues app does not need to share the group context, so launching it should not cause other workspaces to close.

Migration

The new workspace system is (unfortunately) a backwards-incompatible change. To migrate:

1. Update routes.json

The workspaces2, workspaceWindows2, and workspaceGroups2 sections should be used to declare workspaces / windows / groups. Follow the schema defined here. Of note:

  • Previously, routes.json has a workspaces section and a workspaceGroups section. Instead, there are now 3 sections: workspaces2, workspaceWindows2 and workspaceGroups2. Workspaces belong to windows, and windows belong to groups. The hierarchy must be explicit and intentional.

  • The type field no longer exists when defining a workspace. It served two purposes:

    • Workspaces with the same type were grouped into the same window. Instead, a workspace now has the window field to specify the window it belongs to.

    • The type field was also used to match the corresponding ActionMenuButton of the same type, to associate the icon with the window. Instead, a workspace window now has an optional icon field to specify the icon to display in the workspace action menu.

  • The canHide field no longer exists when defining a workspace. Instead, a workspace window can be hidden (minimized) if and only if it has an icon defined.

2. Update function calls

  • Existing calls to launchWorkspace and launchWorkspaceGroup need to be switched to launchWorkspace2 and launchWorkspaceGroup2. The launchWorkspace2 function takes in not only workspace props, but also (optionally) the window props and group props. The launchWorkspaceGroup2 function takes in group props (same as before). As part of the migration, developers need to re-consider what the right props (context) should be at each level.

  • The workspace action menu is only rendered when launchWorkspaceGroup2 is called, provided that the group has at least one window with an icon. Similarly, closeWorkspaceGroup2 removes the action menu.

  • closeWorkspace is no longer a global function. Instead, it is a method on Workspace2DefinitionProps, passed as a prop to workspace components. It accepts an optional options object:

    • closeWindow: If true, the workspace's window, along with all workspaces within it, will be closed.

    • discardUnsavedChanges: If true, the "unsaved changes" modal will be suppressed. Use this when closing the workspace immediately after changes are saved.

3. Update workspace components

Each workspace component should be wrapped in a <Workspace2> at the top level:

  • The workspace component should be of type Workspace2Definition<X, Y, Z> (or equivalently React.FC<Workspace2DefinitionProps<X, Y, Z>>), where X, Y, Z are the workspace props type, workspace window props type, and workspace group props type respectively.

  • To access workspace/window/group props from deeper in the component tree (i.e. not the top-level workspace component), use the useWorkspace2Context() hook.

  • The <Workspace2> component takes in the following props:

    • title: the workspace title. (Previously, the workspace title was defined as an i18n key in the workspace definition in routes.json.)

    • hasUnsavedChanges: to denote whether the workspace has unsaved changes. (Previously, this was done by calling promptBeforeClosing(() => isDirty).)

Child workspaces:

The new system has proper support for child workspaces, which can be launched using the launchChildWorkspace function (available in Workspace2DefinitionProps). A child workspace is rendered in the same workspace window as its parent.

launchChildWorkspace is async — it returns a Promise<void> because it may need to prompt the user before proceeding. Specifically, if the parent workspace already has child workspaces above it in the stack, those workspaces are replaced by the new child. If any of the replaced workspaces have unsaved changes, the user is prompted to confirm discarding them before the new child workspace is opened.

Note that launchChildWorkspace can only be called from a workspace that is currently open in a window. To open a workspace as a root (non-child) workspace, use launchWorkspace2 instead.

4. Update action menu buttons

Components that render ActionMenuButton should be switched to render ActionMenuButton2 instead. The new props are:

Prop

Type

Description

 

Prop

Type

Description

 

icon

(props: object) => JSX.Element

A function that returns the icon element

 

label

string

The button label text

 

tagContent

string

React.ReactNode (optional)

Content to display as a badge on the icon

workspaceToLaunch

{ workspaceName, workspaceProps?, windowProps? }

The workspace to launch when the button is clicked and its window is not yet open

 

onBeforeWorkspaceLaunch

() => Promise<boolean> (optional)

An async callback that runs before launching the workspace. If it returns false, the launch is cancelled. This does not run if the window is already opened (clicking the button just toggles visibility).

 

Note that the component still needs to be exported in the module's index.ts file, but need not be declared as an extension in routes.json.

Note: The names of these types / components / functions with the "2" at the end are temporary while we are transitioning between the old system and the new.

Migration Examples

  • Migration in the Dispensing App. This is a relatively simple migration, as the 3 workspaces defined in the app act standalone and do not share data or interact with other workspaces in any way.

  • Migration of "Add patient to queue" workspace window in the Service Queues App. This is a more complex, but representative, migration example. Note that the "Add patient to queue" workspace window contains 3 workspaces (the patient search workspace, the actual create queue entry workspace, and the start visit workspace.) All three workspaces (from 3 different ESMs) need to be migrated to the new system.