feat(ui): refresh button on all list pages#165
Conversation
No way to refresh a list without F5. Adds a shared components/RefreshButton.vue and wires it into every list page: the 12 registers plus Reviews and Inbox. Each view gets a reload() that re-fetches its list (and stats) with a dedicated 'refreshing' state, so only the button spins and scroll/ selection/filters are preserved — no full SPA reload. Dashboard deferred (summary page, loads on nav). Closes #164.
unidoc-alip
left a comment
There was a problem hiding this comment.
Clean, mechanical rollout of a shared RefreshButton.vue wired into 14 list views, matching issue #164's scope exactly. Verification turned up one real must-fix: in four views, reload() routes through a loading-toggling helper, which brings back the exact full-list-disappears flicker the PR is meant to eliminate. Also flagging an unaddressed gap (button unreachable after a failed initial load, on 12 of 14 views) and a minor duplicate-fetch nit. No security concerns — this is pure UI re-fetch wiring, no new endpoints or unescaped content.
1. [must-fix] web/src/views/CorrectiveActions.vue line 571
reload() calls await loadAll(), but loadAll() itself does loading.value = true / loading.value = false around the fetch. The template is v-if="loading" → ListSkeleton / v-else-if="error" / v-else → main content (which is where RefreshButton and the list live). So every refresh click flips loading back to true, unmounts the entire list + the RefreshButton itself, and shows the skeleton in its place — the same "list disappears, scroll resets" behavior the PR is meant to eliminate. The other views affected by the identical pattern: Incidents.vue (loadAll at line 644), Objectives.vue (loadAll at line 623), Programs.vue (loadAll at line 342).
Suggested fix — extract the fetch body out of loadAll() so reload() can call it without touching loading:
async function loadAll() {
loading.value = true
error.value = null
try {
await fetchAll()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
async function fetchAll() {
await Promise.all([loadActions(), loadStats()])
}
async function reload() {
refreshing.value = true
try {
await fetchAll()
} finally {
refreshing.value = false
}
}loadActions()/loadStats() already have their own try/catch that sets error.value, so error handling on refresh is unaffected. Apply the same split in Incidents.vue (fetchAll = Promise.all([loadIncidents(), loadStats()])), Objectives.vue (fetchAll = the api.getPrograms() + loadObjectives() body), and Programs.vue (extract the api.getPrograms() call into a fetchPrograms() that reload() can call directly).
2. [should-fix] web/src/views/Assets.vue line 10
The template gates on v-if="loading" / v-else-if="error" / v-else (main content), and RefreshButton only lives in the v-else (success) branch. So if the initial list load fails, the user lands on the error screen with no RefreshButton in sight — the only way to retry is still a full F5, which is exactly the problem issue #164 was filed to fix. This affects every view using this same three-way template split: Assets, Audit, CorrectiveActions, Incidents, Legal, Objectives, Programs, Reviews, Risks, Suppliers, Systems, and Inbox (Changes.vue and Tasks.vue are fine — they use toast-based errors instead of a blocking error screen, so the button stays visible there).
Suggested fix — add a retry control to the error branch:
<div v-else-if="error" class="max-w-5xl mx-auto px-8 py-12">
<div class="bg-red-950/40 border border-red-900/50 rounded-lg p-6 text-red-300 text-sm flex items-center justify-between gap-4">
<span>{{ error }}</span>
<RefreshButton :loading="refreshing" @refresh="reload" />
</div>
</div>This alone isn't sufficient: none of Assets, Legal, Risks, Suppliers, Systems, Audit, or Inbox's primary loaders ever reset error.value = null on entry, so a successful retry would leave the stale error banner showing forever since v-else-if="error" stays true. Those loaders need error.value = null added at the top of the fetch too (CorrectiveActions/Incidents/Objectives/Programs already do this inside loadAll(), so they're covered by the fix above once fetchAll() is wired to reload()).
3. [nit] web/src/views/Risks.vue line 432
reload() does await loadRisks(); loadAllAdvisories() — but loadRisks() already fires loadAllAdvisories() itself (fire-and-forget) as part of its own body (line 982), so every refresh click triggers two advisory fetches instead of one.
Suggested fix — drop the redundant call since loadRisks() already covers it:
async function reload() {
refreshing.value = true
try {
await loadRisks()
} finally {
refreshing.value = false
}
}…pe advisory - reload() no longer calls the loading-toggling loadAll(); extracted fetchAll() in CorrectiveActions/Incidents/Objectives/Programs so refresh updates the list in place instead of flipping the whole view (and the button) to the skeleton. - RefreshButton added to the error branch of the 12 error-screen views, and reload() now resets error.value + catches, so a failed initial load is recoverable without F5 and a successful retry clears the banner. - Risks reload() drops the redundant loadAllAdvisories() (loadRisks already fires it) — no more double advisory fetch per refresh.
|
Thanks for the thorough review @unidoc-alip — all three addressed: 1. [must-fix] loadAll() skeleton flicker on refresh. Fixed exactly as suggested. Extracted a 2. [should-fix] No retry after a failed initial load. Added 3. [nit] Risks double advisory fetch.
|
Closes #164 — there's no way to refresh a list without F5 (which reloads the whole SPA and loses scroll/selection/filters).
Adds a shared
components/RefreshButton.vueand wires it into every list page:Each view gets a
reload()that re-fetches its list (the register loaders already cascade to stats) using a dedicatedrefreshingref — the icon spins, the button disables, and the list updates in place. No full reload, no auth round-trip, scroll/selection/filters preserved.Dashboard is intentionally deferred — it's a summary that loads on navigation, not a stale-list surface. Easy follow-up if wanted.
just build-webgreen.