feat(teams): the forum panel core fills, and the operator's controls
The forum had nowhere to live. TEAMS.md 3.1 gave it a CORE page, and phase 3 deleted every core Team page — Teams is a contract primitive and core does not own the word for one. So the forum follows the activity feed: module-uo declares a second place on its guild page and core fills it. TWO slots rather than one, because a slot holds one component and the first fill wins. Stacking the feed and the forum into a single fill would take from the module the ability to place core's two contributions separately on its own page, which is the whole point of the module owning it. The panel navigates by SEARCH PARAM (?thread=12) rather than by route. A thread has to be linkable and core cannot mount a route for one — the route belongs to the module's page — so a search param gives a shareable URL under whatever path the module chose, with the back button intact and no core route anywhere in it. That is why the fill is one component holding both a list view and a detail view. Post bodies arrive already rendered by the server under the current image policy, which is why they are set as HTML here rather than sanitised again: the body was cleaned on write with the forum's own profile, and any <img> in it was emitted by core's own renderer with a fixed attribute set. A client-side sanitiser would have to strip exactly the tag core just decided to add. The published image mode is read only to decide which composer to draw — never what renders. The composer puts an uploaded file's URL into the body as TEXT, not as a tag. The author never writes markup, which is what keeps the operator's policy enforceable. The admin panel carries both settings, the always-on help text, and the confirmation dialog with its two checkboxes and one recorded acknowledgement — plus the three additions the org lead settled: attribution and staff removal, the warning that disabling later does not delete existing files, and who "users" actually means. A stale acknowledgement raises a banner and freezes the settings; it does not turn uploads off. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,26 @@ export const api = {
|
||||
if (opts.offset != null) qs.set('offset', String(opts.offset))
|
||||
return req(`/public/teams/${encodeURIComponent(slug)}/activity${withQs(qs.toString())}`)
|
||||
},
|
||||
// The Team FORUM, under /player because a participant may be a plain player and
|
||||
// a leader is a player (TEAMS.md §2.11). Core's, for the same reason the feed is
|
||||
// core's: only core resolves whether this viewer is inside the Team, and the
|
||||
// member/guest split is a security boundary. The module renders the PLACE.
|
||||
teamForumThreads: (slug) => req(`/player/teams/${encodeURIComponent(slug)}/forum/threads`),
|
||||
teamForumThread: (slug, id) => req(`/player/teams/${encodeURIComponent(slug)}/forum/threads/${id}`),
|
||||
teamForumPost: (slug, body) =>
|
||||
req(`/player/teams/${encodeURIComponent(slug)}/forum/threads`, { method: 'POST', body }),
|
||||
teamForumModerate: (slug, id, body) =>
|
||||
req(`/player/teams/${encodeURIComponent(slug)}/forum/threads/${id}/moderate`, { method: 'POST', body }),
|
||||
teamForumUpload: (slug, file) => {
|
||||
const fd = new FormData()
|
||||
fd.append('image', file)
|
||||
return req(`/player/teams/${encodeURIComponent(slug)}/forum/uploads`, { method: 'POST', body: fd, raw: true })
|
||||
},
|
||||
teamGrantList: (slug) => req(`/player/teams/${encodeURIComponent(slug)}/grants`),
|
||||
teamGrantAdd: (slug, body) =>
|
||||
req(`/player/teams/${encodeURIComponent(slug)}/grants`, { method: 'POST', body }),
|
||||
teamGrantRevoke: (slug, userId) =>
|
||||
req(`/player/teams/${encodeURIComponent(slug)}/grants/${userId}`, { method: 'DELETE' }),
|
||||
wikiTags: () => req('/public/wiki/tags'),
|
||||
wikiPage: (slug) => req(`/public/wiki/${slug}`),
|
||||
// CMS pages (block-based). Published-only for the public; a draft-preview link
|
||||
@@ -284,6 +304,13 @@ export const api = {
|
||||
req(`/admin/teams/${id}/leader-override`, { method: 'POST', body }),
|
||||
clearTeamLeaderOverride: (id, memberKey) =>
|
||||
req(`/admin/teams/${id}/leader-override/${encodeURIComponent(memberKey)}`, { method: 'DELETE' }),
|
||||
teamForumSettings: () => req('/admin/teams/forum/settings'),
|
||||
teamForumUploads: (opts = {}) => {
|
||||
const qs = new URLSearchParams()
|
||||
if (opts.deleted) qs.set('deleted', '1')
|
||||
return req(`/admin/teams/forum/uploads${withQs(qs.toString())}`)
|
||||
},
|
||||
teamForumModeration: (id) => req(`/admin/teams/${id}/forum/moderation`),
|
||||
teamReviewQueue: () => req('/admin/teams/review'),
|
||||
teamRequests: (status) => req(`/admin/teams/requests${status ? `?status=${status}` : ''}`),
|
||||
decideTeamRequest: (id, status, note) =>
|
||||
|
||||
Reference in New Issue
Block a user