mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
86513fda0d
On a managed install the operator runs place search and routing, the plugin runtime, the integrations and the documentation, so the admin and the users no longer see switches and guides for them: - The place search and routing card, the transit provider and the search log are gone from the admin settings (desktop and mobile). places_provider is a managed-locked setting, and the transit provider, search log and wanderer routes refuse writes there. Share place details stays the admin's, as a card of its own, hidden when TREK_PLACES_CONTRIBUTE switches it off. - File types and trip reminders move to the right column, so the columns stay balanced. - Plugins disappear from the admin and the user settings, wanderer from Tours. - The Help & Docs page and its links are not offered. The help center stays and leaves out guides about what the operator runs (keys, OIDC, backups, storage, releases, plugins, document sync) and about integrations that are off. - Mapbox GL is offered only with the operator's token, Amap tiles only where the operator set Amap up. - The mobile admin no longer shows OIDC and the WebAuthn relying party fields, matching the desktop tab. For every install: - Notification matrices list only events the install can raise: no plugin row without the plugin runtime, no Vacay, collection, photo, Synology, wanderer or packing rows while those are off. On a managed install the release and replica alerts are left out. - The journey entry editor offers External photos only where a photo provider is switched on, as the phone already did. - app-config is asked again after a failed answer. A page opened while the server restarted kept every default for the session, which on a managed install brought back the operator's settings.
202 lines
11 KiB
TypeScript
202 lines
11 KiB
TypeScript
import type { HelpContext, HelpGuide } from './types'
|
|
import { useAddonStore } from '../store/addonStore'
|
|
import { useAuthStore } from '../store/authStore'
|
|
import { dashboardContext, dashboardGuides } from './contexts/dashboard'
|
|
import { vacayContext, vacayGuides } from './contexts/vacay'
|
|
import { atlasContext, atlasGuides } from './contexts/atlas'
|
|
import { collectionsContext, collectionsGuides } from './contexts/collections'
|
|
import { journeyContext, journeyGuides, journalContext, journalGuides, studioContext, studioGuides } from './contexts/journey'
|
|
import { settingsContext, settingsTabContexts, settingsGuides } from './contexts/settings'
|
|
import { adminContext, adminTabContexts, adminGuides } from './contexts/admin'
|
|
import { tripContext, tripGuides } from './contexts/trip'
|
|
import { tripPlacesContext, tripPlacesGuides } from './contexts/tripPlaces'
|
|
import { tripDaysContext, tripDaysGuides } from './contexts/tripDays'
|
|
import { tripRoadtripContext, tripRoadtripGuides } from './contexts/tripRoadtrip'
|
|
import { tripCollabContext, tripCollabGuides } from './contexts/tripCollab'
|
|
import { tripFilesContext, tripFilesGuides } from './contexts/tripFiles'
|
|
import { tripCostsContext, tripCostsGuides } from './contexts/tripCosts'
|
|
import { tripListsContext, tripListsGuides } from './contexts/tripLists'
|
|
import { tripBookingsContext, tripBookingsGuides } from './contexts/tripBookings'
|
|
import { tripTransportsContext, tripTransportsGuides } from './contexts/tripTransports'
|
|
import { tripMapContext, tripMapGuides } from './contexts/tripMap'
|
|
import { tripDayDetailContext, tripDayDetailGuides } from './contexts/tripDayDetail'
|
|
import { tripPlaceContext, tripPlaceGuides } from './contexts/tripPlace'
|
|
import { tripToursContext, tripToursGuides } from './contexts/tripTours'
|
|
import { tripTourDetailContext, tripTourDetailGuides } from './contexts/tripTourDetail'
|
|
import { tripTourPlannerContext, tripTourPlannerGuides } from './contexts/tripTourPlanner'
|
|
import { tripTourImportContext, tripTourImportGuides } from './contexts/tripTourImport'
|
|
import { tripTourMapContext, tripTourMapGuides } from './contexts/tripTourMap'
|
|
import { tripTourStagesContext, tripTourStagesGuides } from './contexts/tripTourStages'
|
|
import { tripTourHikeContext, tripTourHikeGuides } from './contexts/tripTourHike'
|
|
import { tripTourWalkContext, tripTourWalkGuides } from './contexts/tripTourWalk'
|
|
import { tripTourBikeContext, tripTourBikeGuides } from './contexts/tripTourBike'
|
|
import { tripTourSkiContext, tripTourSkiGuides } from './contexts/tripTourSki'
|
|
import { tripTourPaddleContext, tripTourPaddleGuides } from './contexts/tripTourPaddle'
|
|
import { tripTourHorseContext, tripTourHorseGuides } from './contexts/tripTourHorse'
|
|
import { notificationsContext, notificationsGuides } from './contexts/notifications'
|
|
|
|
/**
|
|
* Everything the help center knows, composed from one file per screen under
|
|
* `contexts/`. A screen announces which context it is with `<HelpAnchor id>`
|
|
* (or `useHelpContext`), and the panel looks the rest up here.
|
|
*/
|
|
|
|
// The order of the screen switcher: the trip right under My Trips, where it is opened from.
|
|
const CONTEXT_LIST: HelpContext[] = [
|
|
dashboardContext,
|
|
// The trip and its own screens, in the order a reader meets them: the three
|
|
// columns of the plan, the two panels that open over it, then the tabs in the
|
|
// order the tab bar has them, and the drive last because an addon decides it.
|
|
tripContext, tripDaysContext, tripPlacesContext, tripMapContext,
|
|
tripPlaceContext, tripDayDetailContext,
|
|
tripTransportsContext, tripBookingsContext, tripListsContext, tripCostsContext, tripFilesContext, tripCollabContext,
|
|
tripRoadtripContext,
|
|
// Tours: the list and the card on the Plan tab, then the Tours tab with its
|
|
// jobs and one screen per kind of tour, in the order the type picker has them.
|
|
tripToursContext, tripTourDetailContext,
|
|
tripTourPlannerContext, tripTourImportContext, tripTourMapContext, tripTourStagesContext,
|
|
tripTourHikeContext, tripTourWalkContext, tripTourBikeContext, tripTourSkiContext, tripTourPaddleContext, tripTourHorseContext,
|
|
notificationsContext,
|
|
vacayContext, atlasContext, collectionsContext, journeyContext, journalContext, studioContext,
|
|
settingsContext, ...settingsTabContexts, adminContext, ...adminTabContexts,
|
|
]
|
|
const GUIDE_LIST: HelpGuide[] = [
|
|
...dashboardGuides, ...vacayGuides, ...atlasGuides, ...collectionsGuides, ...journeyGuides, ...journalGuides, ...studioGuides,
|
|
...settingsGuides, ...adminGuides,
|
|
...tripGuides, ...tripDaysGuides, ...tripPlacesGuides, ...tripMapGuides,
|
|
...tripPlaceGuides, ...tripDayDetailGuides,
|
|
...tripTransportsGuides, ...tripBookingsGuides, ...tripListsGuides, ...tripCostsGuides, ...tripFilesGuides, ...tripCollabGuides,
|
|
...tripRoadtripGuides,
|
|
...tripToursGuides, ...tripTourDetailGuides,
|
|
...tripTourPlannerGuides, ...tripTourImportGuides, ...tripTourMapGuides, ...tripTourStagesGuides,
|
|
...tripTourHikeGuides, ...tripTourWalkGuides, ...tripTourBikeGuides, ...tripTourSkiGuides, ...tripTourPaddleGuides, ...tripTourHorseGuides,
|
|
...notificationsGuides,
|
|
]
|
|
|
|
export const HELP_CONTEXTS: ReadonlyMap<string, HelpContext> = new Map(CONTEXT_LIST.map(c => [c.id, c]))
|
|
export const HELP_GUIDES: ReadonlyMap<string, HelpGuide> = new Map(GUIDE_LIST.map(g => [g.id, g]))
|
|
|
|
/**
|
|
* What a managed install leaves out: the admin screens it hides (AdminPage) and
|
|
* the guides about what the operator runs there, such as keys, OIDC, place
|
|
* search, routing, backups, storage, releases and plugins. A guide that walks
|
|
* somebody through a panel they cannot open is worse than none.
|
|
*/
|
|
const MANAGED_HIDDEN_CONTEXTS: ReadonlySet<string> = new Set([
|
|
'admin-plugins', 'admin-storage', 'admin-github', 'admin-backup', 'settings-plugins',
|
|
])
|
|
const MANAGED_HIDDEN_GUIDES: ReadonlySet<string> = new Set([
|
|
'oidc', 'instance-keys', 'places-transit', 'rotate-jwt-secret', 'own-routing-engine',
|
|
'install-plugin', 'storage-backends', 'release-history', 'create-backup', 'auto-backup',
|
|
'plugin-settings', 'files-sync', 'files-sync-settings', 'document-providers',
|
|
])
|
|
|
|
/**
|
|
* Guides about an integration, shown on a managed install only while it is on
|
|
* there: the operator decides which integrations the instance has, and a guide
|
|
* to connect one it does not offer leads nowhere.
|
|
*/
|
|
const addonOn = (id: string) => (): boolean => useAddonStore.getState().isEnabled(id)
|
|
const MANAGED_FEATURE_GUIDES: Readonly<Record<string, () => boolean>> = {
|
|
'connect-dawarich': addonOn('dawarich'),
|
|
'dawarich-countries': addonOn('dawarich'),
|
|
'dawarich-wishes': addonOn('dawarich'),
|
|
'dawarich-stays': addonOn('dawarich'),
|
|
'journey-day-stays': addonOn('dawarich'),
|
|
'map-dawarich-trail': addonOn('dawarich'),
|
|
'connect-airtrail': addonOn('airtrail'),
|
|
'airtrail-import': addonOn('airtrail'),
|
|
'photo-providers': addonOn('memories'),
|
|
'mcp-oauth': addonOn('mcp'),
|
|
'api-keys': addonOn('mcp'),
|
|
'mcp-tokens-admin': addonOn('mcp'),
|
|
'tour-connect-wanderer': () => useAddonStore.getState().toursWanderer,
|
|
'tour-wanderer-watch': () => useAddonStore.getState().toursWanderer,
|
|
'tour-import-wanderer': () => useAddonStore.getState().toursWanderer,
|
|
}
|
|
|
|
/** Read at call time: `managed` arrives with app-config, after the registry loaded. */
|
|
const isManaged = (): boolean => useAuthStore.getState().managed
|
|
const shownContext = (c: HelpContext): boolean => !(isManaged() && MANAGED_HIDDEN_CONTEXTS.has(c.id))
|
|
const shownGuide = (g: HelpGuide): boolean => {
|
|
if (!isManaged()) return true
|
|
if (MANAGED_HIDDEN_GUIDES.has(g.id) || MANAGED_HIDDEN_CONTEXTS.has(g.context)) return false
|
|
const feature = MANAGED_FEATURE_GUIDES[g.id]
|
|
return feature ? feature() : true
|
|
}
|
|
|
|
export function getHelpContext(id: string | null | undefined): HelpContext | null {
|
|
const context = id ? HELP_CONTEXTS.get(id) : undefined
|
|
return context && shownContext(context) ? context : null
|
|
}
|
|
|
|
export function getHelpGuide(id: string | null | undefined): HelpGuide | null {
|
|
const guide = id ? HELP_GUIDES.get(id) : undefined
|
|
return guide && shownGuide(guide) ? guide : null
|
|
}
|
|
|
|
/** The guides of a context, in the order the context lists them. */
|
|
export function guidesFor(context: HelpContext): HelpGuide[] {
|
|
return context.guides.map(id => HELP_GUIDES.get(id)).filter((g): g is HelpGuide => g !== undefined && shownGuide(g))
|
|
}
|
|
|
|
export function allHelpGuides(): HelpGuide[] {
|
|
return isManaged() ? GUIDE_LIST.filter(shownGuide) : GUIDE_LIST
|
|
}
|
|
|
|
/** Every screen that has help, in registration order, for the screen switcher. */
|
|
export function allHelpContexts(): HelpContext[] {
|
|
return isManaged() ? CONTEXT_LIST.filter(shownContext) : CONTEXT_LIST
|
|
}
|
|
|
|
/** The screens that stand on their own; their sub-screens hang under them in the switcher. */
|
|
export function topLevelHelpContexts(): HelpContext[] {
|
|
return CONTEXT_LIST.filter(c => !c.parent && shownContext(c))
|
|
}
|
|
|
|
/** The sub-screens of a screen, in registration order. */
|
|
export function childHelpContexts(parentId: string): HelpContext[] {
|
|
return CONTEXT_LIST.filter(c => c.parent === parentId && shownContext(c))
|
|
}
|
|
|
|
/** The screen and the screens above it, outermost first, for a breadcrumb. */
|
|
export function helpContextTrail(context: HelpContext): HelpContext[] {
|
|
const trail: HelpContext[] = [context]
|
|
let parent = context.parent ? HELP_CONTEXTS.get(context.parent) : undefined
|
|
while (parent && !trail.includes(parent)) {
|
|
trail.unshift(parent)
|
|
parent = parent.parent ? HELP_CONTEXTS.get(parent.parent) : undefined
|
|
}
|
|
return trail
|
|
}
|
|
|
|
// ── Catalogue keys ──────────────────────────────────────────────────────────
|
|
|
|
export const ctxKey = (id: string, part: 'title' | 'summary'): string => `help.ctx.${id}.${part}`
|
|
export const ctxBulletKey = (id: string, n: number): string => `help.ctx.${id}.bullet.${n}`
|
|
export const guideKey = (id: string, part: 'title' | 'goal' | 'result' | 'link'): string =>
|
|
`help.guide.${id}.${part}`
|
|
export const guideStepKey = (id: string, n: number): string => `help.guide.${id}.step.${n}`
|
|
export const guideTipKey = (id: string, n: number): string => `help.guide.${id}.tip.${n}`
|
|
|
|
// ── Media paths ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Generated artwork under `public/help-media/`. WebP and WebM on purpose: the
|
|
* service worker precaches `png`, and a couple of hundred help pictures do not
|
|
* belong in every visitor's first download.
|
|
*/
|
|
const MEDIA_BASE = '/help-media'
|
|
|
|
export const helpMedia = {
|
|
hero: (contextId: string): string => `${MEDIA_BASE}/ctx/${contextId}.webp`,
|
|
step: (guideId: string, n: number): string => `${MEDIA_BASE}/${guideId}/step-${n}.webp`,
|
|
result: (guideId: string): string => `${MEDIA_BASE}/${guideId}/result.webp`,
|
|
clip: (guideId: string, n: number): string => `${MEDIA_BASE}/${guideId}/step-${n}.loop.webp`,
|
|
}
|
|
|
|
/** Wiki route for a doc link, with the heading anchor when the link names one. */
|
|
export function docsRoute(link: { slug: string; anchor?: string }): string {
|
|
return `/help/${link.slug}${link.anchor ? `#${link.anchor}` : ''}`
|
|
}
|