Enable in-place column sync, caching & reload#499
Conversation
closes #346 Avoid full-page reloads after column operations by adding in-place column syncing, caching, and targeted refresh behavior. Adds cachedImageInfo/cachedAnnotations, methods to reload and refresh columns (reloadColumns, refreshFromCache), helpers to sync TPEN.activeProject from local state (syncProjectColumnsFromExisting, updateColumnsFromResponse, generateColumnId, getProjectPage, buildAssignedAnnotations), and local mutations for columns (create/extend/merge/clearColumnLocal). Column label handling now preserves rawLabel and maps autogenerated labels to "Unnamed" consistently. Server responses are parsed and used to update state when available; otherwise local fallbacks are applied. Also dispatches a "tpen-columns-updated" event and adds a listener in ColumnSelector to refresh only when updates target the current page.
There was a problem hiding this comment.
Pull request overview
This PR enables in-place column synchronization after column operations by replacing full-page reloads with targeted refresh behavior. It introduces caching mechanisms, event-driven updates, and local state mutations to keep the UI synchronized without interrupting the user experience.
Changes:
- Replaces
window.location.reload()withreloadColumns()after column operations (merge, extend, create, clear) - Adds caching of image info and annotations (
cachedImageInfo,cachedAnnotations) for in-place re-rendering - Implements column synchronization helpers to keep
TPEN.activeProjectaligned with local state - Dispatches
tpen-columns-updatedevents and adds a listener in ColumnSelector to refresh only when the current page is affected
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| interfaces/manage-columns/index.js | Adds caching, column reload logic, local mutation methods, and replaces full-page reloads with targeted refresh |
| components/create-column/index.js | Mirrors the same caching and reload changes as manage-columns for consistency |
| components/column-selector/index.js | Adds event listener for column updates and refreshes selector only when the current page is affected |
Comments suppressed due to low confidence (10)
interfaces/manage-columns/index.js:444
- The parameter
labelis declared but never used in the function body. Consider removing it or clarifying its intended purpose if it should be incorporated into the generated ID.
generateColumnId(label) {
return globalThis.crypto?.randomUUID?.() ?? `column-${Date.now()}-${Math.random().toString(16).slice(2)}`
}
interfaces/manage-columns/index.js:705
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.mergeColumnsLocal(newLabel, columnLabelsToMerge)
}
interfaces/manage-columns/index.js:807
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.extendColumnLocal(columnToExtend, annotationIdsToAdd)
}
interfaces/manage-columns/index.js:1030
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.createColumnLocal(columnLabel, selectedIDs)
}
interfaces/manage-columns/index.js:1056
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.clearColumnsLocal()
}
components/create-column/index.js:371
- The parameter
labelis declared but never used in the function body. Consider removing it or clarifying its intended purpose if it should be incorporated into the generated ID.
generateColumnId(label) {
return globalThis.crypto?.randomUUID?.() ?? `column-${Date.now()}-${Math.random().toString(16).slice(2)}`
}
components/create-column/index.js:631
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.mergeColumnsLocal(newLabel, columnLabelsToMerge)
}
components/create-column/index.js:732
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.extendColumnLocal(columnToExtend, annotationIdsToAdd)
}
components/create-column/index.js:959
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.createColumnLocal(columnLabel, selectedIDs)
}
components/create-column/index.js:985
- The empty catch block silently swallows parsing errors. Consider logging the error or providing a fallback message to aid debugging if JSON parsing fails unexpectedly.
let data = null
try { data = await res.json() } catch {}
if (!this.updateColumnsFromResponse(data)) {
this.clearColumnsLocal()
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@static-reviewer |
Static Review CommentsBranch:
Critical Issues 🔴Issue 1: Response body consumed twice —
|
|
If copilot is happy with this, so am I |
Make column label checks synchronous and simplify ID/label handling across create-column and manage-columns. Key changes: - Remove unnecessary async from columnLabelCheck and stop awaiting it. - Use Array.some instead of find to check assigned annotation IDs. - Improve cachedAnnotations check to ensure non-empty arrays. - Generate column IDs without unused label param; keep robust UUID fallback. - Remove legacy id normalization; rely on API response handling. - Fix merge UI index handling (use label index) and stop filtering out empty indexes when merging. - Consider rawLabel when detecting duplicate labels. - Parse response bodies directly (const data = await res.json()) and reset workspace UI (uncheck checkboxes and call handleModeChange) after merge/extend operations to avoid stale state. - When extending locally, map to originalColumnLabels to preserve labels. These edits address correctness issues, avoid stale UI/annotation state, and simplify logic for maintainability.
There was a problem hiding this comment.
Manual Testing Comments
- Clear All Columns shows error toast and success toast. It seems to succeed, but the interface does not update. This is Major Issue 1 from the static review.
- Once I select 'Extend Column Mode' I cannot select lines. Even after I unselect it. This is a pre-existing issue not introduced by this PR (it currently happens on
mainas well).
Static Review Comments
| Category | Issues Found |
|---|---|
| 🔴 Critical | 0 |
| 🟠 Major | 1 |
| 🟡 Minor | 3 |
| 🔵 Suggestions | 1 |
Critical Issues 🔴
None.
Major Issues 🟠
Issue 1: res.json() called after res.ok check but response may not have JSON body
File: components/create-column/index.js:626,729,958,983 and interfaces/manage-columns/index.js:700,804,1029,1054
Category: Unhandled Error
Problem:
After a successful API response, the code calls const data = await res.json() to extract column data from the response. If the server returns a 204 No Content, an empty body, or non-JSON body, res.json() will throw, and the catch block will show a generic error toast — even though the operation actually succeeded (the success toast was already dispatched).
This is most likely to happen with the DELETE /clear-columns endpoint, which may return an empty body on success.
Current Code (e.g., clearAllSelections):
if (!res.ok) throw new Error(`Server error: ${res.status}`)
TPEN.eventDispatcher.dispatch("tpen-toast", {
status: "info", message: 'All columns cleared successfully.'
})
const data = await res.json() // Could throw on empty body
if (!this.updateColumnsFromResponse(data)) {
this.clearColumnsLocal()
}
await this.reloadColumns()Suggested Fix:
if (!res.ok) throw new Error(`Server error: ${res.status}`)
TPEN.eventDispatcher.dispatch("tpen-toast", {
status: "info", message: 'All columns cleared successfully.'
})
let data = null
try { data = await res.json() } catch { /* empty response body */ }
if (!data || !this.updateColumnsFromResponse(data)) {
this.clearColumnsLocal()
}
await this.reloadColumns()How to Verify:
- Clear all columns on a page
- Check network tab — if server returns empty body, verify no error toast appears after the success toast
Minor Issues 🟡
Issue 2: extendColumn() in create-column still uses indexOf(label) — wrong index for duplicate labels
File: components/create-column/index.js:677
Category: Logic Error
Problem:
The mergeColumns() method was correctly updated to use the labelIndex parameter from forEach, but extendColumn() in the same file was not. It still uses columnLabels.indexOf(label), which returns the first occurrence. If two columns have the same display label (e.g., two "Unnamed 1" entries after a label collision), indexOf will always resolve to the first match, making it impossible to select the second one.
The manage-columns/index.js version of extendColumn() (line 752) was correctly updated to use labelIndex. This is an inconsistency between the two files.
Issue 3: createColumn() duplicate check doesn't compare rawLabel — inconsistent with mergeColumns()
File: components/create-column/index.js:929-932 and interfaces/manage-columns/index.js:1000-1003
Category: Logic Error / Inconsistency
Problem:
The mergeColumns() duplicate check was updated to compare against both col.label (display label) and col.rawLabel (server label). However, createColumn() in both files still only checks against col.label. This means a user could create a column with a label that matches an existing column's raw/server label (e.g., "Column 65f1a2b3c4d5e6f7a8b9c0d1"), which would likely cause a server-side conflict or unexpected behavior.
Current Code (both files):
const duplicate = this.existingColumns.some(col => {
const existingLabel = (col.label ?? "").toString().trim()
return existingLabel === columnLabel
})Suggested Fix:
const duplicate = this.existingColumns.some(col => {
const displayLabel = (col.label ?? "").toString().trim()
const rawLabel = (col.rawLabel ?? "").toString().trim()
return displayLabel === columnLabel || rawLabel === columnLabel
})How to Verify:
- Have a column with an auto-generated label (displays as "Unnamed 1" but raw is "Column abc123...")
- Try creating a new column with the raw label string as the title
- Verify the duplicate check catches it
Current Code:
// components/create-column/index.js:662
columnLabels.forEach(label => {
// ...
this.workspaceCleanup.onElement(btn, 'click', () => {
// ...
columnToExtend = columnLabels.indexOf(label) !== -1 ? columnLabels.indexOf(label) : ''Suggested Fix:
// components/create-column/index.js:662
columnLabels.forEach((label, labelIndex) => {
// ...
this.workspaceCleanup.onElement(btn, 'click', () => {
// ...
columnToExtend = labelIndexIssue 4: generateColumnId() accepts a parameter that's never used
File: components/create-column/index.js:369 and interfaces/manage-columns/index.js:442
Category: Dead Code / Code Hygiene
Problem:
The method signature is generateColumnId() with no parameters, but it's called with arguments in several places: this.generateColumnId(rawLabel), this.generateColumnId(newLabel). The passed argument is silently ignored. This isn't a bug (the function works), but it's misleading — a reader might think the label is used for ID generation.
Suggestions 🔵
Suggestion 1: Significant code duplication between create-column and manage-columns
Category: Code Quality / Maintainability
The following methods are nearly identical across both files (~160 lines duplicated):
reloadColumns()getProjectPage()generateColumnId()updateColumnsFromResponse()syncProjectColumnsFromExisting()buildAssignedAnnotations()refreshFromCache()mergeColumnsLocal()extendColumnLocal()createColumnLocal()clearColumnsLocal()columnLabelCheck()
Consider extracting these into a shared mixin or utility module (e.g., utilities/columnManager.js) that both components can import. This would make future bug fixes and feature changes a single-point edit instead of requiring synchronized changes across two files. This is a follow-up concern — not necessarily something for this PR.
If there are significant code changes in response to this review please test those changes. Run the application manually and test or perform internal application tests when applicable.
|
Just trouble with the return type, so I over-engineered the response. It may become a helper util later. |
not perfect, but avoids addressing the issue with duplicate code in these two areas.
thehabes
left a comment
There was a problem hiding this comment.
A couple of quirks that have their own issue, but otherwise functioning as intended.
closes #346
Avoid full-page reloads after column operations by adding in-place column syncing, caching, and targeted refresh behavior. Adds cachedImageInfo/cachedAnnotations, methods to reload and refresh columns (reloadColumns, refreshFromCache), helpers to sync TPEN.activeProject from local state (syncProjectColumnsFromExisting, updateColumnsFromResponse, generateColumnId, getProjectPage, buildAssignedAnnotations), and local mutations for columns (create/extend/merge/clearColumnLocal). Column label handling now preserves rawLabel and maps autogenerated labels to "Unnamed" consistently. Server responses are parsed and used to update state when available; otherwise local fallbacks are applied. Also dispatches a "tpen-columns-updated" event and adds a listener in ColumnSelector to refresh only when updates target the current page.