Skip to content

feat(editor): consistent schema gestures, SQL formatter, undo-friendly inserts - #3

Merged
BorisTyshkevich merged 5 commits into
mainfrom
feat/editor-ux
Jun 20, 2026
Merged

feat(editor): consistent schema gestures, SQL formatter, undo-friendly inserts#3
BorisTyshkevich merged 5 commits into
mainfrom
feat/editor-ux

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

A batch of editor/schema UX improvements.

Schema tree — consistent click model

Previously column single-click inserted the name, so a double-click pasted it twice (while tables inserted once on double-click). Now uniform:

single-click double-click shift-click
database expand insert db (cursor) SHOW CREATE DATABASE db (top line)
table expand SELECT * FROM db.table LIMIT 100 (top line) SHOW CREATE db.table (top line)
column (nothing) col (cursor) col::type (cursor)
  • "top line" = prepended as a new first line (doesn't replace existing content).
  • SHOW CREATE is used without TABLE so it covers tables/MVs/dictionaries.

Editor — real undo/redo for programmatic inserts

Programmatic edits (drag, double-click insert, format, Tab) now go through execCommand('insertText') with a manual-splice fallback, so they land on the native undo stack — ⌘Z / ⌘⇧Z work for them, not just for typing. New insertTopLine / replaceEditor helpers.

Format query (⌘⇧F)

Runs SELECT formatQuery(<editor SQL>) on the server and replaces the editor with the formatted output; failures surface via a toast.

Shortcuts

  • Removed ⌘T / ⌘W interception — the browser keeps its native new-tab/close-tab; the + and × buttons still create/close tabs.
  • Added to the shortcuts modal: Format query (⌘⇧F), Undo (⌘Z), Redo (⌘⇧Z).

Verification

In-browser (Chrome) against the built bundle: each gesture inserts the right text; execCommand('undo') reverts a programmatic insert (undo works); shortcuts modal shows the new list. npm test → 346 pass; editor/schema/shortcuts 100%, app.js within its gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef

Isolator acm and others added 5 commits June 20, 2026 11:42
…serts

Schema tree (consistent click model; was: column single-click inserted, so a
double-click pasted twice):
- single-click = expand (db/table) / nothing (column)
- double-click = insert — db: name (cursor); table: `SELECT * FROM db.t LIMIT 100`
  (prepended as a top line, not replacing); column: name (cursor)
- shift-click = db: `SHOW CREATE DATABASE db`; table: `SHOW CREATE db.t`
  (ClickHouse SHOW CREATE handles table/MV/dictionary); column: `col::type`
  (top-line for the statements, cursor for identifiers)

Editor:
- programmatic edits now go through execCommand('insertText') (with a manual-
  splice fallback), so drag/double-click/format inserts join the native undo
  stack — ⌘Z / ⌘⇧Z work for them, not just typing. New `insertTopLine` +
  `replaceEditor` helpers; Tab-insert routed through the same path.

Format query: new ⌘⇧F → `SELECT formatQuery(<editor sql>)` on the server,
replaces the editor with the result (errors surface via a toast).

Shortcuts: drop ⌘T/⌘W (let the browser keep new-tab/close-tab; the +/× buttons
remain); add Format, Undo, Redo to the modal list.

Verified in-browser: every gesture inserts the right text; execCommand('undo')
reverts a programmatic insert; shortcuts modal shows the new list. 346 tests
pass; editor/schema/shortcuts at 100%, app.js within gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
⌘⇧F / Ctrl+Shift+F collides with browser find/Lens muscle memory (Ctrl+Shift+F
is Google Lens "search page" on Chrome Win/Linux). Rebind Format to
⌘/Ctrl+Shift+Enter — it pairs with Run (⌘/Ctrl+Enter), and Shift+Enter has no
browser default on macOS, Windows, or Linux. The Enter handler now branches on
Shift (format when signed in, else run). Shortcuts modal updated to ⌘⇧↵.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
The shortcuts modal now has a "Schema tree" section listing the click /
double-click / shift-click gestures (expand, insert, insert DDL/col::type),
reusing the existing row+chip styling under a small divider+subheading — so the
gestures are discoverable next to the keyboard shortcuts. Terse by design; the
per-row tooltips still carry the detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
…Query)

Shift-click on a table/db now fetches the real DDL via SHOW CREATE and pretty-
prints it through formatQuery(), inserting the formatted CREATE statement as a
top line — instead of inserting the literal "SHOW CREATE …" command. Two server
round-trips by design (show + format); if formatting fails it falls back to the
raw DDL, and a SHOW CREATE failure surfaces via a toast. New `insertCreate`
action; schema shift-click handlers call it ('db.table' for tables,
'DATABASE db' for databases).

