Skip to content

fix: prevent grid flicker#498

Merged
zknpr merged 9 commits into
zknpr:devfrom
yukina3230:fix/prevent-grid-flicker
Jun 21, 2026
Merged

fix: prevent grid flicker#498
zknpr merged 9 commits into
zknpr:devfrom
yukina3230:fix/prevent-grid-flicker

Conversation

@yukina3230

@yukina3230 yukina3230 commented Jun 21, 2026

Copy link
Copy Markdown

Description

  • Prevents the grid from clearing (innerHTML = '') and flashing a spinner before new data is fetched during filter/sort/search actions.
  • Keeps existing rows visible during the fetch; renderDataGrid() now atomically swaps the data once it arrives.
  • Only displays the loading spinner on the initial load or when dealing with an empty table.
  • Optimizes performance by reducing redundant DOM operations (removes one clear/render cycle per action).

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Documentation update
  • Refactor / chore (no functional change)

Checklist

  • npm test passes locally
  • npm run build completes without errors
  • I followed the project's coding standards (parameterized SQL, escaped identifiers, textContent rendering, strict CSP)
  • I added or updated tests for my changes
  • I updated the documentation (README / CHANGELOG / CLAUDE.md) where relevant
  • My commits follow Conventional Commits

Screenshots

Before:

before_flicker.webm

After:

after_flicker.webm

Summary by CodeRabbit

  • Bug Fixes
    • Reduced loading-spinner and grid flicker by better distinguishing same-table refreshes from table switches.
    • Improved scroll position preservation by capturing/restoring the grid position more reliably right before re-rendering when enabled.
    • Streamlined data fetching by reusing computed column selections for count and results queries.

@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

@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.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 53c2b496-96f0-44b4-b48a-7cce47b0ee1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

In loadTableData, a hasRenderedGrid flag is cached by comparing the current table to state.renderedTable to suppress showLoading() and scroll-capture flicker on same-table refetches. After rendering, state.renderedTable is updated to track the newly loaded table. A columnNames variable is extracted once and reused for both count and data query options, removing a redundant later derivation.

Changes

loadTableData flicker reduction, grid state tracking, and columnNames dedup

Layer / File(s) Summary
Grid render state field
core/ui/modules/state.js
Adds state.renderedTable property (initialized to null) to track which table grid is currently rendered, supporting distinction between same-table refetches and table switches.
loadTableData spinner and scroll flicker suppression
core/ui/modules/grid-data.js
Caches hasRenderedGrid at function entry by checking if state.renderedTable matches the current state.selectedTable; showLoading() is called only when the grid was not already rendered; scroll-position capture is deferred until post-fetch and depends on saveScrollPosition flag; state.renderedTable is set after grid render to support the cached comparison on next load.
columnNames deduplication
core/ui/modules/grid-data.js
Extracts columnNames from state.tableColumns.map(...) into a local variable before constructing countOptions, and removes the later redundant re-derivation of the same array for the data query.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A bunny cached grids with great care,
To banish the spinner's blank stare!
Now renderedTable knows,
What state column it shows,
And columnNames lives once, beyond compare! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: prevent grid flicker' directly addresses the main objective of this PR, which is to eliminate visual flicker in the data grid during filter, sort, and search operations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread core/ui/modules/grid-data.js Outdated
Comment on lines +45 to +47
// 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'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;
Suggested change
// 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'));

Comment thread core/ui/modules/grid-data.js Outdated
// 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')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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>
@zknpr

zknpr commented Jun 21, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +66 to +68
if (!isSameTableGrid) {
showLoading();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>
@zknpr

zknpr commented Jun 21, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread core/ui/modules/grid-events.js Outdated
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@zknpr

zknpr commented Jun 21, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread core/ui/modules/grid-data.js Outdated
Comment on lines 183 to 184
if (showSpinner && loadToken === activeLoadToken) {
state.isLoadingData = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +81 to +82
if (!isSameTableGrid) {
showLoading();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge 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 👍 / 👎.

Comment on lines 159 to +162
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread core/ui/modules/grid-data.js Outdated
Comment on lines 183 to 184
if (showSpinner && loadToken === activeLoadToken) {
state.isLoadingData = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread core/ui/modules/grid-data.js Outdated
Comment on lines +108 to +109
const totalRecordCount = await backendApi.fetchTableCount(requestedTable, countOptions);
if (loadToken !== activeLoadToken) return; // a newer load superseded this one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@zknpr zknpr changed the base branch from main to dev June 21, 2026 19:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread core/ui/modules/grid-data.js Outdated
Comment on lines 194 to 195
if (!isSuperseded()) {
state.isLoadingData = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread core/ui/modules/grid-events.js Outdated
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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();})();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +92 to +94
if (!isSameTableGrid) {
showLoading();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@zknpr zknpr merged commit c0265e1 into zknpr:dev Jun 21, 2026
1 of 2 checks passed
zknpr pushed a commit that referenced this pull request Jun 21, 2026
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>
@zknpr

zknpr commented Jun 21, 2026

Copy link
Copy Markdown
Owner

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 dev and it'll ship in the next release.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants