From c57310c50556f6ecac3acd1864b99567f6f0bfa5 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 18 Aug 2026 14:35:36 -0500 Subject: [PATCH] feat(guilds): a third place on the guild page, and where that page lives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two lines only core cannot supply for itself. `uo.guild.header` is a third declared slot, at the top of the page, for core's per-Team notification control. A third rather than a corner of the feed because a slot holds one component and the first fill wins: the control is an action ON this page and the other two are content IN it, and separate slots are what let this module say so. `pageUrlTemplate` tells core where a guild page actually is. Teams are a contract primitive with no core surface — core owns the tables and the access rules, this module owns the word "guild" and therefore the page — which leaves core unable to write a link to one. A notification email that cannot take you to the thread it is about is most of the way to useless. Core substitutes `{externalId}` and does nothing else with it; a template naming its own host is refused at registration. Co-Authored-By: Claude --- client/src/entry.jsx | 7 +++++++ client/src/routes/public/Guild.jsx | 7 +++++++ client/test/registration.test.js | 14 ++++++++++---- server/model/teamProvider/teamProvider.model.js | 16 +++++++++++++++- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/client/src/entry.jsx b/client/src/entry.jsx index ef9177d..90209b3 100644 --- a/client/src/entry.jsx +++ b/client/src/entry.jsx @@ -202,6 +202,13 @@ registry.declareModuleSlot(ID, 'uo.guild.detail') // empty. registry.declareModuleSlot(ID, 'uo.guild.forum') +// And a THIRD, at the top of the same page, for core's per-Team notification +// control (TEAMS.md §6.3). Same reasoning as the other two and a different place: +// muting a guild is an action ON this page, so it sits with the page's heading +// rather than after its content. Core resolves whether this viewer is in the +// Team at all — this module neither knows nor asks. +registry.declareModuleSlot(ID, 'uo.guild.header') + // `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 // mismatch between the core that validated the manifest and the core that diff --git a/client/src/routes/public/Guild.jsx b/client/src/routes/public/Guild.jsx index 945cc21..eea0df8 100644 --- a/client/src/routes/public/Guild.jsx +++ b/client/src/routes/public/Guild.jsx @@ -75,6 +75,13 @@ export default function Guild() { {data.alliance && ` · ${data.alliance}`}

+ {/* A third place for core, up here rather than below the roster: core + puts this guild's notification control in it, and a control that + acts on the page belongs beside the page's title and not after its + content. Empty for a visitor with no membership, and on a core + that fills nothing. */} + + {roster.length > 0 && (
diff --git a/client/test/registration.test.js b/client/test/registration.test.js index 5378bea..9e41927 100644 --- a/client/test/registration.test.js +++ b/client/test/registration.test.js @@ -214,10 +214,16 @@ it('declares its own guild slots, for core to fill', () => { // core page: core owns the activity feed and the forum, this module owns the // word "guild", so this module declares the places and core puts them in. // - // 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']) + // THREE slots rather than one because a slot holds one component: stacking the + // feed, the forum and the notification control into a single fill would take + // away this module's ability to place them separately on its own page — and it + // does place them separately, the control above the roster and the other two + // below it. + assert.deepEqual([...registered.declaredSlots], [ + 'uo.guild.detail', + 'uo.guild.forum', + 'uo.guild.header', + ]) }) it('every declared slot is rendered by the page that owns it', () => { diff --git a/server/model/teamProvider/teamProvider.model.js b/server/model/teamProvider/teamProvider.model.js index 8b5b76b..4196520 100644 --- a/server/model/teamProvider/teamProvider.model.js +++ b/server/model/teamProvider/teamProvider.model.js @@ -317,4 +317,18 @@ async function projectRoster(externalId, members, viewer) { } } -module.exports = { getTeams, getTeamMembers, getTeamLeaders, projectRoster, boardIsCurrent } +// Where core should point a link at a guild (MODULE_API 1.6.0, TEAMS.md §6.4). +// +// **Core cannot work this out for itself, and it is not supposed to.** Teams are +// a contract primitive with no core surface — this module owns the guild page, +// because core does not own the word "guild" — so the one thing core needs back +// is where the page it does not own actually lives. A notification email that +// cannot link to the thread it is about is most of the way to useless. +// +// A relative path with `{externalId}` substituted, matching `Guild.jsx`'s route +// (`/uo/guilds/:id`). Core does the substitution and nothing else with it; a +// template naming its own host is refused at registration, which is why this is +// data and not a callback. +const pageUrlTemplate = '/uo/guilds/{externalId}' + +module.exports = { getTeams, getTeamMembers, getTeamLeaders, projectRoster, boardIsCurrent, pageUrlTemplate }