From 8f5f7b058d1f7d749e92f19c9242a442b67e8ead Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 17:42:06 +0000 Subject: [PATCH 01/13] feat: datatable CSV download + sidebar datasources grouping - SqlDataTable: add CSV download button (visible on hover, exports all filtered/sorted rows with UTF-8 BOM for Excel compatibility) - room.tsx: replace RoomShell.Sidebar with custom sidebar div to suppress auto-positioned panel buttons; add DataPanelSidebarButton that renders the Sources toggle after the flex-1 spacer, grouped with Doc / Settings / ThemeSwitch instead of isolated at the very top https://claude.ai/code/session_016KJg349GAjQ3JkcTHpsjzS --- src/app/components/SqlDataTable.tsx | 49 +++++++++++++++++++++++------ src/app/room.tsx | 39 ++++++++++++++++++++--- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/app/components/SqlDataTable.tsx b/src/app/components/SqlDataTable.tsx index f3cafa7..a8e5c12 100644 --- a/src/app/components/SqlDataTable.tsx +++ b/src/app/components/SqlDataTable.tsx @@ -7,6 +7,25 @@ import { useMemo, useState } from 'react' import DataTablePaginated from '@sqlrooms/data-table/dist/DataTablePaginated' import { rawTableDataStore as _rawTableDataStore } from '../../lib/tableDataStore' import { parseColumnRoles, getTableColumnDisplayNames } from '../../lib/EChartSqlParser' +import { DownloadIcon } from 'lucide-react' + +function downloadCsv(data: any[], filename: string) { + if (!data.length) return + const keys = Object.keys(data[0]) + const esc = (v: any) => { + if (v == null) return '' + const s = String(v) + return /[,"\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s + } + const csv = [keys.map(esc).join(','), ...data.map(row => keys.map(k => esc(row[k])).join(','))].join('\n') + const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = Object.assign(document.createElement('a'), { href: url, download: filename }) + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} // Types Arrow numériques tels que retournés par String(field.type) de duckdb-wasm const NUMERIC_ARROW_RE = /^(Int|Uint|Float|Decimal)/i @@ -111,16 +130,26 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab if (!rawResults?.length) return null return ( -
- {searchable && ( - { setSearch(e.target.value); setPagination(p => ({ ...p, pageIndex: 0 })) }} - /> - )} +
+
+ {searchable && ( + { setSearch(e.target.value); setPagination(p => ({ ...p, pageIndex: 0 })) }} + /> + )} + +
s.layout.togglePanel) + const layoutConfig = useBaseRoomShellStore(s => s.layout.config) + const panels = useBaseRoomShellStore(s => s.layout.panels) + const initialized = useBaseRoomShellStore(s => s.room.initialized) + + const panel = panels?.['data'] + const isSelected = useMemo(() => getMosaicLeaves(layoutConfig?.nodes).includes('data'), [layoutConfig]) + + if (!panel) return null + return ( + togglePanel('data')} + /> + ) +} + function DbEngineIcon({ className }: { className?: string }) { const dbEngine = useNotebookStore(s => s.dbEngine) return {dbEngine === 'ducklings' ? '🐤' : '🦆'} @@ -84,6 +112,9 @@ function SidebarControls() { {/* Ancrés en bas via spacer */}
+ {/* Sources de données — en haut du groupe ancré */} + + {/* Documentation (gist) */} - +
- +
From 5470a302cdaf9c9035f3eed9f898414e4e829fae Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 20:12:42 +0000 Subject: [PATCH 02/13] refactor(SqlDataTable): use QueryDataTableActionsMenu for export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace manual downloadCsv + hover button with QueryDataTableActionsMenu passed as footerActions to DataTablePaginated — the export button now sits right of the row count in the table footer, as provided by the @sqlrooms/data-table library. Also switch import to named export from package root instead of deep dist path. https://claude.ai/code/session_016KJg349GAjQ3JkcTHpsjzS --- src/app/components/SqlDataTable.tsx | 60 +++++++---------------------- 1 file changed, 14 insertions(+), 46 deletions(-) diff --git a/src/app/components/SqlDataTable.tsx b/src/app/components/SqlDataTable.tsx index a8e5c12..b422447 100644 --- a/src/app/components/SqlDataTable.tsx +++ b/src/app/components/SqlDataTable.tsx @@ -1,33 +1,9 @@ -/** - * SqlDataTable — remplace SimpleDatatables pour les cellules sql/table. - * Utilise DataTablePaginated de @sqlrooms/data-table avec tri et pagination - * client-side. Gère les colonnes spéciales PERCENT et TREND. - */ import { useMemo, useState } from 'react' -import DataTablePaginated from '@sqlrooms/data-table/dist/DataTablePaginated' +import { DataTablePaginated, QueryDataTableActionsMenu } from '@sqlrooms/data-table' import { rawTableDataStore as _rawTableDataStore } from '../../lib/tableDataStore' import { parseColumnRoles, getTableColumnDisplayNames } from '../../lib/EChartSqlParser' -import { DownloadIcon } from 'lucide-react' +import { ConfigManager } from '../../lib/ConfigManager' -function downloadCsv(data: any[], filename: string) { - if (!data.length) return - const keys = Object.keys(data[0]) - const esc = (v: any) => { - if (v == null) return '' - const s = String(v) - return /[,"\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s - } - const csv = [keys.map(esc).join(','), ...data.map(row => keys.map(k => esc(row[k])).join(','))].join('\n') - const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = Object.assign(document.createElement('a'), { href: url, download: filename }) - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) -} - -// Types Arrow numériques tels que retournés par String(field.type) de duckdb-wasm const NUMERIC_ARROW_RE = /^(Int|Uint|Float|Decimal)/i function colMeta(schemaTypes: Record, key: string) { @@ -42,6 +18,7 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab const rawResults = _rawTableDataStore.get(cell._id) || cell._results || [] const schemaTypes: Record = cell._schemaTypes || {} + const cellQuery = ConfigManager.getCellQuery(cell, 'main') || '' const { columns, allColKeys } = useMemo(() => { if (!rawResults?.length) return { columns: [], allColKeys: [] } @@ -130,26 +107,16 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab if (!rawResults?.length) return null return ( -
-
- {searchable && ( - { setSearch(e.target.value); setPagination(p => ({ ...p, pageIndex: 0 })) }} - /> - )} - -
+
+ {searchable && ( + { setSearch(e.target.value); setPagination(p => ({ ...p, pageIndex: 0 })) }} + /> + )} : null} />
) From fe1b4ef257006502faf2a213f0bce464558ca93a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 04:37:16 +0000 Subject: [PATCH 03/13] fix(SqlDataTable): replace QueryDataTableActionsMenu with inline CSV export QueryDataTableActionsMenu uses useRoomStore internally and requires RoomStateProvider in the tree. When SqlDataTable is rendered inside ChildGroupModal (portaled outside ), this crashes with "Missing RoomStateProvider in the tree". Replace with a self-contained CSV download button that has no sqlrooms context dependency. https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- src/app/components/SqlDataTable.tsx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/app/components/SqlDataTable.tsx b/src/app/components/SqlDataTable.tsx index b422447..cdb5e7e 100644 --- a/src/app/components/SqlDataTable.tsx +++ b/src/app/components/SqlDataTable.tsx @@ -1,8 +1,7 @@ import { useMemo, useState } from 'react' -import { DataTablePaginated, QueryDataTableActionsMenu } from '@sqlrooms/data-table' +import { DataTablePaginated } from '@sqlrooms/data-table' import { rawTableDataStore as _rawTableDataStore } from '../../lib/tableDataStore' import { parseColumnRoles, getTableColumnDisplayNames } from '../../lib/EChartSqlParser' -import { ConfigManager } from '../../lib/ConfigManager' const NUMERIC_ARROW_RE = /^(Int|Uint|Float|Decimal)/i @@ -11,6 +10,18 @@ function colMeta(schemaTypes: Record, key: string) { return { type: type || '', isNumeric: type ? NUMERIC_ARROW_RE.test(type) : false } } +function downloadCsv(data: any[], filename: string) { + if (!data?.length) return + const keys = Object.keys(data[0]) + const escape = (v: any) => `"${String(v ?? '').replace(/"/g, '""')}"` + const csv = [keys.map(escape).join(','), ...data.map(row => keys.map(k => escape(row[k])).join(','))].join('\n') + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url; a.download = filename; a.click() + URL.revokeObjectURL(url) +} + export function SqlDataTable({ cell, searchable = false }: { cell: any; searchable?: boolean }) { const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) const [sorting, setSorting] = useState([]) @@ -18,7 +29,6 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab const rawResults = _rawTableDataStore.get(cell._id) || cell._results || [] const schemaTypes: Record = cell._schemaTypes || {} - const cellQuery = ConfigManager.getCellQuery(cell, 'main') || '' const { columns, allColKeys } = useMemo(() => { if (!rawResults?.length) return { columns: [], allColKeys: [] } @@ -126,7 +136,14 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab onPaginationChange={setPagination} onSortingChange={setSorting} fontSize="text-xs" - footerActions={cellQuery ? : null} + footerActions={rawResults?.length ? ( + + ) : null} />
) From f21bb412671eff4d13097d7cffbc57e0dda4f2c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 04:39:25 +0000 Subject: [PATCH 04/13] Revert "fix(SqlDataTable): replace QueryDataTableActionsMenu with inline CSV export" This reverts commit fe1b4ef257006502faf2a213f0bce464558ca93a. --- src/app/components/SqlDataTable.tsx | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/app/components/SqlDataTable.tsx b/src/app/components/SqlDataTable.tsx index cdb5e7e..b422447 100644 --- a/src/app/components/SqlDataTable.tsx +++ b/src/app/components/SqlDataTable.tsx @@ -1,7 +1,8 @@ import { useMemo, useState } from 'react' -import { DataTablePaginated } from '@sqlrooms/data-table' +import { DataTablePaginated, QueryDataTableActionsMenu } from '@sqlrooms/data-table' import { rawTableDataStore as _rawTableDataStore } from '../../lib/tableDataStore' import { parseColumnRoles, getTableColumnDisplayNames } from '../../lib/EChartSqlParser' +import { ConfigManager } from '../../lib/ConfigManager' const NUMERIC_ARROW_RE = /^(Int|Uint|Float|Decimal)/i @@ -10,18 +11,6 @@ function colMeta(schemaTypes: Record, key: string) { return { type: type || '', isNumeric: type ? NUMERIC_ARROW_RE.test(type) : false } } -function downloadCsv(data: any[], filename: string) { - if (!data?.length) return - const keys = Object.keys(data[0]) - const escape = (v: any) => `"${String(v ?? '').replace(/"/g, '""')}"` - const csv = [keys.map(escape).join(','), ...data.map(row => keys.map(k => escape(row[k])).join(','))].join('\n') - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url; a.download = filename; a.click() - URL.revokeObjectURL(url) -} - export function SqlDataTable({ cell, searchable = false }: { cell: any; searchable?: boolean }) { const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }) const [sorting, setSorting] = useState([]) @@ -29,6 +18,7 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab const rawResults = _rawTableDataStore.get(cell._id) || cell._results || [] const schemaTypes: Record = cell._schemaTypes || {} + const cellQuery = ConfigManager.getCellQuery(cell, 'main') || '' const { columns, allColKeys } = useMemo(() => { if (!rawResults?.length) return { columns: [], allColKeys: [] } @@ -136,14 +126,7 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab onPaginationChange={setPagination} onSortingChange={setSorting} fontSize="text-xs" - footerActions={rawResults?.length ? ( - - ) : null} + footerActions={cellQuery ? : null} />
) From bb19ce72cdfa9b4b85c1beb9372a4ec05cdb7b7a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 04:39:44 +0000 Subject: [PATCH 05/13] fix(ChildGroupModal): move inside RoomShell to restore RoomStateProvider context ChildGroupModal rendered outside lacked RoomStateProvider, crashing any sqlrooms component (e.g. QueryDataTableActionsMenu) used inside child group cells. Moving it as a child of RoomShell gives it full access to the provider while Dialog still portals to document.body. https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- src/app/room.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/room.tsx b/src/app/room.tsx index 4dacc37..d879dbe 100644 --- a/src/app/room.tsx +++ b/src/app/room.tsx @@ -156,6 +156,7 @@ export function Room() { + {/* Modals globaux — portals vers document.body, indépendants du layout */} @@ -166,7 +167,6 @@ export function Room() { - From ad251f92d320d97f8f5f3e1e9d481addada9b318 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 05:05:03 +0000 Subject: [PATCH 06/13] fix(ci): remove [skip ci] from build-cdn commit to trigger auto-deploy The [skip ci] tag in the dist-cdn commit message was preventing deploy.yml from running automatically after build-cdn.yml. Removing it lets the push to main chain into the deploy workflow as expected. https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- .github/workflows/build-cdn.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-cdn.yml b/.github/workflows/build-cdn.yml index 93165d5..3e92c05 100644 --- a/.github/workflows/build-cdn.yml +++ b/.github/workflows/build-cdn.yml @@ -30,5 +30,5 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add dist-cdn/ - git diff --cached --quiet || git commit -m "chore: rebuild dist-cdn [skip ci]" + git diff --cached --quiet || git commit -m "chore: rebuild dist-cdn" git push origin main From e79fadf0f4ce40dac817f735e64856c370f23154 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 05:07:08 +0000 Subject: [PATCH 07/13] fix(ci): trigger deploy.yml explicitly after build-cdn via gh workflow run GITHUB_TOKEN commits never trigger other workflows (GitHub anti-loop protection), so removing [skip ci] alone was not enough. Instead, explicitly dispatch deploy.yml on main at the end of build-cdn.yml. https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- .github/workflows/build-cdn.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-cdn.yml b/.github/workflows/build-cdn.yml index 3e92c05..e3a9d31 100644 --- a/.github/workflows/build-cdn.yml +++ b/.github/workflows/build-cdn.yml @@ -30,5 +30,10 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add dist-cdn/ - git diff --cached --quiet || git commit -m "chore: rebuild dist-cdn" + git diff --cached --quiet || git commit -m "chore: rebuild dist-cdn [skip ci]" git push origin main + + - name: Trigger deploy + run: gh workflow run deploy.yml --ref main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f32727ed6ccffb03acd15411d16ac7611e090164 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 10:12:17 +0000 Subject: [PATCH 08/13] test: hello world console log https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- src/app/room.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/room.tsx b/src/app/room.tsx index d879dbe..f8291c0 100644 --- a/src/app/room.tsx +++ b/src/app/room.tsx @@ -146,6 +146,7 @@ function SidebarControls() { export function Room() { const showLayout = useNotebookStore(s => s.showLayout) + console.log('hello world') return ( <> From da6c6774058d3d37d7396bbfb12865dc752b6757 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 13:32:07 +0000 Subject: [PATCH 09/13] fix(ci): add actions: write permission to allow gh workflow run dispatch https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- .github/workflows/build-cdn.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-cdn.yml b/.github/workflows/build-cdn.yml index e3a9d31..c2fe202 100644 --- a/.github/workflows/build-cdn.yml +++ b/.github/workflows/build-cdn.yml @@ -8,6 +8,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + actions: write steps: - uses: actions/checkout@v4 From b601524ddc8854f39e25c68a0ad781ec2861fb99 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 13:50:12 +0000 Subject: [PATCH 10/13] fix(ci): add NODE_OPTIONS memory limit to auto-merge deploy-dev build build:cdn without --max-old-space-size=6144 causes OOM crash on GitHub Actions runners, silently skipping the deploy. Align with build-cdn.yml and deploy.yml which already set this flag. https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- .github/workflows/auto-merge-claude.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/auto-merge-claude.yml b/.github/workflows/auto-merge-claude.yml index 1d8ae05..0f3ceea 100644 --- a/.github/workflows/auto-merge-claude.yml +++ b/.github/workflows/auto-merge-claude.yml @@ -62,6 +62,8 @@ jobs: - run: npm ci - run: npm run build:cdn + env: + NODE_OPTIONS: --max-old-space-size=6144 - name: Prepare site files run: | From c1c68a9f59523611c78ee21282fd93e57f803124 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 13:56:11 +0000 Subject: [PATCH 11/13] test: console.log v2 to verify auto-deploy chain https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- src/app/room.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/room.tsx b/src/app/room.tsx index f8291c0..5785ede 100644 --- a/src/app/room.tsx +++ b/src/app/room.tsx @@ -146,7 +146,7 @@ function SidebarControls() { export function Room() { const showLayout = useNotebookStore(s => s.showLayout) - console.log('hello world') + console.log('hello world v2') return ( <> From e2d7a85db7b3fada650e0c8263ccba98786c0b8b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 14:11:18 +0000 Subject: [PATCH 12/13] fix(SqlDataTable): strip trailing semicolons from query passed to export DuckDB rejects queries ending with ';' in exportToCsv. Strip them inline before passing to QueryDataTableActionsMenu. Also removes test console.log. https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- src/app/components/SqlDataTable.tsx | 2 +- src/app/room.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/SqlDataTable.tsx b/src/app/components/SqlDataTable.tsx index b422447..4d334bb 100644 --- a/src/app/components/SqlDataTable.tsx +++ b/src/app/components/SqlDataTable.tsx @@ -126,7 +126,7 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab onPaginationChange={setPagination} onSortingChange={setSorting} fontSize="text-xs" - footerActions={cellQuery ? : null} + footerActions={cellQuery ? : null} />
) diff --git a/src/app/room.tsx b/src/app/room.tsx index 5785ede..89e5386 100644 --- a/src/app/room.tsx +++ b/src/app/room.tsx @@ -146,7 +146,7 @@ function SidebarControls() { export function Room() { const showLayout = useNotebookStore(s => s.showLayout) - console.log('hello world v2') + return ( <> From 4c7c2d8cbca07b6f8a8ec99a127ce6ed34ea69da Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 14:35:30 +0000 Subject: [PATCH 13/13] fix(SqlDataTable): use sanitizeQuery from @sqlrooms/duckdb for export query https://claude.ai/code/session_01PqdRqnUJAB2ZRuYQd3qxmS --- src/app/components/SqlDataTable.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/SqlDataTable.tsx b/src/app/components/SqlDataTable.tsx index 4d334bb..c364641 100644 --- a/src/app/components/SqlDataTable.tsx +++ b/src/app/components/SqlDataTable.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react' import { DataTablePaginated, QueryDataTableActionsMenu } from '@sqlrooms/data-table' +import { sanitizeQuery } from '@sqlrooms/duckdb' import { rawTableDataStore as _rawTableDataStore } from '../../lib/tableDataStore' import { parseColumnRoles, getTableColumnDisplayNames } from '../../lib/EChartSqlParser' import { ConfigManager } from '../../lib/ConfigManager' @@ -18,7 +19,7 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab const rawResults = _rawTableDataStore.get(cell._id) || cell._results || [] const schemaTypes: Record = cell._schemaTypes || {} - const cellQuery = ConfigManager.getCellQuery(cell, 'main') || '' + const cellQuery = sanitizeQuery(ConfigManager.getCellQuery(cell, 'main') || '') const { columns, allColKeys } = useMemo(() => { if (!rawResults?.length) return { columns: [], allColKeys: [] } @@ -126,7 +127,7 @@ export function SqlDataTable({ cell, searchable = false }: { cell: any; searchab onPaginationChange={setPagination} onSortingChange={setSorting} fontSize="text-xs" - footerActions={cellQuery ? : null} + footerActions={cellQuery ? : null} />
)