Skip to content

Community nav/sidebar/branding admin settings reuse existing endpoints instead of a new CRUD surface

Admins can now customize the community's navbar items (activate/deactivate/reorder/relabel/re-icon), reorder the sidebar's Spaces/SpaceGroups, and edit the community's logo/PWA branding — all from /settings/community/* inside frontend/apps/web itself (not the separate frontend/apps/admin backoffice SPA). Most of this reused backend that already existed; only Community.nav_config and one new headless endpoint were actually new.

Context

The three sub-features could each have been built as a dedicated CRUD surface (new models, new ViewSets, new endpoints). Investigation before building found the backend was already most of the way there:

  • CommunitySettingsView (apps.communities.views_settings, /api/v1/community/admin/settings/) already retrieve/updates logo_light/logo_dark/pwa_icon/pwa_icon_maskable/pwa_short_name, gated IsAdminOrModerator. It's already consumed by frontend/apps/admin via raw fetch + FormData (multipart).
  • SpaceGroupViewSet/SpaceViewSet (apps.spaces.views.space_groups/spaces, /api/v1/space-groups/, /api/v1/spaces/) are full ModelViewSets with order as a plain writable field — reordering is just PATCH {order: n} per moved row.
  • Nothing existed for the navbar items themselves — the list was a hardcoded array in Sidebar.tsx.

Decision

  • Nav items: add Community.nav_config (JSONField(default=list), sparse list of {key, enabled, order, label, icon_type, icon_value} overrides) — same precedent as the existing theme JSONField — and add it to CommunitySettingsSerializer's fields rather than a new model/endpoint. Sidebar.tsx merges it with a hardcoded default-metadata table (frontend/apps/web/src/lib/navItems.tsx) so a key absent from nav_config (a community that never configured it, or a future new built-in item) falls back to its default rather than needing a migration.
  • Sidebar reorder: no new endpoint. The settings page PATCHes order directly on SpaceGroupViewSet/SpaceViewSet per moved row, reusing the existing Space.order/SpaceGroup.order fields that already drive Sidebar.tsx's rendering order. Reorder is within-group only — dragging a Space into a different group (a space_group FK change) is out of scope; no create/edit/delete UI for Spaces/SpaceGroups was added here.
  • Branding (logo/PWA): CommunitySettingsView itself is left untouched — its FileFields only ever accept raw multipart bytes, which is exactly what frontend/apps/admin's hand-written fetch(..., {body: formData}) already sends. Orval's generated React Query hooks always serialize the request body as JSON (Content-Type: application/json, JSON.stringify(...)), so they can never drive a real file upload — reusing CommunitySettingsView from apps/web was not actually possible once this was discovered mid-implementation. Instead, a new headless endpoint, CommunityBrandingView (PATCH /api/headless/v1/community_branding), was added — modeled directly on the existing SpaceUpdateView's banner/icon/logo handling: the frontend uploads the raw file via the already-established useUploadMedia() hook (itself a deliberate, documented exception to "Orval hooks are the single source of truth," for the same reason — multipart isn't representable in the generated client), then PATCHes the resulting Media id; the view resolves it to an actual file via _pick_media_file. Gated admin-only (not IsAdminOrModerator like CommunitySettingsView and the Space/SpaceGroup viewsets) — a new decision for a new admin-facing feature, not a tightening of any existing endpoint's permissions.
  • A schema-collision bug was found and fixed in passing: apps.headless.schemas.SpaceSerializer (an OpenAPI-only contract class) and apps.spaces.serializers.spaces.SpaceSerializer (the real ModelViewSet serializer for /api/v1/spaces/) shared the same drf-spectacular component name ("Space"), so the OpenAPI response schema — and therefore the Orval-generated TS type — for one of them was silently wrong. Gave the headless one an explicit ref_name = "HeadlessSpace" (Meta.ref_name); existing consumers of the old, ambiguous Space import (Sidebar.tsx) were updated to HeadlessSpace, the shape they actually always meant.

Consequences

  • Positive: No new relational model or ViewSet for nav items; no new endpoint for sidebar reorder at all. The only genuinely new backend surface is one JSONField + one small headless view, both directly analogous to existing patterns in this codebase.
  • Positive: frontend/apps/admin's existing branding UI (components/Settings/CommunitySettings.tsx) is completely untouched and keeps working — CommunitySettingsView's contract (including its FileFields) was never changed.
  • Negative: Branding now has two write paths onto the same four Community fields — CommunitySettingsView (raw multipart, admin or moderator, consumed by apps/admin) and CommunityBrandingView (Media-id JSON, admin only, consumed by apps/web). They're not in conflict (both end up setting the same model fields), but a future reader touching branding needs to know both exist and why.
  • Negative: The Space/HeadlessSpace schema-naming collision was almost certainly present (silently) for other class-name collisions between apps/headless/schemas.py's OpenAPI-only contract classes and real ModelSerializers elsewhere in the codebase — this fix covers only the one collision that blocked this feature (needing Space.order/space_group correctly typed for the sidebar reorder page), not a full audit.
  • backend/apps/communities/models/community.pynav_config field, NAV_CONFIG_KEYS/NAV_CONFIG_ICON_TYPES.
  • backend/apps/communities/views_settings.pyCommunitySettingsSerializer.validate_nav_config.
  • backend/apps/communities/views.pyPublicCommunityInfoView (how a non-admin member's Sidebar.tsx actually reads nav_config, since the admin-settings endpoint is admin/moderator-gated).
  • backend/apps/headless/views.py::CommunityBrandingView, backend/apps/headless/schemas.py::CommunityBrandingUpdateRequestSerializer.
  • frontend/apps/web/src/lib/navItems.tsx, frontend/apps/web/src/components/Sidebar.tsx.
  • frontend/apps/web/src/routes/settings/community/ — the three admin settings pages.
  • ADR 0010 — the visible_to/accessible_to contract this feature's read paths respect (unrelated to but adjacent to the admin-only write gate here).

Strum — Documentação.