Skip to content

Add Library drag assignment to Dashboard panels and variables #428

Description

@BorisTyshkevich

Status

Drag path shipped in PR #484 (merged 2026-07-27). The only unmet acceptance
bullet is 9 — the keyboard-accessible Add to dashboard… command — which was
deferred to #483 by owner decision the same day. So there is no remaining work
tracked in this issue: it stays open only as the umbrella for that deferred
bullet, and closes when #483 lands (or immediately, if the bullet is considered
#483's to own).

Assignment is therefore pointer-only on main today. That is the accessibility
gap #483 exists to close, and the reason bullet 9 was not silently dropped.

Required foundations have shipped:

This issue owns one item of #447 phase 3 — the drag/drop of Library SQL onto
Variables rows. It does not own that phase. Corrected 2026-07-27; the original
wording ("owns #447 phase 3") over-claimed and would have read as though merging
#484 completed the phase.

#447 phase 3's other six items stay with #447: the paste/copy workflow in the
variable editor, rewritten shipped examples on inferred variables, import/export
coverage for variableConfigs, docs and E2E coverage, the final dead-code audit for
removed filter terminology, and reconciling or closing superseded #450.

#465 is not a prerequisite. It restores the two-String-column shape check on an individual variable tab's Run action; assignment here only copies SQL and preserves the current validation/runtime behaviour.

The old curated-filter/provider design no longer exists. There are no filter-role queries, persisted filter objects, source-query IDs, target lists, or filter configuration dialogs.

Goal

A Library query drag has three destination-dependent meanings:

Library query -> Dashboard row / Panels group -> create an independent panel-owned query copy and tile
Library query -> existing Variables row        -> copy its SQL text into that Dashboard variable's option SQL
Library query -> main SQL editor               -> insert the existing `( … )` subquery text at the drop position

The third path is already implemented by PR #40 and must remain behaviourally unchanged. This issue extends the same Library drag source with stable identity for Dashboard assignment without breaking the existing editor drop.

Drag payload and source rules

Only rows in the lower Library list may be assigned to Dashboards. A Library query has zero Dashboard owners according to buildQueryOwnershipIndex.

History rows remain draggable only for the existing editor-subquery insertion and are never Dashboard-assignment sources.

A Library drag must publish two independent payloads:

  1. the existing SUBQUERY_MIME SQL snapshot used by the SQL editor;
  2. a new internal identity payload used only by Dashboard/Variables targets.

Suggested identity payload:

interface LibraryQueryDragPayload {
  kind: 'library-query';
  workspaceId: string;
  queryId: string;
}

Requirements:

  • Dashboard assignment must never trust SQL or a complete saved-query object from dataTransfer;
  • Dashboard assignment re-resolves the query by ID from the latest committed workspace inside mutateWorkspace;
  • editor insertion continues to use the SQL snapshot and toSubquery(...) exactly as today;
  • initiating a drag must not open the Library query;
  • normal click, star, rename, delete, and existing editor drop behaviour remain usable;
  • workspace switch, missing source, or a source that is no longer a Library query cancels safely.

Do not cancel merely because the same workspace changed after drag start. Re-read the latest workspace and apply the semantic operation when the source and target still exist.

Supported destinations

Panel assignment

Accept on:

Dashboard row -> panel assignment
Panels group  -> panel assignment

Both destinations have the same persistence semantics.

Variable option-SQL assignment

Accept on an individual inferred Variables row:

Variables
  country : String   <- valid destination

The Variables group itself is not a destination because it does not identify which variable receives the SQL.

An orphaned configuration that is no longer inferred is not a valid assignment destination. It can still be edited or deleted through its existing UI.

Existing editor insertion

The main SQL editor remains an accepted destination through SUBQUERY_MIME:

  • insert at the actual drop position;
  • wrap with the existing ( … ) subquery formatting;
  • make the edit undoable;
  • perform no workspace mutation.

