refactor(client): delete the UO client half (phase 3, slice 3) #139
@@ -51,7 +51,10 @@ function safeParse(text) {
|
|||||||
// The `api` object below stays core's own binding surface. Its `atlas` and
|
// The `api` object below stays core's own binding surface. Its `atlas` and
|
||||||
// `shard` namespaces are module bindings that only still live here because
|
// `shard` namespaces are module bindings that only still live here because
|
||||||
// Phase 3 has not moved them yet.
|
// Phase 3 has not moved them yet.
|
||||||
export { req as request }
|
// `BASE` goes with it: a module that needs an EventSource URL cannot go through
|
||||||
|
// `req` (fetch-only) and must not hardcode `/api/v1`, which is core's choice of
|
||||||
|
// mount point and not a promise it has made.
|
||||||
|
export { req as request, BASE }
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
// ----- auth -----
|
// ----- auth -----
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
import { publishSharedDependencies } from './modules/shared.js'
|
import { publishSharedDependencies } from './modules/shared.js'
|
||||||
import { declareSlot, registerExtension, registerFeatureProvider } from './modules/registry.js'
|
import { declareSlot } from './modules/registry.js'
|
||||||
import { useShardFlags } from './lib/useShardFeatures.js'
|
|
||||||
import ShardStatusLink from './components/ShardStatusLink.jsx'
|
|
||||||
import UserShardSections from './routes/admin/views/UserShardSections.jsx'
|
|
||||||
import './styles/theme.css'
|
import './styles/theme.css'
|
||||||
|
|
||||||
// Publish window.__rg BEFORE rendering and before any module chunk evaluates.
|
// Publish window.__rg BEFORE rendering and before any module chunk evaluates.
|
||||||
@@ -17,18 +14,12 @@ import './styles/theme.css'
|
|||||||
// (docs/website/MODULE_API.md §3.2).
|
// (docs/website/MODULE_API.md §3.2).
|
||||||
publishSharedDependencies()
|
publishSharedDependencies()
|
||||||
|
|
||||||
// Core registers through the same seam a module uses, and registers FIRST — the
|
// Core registered a feature provider here until slice 3, under owner id `core`
|
||||||
// client twin of the server's `registries.registerCore()` (MODULE_SYSTEM.md
|
// and namespace `uo`, so that the seam was exercised by real content from the
|
||||||
// §1.9). The ten shard-gated rows in the public header are core's only because
|
// day it was built. That prediction paid out exactly as written: the extraction
|
||||||
// Phase 3 has not moved them yet; routing them through the registry now means
|
// deleted the registration and the hook it named, and SiteHeader was not touched.
|
||||||
// SiteHeader holds one mechanism instead of two, and the extraction becomes a
|
// There is nothing for core to register now — no core nav row carries a
|
||||||
// deletion rather than a rewrite made under extraction pressure.
|
// `feature` — and the filter is a correct no-op until a module supplies one.
|
||||||
//
|
|
||||||
// The owner id is `core`, which is what a nav row with no `moduleId` resolves
|
|
||||||
// against (modules/featureGate.js). The namespace is `uo`, so a module that
|
|
||||||
// wants to read these flags — the `uo` module itself, once it owns them — asks
|
|
||||||
// for them by the name they will always have had.
|
|
||||||
registerFeatureProvider('core', 'uo', useShardFlags)
|
|
||||||
|
|
||||||
// ── Extension slots (MODULE_API.md §3.7) ───────────────────────────────────
|
// ── Extension slots (MODULE_API.md §3.7) ───────────────────────────────────
|
||||||
//
|
//
|
||||||
@@ -48,15 +39,22 @@ declareSlot('site.footer.status')
|
|||||||
// resource, one extension point, two halves. The module with routes under
|
// resource, one extension point, two halves. The module with routes under
|
||||||
// /api/v1/admin/users/:id is the module with something to show on that page.
|
// /api/v1/admin/users/:id is the module with something to show on that page.
|
||||||
declareSlot('admin.users.detail')
|
declareSlot('admin.users.detail')
|
||||||
|
// The invite-acceptance page's optional next step. Core owns invites — staff are
|
||||||
|
// invited too — and owned the game-account step inside them until slice 3, which
|
||||||
|
// meant core reading a `gameAccountSignup` flag and posting to a shard route.
|
||||||
|
//
|
||||||
|
// Named for the place, like the other two: it is "the point after an invite has
|
||||||
|
// been accepted and before the invitee is sent on", not "create a game account".
|
||||||
|
// Whether there is a step at all is the filling module's decision, made from
|
||||||
|
// data core does not have; core renders the shell and a skip control, and hands
|
||||||
|
// over `onDone`. With the slot unfilled the invitee goes straight to the portal,
|
||||||
|
// which is what core's own code did whenever the flag was off.
|
||||||
|
declareSlot('player.invite.accepted')
|
||||||
|
|
||||||
// And core fills both itself, under owner id `core`, with the components that
|
// Core filled the first two itself until slice 3, with the components that were
|
||||||
// were inline in SiteFooter.jsx and UserDetail.jsx until this slice. The page
|
// inline in SiteFooter.jsx and UserDetail.jsx. Both are gone: the module fills
|
||||||
// renders exactly what it rendered before, and the mechanism is exercised by
|
// all three, and core's own fills had to go for it to be able to — the first
|
||||||
// core's own content from the day it lands rather than first proved by the
|
// fill wins, and core registered first (§3.7).
|
||||||
// change that depends on it. Slice 3 deletes these three lines and the two files
|
|
||||||
// they name, and the module registers the same two slots on its way in.
|
|
||||||
registerExtension('core', 'site.footer.status', ShardStatusLink)
|
|
||||||
registerExtension('core', 'admin.users.detail', UserShardSections)
|
|
||||||
|
|
||||||
// Render on DOMContentLoaded rather than immediately, and that is the one line
|
// Render on DOMContentLoaded rather than immediately, and that is the one line
|
||||||
// of core's boot the module system changes.
|
// of core's boot the module system changes.
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
// Phase 2, PR 8 of docs/website/MODULE_SYSTEM.md §2.7 (§1.5 states the problem);
|
// 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.
|
// the contract is docs/website/MODULE_API.md §3.3.
|
||||||
//
|
//
|
||||||
// Ten of the sixteen rows in the public header carry a `feature`, and every one
|
// Nine of the sixteen rows in the public header used to carry a `feature`, and
|
||||||
// of them is a shard surface an admin can disable or gate to a higher audience.
|
// every one of them was a shard surface an admin can disable or gate to a higher
|
||||||
// The provider that answers those questions — `useShardFeatures` — moves out
|
// audience. The provider that answered those questions moved out with the module
|
||||||
// with the module, so core cannot keep calling it directly and still be a core.
|
// in Phase 3 slice 3, and core cannot call it directly and still be a core. It
|
||||||
// It keeps a generic seam instead, and the module fills it.
|
// keeps this generic seam instead, and the module fills it.
|
||||||
//
|
//
|
||||||
// **The namespace comes from the registration, not from the string.** A row's
|
// **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
|
// `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
|
// 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
|
// 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 is what
|
// carry no `moduleId` and resolve against the owner id `core` — which nothing
|
||||||
// core registers `useShardFeatures` under until Phase 3 moves those rows into
|
// registers now that the shard rows are gone, and that is the correct resting
|
||||||
// the module and they arrive stamped `uo` instead.
|
// state rather than a gap: no core nav row carries a `feature`.
|
||||||
//
|
//
|
||||||
// Everything here fails OPEN, and that is deliberate and unchanged from
|
// Everything here fails OPEN, and that is deliberate: this is presentation, the
|
||||||
// useShardFeatures' own posture: this is presentation, the gate is server-side
|
// gate is server-side
|
||||||
// (a disabled feature 404s and an out-of-rung one 403s whether or not a link was
|
// (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.
|
// 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
|
// The one thing a UI mistake must never do here is hide a page from someone
|
||||||
|
|||||||
@@ -79,8 +79,18 @@ export function registerRoutes(id, byArea) {
|
|||||||
* core group, `order` sorts within it, and an unknown group name appends rather
|
* core group, `order` sorts within it, and an unknown group name appends rather
|
||||||
* than dropping the item — a mis-typed group must cost a position, never a link.
|
* than dropping the item — a mis-typed group must cost a position, never a link.
|
||||||
*
|
*
|
||||||
|
* `icon` is a component core renders exactly as it renders its own rows' icons
|
||||||
|
* (1.3.0). It exists because without it the six UO rows would have extracted as
|
||||||
|
* the only text-only entries in a sidebar where every other row has a glyph,
|
||||||
|
* which reads as breakage rather than as a design. Core does not supply a
|
||||||
|
* fallback: a module that omits it gets no icon, the same as a core row that
|
||||||
|
* omits it, and inventing one would be core making a presentation choice for
|
||||||
|
* content it knows nothing about. Note that `icon` is already among the fields
|
||||||
|
* an override may not touch (lib/navOverrides.js) — the concept predates a
|
||||||
|
* module being able to supply one.
|
||||||
|
*
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {{area: string, items: Array<{label, to, group?, order?, roles?, feature?}>}} spec
|
* @param {{area: string, items: Array<{label, to, group?, order?, roles?, feature?, icon?}>}} spec
|
||||||
*/
|
*/
|
||||||
export function registerNav(id, spec) {
|
export function registerNav(id, spec) {
|
||||||
const { area, items } = spec || {}
|
const { area, items } = spec || {}
|
||||||
@@ -92,10 +102,10 @@ export function registerNav(id, spec) {
|
|||||||
/**
|
/**
|
||||||
* The hook that answers "which of this module's features may this viewer see".
|
* The hook that answers "which of this module's features may this viewer see".
|
||||||
*
|
*
|
||||||
* Core keeps a generic flag context and owns none of the semantics — `uo` fills
|
* Core keeps a generic flag context and owns none of the semantics
|
||||||
* its namespace with today's `useShardFeatures` (MODULE_SYSTEM.md §1.5). With no
|
* (MODULE_SYSTEM.md §1.5). With no module installed the nav filter is a correct
|
||||||
* module installed the nav filter is a correct no-op, because no core nav item
|
* no-op, because no core nav item carries a `feature` — which has been literally
|
||||||
* carries a `feature` today.
|
* true since Phase 3 slice 3 took the nine shard-gated rows out.
|
||||||
*/
|
*/
|
||||||
export function registerFeatureProvider(id, namespace, hook) {
|
export function registerFeatureProvider(id, namespace, hook) {
|
||||||
providers.set(namespace, { id, hook })
|
providers.set(namespace, { id, hook })
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { Loading, ErrorState, EmptyState } from '../components/PageState.jsx'
|
|||||||
import { useAsync } from '../lib/useAsync.js'
|
import { useAsync } from '../lib/useAsync.js'
|
||||||
import { useAuth } from '../contexts/AuthContext.jsx'
|
import { useAuth } from '../contexts/AuthContext.jsx'
|
||||||
import { useSite } from '../contexts/SiteContext.jsx'
|
import { useSite } from '../contexts/SiteContext.jsx'
|
||||||
import { request, ApiError } from '../api/client.js'
|
import { request, ApiError, BASE } from '../api/client.js'
|
||||||
|
|
||||||
// The UI kit is CURATED AND CLOSED (§3.4), not a re-export of components/. These
|
// The UI kit is CURATED AND CLOSED (§3.4), not a re-export of components/. These
|
||||||
// seven are what the smallest UO page already needs beyond React and the router:
|
// seven are what the smallest UO page already needs beyond React and the router:
|
||||||
@@ -66,11 +66,16 @@ const ui = {
|
|||||||
useSite,
|
useSite,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The request PRIMITIVE, not the `api` object (§3.5). `api.atlas` and `api.shard`
|
// The request PRIMITIVE, not the `api` object (§3.5): a module builds its own
|
||||||
// are module bindings that only still live in core's client because Phase 3 has
|
// namespace over `request` and owns the paths it calls, which is right, because
|
||||||
// not moved them; a module builds its own namespace over `request` and owns the
|
// it owns the routes at the other end.
|
||||||
// paths it calls — which is right, because it owns the routes at the other end.
|
//
|
||||||
const api = { request, ApiError }
|
// `BASE` was in §3.5 from the start and missing from this object until slice 3,
|
||||||
|
// which is when something first needed it. `request` is fetch-only, so an
|
||||||
|
// EventSource — the shard's live feed is two of them — has to build its own URL,
|
||||||
|
// and the alternative is a module hardcoding `/api/v1`: an assertion about where
|
||||||
|
// core mounts its API that core has never promised to keep.
|
||||||
|
const api = { request, ApiError, BASE }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish `window.__rg`. Called by main.jsx before it renders, and before any
|
* Publish `window.__rg`. Called by main.jsx before it renders, and before any
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
// that the two files can drift, so a test asserts they agree
|
// that the two files can drift, so a test asserts they agree
|
||||||
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
|
// (client/test/moduleRegistry.test.js) rather than trusting a bump to remember
|
||||||
// both.
|
// both.
|
||||||
|
// 1.3.0 — three additions, all from Phase 3 slice 3 needing them: a nav item may
|
||||||
|
// carry an `icon` component (§3.3), core declares a third slot
|
||||||
|
// `player.invite.accepted` (§3.7), and `window.__rg.api` gained `BASE`, which
|
||||||
|
// §3.5 always documented and shared.js never published. Additive throughout: a
|
||||||
|
// module written against 1.2.0 is unaffected. The server half is untouched and
|
||||||
|
// bumps anyway, for the reason below.
|
||||||
// 1.2.0 — `registry` gained `registerExtension` and core gained extension slots
|
// 1.2.0 — `registry` gained `registerExtension` and core gained extension slots
|
||||||
// (MODULE_API.md §3.7). The first change to window.__rg since 1.0.0, and an
|
// (MODULE_API.md §3.7). The first change to window.__rg since 1.0.0, and an
|
||||||
// addition: a module that never fills a slot is unaffected. The server half is
|
// addition: a module that never fills a slot is unaffected. The server half is
|
||||||
@@ -20,4 +26,4 @@
|
|||||||
// but the two halves state ONE version: a module declares a single coreApi range
|
// but the two halves state ONE version: a module declares a single coreApi range
|
||||||
// and is served one chunk, so a client that claimed 1.0.0 while the server
|
// and is served one chunk, so a client that claimed 1.0.0 while the server
|
||||||
// answered 1.1.0 would be two answers to one question.
|
// answered 1.1.0 would be two answers to one question.
|
||||||
export const MODULE_API_VERSION = '1.2.0'
|
export const MODULE_API_VERSION = '1.3.0'
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useAuth } from '../../contexts/AuthContext.jsx'
|
import { useAuth } from '../../contexts/AuthContext.jsx'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
import PlayerShell, { honeypotStyle } from './PlayerShell.jsx'
|
import PlayerShell, { honeypotStyle } from './PlayerShell.jsx'
|
||||||
import CreateGameAccountForm from '../../components/CreateGameAccountForm.jsx'
|
import Slot from '../../modules/Slot.jsx'
|
||||||
|
import { extensionFor } from '../../modules/registry.js'
|
||||||
|
|
||||||
// Public, token-gated invite acceptance (/invite/:token). Validates the invite,
|
// Public, token-gated invite acceptance (/invite/:token). Validates the invite,
|
||||||
// lets the invitee set a username + password (their email + role are pre-assigned),
|
// lets the invitee set a username + password (their email + role are pre-assigned),
|
||||||
// creates the account at that role and logs them in. For a player invite it then
|
// creates the account at that role and logs them in.
|
||||||
// offers the built-in "create game account" step before sending them to the portal.
|
//
|
||||||
|
// For a PLAYER invite there may then be one more step, supplied by an installed
|
||||||
|
// module through the `player.invite.accepted` slot: core rendered a UO
|
||||||
|
// game-account form here itself until Phase 3 slice 3, reading a
|
||||||
|
// `gameAccountSignup` flag out of its own settings and posting to a shard route.
|
||||||
|
// Neither of those is core's. What core keeps is the shell, the skip control and
|
||||||
|
// the destination; whether there is a step at all is the module's call, made
|
||||||
|
// from data core does not have.
|
||||||
export default function AcceptInvite() {
|
export default function AcceptInvite() {
|
||||||
const { token } = useParams()
|
const { token } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -16,7 +24,6 @@ export default function AcceptInvite() {
|
|||||||
|
|
||||||
const [invite, setInvite] = useState(null) // fields email and role
|
const [invite, setInvite] = useState(null) // fields email and role
|
||||||
const [loadErr, setLoadErr] = useState('')
|
const [loadErr, setLoadErr] = useState('')
|
||||||
const [signupOk, setSignupOk] = useState(false)
|
|
||||||
|
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -30,14 +37,20 @@ export default function AcceptInvite() {
|
|||||||
api.getInvite(token)
|
api.getInvite(token)
|
||||||
.then((iv) => active && setInvite(iv))
|
.then((iv) => active && setInvite(iv))
|
||||||
.catch((err) => active && setLoadErr(err.status === 404 ? 'This invitation is invalid or has expired.' : 'Could not load this invitation.'))
|
.catch((err) => active && setLoadErr(err.status === 404 ? 'This invitation is invalid or has expired.' : 'Could not load this invitation.'))
|
||||||
api.publicSettings()
|
|
||||||
.then((s) => active && setSignupOk(Boolean(s?.gameAccountSignup)))
|
|
||||||
.catch(() => {})
|
|
||||||
return () => { active = false }
|
return () => { active = false }
|
||||||
}, [token])
|
}, [token])
|
||||||
|
|
||||||
const dest = invite && invite.role === 'player' ? '/player' : '/admin'
|
const dest = invite && invite.role === 'player' ? '/player' : '/admin'
|
||||||
|
|
||||||
|
// Whether anything is installed that wants the post-acceptance step. Read
|
||||||
|
// rather than rendered blind because it decides a NAVIGATION, not just what
|
||||||
|
// appears: with nothing filled there is no screen to show, so the invitee goes
|
||||||
|
// straight to their destination. This is the one legitimate reason to ask
|
||||||
|
// whether a slot is filled — the answer changes control flow, not decoration
|
||||||
|
// (decoration goes inside `<Slot wrap>`, which is why `hasExtension` is gone).
|
||||||
|
const hasNextStep = Boolean(extensionFor('player.invite.accepted'))
|
||||||
|
const finish = useCallback(() => navigate('/player', { replace: true }), [navigate])
|
||||||
|
|
||||||
async function onSubmit(e) {
|
async function onSubmit(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
@@ -48,8 +61,9 @@ export default function AcceptInvite() {
|
|||||||
await api.acceptInvite(token, username.trim(), password, { company })
|
await api.acceptInvite(token, username.trim(), password, { company })
|
||||||
await refresh() // pull the freshly-issued session into context
|
await refresh() // pull the freshly-issued session into context
|
||||||
setAccepted(true)
|
setAccepted(true)
|
||||||
// Staff invites are web-only — no game step; go straight in.
|
// Staff invites go straight in, and so does a player invite when nothing
|
||||||
if (!(invite.role === 'player' && signupOk)) navigate(dest, { replace: true })
|
// is installed that has a step to offer.
|
||||||
|
if (!(invite.role === 'player' && hasNextStep)) navigate(dest, { replace: true })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 409) setError('That username is already taken, or the invite was already used.')
|
if (err.status === 409) setError('That username is already taken, or the invite was already used.')
|
||||||
else if (err.status === 404) setError('This invitation is invalid or has expired.')
|
else if (err.status === 404) setError('This invitation is invalid or has expired.')
|
||||||
@@ -78,19 +92,22 @@ export default function AcceptInvite() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Accepted: optional game-account step (player invites) ──────────────────
|
// ── Accepted: a module's optional next step (player invites) ───────────────
|
||||||
|
//
|
||||||
|
// Only reachable when the slot is filled — `onSubmit` navigates away otherwise
|
||||||
|
// — so there is no empty-shell case to guard here.
|
||||||
|
//
|
||||||
|
// The subtitle is core's and says nothing about what the step is: naming it
|
||||||
|
// would be core describing content it does not own, and the wrong description
|
||||||
|
// is worse than a general one. "Skip" stays core's too, because where it goes
|
||||||
|
// is core's decision, and it is rendered outside the slot deliberately — an
|
||||||
|
// extension that throws must not take the way out with it.
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
return (
|
return (
|
||||||
<PlayerShell subtitle="Set up your game account">
|
<PlayerShell subtitle="One more step">
|
||||||
<p className="sans" style={{ marginTop: 0, color: 'var(--muted)', fontSize: '0.9rem', lineHeight: 1.6 }}>
|
<Slot name="player.invite.accepted" onDone={finish} />
|
||||||
Your account is ready. Create a game account now to play, or skip and do it later from your portal.
|
|
||||||
</p>
|
|
||||||
<CreateGameAccountForm
|
|
||||||
submit={api.player.shard.createAccount}
|
|
||||||
onCreated={() => navigate('/player', { replace: true })}
|
|
||||||
/>
|
|
||||||
<p className="sans" style={{ textAlign: 'center', margin: '18px 0 0' }}>
|
<p className="sans" style={{ textAlign: 'center', margin: '18px 0 0' }}>
|
||||||
<button type="button" onClick={() => navigate('/player', { replace: true })} className="btn" style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer' }}>
|
<button type="button" onClick={finish} className="btn" style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer' }}>
|
||||||
Skip for now →
|
Skip for now →
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -132,6 +132,36 @@ test('a module row carries its moduleId through, which is how the gate finds it'
|
|||||||
assert.equal(row.feature, 'atlas')
|
assert.equal(row.feature, 'atlas')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('a module row carries its icon through, and an override cannot touch it', () => {
|
||||||
|
// 1.3.0. Without an `icon` the six extracted UO rows would have been the only
|
||||||
|
// text-only entries in a sidebar where every other row has a glyph. Core
|
||||||
|
// renders whatever component the row carries and supplies no fallback — an
|
||||||
|
// invented one would be core making a presentation choice for content it knows
|
||||||
|
// nothing about.
|
||||||
|
const Glyph = () => null
|
||||||
|
registerNav('uo', {
|
||||||
|
area: 'admin',
|
||||||
|
items: [{ label: 'Shard', to: '/admin/uo/link', group: 'System', icon: Glyph }],
|
||||||
|
})
|
||||||
|
const merged = withModuleNav(ADMIN, 'admin')
|
||||||
|
const row = merged.find((g) => g.title === 'System').items.find((i) => i.to === '/admin/uo/link')
|
||||||
|
assert.equal(row.icon, Glyph)
|
||||||
|
|
||||||
|
// `icon` was already on navOverrides' list of fields an override may not
|
||||||
|
// touch, from long before a module could supply one. It still is.
|
||||||
|
const overridden = applyNavOverrides(merged, { '/admin/uo/link': { label: 'Renamed', icon: 'nope' } })
|
||||||
|
const after = overridden.find((g) => g.title === 'System').items.find((i) => i.to === '/admin/uo/link')
|
||||||
|
assert.equal(after.label, 'Renamed')
|
||||||
|
assert.equal(after.icon, Glyph)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a row with no icon simply has none, the same as a core row with none', () => {
|
||||||
|
registerNav('uo', { area: 'player', items: [{ label: 'Characters', to: '/player/uo/characters', order: 0 }] })
|
||||||
|
const row = withModuleNav([{ to: '/account', label: 'Account' }], 'player')[0]
|
||||||
|
assert.equal(row.to, '/player/uo/characters')
|
||||||
|
assert.equal(row.icon, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
test('areas do not leak into one another', () => {
|
test('areas do not leak into one another', () => {
|
||||||
registerNav('uo', { area: 'admin', items: [{ label: 'Shard', to: '/admin/uo/link', group: 'System' }] })
|
registerNav('uo', { area: 'admin', items: [{ label: 'Shard', to: '/admin/uo/link', group: 'System' }] })
|
||||||
assert.equal(withModuleNav(PUBLIC, 'public'), PUBLIC)
|
assert.equal(withModuleNav(PUBLIC, 'public'), PUBLIC)
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
|
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
|
||||||
// has nothing to say about a website module) and from any module's own version.
|
// has nothing to say about a website module) and from any module's own version.
|
||||||
|
|
||||||
|
// 1.3.0 — three CLIENT additions from Phase 3 slice 3: a nav item may carry an
|
||||||
|
// `icon`, core declares a `player.invite.accepted` slot, and `window.__rg.api`
|
||||||
|
// gained `BASE` (which §3.5 always specified and shared.js never published).
|
||||||
|
// Nothing on the server changed; this file bumps for the reason below.
|
||||||
|
//
|
||||||
// 1.2.0 — the CLIENT registry gained `registerExtension` and core gained client
|
// 1.2.0 — the CLIENT registry gained `registerExtension` and core gained client
|
||||||
// extension slots (MODULE_API.md §3.7): the twin of this half's declareSlot /
|
// extension slots (MODULE_API.md §3.7): the twin of this half's declareSlot /
|
||||||
// registerExtension, for module content inside a core *page* rather than under a
|
// registerExtension, for module content inside a core *page* rather than under a
|
||||||
@@ -21,6 +26,6 @@
|
|||||||
// an admin action a module performs belongs in core's one audit log, the
|
// an admin action a module performs belongs in core's one audit log, the
|
||||||
// extension slot needs the user its prefix names, and §2.7 forbids a module
|
// extension slot needs the user its prefix names, and §2.7 forbids a module
|
||||||
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
|
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
|
||||||
const MODULE_API_VERSION = '1.2.0'
|
const MODULE_API_VERSION = '1.3.0'
|
||||||
|
|
||||||
module.exports = { MODULE_API_VERSION }
|
module.exports = { MODULE_API_VERSION }
|
||||||
|
|||||||
Reference in New Issue
Block a user