Workspaces & views
A workspace is a saved document. It contains one or more views (tabs), and each view is a 12-column grid of widget instances. This page covers the model, the user workflows, and how saving works.
The model
Defined in src/workspaces/types.ts:
interface WidgetLayout {
x: number;
y: number;
w: number;
h: number;
minW?: number;
minH?: number;
}
interface WidgetInstance {
id: string;
widgetType: string; // registry key
title?: string;
hideHeader?: boolean;
layout: WidgetLayout;
config?: Record<string, unknown>;
actions?: WidgetAction[]; // cross-widget wiring
}
interface WidgetGroup {
id: string;
title?: string;
hideHeader?: boolean;
layout: WidgetLayout; // the group's tile on the grid
memberIds: string[]; // member widget ids, in tab order
activeMemberId: string; // the tab currently shown
}
interface ViewConfig {
id: string;
name: string;
description?: string;
widgets: WidgetInstance[];
groups?: WidgetGroup[]; // tabbed groups; members stay in `widgets`
layoutTree?: LayoutNode; // BSP tiling tree; source of truth for placement
gridCols?: number; // defaults to 12
createdAt: string;
updatedAt: string;
}
interface WorkspaceConfig {
id: string;
name: string;
description?: string;
views: ViewConfig[];
activeViewId?: string;
createdAt: string;
updatedAt: string;
schemaVersion: number;
portalItemId?: string; // backing ArcGIS item
}
A lightweight WorkspaceSummary (id, name, viewCount, updatedAt,
owner?, canEdit?) is used for the index page and the workspace switcher
without loading the full document.
Helper factories
| Function | Purpose |
|---|---|
createEmptyWorkspace({ id, name, now }) | New workspace with one starter view. |
createEmptyView({ id, name, now }) | New empty view. |
toWorkspaceSummary(workspace) | Derive a summary from a full config. |
duplicateView(view, newId) | Deep-copy a view with fresh ids. |
Workflows
All workflows are driven through useWorkspaces() (see
State management).
Workspaces
- Create —
createWorkspace(name)persists a new workspace and makes it active. - Open — the workspaces index page
lists all workspaces; selecting one calls
setActiveWorkspace(id). - Rename / describe —
updateWorkspace(id, { name, description }), also available through the workspace settings dialog. - Delete —
deleteWorkspace(id).
Appearance preferences
The workspace settings dialog also exposes an Appearance section with
device-level display options. These are stored in localStorage (not in the
workspace document), so they persist across sessions but only apply to the
current browser/device.
- Hide app brand in header — hides the logo, title (“Fusion Analyst”), and
subtitle (“Analyst Workspace”) along with the trailing divider in the header.
Backed by the
fusion:hide-header-brandkey and theuseHeaderBrandHiddenhook, which keeps the header and the settings toggle in sync within and across tabs.
Views (tabs)
Views are managed from the header's view tab strip
(ViewTabs):
- Add —
createView(name). - Rename —
updateView(id, { name }). - Duplicate —
duplicateView(id)(deep copy with new ids). - Reorder —
reorderView(fromIndex, toIndex). - Delete —
deleteView(id). - Switch —
setActiveView(id).
Widgets
- Add — open the widget picker and call
addWidgetToView(viewId, widgetType). - Place / move / resize — the canvas is a tiling layout; arranging is done with zone buttons and dividers rather than free dragging. See The tiling canvas below.
- Configure — click the gear icon on a widget to toggle the config
drawer; changes call
updateWidgetConfig(viewId, widgetId, config). (Clicking the widget body does not open the drawer, so arranging widgets isn't interrupted.) - Remove —
removeWidgetFromView(viewId, widgetId). - Group into tabs — see Widget groups below.
See Widgets for the editing experience in detail.
Widget groups (tabbed tiles)
Two or more widgets can be combined into a single grid tile with a tab strip,
showing one member at a time — for example several maps that share one slot. A
group is a layout-level container: its members stay as ordinary
WidgetInstances in view.widgets, so their config, cross-widget actions, and
the config drawer all keep working, and ungrouping is lossless.
Key rules enforced by the model:
- A widget belongs to at most one group.
- A group always has at least two members; removing members down to one dissolves the group (the survivor becomes a top-level widget), and an empty group is dropped.
- All members stay mounted at full size; only the active tab is visible, so stateful widgets such as maps keep their extent when you switch tabs.
In edit mode (driven through useWorkspaces()):
- Create — modifier-click (⌘/Ctrl) two or more widgets to multi-select, then
choose Group into tabs from the floating toolbar →
groupWidgets(viewId, ids). - Switch tab —
setActiveGroupMember(viewId, groupId, memberId). - Configure active tab — the group's gear toggles the config drawer for the active tab's widget.
- Reorder tabs — drag a tab →
reorderGroupMembers(viewId, groupId, from, to). - Add a tab — the group's + opens the widget picker →
addWidgetToView(viewId, widgetType, groupId). - Remove a tab — pops the member back out as a top-level widget →
removeWidgetFromGroup(viewId, groupId, widgetId). - Ungroup — the group menu →
ungroupGroup(viewId, groupId).
In view mode the tab strip is read-only and tab switching is ephemeral (it is not persisted).
The pure, view-level helpers behind these operations live in src/workspaces/groups.ts.
The tiling canvas
The canvas is a tiling layout: top-level tiles (widgets and groups) never
overlap and always fill the view edge to edge, with a uniform gap between tiles
and around the canvas frame. The arrangement is stored as a binary space
partitioning (BSP) tree on ViewConfig.layoutTree, which is the source of
truth for placement; each tile's WidgetLayout x/y/w/h is derived from the
tree at render time. Legacy views without a tree have one rebuilt from their
stored rects, normalized so proportions are preserved.
The pure layout engine lives in
src/workspaces/layout/:
bsp.ts (the tree, splits, and ratio math), fromRects.ts (migration from
legacy rects), and placement.ts (pixel geometry for hit-testing and dividers).
In edit mode:
- Place — hovering a tile shows five zone buttons. The four edges split that tile 50/50 and insert the new widget on that side; the center adds it as a tab in that tile. The first widget on an empty view fills the canvas.
- Move — press a tile's header and drag onto another tile, releasing over one of its zone buttons (one continuous gesture).
- Resize — drag the divider between two tiles. Splits store a float
ratio, so resizing is resolution-independent; tiles can shrink to a single grid cell (there is no per-widget minimum).
The canvas components in src/workspaces/canvas/:
| Component | Role |
|---|---|
ViewCanvas | The react-grid-layout grid; renders each top-level widget and group tile, the placement zone buttons, and the resize dividers. Read-only in view mode. |
WidgetFrame | A single widget card: header (also the move handle), config/delete buttons, selection state. |
GroupFrame | A tabbed group tile: tab strip (also the move handle), keep-alive members, add/remove/reorder/ungroup controls. |
WidgetConfigDrawer | Right sidebar to edit the selected widget (Data / Appearance / Actions). |
WidgetPicker | Dialog for adding a widget to the view (or into a group). |
EmptyViewState | Call-to-action shown when a view has no widgets. |
Saving
Saving is explicit. Mutations mark the workspace dirty, and you persist by
clicking Save in the header — enabled only while there are unsaved changes.
The save indicator shows Saving…, Saved, or Retry save based on
saveStatus. Leaving edit mode or switching context while there are unsaved
changes prompts you to save first. See the
save lifecycle and
protecting unsaved changes.
Workspaces are stored as ArcGIS Portal Items in the signed-in user's account — see Persistence.