Rejected destinations

Do not accept Dashboard assignment on:

  • an individual panel row;
  • the Variables group row;
  • an orphan-only variable row;
  • unrelated tree rows or schema objects;
  • Dashboard tiles/canvas content;
  • History rows as sources;
  • Dashboard-owned query rows as sources.

Moving/copying members between Dashboards and tree reordering remain out of scope.

Drag feedback

During a Library drag:

  • highlight eligible Dashboard rows, Panels groups, and inferred Variables rows;
  • show a distinct active-target state on dragenter/dragover;
  • do not present the Variables group itself as eligible;
  • clear all assignment drag state on drop, cancellation, Escape, workspace switch, or drag end;
  • preserve the editor's existing drop feedback;
  • auto-expand a collapsed Dashboard after a short bounded hover delay so Panels and Variables rows become reachable;
  • cancel the hover timer when leaving;
  • auto-expansion is UI state only and must not navigate to the Dashboard or mutate workspace content;
  • dropping must not trigger Dashboard-name navigation, row click actions, disclosure clicks, or trailing action menus.

Panel assignment

Dropping on a Dashboard row or Panels group performs one atomic mutation over the latest StoredWorkspaceV5.

Suggested application command:

copyLibraryQueryToPanel({
  latest,
  sourceQueryId,
  dashboardId,
  newQueryId,
  newTileId,
}): StoredWorkspaceV5 | null

Required transformation:

  1. verify the active/latest workspace has the payload's exact workspaceId;
  2. resolve the source query by exact ID;
  3. verify it still has zero Dashboard owners;
  4. resolve exactly one target Dashboard by stable ID;
  5. mint collision-free query and tile IDs against the latest document;
  6. clone with the shipped cloneQueryForDashboardOwner({ source, newId }) helper;
  7. append the clone to workspace.queries[];
  8. append { id: newTileId, queryId: newQueryId } to the target Dashboard's tiles[];
  9. do not create redundant tile title/description overrides;
  10. normalise the target Dashboard through the active layout implementation and regenerate fallback layout data where required;
  11. increment only the target Dashboard revision, exactly once;
  12. preserve every non-target Dashboard and the source Library query unchanged;
  13. validate the complete workspace candidate;
  14. commit through app.mutateWorkspace(...).

The source remains in Library and is never modified, including its favourite state.

Repeated drops are allowed. Every successful drop creates a new independent query ID and tile ID, even when the same source is dropped repeatedly into one Dashboard.

After success:

  • repaint Library and Dashboard tree projections;
  • expand the target Dashboard and Panels group;
  • focus/select the new panel row;
  • do not automatically open Dashboard View/Edit or execute the query.

Owner revision (2026-07-27). The drop DOES now open the new panel's owned
query copy in the SQL editor, and focuses the new row in the tree. Landing on an
unchanged-looking Dashboard read as "did anything happen?". The Dashboard itself
is still never opened in View/Edit and nothing is executed.

Variable option-SQL assignment

Dropping on an inferred Variables row copies only the source query's SQL text into the target Dashboard's variableConfigs[variableName] entry.

Required semantics:

  • identify the target by exact (dashboardId, variableName);
  • re-derive the target variable from the latest target Dashboard's panel queries;
  • require that the exact variable name is still inferred at commit time;
  • preserve exact case-sensitive variable-name identity;
  • copy SQL text only;
  • create no saved-query clone, tile, role, source ID, owner, target list, or provider mapping;
  • leave the Library query byte-for-byte/canonical-value unchanged;
  • later edits to the Library query do not affect the copied variable SQL;
  • increment only the target Dashboard revision, exactly once;
  • commit through the existing withVariableConfig / Dashboard-variable-config mutation path or an equivalent id-addressed transform over mutateWorkspace.

A blank/whitespace-only Library query is not a deletion gesture: reject the drop as a no-op with a clear message. Removing variable option SQL remains the explicit blank-Save/delete behaviour of the variable editor.

