The three things core owes the client half before it can leave, all additive, all MODULE_API 1.2.0 → 1.3.0. `player.invite.accepted` is the third extension slot. Core's invite page owned a UO game-account step — it read a `gameAccountSignup` flag out of core's own settings and posted to a shard route — and an invite is a core concept that staff receive too, so the page stays and its optional next step becomes a slot. Named for the place, like the other two. Whether there is a step at all is the filling module's call, made from data core does not have; core keeps the shell, the skip control and the destination. `icon` on a nav item, because without it the six extracted UO rows would have been the only text-only entries in a sidebar where every other row has a glyph. Core supplies no fallback — an invented one is core making a presentation choice for content it knows nothing about. `icon` was already among the fields an override may not touch, so the concept predates a module being able to send one. `api.BASE` was in §3.5 from the first draft and never actually published. `request` is fetch-only, so an EventSource builds its own URL, and the shard's live feed is two of them; the alternative is a module hardcoding `/api/v1`, which asserts something about core that core has not promised. `AcceptInvite` is the one legitimate reader of `extensionFor` outside Slot.jsx: the answer decides a NAVIGATION, not a decoration. Decoration goes inside `<Slot wrap>`, which is why `hasExtension` stayed deleted. Co-Authored-By: Claude <noreply@anthropic.com>
57 lines
2.8 KiB
JavaScript
57 lines
2.8 KiB
JavaScript
// Which nav rows a viewer may see, when the answer belongs to a module.
|
|
//
|
|
// Phase 2, PR 8 of docs/website/MODULE_SYSTEM.md §2.7 (§1.5 states the problem);
|
|
// the contract is docs/website/MODULE_API.md §3.3.
|
|
//
|
|
// Nine of the sixteen rows in the public header used to carry a `feature`, and
|
|
// every one of them was a shard surface an admin can disable or gate to a higher
|
|
// audience. The provider that answered those questions moved out with the module
|
|
// in Phase 3 slice 3, and core cannot call it directly and still be a core. It
|
|
// keeps this generic seam instead, and the module fills it.
|
|
//
|
|
// **The namespace comes from the registration, not from the string.** A row's
|
|
// `feature` is resolved by the provider its OWN module registered, so a module
|
|
// author writes `feature: 'status'` exactly as it reads today: nothing parses a
|
|
// prefix, and a typo'd namespace is not a thing that can exist. Core's own rows
|
|
// carry no `moduleId` and resolve against the owner id `core` — which nothing
|
|
// registers now that the shard rows are gone, and that is the correct resting
|
|
// state rather than a gap: no core nav row carries a `feature`.
|
|
//
|
|
// Everything here fails OPEN, and that is deliberate: this is presentation, the
|
|
// gate is server-side
|
|
// (a disabled feature 404s and an out-of-rung one 403s whether or not a link was
|
|
// rendered), so an unknown answer shows the link rather than blanking the nav.
|
|
// The one thing a UI mistake must never do here is hide a page from someone
|
|
// entitled to it.
|
|
|
|
/**
|
|
* The predicate the layouts filter their nav with.
|
|
*
|
|
* @param {Map<string, {has: (name: string) => boolean} | null | undefined>} flagsByOwner
|
|
* one entry per registered provider, keyed by the id of the module that
|
|
* registered it. The value is whatever that provider's hook returned this
|
|
* render: a Set-like of the flags this viewer may see, or `null` while the
|
|
* answer is still in flight.
|
|
* @returns {(item: object) => boolean}
|
|
*/
|
|
export function buildFeatureGate(flagsByOwner) {
|
|
return function isVisible(item) {
|
|
if (!item || !item.feature) return true
|
|
const owner = item.moduleId ?? 'core'
|
|
// No provider for this owner: the row names a flag nothing answers for. That
|
|
// is the no-module-installed case — no core row carries a `feature` once the
|
|
// module is out — and it is a correct no-op rather than a hidden row.
|
|
if (!flagsByOwner || !flagsByOwner.has(owner)) return true
|
|
const flags = flagsByOwner.get(owner)
|
|
// Still loading, or a provider that returned something unusable. Both are
|
|
// "we do not know yet", and both show the link.
|
|
if (!flags || typeof flags.has !== 'function') return true
|
|
return flags.has(item.feature)
|
|
}
|
|
}
|
|
|
|
/** The gate an area with no providers gets: everything is visible. */
|
|
export const OPEN_GATE = () => true
|
|
|
|
export default buildFeatureGate
|