fix: prevent grid flicker#498
Conversation
|
@yukina3230 is attempting to deploy a commit to the zknpr's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughIn ChangesloadTableData flicker reduction, grid state tracking, and columnNames dedup
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request improves the table loading experience in core/ui/modules/grid-data.js by caching whether a data grid is currently rendered. This is used to prevent flickering by keeping the existing grid visible during refetches (only showing the spinner on initial load), and to avoid duplicate column name mapping. The review feedback points out two important issues: first, when switching between different tables, the old grid remains in the DOM, causing hasRenderedGrid to incorrectly evaluate to true and suppress the loading spinner (which can be fixed by tracking the active table with a data-table attribute); second, because the grid remains visible during a fetch, users can still scroll, meaning the !showSpinner check should be removed when capturing the final scroll position to avoid losing those scroll updates.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Whether a data grid is currently rendered (vs. a spinner/error/empty state). | ||
| // Cached once instead of re-querying the DOM at each decision point below. | ||
| const hasRenderedGrid = !!(container && container.querySelector('.data-grid')); |
There was a problem hiding this comment.
When switching between different tables, the previous table's .data-grid is still present in the DOM when loadTableData starts. As a result, hasRenderedGrid evaluates to true, which prevents showLoading() from being called. This causes the old table's data to remain visible during the fetch of the new table without any loading indicator, which is highly confusing.
To fix this, we should ensure hasRenderedGrid is only true if we are reloading/filtering the same table. We can track the active table on the container using a data-table attribute.
Note: You will also need to set the data-table attribute on the container when rendering the grid. In core/ui/modules/grid-render.js inside renderDataGrid, add:
container.dataset.table = state.selectedTable;| // Whether a data grid is currently rendered (vs. a spinner/error/empty state). | |
| // Cached once instead of re-querying the DOM at each decision point below. | |
| const hasRenderedGrid = !!(container && container.querySelector('.data-grid')); | |
| // Whether a data grid is currently rendered for the active table (vs. a spinner/error/empty state or a different table). | |
| // Cached once instead of re-querying the DOM at each decision point below. | |
| const hasRenderedGrid = !!(container && container.dataset.table === state.selectedTable && container.querySelector('.data-grid')); |
| // updated the view (and restored scroll) while the fetch was pending. | ||
| // Re-check the DOM here (not the cached flag): this runs after the await, | ||
| // so the rendered state may differ from when the function started. | ||
| if (!showSpinner && container && container.querySelector('.data-grid')) { |
There was a problem hiding this comment.
Since we now keep the existing grid visible even when showSpinner is true (to prevent flicker), the user can still scroll the grid during the fetch. If we only capture the scroll position when !showSpinner is true, any scrolling done during a flicker-free fetch with showSpinner = true will be lost and overwritten by the stale scroll position captured at the start of the function.
We can safely remove the !showSpinner check here. If the grid is currently rendered in the DOM, we should always capture its latest scroll position right before rendering.
| if (!showSpinner && container && container.querySelector('.data-grid')) { | |
| if (container && container.querySelector('.data-grid')) { |
Resolve the two issues Gemini flagged on the flicker fix: - Switching tables left the previous table's grid on screen with no spinner, because hasRenderedGrid was true for the stale grid. Track the rendered table (state.renderedTable) and only suppress the spinner for a same-table refetch; a table switch now shows the spinner. - Scroll done during a flicker-free (showSpinner=true) refetch was lost. Gate the post-await scroll re-capture on saveScrollPosition instead of !showSpinner so live scrolling is preserved, without clobbering the intentional scroll reset on page change / table switch. Regenerated viewer.html bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87f78367f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!isSameTableGrid) { | ||
| showLoading(); | ||
| } |
There was a problem hiding this comment.
Disable stale grid actions while refetching
When a same-table refetch is in progress, this skips showLoading() and leaves the previous grid visible while state.isLoadingData is true, but row/header controls such as onRowNumberClick and onSelectAllClick do not check that flag and the delete path will act on state.selectedRowIds. If the user changes a filter/page/sort and clicks the still-visible old row numbers before the fetch resolves, they can select and delete rows from the stale result set; previously the spinner removed those controls immediately. Please either block pointer/selection actions during the refetch or keep showing a non-interactive loading state.
Useful? React with 👍 / 👎.
The flicker fix keeps the previous grid visible during a same-table refetch (isLoadingData=true, spinner suppressed). Grid click/dblclick/ keydown handlers didn't check that flag, so a user could select and delete rows from the stale result set before the new data arrived, or queue a concurrent reload. Guard handleClick, handleDoubleClick and handleKeydown on isLoadingData, restoring the pre-flicker-fix "non-interactive while loading" behavior without re-introducing the flicker. Typing in filter inputs still works; scroll/hover/resize remain live. Regenerated viewer.html bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0ae9d1cf7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| // Column names are needed both for the global-filter count and the data query. | ||
| const columnNames = state.tableColumns.map(c => c.name); |
There was a problem hiding this comment.
Snapshot columns with the table being loaded
When a load is in flight and the user switches tables before fetchTableCount resolves, this cached columnNames still describes the old table, but the later fetchTableData(state.selectedTable, queryOptions) uses the now-current table name. For tables with different schemas, that sends a SELECT for old columns against the newly selected table and can replace the new table view with a fetch error; capture the table name/columns for the whole request or bail out if the selection changed after the await.
Useful? React with 👍 / 👎.
| // flicker fix keeps the previous grid visible during a same-table refetch, so | ||
| // without this guard a click on the stale row numbers or cells could select | ||
| // (and then delete) rows from the old result set before the new data arrives. | ||
| if (state.isLoadingData) return; |
There was a problem hiding this comment.
Keep loading state true until all refreshes finish
This stale-grid guard depends on a single state.isLoadingData boolean, but each overlapping loadTableData() call clears that boolean in its own finally. If a slow same-table refresh is followed by another filter/sort/page refresh, the first completion makes the old grid clickable again while the newer request is still pending, so users can select or delete rows from results that are about to be replaced; use a request token or in-flight counter for the grid-visible refresh path.
Useful? React with 👍 / 👎.
Concurrent loadTableData() calls (a slow fetch followed by a filter/ sort/page change or a table switch via toolbar controls the grid guard doesn't cover) could mix state across requests: - A table switch mid-fetch paired the old table's snapshotted columns with the new table name in fetchTableData → SELECT old columns against the new table → error view (P2 zknpr#1). - The single isLoadingData boolean was cleared by whichever overlapping load finished first, re-enabling clicks on soon-to-be-replaced rows while a newer load was still pending (P2 zknpr#2). Add a monotonic activeLoadToken: snapshot the requested table/type for the whole request, bail after each await if a newer load superseded this one (no state write, render, or stale error), and only let the latest load clear isLoadingData. Regenerated viewer.html bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07039bf192
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (showSpinner && loadToken === activeLoadToken) { | ||
| state.isLoadingData = false; |
There was a problem hiding this comment.
Clear loading state for superseding background refreshes
When a spinner-backed load is in progress and a background refresh starts (for example refreshContent calls loadTableData(false)), the first load returns with a stale token and won't clear state.isLoadingData; the newer background load also skips this branch because showSpinner is false. Since this commit added isLoadingData guards to grid clicks/double-clicks, the grid remains permanently non-interactive after the refresh completes. The latest token should clear the flag even if it was a no-spinner refresh, or the flag ownership needs to be tracked separately.
Useful? React with 👍 / 👎.
| if (!isSameTableGrid) { | ||
| showLoading(); |
There was a problem hiding this comment.
Preserve the page-change scroll reset
When goToPage() calls loadTableData(true, false), a same-table page load reaches this branch and leaves the old page's grid mounted instead of replacing it with the loading view. Because the grid remains scrollable and handleScroll still writes to state.scrollPosition, any scroll on the old page while the fetch is pending overwrites the {top: 0, left: 0} reset, so the newly loaded page can open at a stale scroll offset rather than the top. Please keep the reset protected when saveScrollPosition is false, for example by showing the spinner or ignoring scroll updates during that load.
Useful? React with 👍 / 👎.
| renderDataGrid(state.scrollPosition.top, state.scrollPosition.left); | ||
| // The on-screen grid now reflects this table; remember it so the next | ||
| // load can distinguish a same-table refetch from a table switch. | ||
| state.renderedTable = requestedTable; |
There was a problem hiding this comment.
Recheck the selected table before rendering
If the user switches tables while the previous table's data fetch is still pending, the new selection first awaits loadTableColumns() and does not start its own loadTableData() token until after that finishes. During that window the old request still has the active token, so this render can put the old table's rows back on screen under the new sidebar selection and clear isLoadingData, making stale rows interactive against the newly selected table. Please also verify state.selectedTable/type still match requestedTable before rendering, or invalidate the token as soon as a table switch begins.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07039bf192
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (showSpinner && loadToken === activeLoadToken) { | ||
| state.isLoadingData = false; |
There was a problem hiding this comment.
Clear loading after background supersedes spinner
Because this finally only clears state.isLoadingData when the currently active request also had showSpinner=true, a loadTableData(false) refresh can leave the UI stuck in loading mode if it starts while a spinner-backed load is still awaiting. That background refresh increments activeLoadToken, so the older spinner load skips this block, and then the background load skips it because showSpinner is false; after common async refresh paths such as refreshContent call loadTableData(false), grid clicks/double-clicks keep returning on the stale state.isLoadingData guard indefinitely.
Useful? React with 👍 / 👎.
| const totalRecordCount = await backendApi.fetchTableCount(requestedTable, countOptions); | ||
| if (loadToken !== activeLoadToken) return; // a newer load superseded this one |
There was a problem hiding this comment.
Bail out after table selection changes
When the user switches tables while this request is awaiting but before the new table's loadTableData() call begins (the sidebar path awaits loadTableColumns() first), activeLoadToken is still owned by the old request, so this check passes and the request goes on to fetch/render requestedTable even though state.selectedTable already points at the newly selected table. Fresh evidence versus the earlier table-switch concern is that the data fetch now uses the snapshotted requestedTable, but this token check still does not invalidate the request when the selection changed.
Useful? React with 👍 / 👎.
Address the follow-up issues Codex raised on the request-token fix:
- isLoadingData could get stuck true: a no-spinner background refresh
(loadTableData(false)) superseding a spinner-backed load meant neither
load cleared the flag (the old one was superseded, the new one skipped
the showSpinner-gated clear), leaving the grid permanently
non-interactive via the new guards. The current load now clears the
flag regardless of showSpinner.
- Table-switch window: the sidebar sets state.selectedTable then awaits
loadTableColumns() before starting its own load, so the old request
still owned the token and could render stale rows under the new
selection. Supersession now also checks requestedTable === selectedTable.
- Page-change scroll reset (P3): handleScroll wrote scrollPosition while a
load was pending, clobbering goToPage's {0,0}. handleScroll now ignores
scroll while isLoadingData; same-table refetches still preserve scroll
via the post-await re-capture.
Unify the checks behind an isSuperseded() predicate. Regenerated bundles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae28d25109
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!isSuperseded()) { | ||
| state.isLoadingData = false; |
There was a problem hiding this comment.
Keep upload loading state out of refresh cleanup
When a no-spinner refresh runs while a BLOB drag/drop upload is still awaiting, this finally can clear a loading flag it did not set: core/ui/modules/dnd.js:176 sets the same state.isLoadingData for uploads, and refreshContent can call loadTableData(false) via core/ui/modules/rpc.js:41. In that overlap, the refresh owns the active load token and reaches this branch, setting the flag false before the upload's finally, which re-enables the grid click/double-click handlers added in this commit and allows edits/selections during an in-flight upload. Please track data-load ownership separately or only clear the flag when this load established it.
Useful? React with 👍 / 👎.
| // flicker fix keeps the previous grid visible during a same-table refetch, so | ||
| // without this guard a click on the stale row numbers or cells could select | ||
| // (and then delete) rows from the old result set before the new data arrives. | ||
| if (state.isLoadingData) return; |
There was a problem hiding this comment.
Block document shortcuts during refetches
Fresh evidence versus the earlier stale-grid action concern: this new loading guard only runs for events delegated through #gridContainer, but the document-level shortcuts in setupGlobalShortcuts (core/ui/viewer.js:187-210) still call onSelectAllClick()/submitDelete() without checking state.isLoadingData. When a pagination/sort/filter refetch leaves the old grid visible and focus is on a toolbar button or the body rather than an input, Ctrl+A followed by Ctrl+Delete selects/deletes row IDs from the stale state.gridData while the new request is pending; the same loading guard needs to cover those shortcut paths.
Useful? React with 👍 / 👎.
The interactivity guard keyed on state.isLoadingData, which is also set by BLOB uploads (dnd.js) and only covered #gridContainer-delegated events. Two consequences: - A no-spinner refresh's finally cleared isLoadingData mid-upload, re-enabling grid handlers during an in-flight upload. - Document-level shortcuts (viewer.js setupGlobalShortcuts) call onSelectAllClick()/submitDelete() directly, bypassing the grid guards, so Ctrl+A then Ctrl+Delete could select/delete stale rows mid-refetch. Introduce a dedicated state.isGridReloading owned solely by loadTableData (set on a spinner-backed load, released by the latest non-superseded load). Revert isLoadingData to its original showSpinner-gated lifecycle so loadTableData never clears the flag an upload set. Point the grid handlers and the global select-all/delete shortcuts at isGridReloading. Regenerated bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 121182d041
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `}n.length>o&&(l+=` | ||
| ... (${(n.length-o).toLocaleString()} more bytes not shown)`),this.hexContainer.value=l}formatSize(n){if(n===0)return"0 B";let o=1024,l=["B","KB","MB","GB"],a=Math.floor(Math.log(n)/Math.log(o));return parseFloat((n/Math.pow(o,a)).toFixed(2))+" "+l[a]}};var be;function ze(){be=new le,document.getElementById("btnCloseCellPreview")?.addEventListener("click",H),document.getElementById("formatJsonBtn")?.addEventListener("click",Vt),document.getElementById("compactJsonBtn")?.addEventListener("click",Wt),document.getElementById("wrapTextBtn")?.addEventListener("click",_t),document.getElementById("openInVsCodeBtn")?.addEventListener("click",we),document.getElementById("btnCancelCellPreview")?.addEventListener("click",H),document.getElementById("cellPreviewSaveBtn")?.addEventListener("click",Ke)}function je(e,n,o){if(t.selectedTableType!=="table"){u("Views are read-only");return}if(t.editingCellInfo){if(t.editingCellInfo.rowIdx===e&&t.editingCellInfo.colIdx===n)return;se()}let l=t.tableColumns[n];if(!l)return;let a=document.getElementById(`cell-${e}-${n}`);if(!a)return;let s=t.gridData[e],i=P(s,n);if(i instanceof Uint8Array){K(e,n,o);return}if(typeof i=="string"){let d=i.trim();if(d.startsWith("{")&&d.endsWith("}")||d.startsWith("[")&&d.endsWith("]"))try{JSON.parse(d),K(e,n,o);return}catch{}}t.editingCellInfo={rowIdx:e,colIdx:n,rowId:o,columnName:l.name,originalValue:i};let r=i===null?"":String(i);a.innerHTML="",a.classList.add("editing");let c=document.createElement("textarea");c.className="cell-input",c.value=r,c.spellcheck=!1,a.appendChild(c),c.focus(),t.activeCellInput=c,c.addEventListener("keydown",Ve),c.addEventListener("blur",We),c.addEventListener("click",d=>d.stopPropagation()),t.isTransitioningEdit=!0,setTimeout(()=>{t.isTransitioningEdit=!1},100)}function Ve(e){e.key==="Enter"&&!e.shiftKey?(e.preventDefault(),_e()):e.key==="Escape"&&(e.preventDefault(),se())}function We(){setTimeout(()=>{t.editingCellInfo&&_e()},100)}async function _e(){if(t.isSavingCell||!t.editingCellInfo||!t.activeCellInput)return;let{rowIdx:e,colIdx:n,rowId:o,columnName:l,originalValue:a}=t.editingCellInfo,s=t.activeCellInput.value,i=a===null?"":String(a);if(s===i){se(),t.selectedCells=[],t.lastSelectedCell=null,M();return}let r=t.tableColumns[n],c=r&&r.notnull===1,d;s===""?c?d="":d=null:!isNaN(Number(s))&&s.trim()!==""?d=Number(s):d=s;try{t.isSavingCell=!0,u("Saving..."),await x.updateCell(t.selectedTable,te(o),l,d,a),t.gridData[e][n+B()]=d,qe(),Ee(e,n,d),t.selectedCells=[],t.lastSelectedCell=null,M(),u("Saved")}catch(m){console.error("Save failed:",m);let y=m.message||String(m);u(`Save failed: ${y}`)}finally{t.isSavingCell=!1}}function se(){if(!t.editingCellInfo)return;let{rowIdx:e,colIdx:n,originalValue:o}=t.editingCellInfo;qe(),Ee(e,n,o),oe()}function qe(){t.activeCellInput&&(t.activeCellInput.removeEventListener("keydown",Ve),t.activeCellInput.removeEventListener("blur",We),t.activeCellInput=null),t.editingCellInfo=null}async function we(){if(!t.cellPreviewInfo)return;let{rowIdx:e,colIdx:n,rowId:o,columnName:l,originalValue:a}=t.cellPreviewInfo,s=t.tableColumns[n],i=document.getElementById("vscode-env")?.dataset.webviewId||"default";try{u("Opening in VS Code..."),H(),await x.openCellEditor({table:t.selectedTable,name:""},te(o),l,{},{value:a,type:{type:s.type},webviewId:i,rowCount:t.gridData.length}),u("Opened in VS Code")}catch(r){console.error("Failed to open in VS Code:",r),u(`Error: ${r.message}`)}}function K(e,n,o){t.editingCellInfo&&se();let l=t.tableColumns[n];if(!l)return;let a=t.gridData[e];if(!a)return;let s=P(a,n);if(s instanceof Uint8Array){be&&be.inspect(s,o,l.name,e,n);return}t.cellPreviewInfo={rowIdx:e,colIdx:n,rowId:o,columnName:l.name,originalValue:s};let i=document.getElementById("cellPreviewModal"),r=document.getElementById("cellPreviewColumnName"),c=document.getElementById("cellPreviewTypeBadge"),d=document.getElementById("cellPreviewTextarea"),m=document.getElementById("cellPreviewReadonlyBadge"),y=document.getElementById("cellPreviewSaveBtn"),f=document.getElementById("wrapTextBtn");r.textContent=l.name,c.textContent=l.type||"TEXT";let p="";s==null?p="":s instanceof Uint8Array?p="[BLOB: "+Array.from(s).map(b=>b.toString(16).padStart(2,"0")).join(" ")+"]":p=String(s),d.value=p;let g=t.selectedTableType!=="table";d.readOnly=g,g?(d.classList.add("readonly"),m.style.display="inline",y.style.display="none"):(d.classList.remove("readonly"),m.style.display="none",y.style.display="inline-block"),ae(),d.style.whiteSpace=t.cellPreviewWrapEnabled?"pre-wrap":"pre",d.style.overflowX=t.cellPreviewWrapEnabled?"hidden":"auto",f.classList.toggle("active",t.cellPreviewWrapEnabled),i.classList.remove("hidden"),d.focus(),d.oninput=ae,d.onkeydown=b=>{b.key==="Escape"?(b.preventDefault(),H()):b.key==="Enter"&&(b.ctrlKey||b.metaKey)&&(b.preventDefault(),Ke())}}function ae(){let e=document.getElementById("cellPreviewTextarea"),n=document.getElementById("cellPreviewCharCount"),o=e.value.length;n.textContent=`${o.toLocaleString()} character${o!==1?"s":""}`}function H(){document.getElementById("cellPreviewModal").classList.add("hidden"),t.cellPreviewInfo=null}async function Ke(){if(!t.cellPreviewInfo)return;if(t.selectedTableType!=="table"){u("Views are read-only");return}let{rowIdx:e,colIdx:n,rowId:o,columnName:l,originalValue:a}=t.cellPreviewInfo,i=document.getElementById("cellPreviewTextarea").value,r=a===null?"":String(a);if(i===r){H(),t.selectedCells=[],t.lastSelectedCell=null,M();return}let c=t.tableColumns[n],d=c&&c.notnull===1,m;i===""?m=d?"":null:!isNaN(Number(i))&&i.trim()!==""?m=Number(i):m=i;try{u("Saving..."),await x.updateCell(t.selectedTable,te(o),l,m,a),t.gridData[e][n+B()]=m,H(),Ee(e,n,m),t.selectedCells=[],t.lastSelectedCell=null,M(),u("Saved")}catch(y){console.error("Save failed:",y),u(`Save failed: ${y.message}`)}}function Vt(){let e=document.getElementById("cellPreviewTextarea");try{let n=JSON.parse(e.value);e.value=JSON.stringify(n,null,2),ae()}catch{u("Content is not valid JSON")}}function Wt(){let e=document.getElementById("cellPreviewTextarea");try{let n=JSON.parse(e.value);e.value=JSON.stringify(n),ae()}catch{u("Content is not valid JSON")}}function _t(){t.cellPreviewWrapEnabled=!t.cellPreviewWrapEnabled;let e=document.getElementById("cellPreviewTextarea"),n=document.getElementById("wrapTextBtn");e.style.whiteSpace=t.cellPreviewWrapEnabled?"pre-wrap":"pre",e.style.overflowX=t.cellPreviewWrapEnabled?"hidden":"auto",n.classList.toggle("active",t.cellPreviewWrapEnabled)}function Ee(e,n,o){let l=document.getElementById(`cell-${e}-${n}`);if(!l)return;l.classList.remove("editing"),o==null?l.classList.add("null-value"):l.classList.remove("null-value");let a=t.tableColumns[n],s=_(o,a?.type,t.dateFormat,a?.name),i=o!=null&&!(o instanceof Uint8Array);l.textContent="";let r=document.createElement("span");if(r.className="cell-text",r.textContent=s,l.appendChild(r),i){let d=document.createElement("span");d.className="expand-icon codicon codicon-link-external",d.title="View full content",l.appendChild(d)}let c=r.scrollWidth>r.clientWidth;l.classList.toggle("has-overflow",c)}function He(){clearTimeout(t.filterTimer),t.filterTimer=setTimeout(()=>{t.filterQuery=document.getElementById("filterInput").value,t.currentPageIndex=0,v(),$()},300)}function Ge(){t.rowsPerPage=parseInt(document.getElementById("pageSizeSelect").value,10),t.currentPageIndex=0,v(),$()}function Je(){let e=document.getElementById("dateFormatSelect");e&&(t.dateFormat=e.value,z(),$())}function Z(e){e>=0&&e<t.totalPageCount&&(t.currentPageIndex=e,t.scrollPosition={top:0,left:0},v(!0,!1))}function Xe(e){t.sortedColumn===e?t.sortAscending=!t.sortAscending:(t.sortedColumn=e,t.sortAscending=!0),v(),$()}function Ie(e){let n=document.querySelector(`.column-filter[data-column="${e}"]`);n&&(t.columnFilters[e]=n.value,t.currentPageIndex=0,v())}function Ye(e,n){e.key==="Enter"&&Ie(n)}function Ze(e,n){e.stopPropagation();let o=t.tableColumns.findIndex(l=>l.name===n);if(o!==-1){if((e.shiftKey||e.metaKey||e.ctrlKey)&&e.preventDefault(),t.selectedRowIds.clear(),e.shiftKey&&t.lastSelectedColumnIndex!==null){e.metaKey||e.ctrlKey||(t.selectedCells=[],t.selectedColumns.clear());let l=Math.min(t.lastSelectedColumnIndex,o),a=Math.max(t.lastSelectedColumnIndex,o),s=new Array;if(t.selectedCells.length>0)for(let i of t.selectedCells)i.colIdx>=l&&i.colIdx<=a&&(s[i.rowIdx]||(s[i.rowIdx]=new Set),s[i.rowIdx].add(i.colIdx));for(let i=l;i<=a;i++){let r=t.tableColumns[i].name;t.selectedColumns.add(r);for(let c=0;c<t.gridData.length;c++){let d=s[c];if(!d||!d.has(i)){let m=T(t.gridData[c],c),y=P(t.gridData[c],i);t.selectedCells.push({rowIdx:c,colIdx:i,rowId:m,value:y})}}}}else if(e.metaKey||e.ctrlKey){let l=t.gridData.length,a=0;for(let i of t.selectedCells)i.colIdx===o&&a++;if(l>0&&a===l)t.selectedCells=t.selectedCells.filter(i=>i.colIdx!==o),t.selectedColumns.delete(n);else{let i=new Set;for(let r of t.selectedCells)r.colIdx===o&&i.add(r.rowIdx);for(let r=0;r<t.gridData.length;r++)if(!i.has(r)){let c=T(t.gridData[r],r),d=P(t.gridData[r],o);t.selectedCells.push({rowIdx:r,colIdx:o,rowId:c,value:d})}t.selectedColumns.add(n)}t.lastSelectedColumnIndex=o}else{let l=t.gridData.length,a=0;for(let i of t.selectedCells)i.colIdx===o&&a++;if(l>0&&a===l&&t.selectedColumns.size===1&&t.selectedColumns.has(n))t.selectedCells=[],t.selectedColumns.clear(),t.lastSelectedColumnIndex=null;else{t.selectedCells=[],t.selectedColumns.clear();for(let i=0;i<t.gridData.length;i++){let r=T(t.gridData[i],i),c=P(t.gridData[i],o);t.selectedCells.push({rowIdx:i,colIdx:o,rowId:r,value:c})}t.selectedColumns.add(n),t.lastSelectedColumnIndex=o}}t.lastSelectedCell=null,M(),L(),O()}}function Qe(e,n){e.stopPropagation(),t.pinnedColumns.has(n)?t.pinnedColumns.delete(n):t.pinnedColumns.add(n),z(),$()}function et(e,n){e.stopPropagation(),t.pinnedRowIds.has(n)?t.pinnedRowIds.delete(n):t.pinnedRowIds.add(n),z(),$()}function tt(e,n){e.stopPropagation(),t.resizingColumn=n,t.resizeStartX=e.clientX,t.resizeStartWidth=t.columnWidths[n]||120,e.target.classList.add("resizing"),document.body.style.userSelect="none",document.body.style.cursor="col-resize",document.addEventListener("mousemove",nt),document.addEventListener("mouseup",ot)}function nt(e){if(!t.resizingColumn)return;let n=e.clientX-t.resizeStartX,o=Math.max(30,t.resizeStartWidth+n);t.columnWidths[t.resizingColumn]=o;let l=t.tableColumns.findIndex(i=>i.name===t.resizingColumn);if(l===-1)return;let a=document.querySelector(`th[data-column="${t.resizingColumn}"]`);a&&(a.style.width=`${o}px`,a.style.minWidth=`${o}px`,a.style.maxWidth=`${o}px`);let s=document.querySelectorAll(`.data-row td:nth-child(${l+2})`);for(let i of s)i.style.width=`${o}px`,i.style.minWidth=`${o}px`,i.style.maxWidth=`${o}px`;t.pinnedColumns.has(t.resizingColumn)}function ot(){if(!t.resizingColumn)return;let e=document.querySelector(".resize-handle.resizing");e&&e.classList.remove("resizing"),t.resizingColumn=null,document.removeEventListener("mousemove",nt),document.removeEventListener("mouseup",ot),document.body.style.userSelect="",document.body.style.cursor="",z()}function lt(e,n,o){if(e.stopPropagation(),(e.shiftKey||e.metaKey||e.ctrlKey)&&e.preventDefault(),t.selectedCells=[],t.lastSelectedCell=null,t.selectedColumns.clear(),e.shiftKey&&t.lastSelectedRowIndex!==null){e.metaKey||e.ctrlKey||t.selectedRowIds.clear();let l=Math.min(t.lastSelectedRowIndex,o),a=Math.max(t.lastSelectedRowIndex,o);for(let s=l;s<=a;s++){let i=T(t.gridData[s],s);t.selectedRowIds.add(i)}}else e.ctrlKey||e.metaKey?(t.selectedRowIds.has(n)?t.selectedRowIds.delete(n):t.selectedRowIds.add(n),t.lastSelectedRowIndex=o):t.selectedRowIds.has(n)&&t.selectedRowIds.size===1?(t.selectedRowIds.delete(n),t.lastSelectedRowIndex=null):(t.selectedRowIds.clear(),t.selectedRowIds.add(n),t.lastSelectedRowIndex=o);M(),L(),O()}function ie(e){if(e.stopPropagation(),t.gridData.length===0)return;t.selectedCells=[],t.lastSelectedCell=null,t.selectedColumns.clear();let n=!0;for(let o=0;o<t.gridData.length;o++){let l=T(t.gridData[o],o);if(!t.selectedRowIds.has(l)){n=!1;break}}if(n)t.selectedRowIds.clear();else for(let o=0;o<t.gridData.length;o++){let l=T(t.gridData[o],o);t.selectedRowIds.add(l)}M(),L(),O()}function at(e,n,o,l){if(e.stopPropagation(),t.isLoadingData||t.isSavingCell||t.isTransitioningEdit||t.editingCellInfo)return;let a=t.gridData[n]?P(t.gridData[n],o):null;if((e.metaKey||e.ctrlKey)&&e.shiftKey&&t.lastSelectedCell){e.preventDefault(),t.selectedRowIds.clear();let s=Math.min(t.lastSelectedCell.rowIdx,n),i=Math.max(t.lastSelectedCell.rowIdx,n),r=Math.min(t.lastSelectedCell.colIdx,o),c=Math.max(t.lastSelectedCell.colIdx,o),d=new Array;for(let m of t.selectedCells)m.rowIdx>=s&&m.rowIdx<=i&&m.colIdx>=r&&m.colIdx<=c&&(d[m.rowIdx]||(d[m.rowIdx]=new Set),d[m.rowIdx].add(m.colIdx));for(let m=s;m<=i;m++){let y=d[m];for(let f=r;f<=c;f++)if(!y||!y.has(f)){let p=T(t.gridData[m],m),g=P(t.gridData[m],f);t.selectedCells.push({rowIdx:m,colIdx:f,rowId:p,value:g})}}}else if(e.metaKey||e.ctrlKey){e.preventDefault(),t.selectedRowIds.clear();let s=t.selectedCells.findIndex(i=>i.rowIdx===n&&i.colIdx===o);s>=0?t.selectedCells.splice(s,1):(t.selectedCells.push({rowIdx:n,colIdx:o,rowId:l,value:a}),t.lastSelectedCell={rowIdx:n,colIdx:o})}else if(e.shiftKey&&t.lastSelectedCell){e.preventDefault(),t.selectedRowIds.clear(),t.selectedCells=[];let s=Math.min(t.lastSelectedCell.rowIdx,n),i=Math.max(t.lastSelectedCell.rowIdx,n),r=Math.min(t.lastSelectedCell.colIdx,o),c=Math.max(t.lastSelectedCell.colIdx,o);for(let d=s;d<=i;d++)for(let m=r;m<=c;m++){let y=T(t.gridData[d],d),f=P(t.gridData[d],m);t.selectedCells.push({rowIdx:d,colIdx:m,rowId:y,value:f})}}else t.selectedRowIds.clear(),t.selectedCells=[{rowIdx:n,colIdx:o,rowId:l,value:a}],t.lastSelectedCell={rowIdx:n,colIdx:o},t.selectedColumns.clear();M(),L(),O()}function st(e,n,o,l){if(t.cellEditBehavior==="vscode"){let a=t.tableColumns[o];if(!a)return;let s=t.gridData[n];if(!s)return;let i=P(s,o);t.cellPreviewInfo={rowIdx:n,colIdx:o,rowId:l,columnName:a.name,originalValue:i},we()}else t.cellEditBehavior==="modal"?K(n,o,l):je(n,o,l)}function it(){document.getElementById("filterInput")?.addEventListener("keyup",He),document.getElementById("pageSizeSelect")?.addEventListener("change",Ge),document.getElementById("dateFormatSelect")?.addEventListener("change",Je),document.getElementById("btnFirst")?.addEventListener("click",()=>Z(0)),document.getElementById("btnPrev")?.addEventListener("click",()=>Z(t.currentPageIndex-1)),document.getElementById("btnNext")?.addEventListener("click",()=>Z(t.currentPageIndex+1)),document.getElementById("btnLast")?.addEventListener("click",()=>Z(t.totalPageCount-1))}function rt(){let e=document.getElementById("gridContainer");e&&(e.addEventListener("mousedown",qt),e.addEventListener("keydown",Kt),e.addEventListener("click",Ht),e.addEventListener("dblclick",Xt),e.addEventListener("mouseover",Yt),e.addEventListener("scroll",Zt,{passive:!0}))}function qt(e){if(e.target.classList.contains("resize-handle")){e.stopPropagation();let n=e.target.closest(".header-cell");n&&n.dataset.column&&tt(e,n.dataset.column)}}function Kt(e){if(!t.isGridReloading&&e.target.classList.contains("column-filter")){let n=e.target.dataset.column;n&&Ye(e,n)}}function Ht(e){if(t.isGridReloading)return;let n=e.target;if(n.closest(".grid-header")){Gt(e,n);return}Jt(e,n)}function Gt(e,n){if(n.closest(".filter-apply-btn")){e.stopPropagation();let l=n.closest(".header-cell");l&&l.dataset.column&&Ie(l.dataset.column);return}if(n.closest(".header-bottom")||n.closest(".column-filter")){e.stopPropagation();return}if(n.closest(".select-column-icon")){e.stopPropagation();let l=n.closest(".header-cell");l&&l.dataset.column&&Ze(e,l.dataset.column);return}if(n.closest(".pin-icon")){e.stopPropagation();let l=n.closest(".header-cell");l&&l.dataset.column&&Qe(e,l.dataset.column);return}if(n.closest(".row-number-header")){ie(e);return}let o=n.closest(".header-top");if(o){let l=o.closest(".header-cell");l&&l.dataset.column&&Xe(l.dataset.column);return}}function Jt(e,n){if(n.closest(".pin-icon")){let l=n.closest(".data-row");if(l){let a=l.dataset.rowid,s=Q(a);et(e,s)}return}if(n.closest(".expand-icon")){let l=n.closest(".data-cell");if(l){let a=parseInt(l.dataset.rowidx,10),s=parseInt(l.dataset.colidx,10),i=Q(l.closest(".data-row").dataset.rowid);K(a,s,i)}return}if(n.closest(".row-number")){let l=n.closest(".data-row");if(l){let a=Q(l.dataset.rowid),s=parseInt(l.dataset.rowidx,10);lt(e,a,s)}return}let o=n.closest(".data-cell");if(o){let l=parseInt(o.dataset.rowidx,10),a=parseInt(o.dataset.colidx,10),s=o.closest(".data-row"),i=Q(s.dataset.rowid);at(e,l,a,i);return}}function Xt(e){if(t.isGridReloading)return;let n=e.target.closest(".data-cell");if(n&&!n.classList.contains("row-number")){let o=parseInt(n.dataset.rowidx,10),l=parseInt(n.dataset.colidx,10),a=n.closest(".data-row"),s=Q(a.dataset.rowid);st(e,o,l,s)}}function Yt(e){let n=e.target.closest(".data-cell");if(n&&!n.classList.contains("checked-overflow")){let o=n.querySelector(".cell-text");if(o){let l=o.scrollWidth>o.clientWidth;n.classList.toggle("has-overflow",l),n.classList.add("checked-overflow")}}}function Zt(e){if(t.isGridReloading)return;let n=e.currentTarget;t.scrollPosition.left=n.scrollLeft,t.scrollPosition.top=n.scrollTop,$()}function Q(e){if(e==null)return e;let n=Number(e);return!isNaN(n)&&e.trim()!==""?n:e}function ct(){document.addEventListener("click",e=>{let n=e.target,o=n.closest(".modal-close, .modal-cancel");if(o){let l=o.dataset.modal;l&&U(l)}n.classList.contains("modal-overlay")&&n.classList.add("hidden")}),document.addEventListener("keydown",e=>{if(e.key==="Escape"){let n=document.querySelector(".modal-overlay:not(.hidden)");n&&(n.classList.add("hidden"),e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation())}})}function j(e){let n=document.getElementById(e);if(n){n.classList.remove("hidden");let o=n.querySelector("input, select, textarea, button");o&&o.focus()}}function U(e){let n=document.getElementById(e);n&&n.classList.add("hidden")}function dt(){document.getElementById("btnAddRow")?.addEventListener("click",Qt),document.getElementById("btnDeleteRows")?.addEventListener("click",tn),document.getElementById("btnAddColumn")?.addEventListener("click",sn),document.getElementById("btnSubmitAddRow")?.addEventListener("click",en),document.getElementById("btnSubmitDelete")?.addEventListener("click",re),document.getElementById("btnSubmitCreateTable")?.addEventListener("click",an),document.getElementById("btnAddColumnDef")?.addEventListener("click",()=>pt()),document.getElementById("columnDefinitions")?.addEventListener("click",e=>{let n=e.target.closest(".btn-remove-col");if(n){let o=n.dataset.colid;ln(o)}}),document.getElementById("btnSubmitAddColumn")?.addEventListener("click",rn)}function Qt(){if(!t.selectedTable||t.selectedTableType!=="table")return;let e=document.getElementById("addRowForm");e.replaceChildren(),t.tableColumns.forEach(n=>{let o=n.notnull===1&&!n.isPrimaryKey,l=document.createElement("div");l.className="form-field";let a=document.createElement("label");if(a.textContent=n.name,o){let r=document.createElement("span");r.style.color="var(--error-color)",r.textContent="*",a.appendChild(document.createTextNode(" ")),a.appendChild(r)}let s=document.createElement("span");s.style.opacity="0.5",s.textContent=` (${n.type})`,a.appendChild(document.createTextNode(" ")),a.appendChild(s);let i=document.createElement("input");i.type="text",i.dataset.column=n.name,i.dataset.required=o.toString(),n.isPrimaryKey?(i.placeholder="Auto (Primary Key)",i.disabled=!0):o?i.placeholder="Required":i.placeholder="NULL",l.appendChild(a),l.appendChild(i),e.appendChild(l)}),j("addRowModal")}async function en(){let e=document.querySelectorAll("#addRowForm input[data-column]:not([disabled])"),n=[];for(let l of e){let a=l.dataset.column,s=l.value.trim();l.dataset.required==="true"&&(s===""||s.toLowerCase()==="null")?(n.push(a),l.style.borderColor="var(--error-color)"):l.style.borderColor=""}if(n.length>0){u(`Required fields missing: ${n.join(", ")}`);return}let o={};for(let l of e){let a=l.dataset.column,s=l.value.trim();s!==""&&(s.toLowerCase()==="null"?o[a]=null:!isNaN(Number(s))&&s!==""?o[a]=Number(s):o[a]=s)}try{u("Inserting row..."),await x.insertRow(t.selectedTable,o),U("addRowModal"),await v(),u("Row inserted - Ctrl+S to save")}catch(l){console.error("Insert failed:",l),u(`Error: ${l.message}`)}}function tn(){if(t.selectedColumns.size>0){let e=Array.from(t.selectedColumns);document.getElementById("deleteConfirmText").textContent=`Are you sure you want to delete ${e.length} column${e.length>1?"s":""} (${e.join(", ")})? This will permanently remove the column${e.length>1?"s":""} and all their data.`}else if(t.selectedRowIds.size>0)document.getElementById("deleteConfirmText").textContent=`Are you sure you want to delete ${t.selectedRowIds.size} row${t.selectedRowIds.size>1?"s":""}?`;else return;j("deleteModal")}async function re(){t.selectedColumns.size>0?await on():t.selectedRowIds.size>0&&await nn()}async function nn(){if(t.selectedRowIds.size===0)return;let e=Array.from(t.selectedRowIds);try{u("Deleting rows..."),await x.deleteRows(t.selectedTable,e),U("deleteModal"),t.selectedRowIds.clear(),await v(),L(),u(`Deleted ${e.length} row${e.length>1?"s":""} - Ctrl+S to save`)}catch(n){console.error("Delete rows failed:",n),u(`Error: ${n.message}`)}}async function on(){if(t.selectedColumns.size===0)return;let e=Array.from(t.selectedColumns);try{u("Deleting columns...");let n=await x.deleteColumns(t.selectedTable,e);if(n&&n.cancelled){u("Delete cancelled"),U("deleteModal");return}U("deleteModal"),t.selectedColumns.clear(),t.selectedCells=[],t.lastSelectedCell=null,await V(),await q(),await v(),L(),u(`Deleted ${e.length} column${e.length>1?"s":""} - Ctrl+S to save`)}catch(n){console.error("Delete columns failed:",n),u(`Error: ${n.message}`)}}var ut=0;function mt(){document.getElementById("newTableName").value="",document.getElementById("columnDefinitions").replaceChildren(),ut=0,pt(!0),j("createTableModal")}function pt(e=!1){let n=document.getElementById("columnDefinitions"),o=++ut,l=document.createElement("div");l.className="column-def-row",l.id=`colDef_${o}`,Object.assign(l.style,{display:"flex",gap:"8px",marginBottom:"8px",alignItems:"center"});let a=document.createElement("input");a.type="text",a.placeholder="Column name",a.className="col-name",a.style.flex="2",e&&(a.value="id"),l.appendChild(a);let s=document.createElement("select");s.className="col-type",s.style.flex="1",["INTEGER","TEXT","REAL","BLOB","NUMERIC"].forEach(f=>{let p=document.createElement("option");p.value=f,p.textContent=f,e&&f==="INTEGER"&&(p.selected=!0),!e&&f==="TEXT"&&(p.selected=!0),s.appendChild(p)}),l.appendChild(s);let i=document.createElement("label");Object.assign(i.style,{display:"flex",alignItems:"center",gap:"4px",cursor:"pointer"});let r=document.createElement("input");r.type="checkbox",r.className="col-pk",r.style.margin="0",e&&(r.checked=!0),i.appendChild(r),i.appendChild(document.createTextNode(" PK")),l.appendChild(i);let c=document.createElement("label");Object.assign(c.style,{display:"flex",alignItems:"center",gap:"4px",cursor:"pointer"});let d=document.createElement("input");d.type="checkbox",d.className="col-nn",d.style.margin="0",c.appendChild(d),c.appendChild(document.createTextNode(" NN")),l.appendChild(c);let m=document.createElement("button");m.className="icon-button btn-remove-col",m.dataset.colid=o.toString(),m.title="Remove",e&&(m.disabled=!0);let y=document.createElement("span");y.className="codicon codicon-close",m.appendChild(y),l.appendChild(m),n.appendChild(l)}function ln(e){let n=document.getElementById(`colDef_${e}`);n&&n.remove()}async function an(){let e=document.getElementById("newTableName").value.trim();if(!e){u("Error: Table name is required");return}if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(e)){u("Error: Invalid table name");return}let n=[],o=document.querySelectorAll(".column-def-row");for(let l of o){let a=l.querySelector(".col-name").value.trim(),s=l.querySelector(".col-type").value,i=l.querySelector(".col-pk").checked,r=l.querySelector(".col-nn").checked;a&&n.push({name:a,type:s,primaryKey:i,notNull:r})}if(n.length===0){u("Error: At least one column is required");return}try{u("Creating table..."),await x.createTable(e,n),U("createTableModal"),await V(),u(`Table "${e}" created - Ctrl+S to save`)}catch(l){console.error("Create table failed:",l),u(`Error: ${l.message}`)}}function sn(){!t.selectedTable||t.selectedTableType!=="table"||(document.getElementById("newColumnName").value="",document.getElementById("newColumnType").value="TEXT",document.getElementById("newColumnDefault").value="",j("addColumnModal"))}async function rn(){let e=document.getElementById("newColumnName").value.trim(),n=document.getElementById("newColumnType").value,o=document.getElementById("newColumnDefault").value.trim();if(!e){u("Error: Column name is required");return}if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(e)){u("Error: Invalid column name");return}try{u("Adding column..."),await x.addColumn(t.selectedTable,e,n,o),U("addColumnModal"),await q(),await v(),u(`Column "${e}" added - Ctrl+S to save`)}catch(l){console.error("Add column failed:",l),u(`Error: ${l.message}`)}}function ft(){let e=document.getElementById("pragmaSettingsContainer");e&&e.addEventListener("change",n=>{let o=n.target;if(o.matches(".setting-extension")){let l=o.dataset.key,a=o.type==="checkbox"?o.checked:o.value;dn(l,a)}else if(o.matches(".setting-pragma")){let l=o.dataset.name,a=o.dataset.type,s=o.value;a==="number"?s=Number(s):a==="bool"&&(s=s==="true"?1:0),un(l,s)}})}async function gt(){let e=document.getElementById("settingsModal");e&&(e.classList.remove("hidden"),await ve())}async function ve(){let e=document.getElementById("pragmaSettingsContainer");e.textContent="Loading settings...";try{let[n,o]=await Promise.all([x.getPragmas(),x.getExtensionSettings()]);cn(n,o)}catch(n){console.error("Failed to load settings:",n),e.textContent=`Error loading settings: ${n.message}`,e.style.color="var(--error-color)"}}function cn(e,n){let o=document.getElementById("pragmaSettingsContainer");if(!o)return;let l=(h,C)=>h.map(k=>{let R=String(k),D=String(C).toUpperCase()===R.toUpperCase(),N=document.createElement("option");return N.value=R,N.selected=D,N.textContent=R,N});o.replaceChildren();let a=h=>{let C=document.createElement("div");C.className="setting-section-title",Object.assign(C.style,{fontWeight:"600",marginBottom:"8px",paddingBottom:"4px",borderBottom:"1px solid var(--border-color)"}),C.textContent=h,o.appendChild(C)},s=(h,C,k)=>{let R=document.createElement("div");R.className="form-field";let w=document.createElement("label");if(C.type==="checkbox"?(Object.assign(w.style,{display:"flex",alignItems:"center",gap:"4px",cursor:"pointer"}),C.style.margin="0",w.appendChild(C),w.appendChild(document.createTextNode(h)),R.appendChild(w)):(w.textContent=h,R.appendChild(w),R.appendChild(C)),k){let D=document.createElement("div");D.className="setting-desc",D.textContent=k,R.appendChild(D)}o.appendChild(R)};if(o.children.length>0){let h=document.createElement("div");h.style.height="16px",o.appendChild(h)}a("Extension Settings");let i=document.createElement("input");i.type="checkbox",i.className="setting-extension",i.dataset.key="autoCommit",i.checked=!!n.autoCommit,s("Auto-Commit Changes",i,"Automatically save changes to disk immediately. If disabled, you must save manually (Ctrl+S).");let r=document.createElement("select");r.className="setting-extension",r.dataset.key="doubleClickBehavior",l(["inline","modal","vscode"],n.cellEditBehavior).forEach(h=>r.appendChild(h)),s("Double Click Behavior",r,"Action when double-clicking a cell");let c=document.createElement("div");c.style.height="16px",o.appendChild(c),a("SQLite Settings (Pragmas)");let d=document.createElement("select");d.className="setting-pragma",d.dataset.name="journal_mode",l(["DELETE","TRUNCATE","PERSIST","MEMORY","WAL","OFF"],e.journal_mode).forEach(h=>d.appendChild(h)),s("Journal Mode",d,"Database journaling mode (WAL is recommended for concurrency)");let m=document.createElement("select");m.className="setting-pragma",m.dataset.name="foreign_keys",m.dataset.type="bool";let y=document.createElement("option");y.value="true",y.textContent="ON";let f=document.createElement("option");f.value="false",f.textContent="OFF",Number(e.foreign_keys)===1?y.selected=!0:f.selected=!0,m.appendChild(y),m.appendChild(f),s("Foreign Keys",m,"Enforce foreign key constraints");let p=document.createElement("select");p.className="setting-pragma",p.dataset.name="synchronous",p.dataset.type="number",[{v:0,t:"OFF (0)"},{v:1,t:"NORMAL (1)"},{v:2,t:"FULL (2)"},{v:3,t:"EXTRA (3)"}].forEach(h=>{let C=document.createElement("option");C.value=h.v,C.textContent=h.t,Number(e.synchronous)===h.v&&(C.selected=!0),p.appendChild(C)}),s("Synchronous",p,"Disk synchronization safety level");let g=document.createElement("select");g.className="setting-pragma",g.dataset.name="locking_mode",l(["NORMAL","EXCLUSIVE"],e.locking_mode).forEach(h=>g.appendChild(h)),s("Locking Mode",g,"");let b=document.createElement("select");b.className="setting-pragma",b.dataset.name="auto_vacuum",b.dataset.type="number",[{v:0,t:"NONE (0)"},{v:1,t:"FULL (1)"},{v:2,t:"INCREMENTAL (2)"}].forEach(h=>{let C=document.createElement("option");C.value=h.v,C.textContent=h.t,Number(e.auto_vacuum)===h.v&&(C.selected=!0),b.appendChild(C)}),s("Auto Vacuum",b,"");let E=document.createElement("input");E.type="number",E.className="setting-pragma",E.dataset.name="cache_size",E.dataset.type="number",E.value=e.cache_size,s("Cache Size",E,"Number of pages (positive) or kilobytes (negative)")}async function dn(e,n){try{await x.updateExtensionSetting(e,n),u(`Updated ${e}`)}catch(o){console.error(`Failed to set ${e}:`,o),u(`Error: ${o.message}`),await ve()}}async function un(e,n){try{u(`Updating ${e}...`),await x.setPragma(e,n),u(`Updated ${e}`)}catch(o){console.error(`Failed to set ${e}:`,o),u(`Error: ${o.message}`),await ve()}}function ht(e,n){let o=new Map;for(let l of e){let a=n&&n[l.colIdx];a&&(o.has(l.colIdx)||o.set(l.colIdx,{name:a.name,type:a.type,values:new Set}),o.get(l.colIdx).values.add(l.value))}return o}function yt(e){let n=Array.from(e||[]);if(n.length===0)return"";if(n.length>1)return"(mixed values)";let o=n[0];return o===null?"NULL":o instanceof Uint8Array?"[BLOB]":String(o)}function Ct(e,n,o){let l=[];for(let a of e){let s=n.get(a.colIdx);if(!s)continue;let i=s.dataset||{},r=i.isnull==="true",c=i.ispatch==="true",d=s.value;if(d===""&&!r)continue;let m=o&&o[a.colIdx];if(!m)continue;let y=d,f="set";if(r)y=null;else if(c)f="json_patch";else{let p=(m.type||"").toUpperCase();(p==="INTEGER"||p==="REAL"||p==="NUMERIC")&&!isNaN(Number(d))&&d.trim()!==""&&(y=Number(d))}l.push({rowId:a.rowId,column:m.name,value:y,originalValue:a.value,operation:f,rowIdx:a.rowIdx,colIdx:a.colIdx})}return l}function bt(){let e=document.getElementById("sidebarPanel");if(!e)return;let n=document.getElementById("sidebarFilterInput");n&&n.addEventListener("input",()=>{t.sidebarFilter=n.value,Be()}),e.addEventListener("click",o=>{let l=o.target;if(l.closest("#btnOpenSettings")){gt();return}if(l.closest("#btnOpenCreateTable")){o.stopPropagation(),mt();return}if(l.closest("#btnReload")){Cn();return}if(l.closest("#btnApplyBatchUpdate")){pn();return}let a=l.closest(".list-item");if(a){let c=a.dataset.name,d=a.dataset.type;if(c&&d){yn(c,d);return}}let s=l.closest(".section-title");if(s){if(l.closest(".icon-button")||s.id==="btnOpenSettings")return;let c=s.dataset.section;c&&hn(c)}let i=l.closest(".btn-batch-null");if(i){let c=i.closest(".batch-field");if(c){let d=parseInt(c.dataset.colidx,10);fn(d)}return}let r=l.closest(".btn-batch-patch");if(r){let c=r.closest(".batch-field");if(c){let d=parseInt(c.dataset.colidx,10);gn(d,r)}return}})}async function V(){if(t.isDbConnected)try{let e=await x.fetchSchema();t.schemaCache.tables=(e.tables||[]).map(n=>({name:n.identifier})),t.schemaCache.views=(e.views||[]).map(n=>({name:n.identifier})),t.schemaCache.indexes=(e.indexes||[]).map(n=>({name:n.identifier,table:n.parentTable})),Be()}catch(e){console.error("Error loading schema:",e),u("Error loading schema")}}function Se(e,n){return n?e.filter(o=>o.name.toLowerCase().includes(n)):e}function Te(e,n,o,l){let a=document.getElementById(e);a&&(a.textContent=l?`${n}/${o}`:o)}function xt(e,n,o,l,a){let s=document.getElementById(e);if(!s)return;if(s.replaceChildren(),n.length===0){let r=document.createElement("li");r.className="list-item",r.style.opacity="0.5",r.textContent=a,s.appendChild(r);return}let i=document.createDocumentFragment();n.forEach(r=>{let c=document.createElement("li");c.className="list-item",t.selectedTable===r.name&&t.selectedTableType===o&&c.classList.add("selected"),c.dataset.name=r.name,o&&(c.dataset.type=o),c.title=r.name;let d=document.createElement("span");d.className=`item-icon codicon ${l}`,c.appendChild(d);let m=document.createElement("span");m.className="item-name",m.textContent=r.name,c.appendChild(m),i.appendChild(c)}),s.appendChild(i)}function mn(e,n,o){let l=document.getElementById(e);if(!l)return;if(l.replaceChildren(),n.length===0){let s=document.createElement("li");s.className="list-item",s.style.opacity="0.5",s.textContent=o,l.appendChild(s);return}let a=document.createDocumentFragment();n.forEach(s=>{let i=document.createElement("li");i.className="list-item",i.title=`${s.name} on ${s.table}`;let r=document.createElement("span");r.className="item-icon codicon codicon-list-selection",i.appendChild(r);let c=document.createElement("div");c.className="item-content";let d=document.createElement("span");d.className="item-name",d.textContent=s.name,c.appendChild(d);let m=document.createElement("span");m.className="item-detail",m.textContent=s.table,c.appendChild(m),i.appendChild(c),a.appendChild(i)}),l.appendChild(a)}function Be(){let e=t.sidebarFilter.toLowerCase(),n=e.length>0,o=Se(t.schemaCache.tables,e),l=Se(t.schemaCache.views,e),a=Se(t.schemaCache.indexes,e);Te("tablesBadge",o.length,t.schemaCache.tables.length,n),Te("viewsBadge",l.length,t.schemaCache.views.length,n),Te("indexesBadge",a.length,t.schemaCache.indexes.length,n),xt("tablesList",o,"table","codicon-table",e?"No matching tables":"No tables"),xt("viewsList",l,"view","codicon-eye",e?"No matching views":"No views"),mn("indexesList",a,e?"No matching indexes":"No indexes")}function O(){let e=document.getElementById("batchUpdateSectionTitle"),n=document.getElementById("batchUpdateList"),o=document.getElementById("batchUpdateCount"),l=document.getElementById("batchUpdateFields");if(!e||!n||!o||!l)return;let a=t.selectedCells.length;if(a===0){e.classList.add("hidden"),n.classList.add("hidden");return}e.classList.remove("hidden"),n.classList.remove("hidden"),e.classList.remove("collapsed"),o.textContent=a;let s=ht(t.selectedCells,t.tableColumns);l.replaceChildren();let i=document.createDocumentFragment();for(let[r,c]of s){let d=yt(c.values),m=document.createElement("div");m.className="form-field batch-field",m.dataset.colidx=r,m.style.marginBottom="8px";let y=document.createElement("label");y.style.fontSize="11px",y.style.color="var(--text-secondary)";let f=document.createTextNode(c.name+" ");y.appendChild(f);let p=document.createElement("span");p.style.opacity="0.7",p.textContent=c.type||"",y.appendChild(p),m.appendChild(y);let g=document.createElement("div");g.style.display="flex",g.style.gap="4px";let b=document.createElement("input");b.type="text",b.className="batch-input",b.placeholder=d,b.dataset.colidx=r,b.style.flex="1",b.style.minWidth="0",g.appendChild(b);let E=document.createElement("button");E.className="btn-secondary btn-batch-null",E.style.padding="2px 6px",E.title="Set to NULL",E.textContent="NULL",g.appendChild(E);let h=document.createElement("button");h.className="btn-secondary btn-batch-patch",h.style.padding="2px 6px",h.title="JSON Patch",h.textContent="{}",g.appendChild(h),m.appendChild(g),i.appendChild(m)}l.appendChild(i)}async function pn(){if(t.selectedCells.length===0)return;let e=document.querySelectorAll(".batch-input"),n=new Map;for(let l of e){let a=parseInt(l.dataset.colidx,10);if(n.set(a,l),l.dataset.ispatch==="true")try{JSON.parse(l.value)}catch{let i=t.tableColumns[a];u(`Invalid JSON for patch in ${i?.name??`column ${a}`}`);return}}let o=Ct(t.selectedCells,n,t.tableColumns);if(o.length===0){u("No values entered for batch update");return}try{u(`Updating ${o.length} cells...`);let l=`Batch update ${o.length} cells`,a=o.map(r=>({rowId:r.rowId,column:r.column,value:r.value,originalValue:r.originalValue,operation:r.operation}));if(await x.updateCellBatch(t.selectedTable,a,l),!o.some(r=>r.operation==="json_patch"))for(let r of o)t.gridData[r.rowIdx][r.colIdx+B()]=r.value;await v(!1);let i=[];for(let r of t.selectedCells){let c=t.gridData[r.rowIdx][r.colIdx+B()];i.push({...r,value:c})}t.selectedCells=i,O(),u("Batch update completed")}catch(l){console.error("Batch update failed:",l),u(`Batch update failed: ${l.message}`)}}function fn(e){let n=document.querySelector(`.batch-input[data-colidx="${e}"]`),o=document.querySelector(`.batch-field[data-colidx="${e}"] .btn-batch-patch`);n&&(n.value="",n.placeholder="SET TO NULL",n.dataset.isnull="true",n.dataset.ispatch="false",n.style.fontStyle="italic",o&&(o.style.background="",o.style.color=""))}function gn(e,n){let o=document.querySelector(`.batch-input[data-colidx="${e}"]`);o&&(o.dataset.ispatch==="true"?(o.dataset.ispatch="false",o.placeholder="(mixed values)",n.style.background="",n.style.color=""):(o.dataset.ispatch="true",o.dataset.isnull="false",o.placeholder='JSON Patch (e.g. {"a": 1})',o.style.fontStyle="normal",n.style.background="var(--accent-color)",n.style.color="white"))}function hn(e){let n=document.getElementById(`${e}List`),o=document.querySelector(`.section-title[data-section="${e}"]`);n&&o&&(n.classList.toggle("hidden"),o.classList.toggle("collapsed"))}async function yn(e,n){t.selectedTable=e,t.selectedTableType=n,t.currentPageIndex=0,t.sortedColumn=null,t.sortAscending=!0,t.filterQuery="",t.columnFilters={},t.selectedRowIds.clear(),t.selectedCells=[],t.lastSelectedCell=null,t.selectedColumns.clear(),t.pinnedColumns.clear(),t.pinnedRowIds.clear(),t.columnWidths={},t.scrollPosition={top:0,left:0},Be();let o=document.getElementById("tableNameLabel");o&&(o.textContent=e);let l=document.getElementById("filterInput");l&&(l.value=""),await q(),await v(!0,!1),$()}async function Cn(){if(t.isDbConnected)try{u("Reloading..."),await x.refreshFile(),await V(),t.selectedTable&&(await q(),await v()),u("Reloaded")}catch(e){console.error("Reload failed:",e),u(`Reload failed: ${e.message}`)}}function wt(){document.getElementById("btnExport")?.addEventListener("click",xn),document.getElementById("btnSubmitExport")?.addEventListener("click",bn),document.getElementById("exportFormat")?.addEventListener("change",Et)}function xn(){if(!t.selectedTable)return;let e=document.getElementById("exportFormat");e&&(e.value="csv");let n=document.getElementById("exportColumns");n&&(n.replaceChildren(),t.tableColumns.forEach(o=>{let l=document.createElement("div"),a=document.createElement("label");Object.assign(a.style,{display:"flex",alignItems:"center",gap:"3px",marginBottom:"4px",fontSize:"13px",cursor:"pointer"});let s=document.createElement("input");s.type="checkbox",s.className="export-col-check",s.value=o.name,s.checked=!0,s.style.margin="0",a.appendChild(s),a.appendChild(document.createTextNode(o.name)),n.appendChild(a)})),Et(),j("exportModal")}function Et(){let e=document.getElementById("exportFormat").value,n=document.getElementById("exportOptions");if(n.replaceChildren(),e==="csv"||e==="excel"){let o=document.createElement("label");Object.assign(o.style,{display:"flex",alignItems:"center",gap:"3px",marginBottom:"4px",fontSize:"13px",cursor:"pointer"});let l=document.createElement("input");l.type="checkbox",l.id="exportHeader",l.checked=!0,l.style.margin="0",o.appendChild(l),o.appendChild(document.createTextNode(" Include Headers")),n.appendChild(o)}else if(e==="sql"){let o=document.createElement("label");Object.assign(o.style,{display:"flex",alignItems:"center",gap:"3px",marginBottom:"4px",fontSize:"13px",cursor:"pointer"});let l=document.createElement("input");l.type="checkbox",l.id="exportTableName",l.checked=!0,l.style.margin="0",o.appendChild(l),o.appendChild(document.createTextNode(" Include Table Name")),n.appendChild(o)}}async function bn(){let e=document.getElementById("exportFormat").value,n=document.querySelectorAll(".export-col-check:checked"),o=Array.from(n).map(a=>a.value);if(o.length===0){u("Error: Select at least one column");return}let l={};if(e==="csv"||e==="excel"?l.header=document.getElementById("exportHeader")?.checked??!0:e==="sql"&&(l.includeTableName=document.getElementById("exportTableName")?.checked??!0),t.selectedTableType==="table"){let a=Array.from(t.selectedRowIds);a.length>0&&(l.rowIds=a)}try{u("Exporting..."),U("exportModal"),await x.exportTable({table:t.selectedTable},o,null,null,{format:e,...l}),u("Export initiated")}catch(a){console.error("Export failed:",a),u(`Export failed: ${a.message}`)}}async function It(){if(t.selectedCells.length!==0)try{let e;if(t.selectedCells.length===1){let n=t.selectedCells[0].value;n==null?e="":n instanceof Uint8Array?e="[BLOB]":e=String(n)}else{let n=[...new Set(t.selectedCells.map(s=>s.rowIdx))].sort((s,i)=>s-i),o=[...new Set(t.selectedCells.map(s=>s.colIdx))].sort((s,i)=>s-i),l=new Map;for(let s of t.selectedCells)l.set(`${s.rowIdx},${s.colIdx}`,s.value);let a=[];for(let s of n){let i=[];for(let r of o){let c=`${s},${r}`,d=l.has(c)?l.get(c):"";d==null?d="":d instanceof Uint8Array?d="[BLOB]":(d=String(d),d=d.replace(/\t/g," ").replace(/\n/g," ")),i.push(d)}a.push(i.join(" "))}e=a.join(` | ||
| `)}await navigator.clipboard.writeText(e),u(`Copied ${t.selectedCells.length} cell${t.selectedCells.length>1?"s":""}`)}catch(e){console.error("Copy failed:",e),u("Copy failed: "+e.message)}}async function vt(){if(t.selectedRowIds.size!==0)try{let e=[];for(let l=0;l<t.gridData.length;l++){let a=t.gridData[l],s;if(t.selectedTableType==="table"?s=a[0]:s=t.currentPageIndex*t.rowsPerPage+l,t.selectedRowIds.has(s)){let i=t.selectedTableType==="table"?1:0,r=a.slice(i).map(c=>c==null?"":c instanceof Uint8Array?"[BLOB]":String(c));e.push(r.join(" "))}}let o=[t.tableColumns.map(l=>l.name).join(" "),...e].join(` | ||
| `);await navigator.clipboard.writeText(o),u(`Copied ${e.length} row${e.length>1?"s":""} to clipboard`)}catch(e){console.error("Copy failed:",e),u("Copy failed: "+e.message)}}async function St(){if(t.selectedCells.length!==0){if(t.selectedTableType!=="table"){u("Views are read-only");return}try{u("Clearing cells...");let e=[];for(let o of t.selectedCells){let l=t.tableColumns[o.colIdx];if(!l)continue;let s=l.notnull===1?"":null;e.push({rowId:o.rowId,column:l.name,value:s,originalValue:o.value,rowIdx:o.rowIdx,colIdx:o.colIdx})}let n=`Clear ${e.length} cell${e.length>1?"s":""}`;await x.updateCellBatch(t.selectedTable,e,n);for(let o of e)t.gridData[o.rowIdx][o.colIdx+B()]=o.value;t.selectedCells=[],t.lastSelectedCell=null,t.selectedColumns.clear(),await v(),L(),u(`${n} - Ctrl+S to save`)}catch(e){console.error("Clear cells failed:",e),u(`Clear failed: ${e.message}`)}}}var ce=50*1024*1024,Re=!1;function Tt(){let e=document.getElementById("gridContainer");if(!e){console.error("gridContainer not found");return}document.addEventListener("dragover",n=>n.preventDefault()),document.addEventListener("drop",n=>n.preventDefault()),e.addEventListener("dragover",wn),e.addEventListener("dragleave",En),e.addEventListener("drop",In)}var F=null;function wn(e){e.preventDefault(),e.dataTransfer.dropEffect="copy";let n=e.target.closest(".data-cell");n&&!n.classList.contains("row-number")?(F&&F!==n&&F.classList.remove("drag-over"),n.classList.add("drag-over"),F=n):F&&(F.classList.remove("drag-over"),F=null)}function En(e){e.target}async function In(e){e.preventDefault(),F&&(F.classList.remove("drag-over"),F=null);let n=e.target.closest(".data-cell");if(!n||n.classList.contains("row-number"))return;if(e.dataTransfer.files.length>0){let l=e.dataTransfer.files[0];await vn(n,l.name,l);return}let o=e.dataTransfer.getData("text/uri-list");if(o){let l=o.split(/\r?\n/);if(l.length>0&&l[0]){let a=l[0],s="unknown_file";try{let i=a.split("/");s=decodeURIComponent(i[i.length-1])}catch(i){console.warn("Failed to parse name from URI",i)}await Sn(n,s,a);return}}}async function vn(e,n,o){if(o.size>ce){let l=(o.size/1048576).toFixed(1),a=(ce/(1024*1024)).toFixed(0);u(`File too large (${l}MB). Maximum is ${a}MB.`);return}try{u(`Reading ${n}...`);let l=await Tn(o),a=new Uint8Array(l);await Bt(e,n,a)}catch(l){console.error("File read failed:",l),u(`File read failed: ${l.message}`)}}async function Sn(e,n,o){try{u(`Fetching ${n}...`);let l=await x.readWorkspaceFileUri(o),a;if(l instanceof Uint8Array)a=l;else if(l&&l.type==="Buffer"&&Array.isArray(l.data))a=new Uint8Array(l.data);else if(l&&typeof l=="object"&&Object.keys(l).some(s=>!isNaN(s)))a=new Uint8Array(Object.values(l));else throw console.error("Unknown data format from backend:",l),new Error("Received invalid data format from backend");await Bt(e,n,a)}catch(l){console.error("URI upload failed:",l),u(`Upload failed: ${l.message}`)}}async function Bt(e,n,o){if(Re){u("Upload already in progress...");return}if(o.byteLength>ce){let c=(o.byteLength/1048576).toFixed(1),d=(ce/(1024*1024)).toFixed(0);u(`File too large (${c}MB). Maximum is ${d}MB.`);return}let l=parseInt(e.dataset.rowidx,10),a=parseInt(e.dataset.colidx,10);if(!t.gridData)return;let s=t.gridData[l];if(!s)return;let i=T(s,l),r=t.tableColumns[a];if(t.selectedTableType!=="table"){u("Cannot upload to a view");return}Re=!0,t.isLoadingData=!0;try{u(`Uploading ${n} (${Bn(o.byteLength)})...`);let c=s[a+B()];await x.updateCell(t.selectedTable,i,r.name,o,c),t.gridData[l][a+B()]=o,Rn(e,o),u(`Uploaded ${n}`)}catch(c){console.error("Upload failed:",c);let d=c.message||String(c);d.includes("timeout")&&(d="Upload timed out. Try a smaller file."),u(`Upload failed: ${d}`)}finally{Re=!1,t.isLoadingData=!1}}function Tn(e){return new Promise((n,o)=>{let l=new FileReader;l.onload=()=>n(l.result),l.onerror=()=>o(l.error),l.readAsArrayBuffer(e)})}function Bn(e){if(e===0)return"0 B";let n=1024,o=["B","KB","MB","GB"],l=Math.floor(Math.log(e)/Math.log(n));return parseFloat((e/Math.pow(n,l)).toFixed(2))+" "+o[l]}function Rn(e,n){let o=_(n);e.textContent="";let l=document.createElement("span");l.className="cell-text",l.textContent=o,e.appendChild(l);let a=document.createElement("span");a.className="expand-icon codicon codicon-link-external",a.title="View full content",e.appendChild(a),e.classList.remove("null-value")}var Rt={async refreshContent(e){return t.isDbConnected&&(await V(),!(t.schemaCache.tables.some(o=>o.name===t.selectedTable)||t.schemaCache.views.some(o=>o.name===t.selectedTable))&&t.selectedTable?(t.selectedTable=null,t.selectedTableType=null,document.getElementById("tableNameLabel").textContent="No table selected",xe()):t.selectedTable&&await v(!1)),{success:!0}},async updateColorScheme(e){return document.documentElement.style.colorScheme=e,{success:!0}},async updateCellEditBehavior(e){return t.cellEditBehavior=e,{success:!0}}};function Ln(){window.addEventListener("message",e=>{let n=e.data;if(n&&n.kind==="invoke"){let{correlationId:l,methodName:a,parameters:s}=n,i=Rt[a];typeof i=="function"?Promise.resolve(i.apply(Rt,s||[])).then(r=>{Ae(l,r)}).catch(r=>{ye(l,r instanceof Error?r.message:String(r))}):ye(l,`Unknown method: ${a}`);return}if(!n||n.channel!=="rpc")return;let o=n.content;o&&o.kind==="response"&&ke(o)})}Ln();async function Pn(){try{bt(),dt(),wt(),ct(),ft(),ze(),it(),rt(),Me(),Tt();let e=document.getElementById("openInVsCodeBtn");e&&(e.style.display="none"),u("Connecting to database...");let n=await x.initialize();t.isDbConnected=!0,await x.ping(),await V(),u("Ready"),xe(),document.addEventListener("keydown",async o=>{if(o.key==="Escape"&&!t.editingCellInfo&&!document.querySelector(".modal-overlay:not(.hidden)")&&oe(),(o.metaKey||o.ctrlKey)&&o.key==="c"){if(t.editingCellInfo||document.activeElement.tagName==="INPUT")return;t.selectedCells.length>0?(o.preventDefault(),await It()):t.selectedRowIds.size>0&&(o.preventDefault(),await vt())}if((o.metaKey||o.ctrlKey)&&o.key==="a"){if(t.editingCellInfo||document.activeElement.tagName==="INPUT")return;t.selectedTable&&(o.preventDefault(),ie(o))}if((o.metaKey||o.ctrlKey)&&(o.key==="Delete"||o.key==="Backspace")){if(t.editingCellInfo||document.activeElement.tagName==="INPUT"||document.activeElement.tagName==="TEXTAREA")return;t.selectedTable&&t.selectedTableType==="table"&&(o.preventDefault(),t.selectedColumns.size>0?await re():t.selectedRowIds.size>0?await re():t.selectedCells.length>0&&await St())}})}catch(e){console.error("Init error:",e),ne(e.message)}}Pn();})(); |
There was a problem hiding this comment.
Guard web shortcuts during grid reloads
In the web demo bundle, the document-level Ctrl+A/Ctrl+Delete handler still only checks editing/input state and never checks t.isGridReloading. During a same-table filter/sort/page refetch, loadTableData() keeps the stale grid mounted and sets isGridReloading, but in this web path a user can still press Ctrl+A then Ctrl+Delete to select/delete row IDs from the stale t.gridData; fresh evidence versus the prior core shortcut concern is that core/ui/viewer.js now has the guard, while this generated web bundle (and core/ui/web-viewer.js) does not.
Useful? React with 👍 / 👎.
core/ui/web-viewer.js has its own copy of the global keyboard-shortcut handler (separate from core/ui/viewer.js). Round 4 guarded viewer.js but not this web-demo entry, so in the web bundle Ctrl+A then Ctrl+Delete could still select/delete rows from the stale gridData during a refetch. Apply the same state.isGridReloading guard to the web-demo select-all and delete shortcuts. Regenerated the web bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e8e651b055
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!isSameTableGrid) { | ||
| showLoading(); | ||
| } |
There was a problem hiding this comment.
Block BLOB drops while stale grid is visible
When this branch skips showLoading() for same-table refetches, the old .data-cell elements remain in the DOM. I checked core/ui/modules/dnd.js and its drop path is registered directly on #gridContainer without checking state.isGridReloading, so during a filter/sort/page reload a user can drag a file onto a stale cell and upload it to the old row/column while the requested result set is about to replace it. Previously the spinner removed those drop targets; please gate drag/drop on the reload flag or disable drop targets during the refetch.
Useful? React with 👍 / 👎.
dnd.js registers dragover/drop on #gridContainer. The flicker fix keeps stale .data-cell elements mounted during a same-table refetch, so a file dropped on a cell mid-reload uploaded to the old row/column that the incoming result set was about to replace. Guard onDrop and onDragOver on state.isGridReloading (no drop target offered, drops ignored, while a reload is in flight). dnd.js is shared by both the extension (viewer.js) and the web demo (web-viewer.js), so this covers both. Audited the rest of the interaction surface: no paste or contextmenu handlers exist, so click + keyboard + drag/drop is the full set of stale-grid entry points. Regenerated both bundles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Press Enter in the global or a column filter to jump between cells whose displayed text contains the active term, cycling with Enter/Shift+Enter, a current/total counter, and an outlined active-match cell. Initial Enter applies the filter (one fetch); subsequent presses cycle locally without re-querying. Match navigation resets on sort/page/page-size/filter-text/date-format changes. Review hardening (Gemini + 4 Codex rounds): fixed the btnApplyFilter MouseEvent-as-direction crash; guarded the toolbar filter against concurrent reloads (state.isGridReloading); preventDefault + IME (isComposing) handling on Enter; String() around formatter output in match scan; failed/superseded filter-submit lifecycle (loadTableData returns success; only a fully-applied load navigates, failures revert for retry); pinned active-match z-index; removed dead filterTimer. Composes with #498/#499/#501. tsc + 454 unit tests green. Co-authored-by: yukina3230 <75545944+yukina3230@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks so much for this, @yukina3230! 🙏 Killing the spinner flash on filter/sort/page refetches is a genuinely nice UX upgrade — the grid staying put feels much smoother. Merged into Heads up: I layered some follow-up hardening on top for the concurrency edges that keeping the grid mounted surfaced (avoiding interaction with the stale grid mid-refetch, overlapping loads, etc.), but the core idea and approach are all yours. Really appreciate the contribution! |
Description
innerHTML = '') and flashing a spinner before new data is fetched during filter/sort/search actions.renderDataGrid()now atomically swaps the data once it arrives.Type of Change
Checklist
npm testpasses locallynpm run buildcompletes without errorstextContentrendering, strict CSP)Screenshots
Before:
before_flicker.webm
After:
after_flicker.webm
Summary by CodeRabbit