Copy the SQL as authored. The existing local option-SQL rules remain authoritative:

No configuration dialog is required.

Open variable-tab interaction

A drop must never silently overwrite an unsaved variable-tab draft.

  • no open tab: commit normally;
  • matching clean variable tab: commit and reconcile it to the committed SQL;
  • matching dirty variable tab: reject the assignment, keep the draft unchanged, and focus/select that tab with a clear message.

The variable tab is identified by (dashboardId, variableName), as shipped by #457.

After a successful variable assignment:

  • repaint the Variables row/control state;
  • keep the Dashboard tree expansion state;
  • focus/select the target variable row;
  • do not automatically switch to the Query surface or run the option SQL.

Owner revision (2026-07-27). The drop DOES now open that variable's
Variable: <name> tab, so the assigned option SQL is there to edit and run. The
option SQL is still not executed automatically.

Commit-window addendum (2026-07-27). The dirty-tab rule below is enforced
inside the mutateWorkspace transform, which closes the queue-and-load window.
It cannot close the window during workspace.commit(...) itself — the write is
already durable by then. That case is REPORTED rather than hidden: the outcome
carries draftDiverged, and the drop shows a non-dismissing toast offering
Discard draft. Refusing to adopt while reporting a clean success was the
failure mode this rule exists to prevent.

Existing editor-subquery path

The existing PR #40 behaviour is part of this issue's integration contract, not new implementation scope.

Adding the identity payload must not change:

  • Library and History rows inserting into the main SQL editor;
  • SUBQUERY_MIME handling;
  • trailing semicolon/FORMAT cleanup performed by toSubquery;
  • drop-position insertion;
  • undo/redo behaviour;
  • editor-only operation with no persistence side effects.

The same Library drag may therefore be interpreted differently by different targets: the editor consumes SQL text; Dashboard destinations consume stable identity.

Concurrency and stale-state rules

Use the current read-before-write model from #343:

enter mutateWorkspace queue
-> load latest committed workspace
-> re-resolve source and target
-> apply one semantic transform
-> validate whole candidate
-> atomically commit
-> project committed truth

Do not introduce or claim repository compare-and-swap. Simultaneous same-instant multi-tab commits remain outside the repository contract.

Abort without mutation when:

  • the active workspace ID differs from the payload;
  • the source query was deleted or became Dashboard-owned;
  • the target Dashboard disappeared or has duplicate IDs;
  • the target variable is no longer inferred;
  • generated IDs cannot be made collision-free;
  • validation or persistence fails;
  • a dirty target variable tab would be overwritten.

Never commit an orphan panel query without its tile, a tile without its query, or a partial variable update.

Keyboard-accessible alternative

DEFERRED to #483 (owner decision, 2026-07-27). This section is the spec for
that issue; PR #484 implements the drag path only. Mobile is also out of scope
for the whole gesture, so no isMobile branches were added — touch drag is
already a non-goal below.

Each Library row must expose an equivalent command:

Add to dashboard…

The chooser contains:

  1. Dashboard selection;
  2. destination kind: Panel or Variable;
  3. for Variable, one of that Dashboard's currently inferred exact variable names.

The chooser calls the same application commands as drag/drop and produces identical data.

Do not recreate the removed curated-filter configuration form. There is no parameter picker, target-panel picker, default picker, selection-mode picker, or provider mapping.

The existing editor-subquery drag remains separate from this assignment chooser.

Suggested boundaries

Pure/application mutation layer

  • copyLibraryQueryToPanel
  • copyLibraryQuerySqlToVariable

Both operate over an explicit latest workspace and return a complete candidate or a typed abort.

Library drag adapter

Publishes both MIME payloads, suppresses row opening during drag, and exposes the keyboard command.

Dashboard-tree drop controller

