Skip to content

feat(statuses): admin-configurable custom statuses + dynamic roadmap …#1589

Open
hodyhq wants to merge 7 commits into
getfider:mainfrom
hodyhq:feat/custom-statuses
Open

feat(statuses): admin-configurable custom statuses + dynamic roadmap …#1589
hodyhq wants to merge 7 commits into
getfider:mainfrom
hodyhq:feat/custom-statuses

Conversation

@hodyhq

@hodyhq hodyhq commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

…(Implements feedback.fider.io/posts/111)

Introduces a tenant-scoped statuses table that supersedes the hardcoded enum.PostStatus list. Admins manage their own catalogue from /admin/statuses — rename, recolor, set the semantic kind, toggle visibility on the home / filter / roadmap, reorder, or add brand-new slugs (e.g. "Under Review", "Triage", "Beta Testing").

Schema

  • statuses (tenant_id, slug, label, kind, color, icon, show_on_home, show_on_roadmap, filterable, sort_order, is_system, is_active, legacy_enum used only for backfill, created_at, updated_at)
  • statuses.kind ∈ {open, active, closed-completed, closed-declined, duplicate}
  • posts.status_slug — new authoritative identifier
  • Migrations seed the six built-ins (open, planned, started, completed, declined, duplicate) for every existing tenant, backfill posts.status_slug from posts.status, then DROP posts.status + statuses.legacy_enum. New tenants auto-seed via SeedTenantStatuses on signup.
  • 202606241500 adds show_on_roadmap with a kind-based backfill so Planned/Started/Completed lanes appear on /roadmap unchanged.

Backend

  • entity.Post carries StatusSlug + StatusKind (joined from the statuses table). enum.PostStatus is no longer a field. JSON keeps "status" emitting the slug so existing clients still parse it.
  • buildPostQuery LEFT JOINs statuses ps on (tenant_id, slug) to expose ps.kind. Default home view filters statuses.kind IN (open, active) — admin-added active statuses surface automatically.
  • CanBeVoted now branches on kind NOT IN (closed-completed, closed-declined, duplicate); markPostAsDuplicate writes status_slug = 'duplicate'.
  • Status-change webhook emits post_status_slug, post_status_kind, post_status_label, post_old_status_slug, post_old_status_label. Slug-aware short-circuit guard handles custom slugs sharing the default int enum.

Roadmap

  • /roadmap lanes derived from tenant.statuses where is_active and show_on_roadmap. Ordered by sort_order. Horizontal scroll when there are more than fit (each column min-width 320px).

Admin UI

  • /admin/statuses lists the catalogue with Up/Down sort-order arrows, toggle-active, Edit (label/color/icon/sort_order/visibility), Delete (refused for system rows and for statuses still attached to posts).

Backwards compatibility

  • Built-in slugs (open, planned, started, completed, declined, duplicate) keep their names so existing webhook receivers reading post_status_slug see the same values they would have read from post_status before.
  • Migrations are additive then drop the legacy int column; rollback is via DB restore.

Closes feedback.fider.io/posts/111

hodyhq and others added 7 commits June 24, 2026 11:16
…(Implements feedback.fider.io/posts/111)

Introduces a tenant-scoped statuses table that supersedes the
hardcoded enum.PostStatus list. Admins manage their own catalogue
from /admin/statuses — rename, recolor, set the semantic kind, toggle
visibility on the home / filter / roadmap, reorder, or add brand-new
slugs (e.g. "Under Review", "Triage", "Beta Testing").

Schema
- statuses (tenant_id, slug, label, kind, color, icon, show_on_home,
  show_on_roadmap, filterable, sort_order, is_system, is_active,
  legacy_enum used only for backfill, created_at, updated_at)
- statuses.kind ∈ {open, active, closed-completed, closed-declined, duplicate}
- posts.status_slug — new authoritative identifier
- Migrations seed the six built-ins (open, planned, started,
  completed, declined, duplicate) for every existing tenant, backfill
  posts.status_slug from posts.status, then DROP posts.status +
  statuses.legacy_enum. New tenants auto-seed via SeedTenantStatuses
  on signup.
- 202606241500 adds show_on_roadmap with a kind-based backfill so
  Planned/Started/Completed lanes appear on /roadmap unchanged.

Backend
- entity.Post carries StatusSlug + StatusKind (joined from the
  statuses table). enum.PostStatus is no longer a field. JSON keeps
  "status" emitting the slug so existing clients still parse it.
- buildPostQuery LEFT JOINs statuses ps on (tenant_id, slug) to
  expose ps.kind. Default home view filters statuses.kind IN
  (open, active) — admin-added active statuses surface automatically.
- CanBeVoted now branches on kind NOT IN
  (closed-completed, closed-declined, duplicate); markPostAsDuplicate
  writes status_slug = 'duplicate'.
- Status-change webhook emits post_status_slug, post_status_kind,
  post_status_label, post_old_status_slug, post_old_status_label.
  Slug-aware short-circuit guard handles custom slugs sharing the
  default int enum.

Roadmap
- /roadmap lanes derived from tenant.statuses where is_active and
  show_on_roadmap. Ordered by sort_order. Horizontal scroll when
  there are more than fit (each column min-width 320px).

Admin UI
- /admin/statuses lists the catalogue with Up/Down sort-order
  arrows, toggle-active, Edit (label/color/icon/sort_order/visibility),
  Delete (refused for system rows and for statuses still attached to
  posts).

Backwards compatibility
- Built-in slugs (open, planned, started, completed, declined,
  duplicate) keep their names so existing webhook receivers reading
  post_status_slug see the same values they would have read from
  post_status before.
- Migrations are additive then drop the legacy int column; rollback
  is via DB restore.

Closes feedback.fider.io/posts/111
- actions/post_test.go: SetResponse.Status is now a slug string,
  swap enum.PostDeleted for "deleted".
- tasks/delete_post_test.go: post.Status is gone; webhook payload
  emits post_status_slug now — update both assertions and the
  fixture's StatusSlug.
- ESLint --fix on LF line endings across the PR set (CRLF leaked in
  from the Windows checkout). Drop unused _e parameter and
  ButtonClickEvent import on ManageStatuses.page.tsx.
- seed status catalogue in setup.sql so postgres tests see the new
  LEFT JOIN ps.kind path return rows
- register no-op SeedTenantStatuses handler in CreateTenant tests
- register GetStatusBySlug handler in SetResponse tests
- defer recover() in attachTenantStatuses middleware so unit tests
  with a bus that lacks the handler still pass (errors here are
  already non-fatal per the docstring)
- set StatusSlug on the global feed test fixture so i18n lookup
  resolves instead of returning a Missing-Translation warning
- restore upstream VoteCounter layout (count inside button)
The migration 202606231500 drops legacy_enum from statuses, so
seeding it in the test fixture fails with 'column does not exist'.
Vote handlers gate via Post.CanBeVoted(), which reads StatusKind.
SetPostResponse only mutated StatusSlug on the in-memory Post, so
callers that reused the struct (e.g. AddVote right after marking
a post completed) saw the stale empty kind and skipped the closed
check, letting votes through on closed posts.
'deleted' is the internal tombstone slug and has no statuses row by
design. Treat the not-found result from the kind lookup as empty
kind rather than failing the whole command.
The empty-state check excluded closed-completed columns, so a roadmap
with only Completed posts rendered as 'waiting for its first update'.
With custom statuses, show_on_roadmap is the explicit opt-in; any
column flagged on should keep the board visible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant