Skip to main content

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

FunctionPurpose
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

  • CreatecreateWorkspace(name) persists a new workspace and makes it active.
  • Open — the workspaces index page lists all workspaces; selecting one calls setActiveWorkspace(id).
  • Rename / describeupdateWorkspace(id, { name, description }), also available through the workspace settings dialog.
  • DeletedeleteWorkspace(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-brand key and the useHeaderBrandHidden hook, 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):

  • AddcreateView(name).
  • RenameupdateView(id, { name }).
  • DuplicateduplicateView(id) (deep copy with new ids).
  • ReorderreorderView(fromIndex, toIndex).
  • DeletedeleteView(id).
  • SwitchsetActiveView(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.)
  • RemoveremoveWidgetFromView(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 tabsetActiveGroupMember(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/:

ComponentRole
ViewCanvasThe 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.
WidgetFrameA single widget card: header (also the move handle), config/delete buttons, selection state.
GroupFrameA tabbed group tile: tab strip (also the move handle), keep-alive members, add/remove/reorder/ungroup controls.
WidgetConfigDrawerRight sidebar to edit the selected widget (Data / Appearance / Actions).
WidgetPickerDialog for adding a widget to the view (or into a group).
EmptyViewStateCall-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.