feat: edit parent navigation from child page

scarlett
NGPixel 4 weeks ago
parent 123a49dff2
commit 634f4b0555
No known key found for this signature in database

@ -98,6 +98,53 @@ async function routes(app: FastifyInstance) {
}
)
/**
* GET THE MENU A PAGE INHERITS
*/
app.get<{ Params: { siteId: string; pageId: string } }>(
'/sites/:siteId/navigation/pages/:pageId/inherited',
{
config: {
permissions: ['manage:navigation']
},
schema: {
summary: 'Get the menu a page inherits',
description:
"The id of the menu this page falls back to while it inherits: the nearest ancestor that overrides one, or the site-wide menu when no ancestor does.\n\nWhat the navigation editor asks so that a page which inherits can edit the sidebar it shows without being opened on the ancestor that owns it. Null when the nearest ancestor hides the sidebar, which leaves nothing to inherit — and nothing to edit. Not the same question as the page's own `navigationId`, which is what the CURRENT mode resolved to.",
tags: ['Navigation'],
params: {
type: 'object',
properties: {
siteId: { type: 'string', format: 'uuid' },
pageId: { type: 'string', format: 'uuid' }
},
required: ['siteId', 'pageId']
},
response: {
200: {
description: 'The inherited menu',
type: 'object',
properties: {
navigationId: {
type: ['string', 'null'],
description:
'The menu this page inherits. Null when the sidebar above it is hidden.'
}
}
}
}
}
},
async (req) => {
return {
navigationId: await WIKI.models.navigation.inheritedNavId(
req.params.siteId,
req.params.pageId
)
}
}
)
/**
* UPDATE NAVIGATION
*/
@ -113,7 +160,7 @@ async function routes(app: FastifyInstance) {
schema: {
summary: 'Set how a page resolves its navigation',
description:
"Records the mode on the tree entry and repoints every descendant that still inherits, stopping at any that overrides or hides in between.\n\nSending `items` stores them as this entry's menu as well — for the home page that is the site-wide menu, which is what every other page inherits. Leaving `items` out changes only the mode.",
'Records the mode on the tree entry and repoints every descendant that still inherits, stopping at any that overrides or hides in between.\n\nSending `items` stores them as the menu the mode resolves to, and leaving them out changes only the mode. With `inherit` that menu belongs to an ancestor — the same one `navigation/pages/{pageId}/inherited` names — so editing a menu from a page that inherits it edits it where it lives, for every page using it; for the home page that is the site-wide menu, which is what every other page inherits by default. Refused when the mode is `inherit` and the sidebar above the page is hidden, since then there is no menu to store items in.',
tags: ['Navigation'],
params: {
type: 'object',

@ -2161,6 +2161,7 @@
"linkPicker.url": "URL",
"navEdit.clearItems": "Clear All Items",
"navEdit.editMenuItems": "Edit Menu Items",
"navEdit.editingInherited": "Inherited menu — shared with every page using it",
"navEdit.emptyMenuText": "Click the Add button to add your first menu item.",
"navEdit.expandByDefault": "Expand by Default",
"navEdit.expandByDefaultHint": "Whether the submenu is already expanded when the page loads.",

@ -108,6 +108,20 @@ class Navigation {
await WIKI.db.delete(navigationTable).where(inArray(navigationTable.id, ids))
}
/** The tree entry a navigation change is addressed to. */
private async getEntry(siteId: string, pageId: string) {
const entries = await WIKI.db
.select()
.from(treeTable)
.where(and(eq(treeTable.id, pageId), eq(treeTable.siteId, siteId)))
.limit(1)
const entry = entries[0]
if (!entry) {
throw new CustomError('navInvalidPage', 'This page does not exist.', 404)
}
return entry
}
/**
* The menu a tree entry falls back to: the nearest ancestor that overrides or hides, or the
* site-wide menu when nothing above it does either.
@ -132,6 +146,20 @@ class Navigation {
return rows.length > 0 ? (rows[0].navigationId ?? null) : siteId
}
/**
* The menu a page inherits the one its sidebar shows while its own mode is `inherit`.
*
* `navigationId` on the entry already answers this for a page that IS inheriting, but only for one:
* the navigation editor asks before anything is saved, so that a page can edit the menu it shows
* without being opened on the ancestor that owns it, and so that it can tell there is one to edit.
*
* Null when the nearest ancestor hides the sidebar, which leaves nothing to inherit.
*/
async inheritedNavId(siteId: string, pageId: string): Promise<string | null> {
const entry = await this.getEntry(siteId, pageId)
return this.ancestorNavId(siteId, entry.folderPath ?? '')
}
/**
* Set how a page decides its sidebar, and optionally the menu itself.
*
@ -139,7 +167,8 @@ class Navigation {
* change alters what descendants inherit every entry below it that is still on `inherit` is
* repointed, stopping at any that overrides or hides in between.
*
* @param items When given, the menu stored against this entry, replacing whatever was there
* @param items When given, the menu the mode resolves to, replacing whatever was there this
* entry's own, or the one it inherits when the mode is `inherit`
*/
async updateNavigation({
siteId,
@ -152,15 +181,7 @@ class Navigation {
mode: NavigationMode
items?: NavigationItem[]
}): Promise<UpdateNavigationResult> {
const entries = await WIKI.db
.select()
.from(treeTable)
.where(and(eq(treeTable.id, pageId), eq(treeTable.siteId, siteId)))
.limit(1)
const entry = entries[0]
if (!entry) {
throw new CustomError('navInvalidPage', 'This page does not exist.', 404)
}
const entry = await this.getEntry(siteId, pageId)
// -> Whatever this change resolves to, `inherit` ultimately falls back to the site menu, and a
// site created before that row existed does not have one yet
@ -173,14 +194,29 @@ class Navigation {
const ownNavId = isSiteRoot ? siteId : entry.id
const fullPath = folderPath ? `${folderPath}.${entry.fileName}` : entry.fileName
const ancestorId = await this.ancestorNavId(siteId, folderPath)
if (items) {
/*
Which menu the items belong to is the mode's answer, not the entry's: a page that inherits
shows a menu belonging to an ancestor, so editing the sidebar from that page edits THAT menu
rather than starting one of its own that nothing would point at. For the root home page the two
are the same id the site-wide menu is what it inherits and what it owns.
*/
const targetNavId = mode === 'inherit' ? ancestorId : ownNavId
if (!targetNavId) {
throw new CustomError(
'navNoInheritedMenu',
'This page inherits a hidden sidebar, so there is no menu to save items to.',
400
)
}
await WIKI.db
.insert(navigationTable)
.values({ id: ownNavId, siteId, items })
.values({ id: targetNavId, siteId, items })
.onConflictDoUpdate({ target: navigationTable.id, set: { items } })
}
const ancestorId = await this.ancestorNavId(siteId, folderPath)
// -> A mode that stops applying below this entry hands its descendants back to the ancestor
const wasCascading = ['override', 'hide'].includes(entry.navigationMode)

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="xbqlfAaGyXIjs7KZfYPqqa" x1="12.617" x2="15.347" y1="5.918" y2="41.614" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0370c8"/><stop offset=".484" stop-color="#036fc5"/><stop offset=".775" stop-color="#036abd"/><stop offset="1" stop-color="#0362b0"/></linearGradient><path fill="url(#xbqlfAaGyXIjs7KZfYPqqa)" d="M6,40V8c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2v32c0,1.1-0.9,2-2,2H8C6.9,42,6,41.1,6,40z"/><linearGradient id="xbqlfAaGyXIjs7KZfYPqqb" x1="12.813" x2="15.105" y1="30.387" y2="37.364" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dae0e8"/><stop offset=".326" stop-color="#d0d7e1"/><stop offset=".873" stop-color="#b6c0cc"/><stop offset="1" stop-color="#afbac7"/></linearGradient><path fill="url(#xbqlfAaGyXIjs7KZfYPqqb)" d="M14,30c-2.209,0-4,1.791-4,4s1.791,4,4,4s4-1.791,4-4S16.209,30,14,30z"/><linearGradient id="xbqlfAaGyXIjs7KZfYPqqc" x1="11.5" x2="16.5" y1="34" y2="34" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#11408a"/><stop offset="1" stop-color="#103f8f"/></linearGradient><path fill="url(#xbqlfAaGyXIjs7KZfYPqqc)" d="M14,31.5c-1.381,0-2.5,1.119-2.5,2.5s1.119,2.5,2.5,2.5s2.5-1.119,2.5-2.5 S15.381,31.5,14,31.5z"/><path fill="#fff" d="M12,11h4c0.552,0,1,0.448,1,1v13c0,0.552-0.448,1-1,1h-4c-0.552,0-1-0.448-1-1V12 C11,11.448,11.448,11,12,11z"/><linearGradient id="xbqlfAaGyXIjs7KZfYPqqd" x1="33.06" x2="34.88" y1="5.988" y2="40.876" gradientUnits="userSpaceOnUse"><stop offset=".212" stop-color="#f44f5a"/><stop offset=".698" stop-color="#ee3d4a"/><stop offset="1" stop-color="#e52030"/></linearGradient><path fill="url(#xbqlfAaGyXIjs7KZfYPqqd)" d="M26,40V8c0-1.1,0.9-2,2-2h12c1.1,0,2,0.9,2,2v32c0,1.1-0.9,2-2,2H28C26.9,42,26,41.1,26,40z"/><linearGradient id="xbqlfAaGyXIjs7KZfYPqqe" x1="32.813" x2="35.105" y1="30.387" y2="37.364" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dae0e8"/><stop offset=".326" stop-color="#d0d7e1"/><stop offset=".873" stop-color="#b6c0cc"/><stop offset="1" stop-color="#afbac7"/></linearGradient><path fill="url(#xbqlfAaGyXIjs7KZfYPqqe)" d="M34,30c-2.209,0-4,1.791-4,4s1.791,4,4,4s4-1.791,4-4S36.209,30,34,30z"/><path fill="#b31523" d="M34,31.5c-1.381,0-2.5,1.119-2.5,2.5s1.119,2.5,2.5,2.5s2.5-1.119,2.5-2.5S35.381,31.5,34,31.5z"/><path fill="#fff" d="M32,11h4c0.552,0,1,0.448,1,1v13c0,0.552-0.448,1-1,1h-4c-0.552,0-1-0.448-1-1V12 C31,11.448,31.448,11,32,11z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -128,6 +128,17 @@ const { t } = useI18n()
const state = reactive({
mode: 'inherit',
/**
* The menu this page inherits, resolved on open for any page that is not the root see the
* `inherited` endpoint.
*
* Asked of the server rather than read off `pageStore.navigationId`, which only answers this while
* the SAVED mode is `inherit`: on a page that currently overrides, picking Inherit here has to point
* at the ancestor's menu, and the ancestor holding it is not something the page knows.
*
* Null means nothing to inherit: the sidebar above this page is hidden.
*/
inheritedNavId: null,
loading: 0
})
@ -138,8 +149,9 @@ const isRoot = computed(() => {
})
const canEditMenuItems = computed(() => {
// -> Inheriting edits the menu this page shows where it lives, which needs there to be one
if (!isRoot.value && state.mode === 'inherit') {
return false
return Boolean(state.inheritedNavId)
}
return ['inherit', 'override', 'overrideExact'].includes(state.mode)
})
@ -157,8 +169,39 @@ watch(
// METHODS
/**
* Resolves the menu this page inherits, so that Inherit can offer to edit it.
*
* Quiet on failure: the mode itself is what this menu is for and can still be set, so a resolution
* that did not come back only leaves the Edit Menu Items button out.
*/
async function loadInheritedNav() {
// -> Deliberately outside `state.loading`, which is what the Save button spins on: this runs as the
// menu opens, and a spinner there would read as a save in flight
try {
const resp = await API_CLIENT.get(
`sites/${siteStore.id}/navigation/pages/${pageStore.id}/inherited`
).json()
state.inheritedNavId = resp?.navigationId ?? null
// -> A row appearing under the list makes the menu taller than the popup it was measured for
nextTick(() => {
props.updatePositionHandler()
})
} catch (err) {
console.warn(`Could not resolve the inherited navigation menu: ${apiErrorMessage(err)}`)
}
}
function startEditing() {
siteStore.$patch({ overlay: 'NavEdit', overlayOpts: { mode: state.mode } })
siteStore.$patch({
overlay: 'NavEdit',
overlayOpts: {
mode: state.mode,
// -> A menu this page does not own: only Inherit edits one, and only away from the root, where
// inheriting and owning are the same menu. See NavEditOverlay's `navId`.
...(!isRoot.value && state.mode === 'inherit' && { navId: state.inheritedNavId })
}
})
props.menuHideHandler()
}
@ -197,5 +240,8 @@ async function save() {
onMounted(() => {
state.mode = pageStore.navigationMode
if (!isRoot.value) {
loadInheritedNav()
}
})
</script>

@ -3,6 +3,13 @@
<w-header class="card-header px-4 py-2">
<w-icon name="img:/_assets/icons/fluent-sidebar-menu.svg" left size="md" />
<span>{{ t(`navEdit.editMenuItems`) }}</span>
<!--
Which menu is on screen, when it is not this page's own: an inherited menu is shared with every
page that falls back to it, so a change here is not local to the page it was made from.
-->
<span class="ml-3 text-caption opacity-80" v-if="isEditingInherited">
{{ t('navEdit.editingInherited') }}
</span>
<w-space />
<transition name="syncing">
<w-spinner class="mr-2" v-show="state.loading > 0" color="accent" size="24px" />
@ -554,11 +561,20 @@ const visibilityOptions = [
/**
* The menu being edited.
*
* The home page edits the site-wide menu the one every other page inherits which is why it goes
* through its resolved id rather than its own. Any other page owns a menu keyed by its own id, which
* the server creates on the first save.
* `overlayOpts.navId` is a menu this page does not own: the one it inherits, resolved by the nav menu
* that opened this editor, so that the sidebar a page shows can be edited from that page rather than
* only from the ancestor holding it. Saving writes it back where it lives see `save()`.
*
* Otherwise the page's own menu. The home page edits the site-wide menu the one every other page
* inherits which is why it goes through its resolved id rather than its own. Any other page owns a
* menu keyed by its own id, which the server creates on the first save.
*/
const navId = computed(() => (pageStore.isHome ? pageStore.navigationId : pageStore.id))
const navId = computed(() => {
return siteStore.overlayOpts.navId ?? (pageStore.isHome ? pageStore.navigationId : pageStore.id)
})
/** Whether the menu on screen is an inherited one, which is shared with every page using it. */
const isEditingInherited = computed(() => Boolean(siteStore.overlayOpts.navId))
/**
* Whether the link being edited is a parent one the sidebar draws as a submenu.
@ -777,8 +793,12 @@ async function save() {
}
}
// -> The mode goes with the items: saving a menu for a page that only inherits would store items
// nothing points at
/*
The mode goes with the items, because the mode is what decides which menu they belong to: with
`inherit` the server stores them against the menu this page inherits the one shown on screen,
and the one `navId` was resolved from rather than starting a menu of this page's own that
nothing would point at.
*/
const resp = await API_CLIENT.put(`sites/${siteStore.id}/navigation/pages/${pageStore.id}`, {
json: {
mode: siteStore.overlayOpts.mode ?? pageStore.navigationMode,

Loading…
Cancel
Save