Skip to main content

State management

Application state for workspaces, views, and widgets lives in a single React Context: WorkspacesContext. Components interact with it through the useWorkspaces() hook.

The context object itself, the useWorkspaces() hook, and the WorkspacesContextValue type live in a sibling src/workspaces/state/workspaces-context.ts module (re-exported from WorkspacesContext.tsx). Keeping the createContext call out of the provider's .tsx file gives the context a stable identity across React Fast Refresh, so editing the provider during development no longer throws "used outside provider". Import paths are unchanged — continue importing from @/workspaces/state/WorkspacesContext.

What the provider owns

WorkspacesProvider:

  • Is mounted inside the auth provider and keyed by user.id, so switching users (or ArcGIS connections) gives a fresh, isolated state.
  • Loads the workspace list when the user changes, and loads an individual workspace on demand via setActiveWorkspace.
  • Injects the live ArcGIS portal into the persistence provider (workspacePersistence.setPortal(portal)).
  • Runs a debounced auto-save whenever workspace state is mutated.

The context value

useWorkspaces() returns a WorkspacesContextValue. The most important members:

State

MemberTypeDescription
loadingbooleanWhether a workspace list or workspace is loading.
modeWorkspaceMode"edit" or "view".
workspacesWorkspaceSummary[]Lightweight list for the index and switcher.
activeWorkspaceWorkspaceConfig | nullThe fully loaded active workspace.
activeViewIdstring | nullThe active tab's id.
activeViewViewConfig | nullThe active tab.
selectedWidgetIdstring | nullThe single widget selected for configuration (set only when exactly one is selected).
selectedWidgetIdsstring[]All widgets in the current multi-selection (used for grouping).
saveStatus"idle" | "dirty" | "saving" | "saved" | "error"Drives the save indicator.
hasUnsavedChangesbooleanTrue when saveStatus is "dirty" or "error". Enables the Save button and arms the unsaved-changes guards.
errorstring | nullThe last error message, if any.

Workspace operations

MethodDescription
createWorkspace(name)Create and persist a new workspace.
updateWorkspace(id, patch)Patch name / description.
deleteWorkspace(id)Delete a workspace.
setActiveWorkspace(id)Load and activate a workspace.
persistWorkspace()Save the active workspace; resolves true on success, false on failure/no-op.

View operations

MethodDescription
createView(name)Add a new tab.
updateView(id, patch)Patch a view's name / description.
deleteView(id)Remove a tab.
duplicateView(id)Deep-copy a tab with fresh ids.
reorderView(from, to)Reorder tabs.
setActiveView(id)Switch the active tab.

Widget operations

MethodDescription
addWidgetToView(viewId, widgetType, targetGroupId?)Add a widget instance from the registry; with targetGroupId it is added straight into that group as a new tab.
updateWidgetConfig(viewId, widgetId, config)Update a widget's saved config.
removeWidgetFromView(viewId, widgetId)Remove a widget (also detaches it from any group).
mutateView(viewId, updater)Low-level escape hatch for layout updates.

Widget group operations

Groups combine two or more widgets into one tabbed tile; see Widget groups.

MethodDescription
groupWidgets(viewId, widgetIds)Combine two or more widgets into a new tabbed group.
ungroupGroup(viewId, groupId)Dissolve a group back into top-level widgets.
addWidgetToGroup(viewId, groupId, widgetId)Add an existing widget to a group as the active tab.
removeWidgetFromGroup(viewId, groupId, widgetId)Pop a member out to a top-level widget.
reorderGroupMembers(viewId, groupId, from, to)Reorder a group's tabs.
setActiveGroupMember(viewId, groupId, memberId)Choose which tab is shown.
updateGroup(viewId, groupId, patch)Patch a group's title / hideHeader.

UI state

MethodDescription
setSelectedWidgetId(id | null)Select a single widget for the config drawer.
toggleWidgetSelection(id)Add/remove a widget from the multi-selection (modifier-click).
clearWidgetSelection()Clear the multi-selection.
setMode(mode)Switch between edit and view.
clearError()Dismiss the current error.

Save lifecycle

Saving is explicit: any mutation marks the active workspace dirty, and the user persists by clicking Save in the header. The button is enabled only while there are unsaved changes (hasUnsavedChanges — i.e. dirty or error), so a freshly opened or already-saved workspace shows a disabled Save. The indicator renders directly from saveStatus (Saving… / Saved / Retry save).

Protecting unsaved changes

Because saving is manual, any interaction that would leave edit mode or drop the in-memory workspace is routed through the unsaved-changes guard (useUnsavedChangesGuard() / UnsavedChangesGuardProvider). When hasUnsavedChanges is true the guard prompts the user to Save changes, Continue without saving, or Cancel before the action runs; when there are no unsaved changes the action runs immediately. The guard wraps:

  • Go Live (leaving edit mode for the live view).
  • Sign out, switch workspaces, All workspaces, New workspace, and switch connections in the user menu.

A beforeunload listener is armed while hasUnsavedChanges is true, so closing or reloading the tab triggers the browser's native confirmation as a catch-all safety net.