Owns eligible targets, hover expansion, visual state, payload decoding, and command dispatch. It never mutates workspace documents directly.

Existing SQL editor adapter

Remains unchanged except for regression coverage proving the second MIME payload does not interfere with SUBQUERY_MIME.

Tests

Payload/source

  • Library rows publish both identity and subquery MIME payloads;
  • History rows publish only the existing subquery payload;
  • Dashboard-owned queries cannot start Library assignment;
  • drag start does not open a query;
  • star/rename/delete remain usable;
  • stale/missing/no-longer-Library source cancels safely.

Targeting

  • Dashboard row and Panels group accept panel assignment;
  • inferred Variables rows accept SQL assignment;
  • Variables group, orphan rows, member rows, and unrelated UI reject assignment;
  • hover auto-expands without navigating;
  • drop/cancel/Escape/workspace switch clears every visual state;
  • drop does not trigger row navigation or action-menu handlers.

Panel mutation

  • source remains unchanged and in Library;
  • clone preserves SQL, Spec version, name, description, panel data, variants, time ranges, and extension fields;
  • clone receives a new ID, panel role, and no favourite;
  • exactly one new tile references it;
  • layout/fallback normalisation is correct;
  • target revision increments once;
  • non-target Dashboards remain unchanged;
  • repeated drops create independent copies;
  • validation/persistence failure leaves the workspace unchanged.

Variable mutation

  • exact inferred variable receives the source SQL;
  • no query/tile/owner is created;
  • source remains unchanged;
  • Dashboard revision increments once;
  • case-sensitive variable identity is preserved;
  • orphan and disappeared variable targets abort;
  • blank source SQL does not delete an existing configuration;
  • parameterised or locally invalid SQL is preserved and follows existing diagnostics;
  • result-shape behaviour remains unchanged pending Validate dashboard-variable option SQL on editor Run #465;
  • clean open variable tab reconciles;
  • dirty open variable tab is preserved and assignment is rejected.

Existing editor regression

  • Library drop still inserts a ( … ) subquery at the drop position;
  • History drop still works;
  • editor insertion remains undoable;
  • no Dashboard mutation occurs;
  • the additional identity MIME does not alter editor behaviour.

Concurrency/accessibility

  • same-workspace changes during drag are rebased over latest committed state;
  • workspace switch aborts;
  • source/target deletion aborts without partial writes;
  • keyboard Add to dashboard… matches drag semantics for Panel and Variable destinations.

Acceptance criteria

  • One Library drag supports the three destination semantics documented above.
  • Dashboard/Panels drop creates one independent panel-owned query copy and tile.
  • Variables-row drop copies SQL only into the exact Dashboard variable configuration.
  • Main-editor drop retains the existing subquery-insertion behaviour.
  • Source Library queries are never modified or removed from Library.
  • Assignment uses stable IDs and the latest committed workspace, never names or stale serialized query objects.
  • All persistence mutations are atomic, revision-correct, layout-safe, and fully validated.
  • Dirty variable drafts cannot be overwritten by a drop.
  • A keyboard-accessible Add to dashboard… command provides equivalent Panel/Variable assignment. — DEFERRED to Keyboard-accessible "Add to dashboard…" command for Library queries #483 (owner decision, 2026-07-27). The drag path ships without it, so assignment is pointer-only until Keyboard-accessible "Add to dashboard…" command for Library queries #483 lands.

Non-goals

  • Reimplementing the existing editor-subquery drop.
  • Restoring curated filters, filter-role queries, provider queries, or configuration dialogs.
  • Creating a variable that is not already inferred from panel SQL.
  • Moving/copying existing Dashboard-owned members.
  • Sharing one panel query among members.
  • Reordering Dashboards, Variables, or Panels by this drag.
  • Dropping directly onto the Dashboard canvas or an individual tile.
  • Touch-specific drag interaction.
  • Automatically opening or executing the destination after assignment.
  • Persisting source-copy lineage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions