feat(admin): open controller-link settings entries from the settings index
A settings entry with a controller opens that controller's route from its card (settingsPath); singleton cards still open /settings/<code>. On a linked controller with no navigation entry of its own the rail marks Settings current, the breadcrumbs read Settings > entry label (plus the record title on record and create routes) and the page title uses the entry label. The settings docs describe the admin behaviour; the embedded admin shell is rebuilt.
This commit is contained in:
@@ -3,7 +3,7 @@ import { controllerIdFromPath } from './controllerRoutes'
|
||||
import { t } from './i18n'
|
||||
import { recordCrumb } from '../state/useBreadcrumbs'
|
||||
import { activeEntry } from '../state/useNavigation'
|
||||
import { settingsEntry } from '../state/useSettings'
|
||||
import { settingsEntry, settingsLinkFor } from '../state/useSettings'
|
||||
|
||||
const PRODUCT_TITLE = 'SummerCMS'
|
||||
|
||||
@@ -28,7 +28,7 @@ export function pageIndicator(route: RouteLocationNormalizedLoaded): string {
|
||||
const plugin = activeEntry(String(route.params.vendor ?? ''), String(route.params.plugin ?? ''))
|
||||
const controller = controllerIdFromPath(route.path)
|
||||
const section = plugin?.sideMenu.find((item) => item.controller === controller)
|
||||
const controllerLabel = section?.label ?? plugin?.label ?? ''
|
||||
const controllerLabel = section?.label ?? plugin?.label ?? settingsLinkFor(controller)?.label ?? ''
|
||||
if (route.name === 'create' || route.name === 'record' || route.name === 'preview') {
|
||||
return recordCrumb.value || controllerLabel
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import { controllerIdFromPath, controllerPath } from '../../app/controllerRoutes
|
||||
import { t } from '../../app/i18n'
|
||||
import { recordCrumb } from '../../state/useBreadcrumbs'
|
||||
import { activeEntry, firstControllerPath } from '../../state/useNavigation'
|
||||
import { settingsEntry } from '../../state/useSettings'
|
||||
import { settingsEntry, settingsLinkFor, settingsPath } from '../../state/useSettings'
|
||||
|
||||
// Header breadcrumbs (design: Top header): plugin, controller and record
|
||||
// labels, e.g. "Demo › Widgets › Blue widget". Parent crumbs are muted
|
||||
// links; the current crumb uses the text colour at 600 with
|
||||
// aria-current="page". Separators are 14px chevron-right icons.
|
||||
// aria-current="page". Separators are 14px chevron-right icons. A controller
|
||||
// opened from a settings link entry, with no navigation entry of its own,
|
||||
// reads "Settings › <entry label>".
|
||||
interface Crumb {
|
||||
label: string
|
||||
to?: string
|
||||
@@ -29,11 +31,22 @@ const crumbs = computed<Crumb[]>(() => {
|
||||
return out
|
||||
}
|
||||
const plugin = activeEntry(String(route.params.vendor ?? ''), String(route.params.plugin ?? ''))
|
||||
const id = controllerIdFromPath(route.path)
|
||||
if (!plugin) {
|
||||
return []
|
||||
const link = settingsLinkFor(id)
|
||||
if (!link) {
|
||||
return []
|
||||
}
|
||||
const out: Crumb[] = [
|
||||
{ label: t('backend::lang.nav.settings'), to: '/settings' },
|
||||
{ label: link.label, to: settingsPath(link) },
|
||||
]
|
||||
if ((route.name === 'record' || route.name === 'create') && recordCrumb.value !== '') {
|
||||
out.push({ label: recordCrumb.value })
|
||||
}
|
||||
return out
|
||||
}
|
||||
const out: Crumb[] = [{ label: plugin.label, to: firstControllerPath(plugin) ?? undefined }]
|
||||
const id = controllerIdFromPath(route.path)
|
||||
const section = plugin.sideMenu.find((item) => item.controller === id) ?? null
|
||||
if (section) {
|
||||
out.push({ label: section.label, to: controllerPath(section.controller) ?? undefined })
|
||||
|
||||
@@ -3,9 +3,10 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { PanelLeftOpen, Settings, Sun } from '@lucide/vue'
|
||||
import { t } from '../../app/i18n'
|
||||
import { controllerIdFromPath } from '../../app/controllerRoutes'
|
||||
import { iconFor } from '../../app/icons'
|
||||
import { activeEntry, firstControllerPath, railEntries } from '../../state/useNavigation'
|
||||
import { hasSettings } from '../../state/useSettings'
|
||||
import { hasSettings, settingsLinkFor } from '../../state/useSettings'
|
||||
import { useSidebar } from '../../state/useSidebar'
|
||||
import SectionFlyout from './SectionFlyout.vue'
|
||||
|
||||
@@ -25,8 +26,15 @@ const { collapsed, narrow, setCollapsed } = useSidebar()
|
||||
const active = computed(() => activeEntry(String(route.params.vendor ?? ''), String(route.params.plugin ?? '')))
|
||||
|
||||
// Ustawienia (D-21) is pinned to the bottom and shown only when the admin may
|
||||
// manage at least one settings page.
|
||||
const inSettings = computed(() => route.path === '/settings' || route.path.startsWith('/settings/'))
|
||||
// manage at least one settings page. It is also current on a controller a
|
||||
// settings entry links to, unless that controller has its own navigation
|
||||
// entry (then its plugin stays highlighted).
|
||||
const inSettings = computed(
|
||||
() =>
|
||||
route.path === '/settings' ||
|
||||
route.path.startsWith('/settings/') ||
|
||||
(active.value === null && settingsLinkFor(controllerIdFromPath(route.path)) !== null),
|
||||
)
|
||||
|
||||
const items = computed(() =>
|
||||
railEntries.value.map((entry) => ({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Settings pages the admin may manage (D-21), server-filtered like the
|
||||
// navigation. The rail shows "Ustawienia" only when this list is non-empty.
|
||||
// An entry is either a singleton settings form (/settings/<code>) or a link
|
||||
// that opens an admin controller (its `controller` is set).
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
import { api } from '../api/client'
|
||||
import { controllerPath } from '../app/controllerRoutes'
|
||||
import type { SettingsEntry } from '../api/types'
|
||||
|
||||
const entries = ref<SettingsEntry[]>([])
|
||||
@@ -24,6 +27,25 @@ export function settingsEntry(code: string): SettingsEntry | null {
|
||||
return entries.value.find((entry) => entry.code === code) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a settings entry opens: its controller for a link entry (the index
|
||||
* when the ID is malformed), otherwise its singleton form.
|
||||
*/
|
||||
export function settingsPath(entry: SettingsEntry): string {
|
||||
if (entry.controller) {
|
||||
return controllerPath(entry.controller) ?? '/settings'
|
||||
}
|
||||
return `/settings/${encodeURIComponent(entry.code)}`
|
||||
}
|
||||
|
||||
/** The link entry that opens the given controller, if any. */
|
||||
export function settingsLinkFor(controllerId: string | null): SettingsEntry | null {
|
||||
if (!controllerId) {
|
||||
return null
|
||||
}
|
||||
return entries.value.find((entry) => entry.controller !== '' && entry.controller === controllerId) ?? null
|
||||
}
|
||||
|
||||
/** Entries grouped by category, categories in first-seen order, by order. */
|
||||
export const settingsGroups = computed(() => {
|
||||
const sorted = entries.value
|
||||
@@ -43,5 +65,5 @@ export const settingsGroups = computed(() => {
|
||||
})
|
||||
|
||||
export function useSettings() {
|
||||
return { settings, hasSettings, settingsGroups, loadSettings, settingsEntry }
|
||||
return { settings, hasSettings, settingsGroups, loadSettings, settingsEntry, settingsPath, settingsLinkFor }
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { t } from '../app/i18n'
|
||||
import { iconFor } from '../app/icons'
|
||||
import { settingsGroups } from '../state/useSettings'
|
||||
import { settingsGroups, settingsPath } from '../state/useSettings'
|
||||
|
||||
// Settings index (D-21): every permitted settings page, grouped by category,
|
||||
// with its icon, label and description. The server filters the list.
|
||||
// with its icon, label and description. The server filters the list. A
|
||||
// singleton entry opens its settings form; a link entry opens its controller.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,7 +28,7 @@ import { settingsGroups } from '../state/useSettings'
|
||||
<RouterLink
|
||||
v-for="entry in group.entries"
|
||||
:key="entry.code"
|
||||
:to="`/settings/${encodeURIComponent(entry.code)}`"
|
||||
:to="settingsPath(entry)"
|
||||
:data-settings="entry.code"
|
||||
class="flex items-start gap-3.5 rounded-card border border-border bg-surface p-5 text-text no-underline shadow-card hover:bg-hover"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user