352 tests pass; schema 100%, app.js within gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
A share link carries the SQL in the URL hash (#<base64>). When the recipient
wasn't signed in, the OAuth redirect to the IdP dropped the hash (redirect_uri is
path-only and fragments don't round-trip), and the in-memory tab was wiped by the
full-page navigation — so they landed on a blank /sql after signing in.

bootstrap now stashes the decoded query in sessionStorage (`oauth_shared_sql`),
which survives the same-tab redirect like oauth_state/verifier, and restores it
when the hash is gone, clearing it once consumed on a signed-in render.

Verified in-browser: not-signed-in load stashes the query + shows login; the
post-redirect load (no hash, signed in) restores it into the editor and clears
the stash. 353 tests pass; main.js at 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
@BorisTyshkevich
BorisTyshkevich merged commit d3c5027 into main Jun 20, 2026
2 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the feat/editor-ux branch June 22, 2026 16:43
BorisTyshkevich added a commit that referenced this pull request Jul 18, 2026
…d layout (#308)

* feat(#291): grafana-grid@1 schema + pure layout plugin + registry registration (wave 1)

- schemas/dashboard-layout-grafana-grid-v1.schema.json: rowless 12-col engine,
  items {span 1..12, height compact|medium|large}, no preset; manifest entry +
  regenerated types/validators (fallback slot stays pinned to flow@1)
- src/dashboard/layouts/grafana-grid-layout.ts: DashboardLayoutPlugin +
  computeGrafanaGridLayout (deterministic row-major packing, responsive column
  clamp 12/6/4/2), deriveGrafanaGridPlacement, flow<->grid span conversion,
  deriveFlowFallback (validated flow@1)
- workspace-semantics: isSupportedLayout generalized to the engine map; new
  narrow isFlowLayout for fallback/flow-only call sites so a grafana-grid doc
  can never be accepted as a flow fallback; semantics validation now validates
  a non-flow primary against its own schema AND still requires a flow@1 fallback
- defaultLayoutRegistry registers grafana-grid@1 behind a lazy load()
- tests: grafana-grid-layout (100/100/100/100), layout-registry, workspace-
  semantics, schema-build extended

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* feat(#291): engine-aware dashboard authoring commands (wave 2)

- change-layout switches engines: flow->grid seeds items from flow placements
  (span 1->4, 2->6, 3->12; height carried) and snapshots the flow layout as
  fallback; grid->flow restores the fallback (optionally applying a preset);
  missing fallback -> dashboard-command-layout-fallback-missing diagnostic
- update-placement validates/writes through the ACTIVE engine plugin
  (grid span 1..12, flow 1..3) via new resolveLayoutPluginSync
- add-time seeding under grid uses deriveGrafanaGridPlacement (always explicit)
- regenerateGridFallback shared primitive: every tile/placement mutation under
  grid (commands, tile-membership, saved-query-mutation) deterministically
  regenerates a valid flow@1 fallback - UI never manages fallback itself
- tests: commands engine-switch matrix, fallback regen per mutating command,
  per-engine placement bounds, membership/mutation grid cases

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* feat(#291): rowless grafana-grid UI - viewer wiring, grid render, edit interactions (wave 3)

- dashboard-viewer-session: layout routed through the registry-resolved active
  plugin; DashboardViewState.layout becomes a discriminated union
  ({engine:'flow', ...FlowLayoutModel} | {engine:'grafana-grid', grid});
  containerWidth dep feeds the responsive column clamp (12/6/4/2)
- src/ui/dashboard.ts: single-CSS-grid reconciliation path (.dash-gg-grid,
  8px gap, span + semantic-height classes 118/210/296px), KPI tiles placed
  like any tile; layout select extended to 4 flow presets + Grafana grid
  driving engine-switching change-layout; edit mode (!readOnly): whole-card
  drag-reorder (move-tile), corner-drag resize with pure snap math ->
  one update-placement per drag, tile delete (remove-tile); immediate
  propagation - no Save/Undo (owner decision)
- fixed pre-existing runCommand bug: plugin was hardcoded to flow for
  validation + normalize; now resolves the active engine (post-command
  layout for normalize, so engine switches normalize through the result)
- grafana-grid-layout: GRID_GAP_PX/GRID_HEIGHT_PX + snapGridSpan/
  snapGridHeight pure resize math (100% covered)
- styles.css: grid host/tile/edit-affordance chrome, both themes

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* test(#291): real-browser e2e harness for the grafana-grid layout (wave 3b)

Narrow static harness (dashboard-mobile.html pattern, no createApp): packing
positions vs the pure model, semantic height px, container-width column clamp
12/6/4/2, no horizontal overflow at 360px, corner-drag resize live preview +
single terminal dispatch, hover-revealed chrome + view-mode absence.
6/6 green on chromium and webkit (firefox full-parallel goto flake is
pre-existing; spec passes 6/6 at --workers=1).

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* fix(#291): review round - real-browser layout fixes + engine-dispatch cleanup

Verified findings from the 8-angle review + adversarial verification:
- remove the 768px !important grid-template override that fought the JS
  container-width clamp (span>1 tiles rendered on implicit phantom tracks
  in the 470-768px band; reproduced in real chromium)
- measure the grid's CONTENT-box width (clientWidth minus computed padding)
  for both breakpoint tiers and resize column math (pure contentBoxWidth)
- corner-drag resize pins the tile to its explicit colStart for the drag
  (span clamped to remaining columns) so a mid-row tile can never self-wrap
  and desync the persisted placement from the pointer
- renderDashboard removes the previously-installed window resize listener
  (reloadDashboardRoute re-entry stacked handlers + pinned stale sessions)
- bare same-engine change-layout (no items) preserves existing placements
  instead of silently wiping them via the wholesale branch
- one BUILTIN_SYNC_PLUGINS table now feeds both resolveLayoutPluginSync and
  defaultLayoutRegistry (single registration point for engine #3)
- delete dead resolveActiveLayoutPlugin (stale semantics, zero prod callers);
  drop redundant GrafanaGridLayoutModel.order; regenerateGridFallback owns
  the type guard + tile-ref mapping (3 call sites collapsed, no flow-path
  alloc); setPlacementForActiveEngine dedupes the per-engine write branch
- e2e harness updated to the pinned-resize contract + new mid-row drag test

Gates: npm test 3657 passed, build OK, check:arch OK, full e2e
chromium+webkit 104 passed.

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* chore(#291): CHANGELOG entry + reconcile roadmap #68 and issue body

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* test(#291): fix firefox CI flake in the mid-row resize-clamp e2e

Firefox does not deliver real mouse moves ~100000px outside the viewport,
so the extreme rightward drag never reached the wiring and the span stayed
at 1. Dispatch that single extreme pointermove synthetically on window
(where the resize wiring listens); real mouse input still drives
down/pin/up and the terminal dispatch. 21/21 green locally on
chromium+firefox+webkit at --workers=1.

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

* feat(#291): tile heights become numeric row units 1-16 (~5x taller max)

Owner override on the served build: the corner-drag vertical range was
capped at the 3-tier 296px max. grafana-grid item height is now an integer
1..16 in row units (px = 32 + 88*units; units 1/2/3 land on the legacy
tiers, unit 16 = 1440px ~5x the old cap). The schema accepts the legacy
compact|medium|large strings (anyOf) and normalize() upgrades them to
1/2/3; flow fallback maps units 1->compact, 2->medium, >=3->large; the
resize snap is the exact inverse of the px formula (per-unit stops).
Height tier classes replaced by a computed inline height; e2e heights
scenario now covers units 1/2/3/10 (120/208/296/912px).

Gates: npm test 3673 passed, build OK, check:arch OK, grid+mobile e2e
30 passed chromium+webkit, grid spec 7 passed firefox --workers=1.

Part of #291.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLMprUnwnz4oaz3cnLRKcz

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BorisTyshkevich added a commit that referenced this pull request Aug 5, 2026
… retire dashboardTreeRevision

app.currentWorkspace/app.mainSurface become signal-backed accessor pairs
(peeking getter, notifying setter) so a mutation is its own notification —
the #426/#427 bug class (a write site forgetting to bump the invalidation
counter) becomes structurally impossible. app.committedWorkspace
(ReadonlySignal<StoredWorkspaceV5|null>) and app.treeNavigation
(a computed structural key over kind/dashboardId/currentMember) are the two
tracked reads app-shell.ts's tab-count/tree/Library effects subscribe
through, replacing state.dashboardTreeRevision.

app.currentWorkspace's setter is asymmetric (no null): a transitional null
publication is a named departure operation owned by a new closure-private
surface-retirement coordinator in app.ts (retireToWorkspaceLoading/
-Missing/-Failure/-Login), which batches the publication atomically with
disposing any live shell so a surface never repaints against transitional
state — five independent review passes each found a different call site
where this raced disposal, hence one coordinator with exclusive mutation
authority rather than five patches. SurfaceStatePort/DashboardApp/TabsApp/
DashboardTreeApp are narrowed to readonly on currentWorkspace (the fourth,
DashboardTreeApp, was documented read-only but not type-readonly before
this change).

app.reloadDashboardRoute() (a post-commit fold-and-reassign that would
double-publish once the aggregate is signal-backed) is deleted;
afterLibraryChange's Dashboard branch calls the render-only
app.renderCurrentSurface() instead.

Tests: tests/unit/surface-lifecycle-arch.test.ts (a static-source scan
backing the coordinator's compile/scan-layered "no lifecycle bypass"
claim) and tests/unit/surface-accessor-contracts.test.ts (@ts-expect-error
fixtures for the asymmetric setter and the four narrowed ports) are new,
plus a new app.test.ts describe block covering the issue's Tests #1/#3/#5
and the plan's invariant map (delivery-only no-ops, adversarial-id
collision-freedom, failure-path status/null ordering, the four-arm
live-shell no-repaint sweep, one-commit-exactly-once settlement, and the
mixed-snapshot batch-ordering regression). fake-app.ts's makeApp() installs
real per-call signals on the returned object (object spread evaluates an
accessor pair into a plain value, so the fake needs the same defineProperty
treatment createApp() gets natively) for reactivity parity in fixtures.

No persisted/schema change, no user-visible behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant