From 9d7c21d80075c7c771ae3bfa3e2a6e745c16ba16 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 12 Oct 2026 21:43:07 +0200 Subject: [PATCH] fix(17): WR-07 open a singleton's record from its list route A settings link to a singleton controller only worked on a plain left click; a new tab landed on the list route, which answers 404, and every non-singleton link paid an extra /singleton request. The settings index now links like any controller link, and the list view opens the controller's one record when its list answers 404 and the singleton route answers an id. --- admin/src/views/ListView.vue | 29 ++++++++++++++++- admin/src/views/SettingsIndexView.vue | 44 ++++--------------------- admin/tests/form/SingletonForm.test.ts | 45 +++++++++++++++++++++----- docs/backend/admin-controllers.md | 2 +- docs/backend/settings.md | 2 +- 5 files changed, 73 insertions(+), 49 deletions(-) diff --git a/admin/src/views/ListView.vue b/admin/src/views/ListView.vue index 1673da8..d06c182 100644 --- a/admin/src/views/ListView.vue +++ b/admin/src/views/ListView.vue @@ -31,7 +31,9 @@ import { showToast } from '../state/useToasts' // A restored query the admin did not ask for never breaks the list: if its // load fails for any reason, the stored query is forgotten and the plain list // loads once instead (WR-02). A failed URL query keeps the error, with a reset -// back to the plain list next to it. +// back to the plain list next to it. A singleton controller (D-11) has no +// list: its list route answers 404, and the view opens the controller's one +// record instead (a settings link opened in a new tab, a bookmark). const SEARCH_DEBOUNCE = 300 const route = useRoute() @@ -176,6 +178,10 @@ async function loadList(restored = false): Promise { void router.replace({ path: route.path, query: {} }) return } + if (!result?.data && result?.response?.status === 404 && (await openSingleton(current))) { + // Keeps loading while the record opens; no error flashes. + return + } rows.value = result?.data?.data ?? [] meta.value = result?.data?.meta ?? null listFailed.value = !result?.data @@ -192,6 +198,27 @@ async function loadList(restored = false): Promise { } } +/** + * Opens the one record of a singleton controller (D-11) in place of its list, + * which answers 404. Reports whether it took over: false when the controller + * is not a singleton (its 404 stands). + */ +async function openSingleton(current: number): Promise { + try { + const { data } = await api.GET('/{vendor}/{plugin}/{controller}/singleton', { params: { path } }) + if (current !== generation) { + return true + } + if (!data) { + return false + } + await router.replace(`${basePath}/${data.data.id}`) + return true + } catch { + return false + } +} + function fetchList() { return api.GET('/{vendor}/{plugin}/{controller}', { params: { path, query: query.value } }) } diff --git a/admin/src/views/SettingsIndexView.vue b/admin/src/views/SettingsIndexView.vue index f47e53b..77e3475 100644 --- a/admin/src/views/SettingsIndexView.vue +++ b/admin/src/views/SettingsIndexView.vue @@ -1,9 +1,5 @@