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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Changed
- **Workbench File menu reorganized around primary workspace actions, with
`Replace workspace…` renamed to `Open workspace…`** (#342). The first four
rows are now one unlabeled group in a fixed order — New workspace…, Open
workspace…, Export workspace… (`.json`), Import queries… — with the
`IMPORT / REPLACE` and `EXPORT` section headings removed. Below a
separator, `Share / Publish` (Download Markdown/SQL) now comes before
`Variable history` (recent-values toggle + clear-all); the query-count
footer stays last. `Open workspace…` is a pure rename — the picker,
transactional planner (`planReplaceWorkspace`), conflict resolution, and
destructive-replace confirmation are unchanged, with the confirm dialog and
success toast copy updated to match ("Open workspace?" / "Opened
workspace"). New `Icon.folderOpen()` (`ui/icons.ts`) replaces the reused
`refresh` glyph on that row. The standalone Dashboard's own File menu
(#302) is untouched.
- **Compact, type-aware widths for every `{name:Type}` variable/filter
input** (#345). `.var-input` no longer reserves a flat 150px for every
field regardless of its declared ClickHouse type — a new pure
Expand Down
48 changes: 29 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,33 +437,43 @@ publishes once persistence succeeds. The header **File ▾** menu carries a name
unsaved-changes dot (changes since the last export or import), and these
resource-oriented operations (#287):

The first four rows are one unlabeled primary-workspace-action group, in this
order (#342):

- **New workspace…** — clears to an empty, default-named workspace (confirms first
when non-empty). Open editor tabs are unaffected.
- **Open workspace…** — atomically replace **both** the query collection and
the Dashboard from a file (confirms first). A multi-Dashboard file asks which
Dashboard to adopt (or none).
- **Export workspace…** (`.json`) — write the one canonical
**`altinity-sql-browser/portable-bundle`** interchange format: every saved
query (catalog order) plus the zero-or-one Dashboard. Uses the deterministic
canonical encoder and never mutates the workspace's identity or Dashboard
revision.
- **Import queries…** — merge a file's queries into the current collection. When
an incoming query's id collides with an existing one, a **conflict dialog**
offers a default action plus per-row overrides — *use existing*, *copy* (import
under a fresh id), *replace*, or *skip*; a byte-identical incoming query is
reused automatically. The Dashboard is untouched (imported favorite flags never
add tiles).
- **Import Dashboard…** — replace the current Dashboard with one from a file
(confirms first when a Dashboard already exists), importing only its referenced
queries. A multi-Dashboard file asks which one.
- **Replace workspace…** — atomically replace **both** the query collection and
the Dashboard from a file (confirms first). A multi-Dashboard file asks which
Dashboard to adopt (or none).
- **Export Dashboard… / Export workspace…** (`.json`) — write the one canonical
**`altinity-sql-browser/portable-bundle`** interchange format. *Export Dashboard*
emits the selected Dashboard plus exactly its dependency-closure of queries;
*Export workspace* emits every saved query (catalog order) plus the zero-or-one
Dashboard. Both use the deterministic canonical encoder and never mutate the
workspace's identity or Dashboard revision. Legacy Library v1/v2 files remain
**importable** (decoded to an in-memory bundle); no new Library-only JSON is
written. See the [schema contracts](docs/library-json-schema.md). Imported SQL
is never run automatically.
- **Share / publish** — **Download Markdown** (`.md`, a `### heading` + fenced
` ```sql ` cookbook) and **Download SQL** (`.sql`, `/* name + description */`
comment blocks, `;`-delimited). Both are **one-way** — lossy by design (no ids
or Spec metadata), so the portable bundle stays the canonical round-trip format.

Below a separator, **Share / Publish** — **Download Markdown** (`.md`, a
`### heading` + fenced ` ```sql ` cookbook) and **Download SQL** (`.sql`,
`/* name + description */` comment blocks, `;`-delimited). Both are **one-way**
— lossy by design (no ids or Spec metadata), so the portable bundle stays the
canonical round-trip format. **Variable history** (the recent-values toggle +
clear-all) follows below its own separator.

The standalone Dashboard has its own **File ▾** menu (#302) for **Import
Dashboard…** / **Export Dashboard…** — importing replaces the current
Dashboard with one from a file (confirms first when a Dashboard already
exists, importing only its referenced queries; a multi-Dashboard file asks
which one), and exporting emits the selected Dashboard plus exactly its
dependency-closure of queries. Legacy Library v1/v2 files remain
**importable** everywhere (decoded to an in-memory bundle); no new
Library-only JSON is written. See the [schema
contracts](docs/library-json-schema.md). Imported SQL is never run
automatically.

The workspace name is editable inline (click it in the header). The **•** dot
appears after any change not yet written to a file and clears on export / import /
Expand Down
70 changes: 39 additions & 31 deletions src/ui/file-menu.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// The header "File ▾" menu (#287 W5): resource-oriented portable-bundle
// workspace operations — New workspace / Import queries / Import Dashboard /
// Replace workspace / Export Dashboard / Export workspace — plus the kept
// Open workspace / Export Dashboard / Export workspace — plus the kept
// "Open as dashboard" (#149), the Variable history section, and the one-way
// Markdown/SQL "share" downloads (buildMarkdownDoc/buildSqlDoc, unchanged).
// #342 reordered the Workbench menu around an unlabeled primary group (New /
// Open / Export workspace / Import queries), renamed "Replace workspace…" to
// "Open workspace…" (same destructive whole-workspace replace semantics —
// see `startOpenWorkspace`/`confirmOpenWorkspace` below), and moved Share/
// Publish above Variable history.
// The legacy Library New/Save-JSON/Open-replace/Append ops are gone; every
// write here resolves a `PortableBundleImportPlan` (workspace/import-planner.js)
// or a repository-level primitive (workspace/workspace-operations.js), then
Expand Down Expand Up @@ -137,7 +142,7 @@ export function openFileMenu(app: App): void {
// (`libraryControls`), and Dashboard import/export moved to the Dashboard
// page's own File menu — none of them appear here anymore.
const importQueriesInput = pickerInput(app, (f) => onImportQueriesFile(app, f));
const replaceWorkspaceInput = pickerInput(app, (f) => onReplaceWorkspaceFile(app, f));
const openWorkspaceInput = pickerInput(app, (f) => onOpenWorkspaceFile(app, f));

// Variable recent-value history (#171): this is the closest thing the app
// has to a "settings" surface today (no dedicated preferences panel exists
Expand All @@ -158,8 +163,18 @@ export function openFileMenu(app: App): void {
h('span', { class: 'fm-label' }, 'Remember recent variable values'));
const countRow = h('div', { class: 'fm-count' }, empty ? 'Workspace is empty' : queries(list.length) + ' in workspace');

// #342: the first four rows are one unlabeled primary-workspace-action
// group (no heading) in this exact order, followed by Share / Publish,
// then Variable history — the query-count footer stays last.
const rows: MenuRow[] = [
{ kind: 'item', icon: Icon.plus(), label: 'New workspace…', onClick: () => newWorkspaceAction(app) },
{ kind: 'item', icon: Icon.folderOpen(), label: 'Open workspace…', onClick: () => openWorkspaceInput.click() },
{ kind: 'item', icon: Icon.download(), label: 'Export workspace…', meta: '.json', onClick: () => exportWorkspaceAction(app) },
{ kind: 'item', icon: Icon.upload(), label: 'Import queries…', onClick: () => importQueriesInput.click() },
{ kind: 'sep' },
{ kind: 'section', label: 'Share / Publish' },
{ kind: 'item', icon: Icon.download(), label: 'Download Markdown', meta: '.md', onClick: () => downloadAction(app, 'md') },
{ kind: 'item', icon: Icon.download(), label: 'Download SQL', meta: '.sql', onClick: () => downloadAction(app, 'sql') },
{ kind: 'sep' },
{ kind: 'section', label: 'Variable history' },
{ kind: 'custom', node: historyToggle, focusable: true },
Expand All @@ -170,24 +185,13 @@ export function openFileMenu(app: App): void {
flashToast('Cleared recent variable values', { document: app.document });
},
},
{ kind: 'sep' },
{ kind: 'section', label: 'Import / replace' },
{ kind: 'item', icon: Icon.upload(), label: 'Import queries…', onClick: () => importQueriesInput.click() },
{ kind: 'item', icon: Icon.refresh(), label: 'Replace workspace…', onClick: () => replaceWorkspaceInput.click() },
{ kind: 'sep' },
{ kind: 'section', label: 'Export' },
{ kind: 'item', icon: Icon.download(), label: 'Export workspace…', meta: '.json', onClick: () => exportWorkspaceAction(app) },
{ kind: 'sep' },
{ kind: 'section', label: 'Share / publish' },
{ kind: 'item', icon: Icon.download(), label: 'Download Markdown', meta: '.md', onClick: () => downloadAction(app, 'md') },
{ kind: 'item', icon: Icon.download(), label: 'Download SQL', meta: '.sql', onClick: () => downloadAction(app, 'sql') },
{ kind: 'custom', node: countRow },
];

const handle = openMenu({ document: doc, trigger: app.dom.fileBtn!, rows });
// The hidden file pickers aren't menu ROWS (no label/click chrome of their
// own) — they're display:none inputs `.click()`-triggered by the Import
// queries / Replace workspace items above. Parent them to the mounted menu
// queries / Open workspace items above. Parent them to the mounted menu
// so they're torn down with it on close (no leak) and `picker(i)`-style
// lookups (`.file-menu input[type=file]`) keep finding them. The item click
// closes the menu (detaching these) BEFORE running its onClick, so the
Expand All @@ -196,7 +200,7 @@ export function openFileMenu(app: App): void {
// the document (the standard detached-input pattern), and it still runs
// synchronously inside the original user gesture.
handle.el.appendChild(importQueriesInput);
handle.el.appendChild(replaceWorkspaceInput);
handle.el.appendChild(openWorkspaceInput);
}

// ── file pickers + bundle decode ────────────────────────────────────────────
Expand Down Expand Up @@ -286,7 +290,7 @@ function afterLibraryChange(app: App): void {
app.updateEditorModeUi!();
renderSavedHistory(app);
// Keep the header "Dashboard →" control in sync when a commit adds/removes
// the workspace's Dashboard (e.g. Replace workspace / Import queries).
// the workspace's Dashboard (e.g. Open workspace / Import queries).
renderDashboardNav(app);
}

Expand Down Expand Up @@ -398,7 +402,7 @@ function openConflictDialog(
// ── multi-dashboard picker ───────────────────────────────────────────────────

/** Show a picker over `dashboards` (bundle array order — presentation order,
* not re-sorted); `allowNone` adds a "No dashboard" row (Replace workspace's
* not re-sorted); `allowNone` adds a "No dashboard" row (Open workspace's
* own owner decision — Import Dashboard never offers it, since it must
* import exactly one). Cancelling calls neither branch of `onPick`. */
function openDashboardPicker(
Expand Down Expand Up @@ -498,8 +502,8 @@ function startImportDashboard(app: App, bundle: PortableBundleV1): void {
function runImportDashboard(app: App, bundle: PortableBundleV1, dashboardId: string): void {
// v1 holds at most one Dashboard, so importing one REPLACES the current
// Dashboard (its tiles/layout/filters). Confirm first when that would discard
// an existing Dashboard — matching New/Replace-workspace, which also gate
// destructive commits (#287; flagged in review — silent, unrecoverable loss).
// an existing Dashboard — matching New workspace/Open workspace, which also
// gate destructive commits (#287; flagged in review — silent, unrecoverable loss).
if (app.state.dashboard) {
openConfirm(app, {
title: 'Import and replace current Dashboard?',
Expand Down Expand Up @@ -529,37 +533,41 @@ function doImportDashboard(app: App, bundle: PortableBundleV1, dashboardId: stri
});
}

// ── actions: Replace workspace ───────────────────────────────────────────────
// ── actions: Open workspace ──────────────────────────────────────────────────
// #342: user-facing rename of "Replace workspace…" — same destructive
// whole-workspace replace semantics (picker → plan → confirm → commit), just
// relabeled. `planReplaceWorkspace` (import-planner.js) keeps its own name;
// it's an internal primitive, not user-facing copy.

function onReplaceWorkspaceFile(app: App, file: File): void {
readBundleFile(app, file, (bundle) => startReplaceWorkspace(app, bundle));
function onOpenWorkspaceFile(app: App, file: File): void {
readBundleFile(app, file, (bundle) => startOpenWorkspace(app, bundle));
}

function startReplaceWorkspace(app: App, bundle: PortableBundleV1): void {
function startOpenWorkspace(app: App, bundle: PortableBundleV1): void {
const dashboards = listBundleDashboards(bundle);
if (dashboards.length > 1) {
openDashboardPicker(app, 'Replace workspace — which dashboard?', dashboards, true, (id) => {
confirmReplaceWorkspace(app, bundle, id === null ? undefined : id);
openDashboardPicker(app, 'Open workspace — which dashboard?', dashboards, true, (id) => {
confirmOpenWorkspace(app, bundle, id === null ? undefined : id);
});
return;
}
confirmReplaceWorkspace(app, bundle, dashboards[0]?.id);
confirmOpenWorkspace(app, bundle, dashboards[0]?.id);
}

function confirmReplaceWorkspace(app: App, bundle: PortableBundleV1, sourceDashboardId: string | undefined): void {
function confirmOpenWorkspace(app: App, bundle: PortableBundleV1, sourceDashboardId: string | undefined): void {
const cur = currentWorkspace(app);
openConfirm(app, {
title: 'Replace workspace?',
body: ['This replaces your current ', h('b', null, String(cur.queries.length)), ' saved ',
title: 'Open workspace?',
body: ['Opening this file replaces your current ', h('b', null, String(cur.queries.length)), ' saved ',
cur.queries.length === 1 ? 'query' : 'queries', cur.dashboard ? ' and your Dashboard' : '',
' with ', h('b', null, String(bundle.queries.length)), ' ', queries(bundle.queries.length),
sourceDashboardId ? ' and the selected Dashboard' : '', ' from the file. Open editor tabs are unaffected.'],
confirmLabel: 'Replace',
confirmLabel: 'Open workspace',
onConfirm: () => {
const workspace = currentWorkspace(app);
withQueryDecisions(app, workspace.queries, bundle.queries, (decisions) => {
const plan = planReplaceWorkspace(workspace, bundle, sourceDashboardId, decisions, app.genId);
void commitPlan(app, plan, 'Replaced workspace');
void commitPlan(app, plan, 'Opened workspace');
});
},
});
Expand Down
3 changes: 3 additions & 0 deletions src/ui/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export const Icon = {
undo: () => svg('M4.5 3.5 2 6l2.5 2.5M2 6h5a2.5 2.5 0 0 1 2.5 2.5', 12, 12),
redo: () => svg('M7.5 3.5 10 6l-2.5 2.5M10 6H5a2.5 2.5 0 0 0-2.5 2.5', 12, 12),
bookmark: () => iconEl('<path d="M3.5 1.8h5a.6.6 0 0 1 .6.6v8.2l-3.1-2-3.1 2V2.4a.6.6 0 0 1 .6-.6z"/>', 12, 12, 1.3),
// File menu "Open workspace…" (#342): an open folder, distinct from the
// closed-loop `refresh` glyph it replaces there.
folderOpen: () => iconEl('<path d="M1.4 4.2c0-.6.5-1 1-1h2l1 1.1h3.6c.5 0 1 .4 1 1v.4"/><path d="M1.5 5.7h8.3c.6 0 1.1.6.9 1.2l-.8 2.7a1 1 0 0 1-1 .7H2.3a1 1 0 0 1-1-.7l-.8-2.7a.9.9 0 0 1 .9-1.2z"/>', 12, 12, 1.3),
// #314 — the schema-surface "Open engine/type reference" actions' icon (an
// open book), distinct from `shortcuts` (a keyboard glyph) above.
book: () => iconEl('<path d="M6 2.8C5 1.9 3.5 1.6 2 1.8v7.6c1.5-.2 3 .1 4 1zM6 2.8c1-.9 2.5-1.2 4-1v7.6c-1.5-.2-3 .1-4 1z"/>', 12, 12, 1.2),
Expand Down
3 changes: 2 additions & 1 deletion src/workspace/import-planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ function mergeIncomingQueries(
return [...merged, ...additions];
}

/** REPLACE the query catalog wholesale (Replace workspace): only queries
/** REPLACE the query catalog wholesale (File menu's "Open workspace…", #342):
* only queries
* reachable from the incoming bundle survive, in bundle order; 'use-existing'
* keeps the existing query's own content (still under the shared id) rather
* than the incoming content. */
Expand Down
3 changes: 2 additions & 1 deletion src/workspace/workspace-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export function importQueries(

/** Replace the workspace's queries AND Dashboard atomically while preserving
* its identity (`id`/`name`) — the repository-level primitive behind "Replace
* from bundle" and "Replace workspace" (#280). The parsing/validation of an
* from bundle" and the File menu's "Open workspace…" (#280; user-facing
* label renamed by #342). The parsing/validation of an
* external bundle and the dependency-closure/ID-remapping that select the
* `queries`/`dashboard` passed here are the Phase-5 import planner's job; this
* only assembles the candidate the repository then commits in one transaction. */
Expand Down
Loading
Loading