diff --git a/admin/src/app/pageTitle.ts b/admin/src/app/pageTitle.ts index fedcf9d..9cb9d3e 100644 --- a/admin/src/app/pageTitle.ts +++ b/admin/src/app/pageTitle.ts @@ -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 } diff --git a/admin/src/components/shell/Breadcrumbs.vue b/admin/src/components/shell/Breadcrumbs.vue index 8097a91..9d80eea 100644 --- a/admin/src/components/shell/Breadcrumbs.vue +++ b/admin/src/components/shell/Breadcrumbs.vue @@ -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 › ". interface Crumb { label: string to?: string @@ -29,11 +31,22 @@ const crumbs = computed(() => { 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 }) diff --git a/admin/src/components/shell/PluginRail.vue b/admin/src/components/shell/PluginRail.vue index e373301..384d392 100644 --- a/admin/src/components/shell/PluginRail.vue +++ b/admin/src/components/shell/PluginRail.vue @@ -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) => ({ diff --git a/admin/src/state/useSettings.ts b/admin/src/state/useSettings.ts index 61a651d..c614dc2 100644 --- a/admin/src/state/useSettings.ts +++ b/admin/src/state/useSettings.ts @@ -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/) 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([]) @@ -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 } } diff --git a/admin/src/views/SettingsIndexView.vue b/admin/src/views/SettingsIndexView.vue index 60a9849..89fe04a 100644 --- a/admin/src/views/SettingsIndexView.vue +++ b/admin/src/views/SettingsIndexView.vue @@ -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.