feat(guilds): declare a second place on the guild page, for core's forum #12

Merged
whitlocktech merged 2 commits from feature/teams-phase4-forum-slot into edge 2026-08-18 14:17:52 +00:00
4 changed files with 27 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
{ {
"$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/uo and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves. Pinned rather than tracking `edge` on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together.", "$comment": "The core this module is proved against. MODULE_API.md §5.3: the frozen-manifest job clones RunicGateway/website at this exact ref, drops this module in as modules/uo and runs CORE's own routeManifest.js — nothing else can answer whether the URLs the module claims are the URLs it actually serves. Pinned rather than tracking `edge` on purpose: core moves for reasons that have nothing to do with this module, and a bump is then a deliberate commit saying which core the module was last proved against, instead of an unexplained red X on someone else's PR. Bump it, regenerate routes.manifest.json, and commit both together.",
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
"ref": "87230c879aa6e9adde3507718aed6bc4e4d86009", "ref": "7ed2ac99838f4bd64e1df324fe4961673648b0e6",
"refName": "edge @ phase 3 slice 4 (website#140)" "refName": "edge @ Teams phase 3 (website#152)"
} }

View File

@@ -194,6 +194,14 @@ registry.registerExtension(ID, 'player.invite.accepted', InviteGameAccountStep)
// enough; on a core that knows nothing of Teams it simply stays empty. // enough; on a core that knows nothing of Teams it simply stays empty.
registry.declareModuleSlot(ID, 'uo.guild.detail') registry.declareModuleSlot(ID, 'uo.guild.detail')
// A SECOND place on the same page, for core's Team forum (TEAMS.md Part 5). Two
// declarations rather than one, because a slot holds one component and this module
// wants to decide where each of core's two contributions sits on its own page —
// the feed reads as part of the guild's story, the forum is a room you go into.
// Neither knows the other exists, and a core that fills only one leaves the other
// empty.
registry.declareModuleSlot(ID, 'uo.guild.forum')
// `module.json`'s `coreApi` range is checked by the loader before this file is // `module.json`'s `coreApi` range is checked by the loader before this file is
// ever served, so there is nothing to re-check here. It is logged because a // ever served, so there is nothing to re-check here. It is logged because a
// mismatch between the core that validated the manifest and the core that // mismatch between the core that validated the manifest and the core that

View File

@@ -102,6 +102,11 @@ export default function Guild() {
that does not fill it, or when there is nothing to show. The guild that does not fill it, or when there is nothing to show. The guild
is named in OUR terms — core maps its own Team from these two. */} is named in OUR terms — core maps its own Team from these two. */}
<Slot name="uo.guild.detail" externalId={String(id)} moduleId="uo" /> <Slot name="uo.guild.detail" externalId={String(id)} moduleId="uo" />
{/* And the Team forum, in its own place below the feed. Core resolves
who may read it — membership and manual grants are core's rules —
so this module renders the room and never its door policy. */}
<Slot name="uo.guild.forum" externalId={String(id)} moduleId="uo" />
</> </>
)} )}
</div> </div>

View File

@@ -209,16 +209,22 @@ it('registers under exactly one module id, matching the manifest', () => {
assert.deepEqual([...owners], [manifest.id]) assert.deepEqual([...owners], [manifest.id])
}) })
it('declares its own guild-detail slot, for core to fill', () => { it('declares its own guild slots, for core to fill', () => {
// The inverted direction (TEAMS.md Part 3). Teams are a core primitive with no // The inverted direction (TEAMS.md Part 3). Teams are a core primitive with no
// core page: core owns the activity feed and this module owns the word "guild", // core page: core owns the activity feed and the forum, this module owns the
// so this module declares the place and core puts the feed in it. // word "guild", so this module declares the places and core puts them in.
assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail']) //
// TWO slots rather than one because a slot holds one component: stacking the
// feed and the forum into a single fill would take away this module's ability
// to place them separately on its own page.
assert.deepEqual([...registered.declaredSlots], ['uo.guild.detail', 'uo.guild.forum'])
}) })
it('the declared slot is rendered by the page that owns it', () => { it('every declared slot is rendered by the page that owns it', () => {
// A slot nothing renders is a slot core fills into the void. Asserted against // A slot nothing renders is a slot core fills into the void. Asserted against
// the source rather than the chunk, since the chunk is minified. // the source rather than the chunk, since the chunk is minified.
const page = fs.readFileSync(path.resolve(HERE, '..', 'src', 'routes', 'public', 'Guild.jsx'), 'utf8') const page = fs.readFileSync(path.resolve(HERE, '..', 'src', 'routes', 'public', 'Guild.jsx'), 'utf8')
assert.match(page, /name="uo\.guild\.detail"/) for (const name of registered.declaredSlots) {
assert.match(page, new RegExp(`name="${name.replace(/\./g, '\.')}"`))
}
}) })