Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flow-palette-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@object-ui/app-shell': minor
---

Flow designer add-node palette follow-ups (#1943): localize the category section headings (Data / Logic / Human / Integration / Flow) to the active console language, and upgrade the "Recently used" list from browser-local storage to per-user cloud sync via `sys_user_preference` (new `FlowPaletteRecentsProvider` / `useFlowPaletteRecents`), with one-shot migration of the legacy localStorage key and a localStorage fallback when offline or outside a provider. Adds a Flow Designer guide to the docs.
126 changes: 126 additions & 0 deletions content/docs/guide/flow-designer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
title: "Flow Designer"
description: "Authoring automation flows on the visual canvas — nodes, edges, the searchable add-node palette, the node inspector, validation, and simulation."
---

# Flow Designer

The **Flow Designer** is the visual editor for `flow` metadata in the console's
metadata admin. It renders a flow's nodes and edges on a pan/zoom canvas so you
can assemble automation — create/update records, branch on conditions, wait for
events, call APIs, route for approval — without hand-editing JSON. The designer
is a thin, **spec-driven** view over `flow` metadata: everything you drop on the
canvas is a node in the flow's schema, so the same document runs unchanged on
the automation engine.

Open it from a `flow` record's **Design** tab. The **Form** tab holds the same
flow as an editable tree; the two stay in sync.

## The canvas

Each node is a card showing its icon, label, `type`, and a one-line summary of
its config. Edges are the arrows between them.

| Gesture | Result |
|---|---|
| Click a card | Select it (opens the node inspector) |
| Drag a card | Reposition it |
| Hover a card → click the bottom **+** | Append a connected child node |
| Zoom controls / **Fit** | Scale and re-center the graph |

Structural problems (an undeclared cycle, an unreachable node) surface three
ways at once: a red ring on the offending card, a badge in its corner, and an
inline banner at the top-left of the canvas. Clicking a banner row selects and
pans to the element it refers to.

## Adding nodes — the palette

The **Add node** button (top-right, edit mode only) opens the node palette: a
searchable, grouped list of every node type the flow can use.

### Search and keyboard

Type in the box at the top to filter across **all** categories at once — the
match is a case-insensitive substring test over each node's **label**, **hint**,
and **type**, so `scr` finds *Screen*, `http` finds *HTTP request*, and a word
that only appears in a hint (`concurrently` → *Parallel*) still surfaces the
node. Clearing the box restores the full grouped list.

The palette is keyboard-navigable end to end:

| Key | Action |
|---|---|
| `↑` / `↓` | Move the highlight (wraps around) |
| `Enter` | Insert the highlighted node |
| `Esc` | Close the palette |

The search box autofocuses when the palette opens, so you can open, type a few
letters, and press `Enter` without touching the mouse.

### Categories

Nodes are grouped into five sections, in this order:

| Category | Node types |
|---|---|
| **Data** | Create / Update / Get / Delete record |
| **Logic** | Decision, Loop, Set variables, Parallel, Try / Catch |
| **Human** | Approval, Screen |
| **Integration** | HTTP request, Connector, Script |
| **Flow** | Subflow, Wait, End |

Section headings are localized to the active console language (e.g. *数据 /
逻辑 / 人工 / 集成 / 流程* in Chinese); the underlying node types are unchanged.

### Recently used

When the search box is empty, a **Recently used** group tops the list with the
node types you inserted most recently (up to five, most-recent first) — so the
nodes you reach for repeatedly stop needing a scroll or a search. The list is
per-user and, when the console is connected to a backend, syncs across devices
(see [User-Scoped State Persistence](/docs/guide/user-state-persistence)); it
falls back to browser-local storage when offline. Types from a plugin that was
since uninstalled drop out of the list automatically.

### Server-merged, plugin-extensible

The palette is **server-driven**. Beyond the built-in node types, the running
engine publishes its registered actions at `GET /api/v1/automation/actions`, and
plugins contribute their own node types there (for example an `approval` node
from an approvals plugin, or third-party `connector_action` providers). The
designer overlays those descriptors onto the built-in list — adopting the
engine's labels and descriptions and appending engine-only types — so the
palette always matches what the connected backend actually supports. Plugin
nodes are searchable exactly like built-ins, including by their registered
`type`. When the endpoint is unreachable the designer falls back to its
built-in defaults, so authoring still works offline.

## The node inspector

Selecting a node opens its inspector: **ID**, **Label**, **Node Type**, an
optional **Description**, and a **Configuration** section. New nodes start with
spec-valid defaults (a *Wait* node already carries a timer config, an *HTTP*
node defaults to `GET`) so a freshly dropped block is never in a broken
intermediate state.

For node types whose engine executor publishes a `configSchema` (ADR-0018), the
inspector renders a **server-driven property form** from that schema — so a
plugin's node gets a real config UI without the designer hardcoding its fields.

## Validate, simulate, inspect runs

The toolbar toggles four side panels:

| Panel | What it shows |
|---|---|
| **Variables** | The flow's declared variables |
| **Problems** | Structural + server validation issues, each clickable to reveal on canvas |
| **Debug** | A step-through **simulator** that walks the graph and highlights the active / visited nodes |
| **Runs** | Execution history for the published flow, fetched from the engine |

## See also

- [User-Scoped State Persistence](/docs/guide/user-state-persistence) — how the
"Recently used" list is stored and synced.
- [Console App](/docs/guide/console) — the reference app that hosts the metadata
admin and its designers.
1 change: 1 addition & 0 deletions content/docs/guide/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"slotted-pages",
"console",
"console-architecture",
"flow-designer",
"metadata-diagnostics",
"user-state-persistence",
"objectos-integration",
Expand Down
11 changes: 6 additions & 5 deletions content/docs/guide/user-state-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "User-Scoped State Persistence"
---

Object UI keeps two pieces of per-user UI state — **Favorites** (pinned apps, starred records, and pinned navigation entries) and **Recent Items** (last visited entities) — alive across reloads, devices, and accounts. The persistence layer is **backend-agnostic**: drop in an adapter, or just let it run on localStorage.
Object UI keeps per-user UI state — **Favorites** (pinned apps, starred records, and pinned navigation entries), **Recent Items** (last visited entities), and **Flow-palette recents** (node types recently added in the flow designer) — alive across reloads, devices, and accounts. The persistence layer is **backend-agnostic**: drop in an adapter, or just let it run on localStorage.

## Design

Expand All @@ -16,21 +16,21 @@ export interface UserDataAdapter<T> {
save(items: T[]): Promise<void>;
}

export type UserStateKind = 'favorites' | 'recent';
export type UserStateKind = 'favorites' | 'recent' | 'flowPaletteRecents';
```

Three layers, top-down:

| Layer | Package | Role |
|---|---|---|
| **Providers** (`FavoritesProvider`, `RecentItemsProvider`) | `@object-ui/app-shell` | Own the state, debounce writes, scope storage by user. |
| **Providers** (`FavoritesProvider`, `RecentItemsProvider`, `FlowPaletteRecentsProvider`) | `@object-ui/app-shell` | Own the state, debounce writes, scope storage by user. |
| **Adapter registry** (`UserStateAdaptersProvider`) | `@object-ui/app-shell` | Lets a bridge component inject adapters at runtime once `dataSource` + `user.id` are available. |
| **Adapters** (`createObjectStackUserStateAdapter`, your own) | `@object-ui/data-objectstack`, custom | Translate `load/save` into HTTP / GraphQL / ObjectQL calls. |

### Three guarantees

1. **localStorage-first.** First paint never blocks on the network. If no adapter is attached, persistence is purely local.
2. **Scoped per `user.id`.** Storage key is `objectui-favorites:u:<id>` (and `objectui-recent-items:u:<id>`). Two accounts on the same browser never see each other's state.
2. **Scoped per `user.id`.** Storage key is `objectui-favorites:u:<id>` (and `objectui-recent-items:u:<id>`, `flow-palette-recents:u:<id>`). Two accounts on the same browser never see each other's state.
3. **Silent degrade.** Adapters must never throw. A 404 / network error means "behave like there is no backend"; the UI keeps working from localStorage.

## Provider tree
Expand Down Expand Up @@ -179,11 +179,12 @@ Each provider keeps a monotonic `hydrationToken`. If the user switches accounts
| `useFavorites()` | `@object-ui/app-shell` | `{ favorites, addFavorite, removeFavorite, toggleFavorite, isFavorite, clearFavorites, setPinned, isPinned, pinnedNavIds }` |
| `useNavPins()` | `@object-ui/app-shell` | Thin shim over `useFavorites` for sidebar pinning — `{ pinnedIds, togglePin, isPinned, applyPins, clearPins }`. |
| `useRecentItems()` | `@object-ui/app-shell` | `{ recentItems, addRecentItem, clearRecentItems }` |
| `useFlowPaletteRecents()` | `@object-ui/app-shell` | `{ recents, recordRecent }` — flow-designer add-node MRU; falls back to localStorage outside a provider. |
| `createObjectStackUserStateAdapter(opts)` | `@object-ui/data-objectstack` | Official adapter against the `user_app_state` object. |

## Limits

- **20** favorites and **20** nav-pins per user (independent buckets), **8** recent items. Enforced by the providers; older entries roll off within their own bucket without evicting the other.
- **20** favorites and **20** nav-pins per user (independent buckets), **8** recent items, **5** flow-palette recents. Enforced by the providers; older entries roll off within their own bucket without evicting the other.
- One JSON blob per (user, kind). Not designed for high-frequency / large payloads — this is UI state, not data.
- No automatic cross-tab sync today (see the roadmap).

Expand Down
17 changes: 14 additions & 3 deletions packages/app-shell/src/console/ConsoleShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { PreviewModeProvider } from '../preview/PreviewModeContext';
import { NavigationProvider } from '../context/NavigationContext';
import { FavoritesProvider } from '../context/FavoritesProvider';
import { RecentItemsProvider } from '../context/RecentItemsProvider';
import { FlowPaletteRecentsProvider } from '../context/FlowPaletteRecentsProvider';
import {
UserStateAdaptersProvider,
useAttachUserStateAdapters,
Expand Down Expand Up @@ -83,9 +84,11 @@ export function ConsoleShell({ children }: { children: ReactNode }) {
<UserStateAdaptersProvider>
<FavoritesProvider>
<RecentItemsProvider>
<Suspense fallback={<LoadingFallback />}>{children}</Suspense>
{/* ADR-0069 — full-screen gate (expired password / required MFA) above all routes */}
<RemediationOverlay />
<FlowPaletteRecentsProvider>
<Suspense fallback={<LoadingFallback />}>{children}</Suspense>
{/* ADR-0069 — full-screen gate (expired password / required MFA) above all routes */}
<RemediationOverlay />
</FlowPaletteRecentsProvider>
</RecentItemsProvider>
</FavoritesProvider>
</UserStateAdaptersProvider>
Expand Down Expand Up @@ -175,6 +178,7 @@ function UserStateBridge() {
if (!user?.id || !dataSource) {
attach('favorites', null);
attach('recent', null);
attach('flowPaletteRecents', null);
return;
}
const favorites = createObjectStackUserStateAdapter({
Expand All @@ -187,11 +191,18 @@ function UserStateBridge() {
userId: user.id,
key: 'ui.recent',
});
const flowPaletteRecents = createObjectStackUserStateAdapter<string>({
dataSource,
userId: user.id,
key: 'ui.flow.palette.recents',
});
attach('favorites', favorites);
attach('recent', recent);
attach('flowPaletteRecents', flowPaletteRecents);
return () => {
attach('favorites', null);
attach('recent', null);
attach('flowPaletteRecents', null);
};
}, [user?.id, dataSource, attach]);

Expand Down
Loading
Loading