feat(saved): remember the result view and auto-run on restore · Altinity/altinity-sql-browser@f415931 · GitHub
Skip to content

Commit f415931

Browse files
feat(saved): remember the result view and auto-run on restore
- Saved queries now persist the result view (Table/JSON/Chart) alongside the chart config; saveQuery captures state.resultView (the transient raw view is not saved), and saved-io export/import/merge carry it. - Opening a saved query reopens that view and runs immediately: the saved-row click calls run({ view }), and run() now starts in the given view (or Table when absent/unknown) instead of always resetting to Table. History rows also re-run on click (Table view). - View ownership lives in run() (which already resets resultView); loadIntoNewTab stays responsible only for tab-level state (sql/savedId/chart). 528 tests pass; core/state/render layers at their per-file gates. Verified e2e on antalya: a one-click saved-Chart query auto-runs straight into its chart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
1 parent 47bde33 commit f415931

9 files changed

Lines changed: 74 additions & 14 deletions

File tree

src/core/saved-io.js

473 Bytes
Binary file not shown.

src/state.js

Lines changed: 8 additions & 0 deletions

src/ui/app.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export function createApp(env = {}) {
328328
}
329329
app.tickElapsed = tickElapsed;
330330

331-
async function run() {
331+
async function run(opts) {
332332
if (app.state.running) return; // already running — cancel via cancel()/Esc
333333
const tab = app.activeTab();
334334
if (!tab.sql.trim()) return;
@@ -339,7 +339,8 @@ export function createApp(env = {}) {
339339
const t0 = now();
340340
tab.result = newResult(fmt);
341341
app.state.resultSort = { col: null, dir: 'asc' };
342-
app.state.resultView = 'table';
342+
// Start in Table unless a caller restores a remembered view (saved-query open).
343+
app.state.resultView = (opts && opts.view === 'json') || (opts && opts.view === 'chart') ? opts.view : 'table';
343344
app.state.running = true;
344345
app.state.runT0 = t0;
345346
app.state.runQueryId = cryptoObj.randomUUID ? cryptoObj.randomUUID() : 'q' + t0;

src/ui/saved-history.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function renderSaved(app, list) {
6868
nameEl = h('span', { class: 'name' }, q.name);
6969
}
7070

71-
const row = h('div', { class: 'saved-row', onclick: () => { if (!editing) app.actions.loadIntoNewTab(q.name, q.sql, q.id, q.chart); } },
71+
const row = h('div', { class: 'saved-row', onclick: () => { if (!editing) { app.actions.loadIntoNewTab(q.name, q.sql, q.id, q.chart); app.actions.run({ view: q.view }); } } },
7272
h('div', { class: 'top' },
7373
star,
7474
nameEl,
@@ -112,7 +112,7 @@ function renderHistory(app, list) {
112112
return;
113113
}
114114
for (const ent of state.history) {
115-
list.appendChild(h('div', { class: 'history-row', onclick: () => app.actions.loadIntoNewTab('From history', ent.sql) },
115+
list.appendChild(h('div', { class: 'history-row', onclick: () => { app.actions.loadIntoNewTab('From history', ent.sql); app.actions.run(); } },
116116
h('button', {
117117
class: 'sv-act del', title: 'Delete',
118118
onclick: (e) => { e.stopPropagation(); deleteHistory(state, ent.id, app.saveJSON); renderSavedHistory(app); },

src/ui/tabs.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export function newTab(app) {
5151
/**
5252
* Open a tab pre-seeded with `name`/`sql` (used by saved/history). `savedId`
5353
* links it to a saved query so the Save button reads "Saved" (restoring a saved
54-
* query); omit it for history entries, which aren't saved.
54+
* query); omit it for history entries, which aren't saved. `chart` is the saved
55+
* chart config `{ cfg, key }`, cloned onto the tab. (The result view is a global
56+
* setting restored via `run({ view })` by the caller, since `run` resets it.)
5557
*/
5658
export function loadIntoNewTab(app, name, sql, savedId = null, chart = null) {
5759
const id = allocTabId(app.state);

tests/unit/app.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,20 @@ describe('query run', () => {
205205
expect(app.activeTab().result.rows).toEqual([['1']]);
206206
expect(app.state.history.length).toBe(1);
207207
});
208+
it('opens in a restored result view, defaulting to table for an unknown/absent view', async () => {
209+
const routes = [[(u, sql) => /SELECT 1/.test(sql), resp({ body: streamBody(['{"meta":[{"name":"a","type":"UInt8"}]}\n', '{"row":{"a":"1"}}\n']) })]];
210+
const { app } = appForRun(routes);
211+
app.activeTab().sql = 'SELECT 1';
212+
app.state.resultView = 'chart';
213+
await app.actions.run(); // no opts → resets to table
214+
expect(app.state.resultView).toBe('table');
215+
await app.actions.run({ view: 'chart' }); // restore a saved chart view
216+
expect(app.state.resultView).toBe('chart');
217+
await app.actions.run({ view: 'json' });
218+
expect(app.state.resultView).toBe('json');
219+
await app.actions.run({ view: 'bogus' }); // unknown view → table
220+
expect(app.state.resultView).toBe('table');
221+
});
208222
it('no-ops on empty SQL', async () => {
209223
const { app } = appForRun([]);
210224
app.activeTab().sql = ' ';

tests/unit/saved-history.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ describe('renderSavedHistory', () => {
2424
const app = makeApp();
2525
app.state.sidePanel = 'saved';
2626
const chart = { cfg: { type: 'pie', x: 0, y: [1], series: null }, key: 'k' };
27-
app.state.savedQueries = [{ id: 's1', name: 'Q1', sql: 'SELECT 1\n-- more', favorite: false, chart }];
27+
app.state.savedQueries = [{ id: 's1', name: 'Q1', sql: 'SELECT 1\n-- more', favorite: false, chart, view: 'chart' }];
2828
renderSavedHistory(app);
2929
const row = app.dom.savedList.querySelector('.saved-row');
3030
expect(row.querySelector('.preview').textContent).toBe('SELECT 1');
3131
click(row);
32-
expect(app.actions.loadIntoNewTab).toHaveBeenCalledWith('Q1', 'SELECT 1\n-- more', 's1', chart); // links the tab + restores chart
32+
// links the tab + restores the chart, then runs in the saved view so results show immediately
33+
expect(app.actions.loadIntoNewTab).toHaveBeenCalledWith('Q1', 'SELECT 1\n-- more', 's1', chart);
34+
expect(app.actions.run).toHaveBeenCalledWith({ view: 'chart' });
3335
byTitle(row, 'Delete').dispatchEvent(new Event('click', { bubbles: true }));
3436
expect(app.state.savedQueries).toHaveLength(0);
3537
expect(app.updateSaveBtn).toHaveBeenCalled();
@@ -128,6 +130,7 @@ describe('renderSavedHistory', () => {
128130
expect(rows[1].textContent).not.toContain('rows');
129131
click(rows[0]);
130132
expect(app.actions.loadIntoNewTab).toHaveBeenCalledWith('From history', 'SELECT 1');
133+
expect(app.actions.run).toHaveBeenCalled(); // re-runs on restore
131134
});
132135

133136
it('history: per-row delete removes just that entry without loading it', () => {

tests/unit/saved-io.test.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ describe('buildExportDoc', () => {
1414
it('handles an empty list', () => {
1515
expect(buildExportDoc([], 'T').queries).toEqual([]);
1616
});
17-
it('carries an optional chart payload, omitting the field when absent', () => {
17+
it('carries optional chart + view, omitting them when absent or invalid', () => {
1818
const chart = { cfg: { type: 'pie', x: 0, y: [1], series: null }, key: 'k' };
1919
const doc = buildExportDoc([
20-
{ id: 's1', name: 'A', sql: '1', favorite: false, chart },
21-
{ id: 's2', name: 'B', sql: '2', favorite: false },
20+
{ id: 's1', name: 'A', sql: '1', favorite: false, chart, view: 'chart' },
21+
{ id: 's2', name: 'B', sql: '2', favorite: false, view: 'bogus' }, // invalid view dropped
22+
{ id: 's3', name: 'C', sql: '3', favorite: false },
2223
], 'T');
2324
expect(doc.queries[0].chart).toEqual(chart);
24-
expect('chart' in doc.queries[1]).toBe(false);
25+
expect(doc.queries[0].view).toBe('chart');
26+
expect('view' in doc.queries[1]).toBe(false);
27+
expect('chart' in doc.queries[2]).toBe(false);
28+
expect('view' in doc.queries[2]).toBe(false);
2529
});
2630
});
2731

@@ -50,6 +54,14 @@ describe('parseImportDoc', () => {
5054
expect(queries[1].chart).toBeUndefined();
5155
expect(queries[2].chart).toBeUndefined();
5256
});
57+
it('keeps a known view and drops an unknown one', () => {
58+
const { queries } = parseImportDoc(env({ queries: [
59+
{ name: 'A', sql: '1', view: 'json' },
60+
{ name: 'B', sql: '2', view: 'wat' }, // not a known view → dropped
61+
] }));
62+
expect(queries[0].view).toBe('json');
63+
expect(queries[1].view).toBeUndefined();
64+
});
5365
it('throws a user message for each invalid envelope', () => {
5466
expect(() => parseImportDoc('{not json')).toThrow('Not a valid JSON file');
5567
expect(() => parseImportDoc(JSON.stringify({ format: 'other' }))).toThrow('Unrecognized file format');
@@ -87,13 +99,16 @@ describe('mergeSaved', () => {
8799
{ id: 's2', name: 'B', sql: '2', favorite: false, chart },
88100
];
89101
const incoming = [
90-
{ id: 's1', name: 'A2', sql: '1b', favorite: false }, // no chart → drop
91-
{ id: 's2', name: 'B2', sql: '2b', favorite: false, chart: chart2 }, // replace
92-
{ name: 'C', sql: '3', favorite: false, chart }, // add with chart
102+
{ id: 's1', name: 'A2', sql: '1b', favorite: false }, // no chart/view → drop
103+
{ id: 's2', name: 'B2', sql: '2b', favorite: false, chart: chart2, view: 'json' }, // replace
104+
{ name: 'C', sql: '3', favorite: false, chart, view: 'chart' }, // add with chart+view
93105
];
94106
const r = mergeSaved(existing, incoming, () => 'g');
95107
expect(r.merged.find((q) => q.id === 's1').chart).toBeUndefined();
108+
expect(r.merged.find((q) => q.id === 's1').view).toBeUndefined();
96109
expect(r.merged.find((q) => q.id === 's2').chart).toEqual(chart2);
110+
expect(r.merged.find((q) => q.id === 's2').view).toBe('json');
97111
expect(r.merged.find((q) => q.name === 'C').chart).toEqual(chart);
112+
expect(r.merged.find((q) => q.name === 'C').view).toBe('chart');
98113
});
99114
});

tests/unit/state.test.js

Lines changed: 17 additions & 0 deletions

0 commit comments

Comments
 (0)