Merge: t-paliad-163 Phase 1 — universal <FilterBar> primitive + /inbox migration
Three slices on mai/riemann/inventor-universal:d5a01e6Slice 1 — RenderSpec.list.row_action + validator + testsde4e133Slice 2 — <FilterBar> scaffolding (axes / url-codec / save-modal)4670cd6Slice 3 — /inbox migrates to <FilterBar>; tabs collapse to chips What ships (Phase 1): - A new frontend/src/client/filter-bar/ module: types.ts — Spec + RenderSpec + AxisDeclaration types axes.ts — registry of supported filter axes url-codec.ts — URL ↔ FilterSpec serialization (round-tripping) save-modal.ts — "Speichern als Sicht" dialog index.ts — <FilterBar> mounts Plus a url-codec.test.ts golden table. - /inbox surface migrates to the bar: Top-level "Zur Genehmigung / Meine Anfragen" tabs collapse into the bar's `approval_viewer_role` chip cluster (incoming / outgoing / both). One control, three mutually exclusive options. Stateful via `?role=` URL param. Bookmark-friendly: legacy `?tab=mine` + `?tab=pending-mine` redirect to `?role=outgoing` and `?role=incoming` respectively for one release. Sortable column headers on the result list (list-shape only; cards/calendar shape-modes defer their own ordering to the spec). - RenderSpec.list gains `row_action` ("navigate" | "expand" | "none") so list-shape surfaces declare row click behaviour explicitly. The validator + tests cover the new field. - system_views.go gains the inbox SystemView definitions so the bar reads its base spec from the same registry that custom views use. m's locked positions (commit `1e23745` design doc; m's greenlight 2026-05-08 21:47): all 11 default picks honoured. Q4 = collapse tabs to chips ✓. Phase 2 surfaces (port /agenda → bar; port /events → bar; port /deadlines → bar; port /appointments → bar) follow as separate PRs. Refs m/paliad#23.
This commit is contained in:
@@ -45,10 +45,23 @@ type RenderSpec struct {
|
||||
// ListConfig is the per-shape config for shape=list. Powers both the
|
||||
// /events table look (density=comfortable) and the activity-feed look
|
||||
// (density=compact + actor/time columns).
|
||||
//
|
||||
// RowAction tells shape-list which row interaction to wire when the
|
||||
// universal <FilterBar> renders the table. "navigate" (the default and
|
||||
// the contract for the existing /agenda/dashboard surfaces) routes a
|
||||
// row click to a per-kind detail page. "complete_toggle" is the
|
||||
// /events deadline-row pattern (checkbox + reopen button). "approve"
|
||||
// is the /inbox approver row (approve/reject buttons + revoke). "none"
|
||||
// is read-only (audit views, retrospective lists).
|
||||
//
|
||||
// shape-list.ts honours this when emitting the table's `entity-table`
|
||||
// classes — `entity-table--readonly` plus `none` skips the navigate
|
||||
// handler entirely.
|
||||
type ListConfig struct {
|
||||
Columns []string `json:"columns,omitempty"`
|
||||
Sort SortOrder `json:"sort,omitempty"`
|
||||
Density ListDensity `json:"density,omitempty"`
|
||||
Columns []string `json:"columns,omitempty"`
|
||||
Sort SortOrder `json:"sort,omitempty"`
|
||||
Density ListDensity `json:"density,omitempty"`
|
||||
RowAction ListRowAction `json:"row_action,omitempty"`
|
||||
}
|
||||
|
||||
// CardsConfig is the per-shape config for shape=cards.
|
||||
@@ -78,6 +91,29 @@ const (
|
||||
DensityCompact ListDensity = "compact"
|
||||
)
|
||||
|
||||
// ListRowAction identifies which row interaction the list-shape renderer
|
||||
// should wire. Defaults to RowActionNavigate when empty so existing
|
||||
// SystemView definitions and saved user views continue to render rows
|
||||
// that route to the per-kind detail page.
|
||||
type ListRowAction string
|
||||
|
||||
const (
|
||||
RowActionNavigate ListRowAction = "navigate"
|
||||
RowActionCompleteToggle ListRowAction = "complete_toggle"
|
||||
RowActionApprove ListRowAction = "approve"
|
||||
RowActionNone ListRowAction = "none"
|
||||
)
|
||||
|
||||
// KnownRowActions is the registry the validator checks against. Adding a
|
||||
// new action = add a const above AND append here AND extend
|
||||
// shape-list.ts's switch.
|
||||
var KnownRowActions = []ListRowAction{
|
||||
RowActionNavigate,
|
||||
RowActionCompleteToggle,
|
||||
RowActionApprove,
|
||||
RowActionNone,
|
||||
}
|
||||
|
||||
type CardsGroupBy string
|
||||
|
||||
const (
|
||||
@@ -148,6 +184,9 @@ func (c *ListConfig) validate() error {
|
||||
default:
|
||||
return fmt.Errorf("%w: unknown list.density %q", ErrInvalidInput, c.Density)
|
||||
}
|
||||
if c.RowAction != "" && !slices.Contains(KnownRowActions, c.RowAction) {
|
||||
return fmt.Errorf("%w: unknown list.row_action %q", ErrInvalidInput, c.RowAction)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,26 @@ func TestRenderSpec_CalendarViewEnum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderSpec_RowActionEnum(t *testing.T) {
|
||||
for _, action := range KnownRowActions {
|
||||
t.Run(string(action), func(t *testing.T) {
|
||||
s := RenderSpec{Shape: ShapeList, List: &ListConfig{RowAction: action}}
|
||||
if err := s.Validate(); err != nil {
|
||||
t.Fatalf("known row_action %q must validate: %v", action, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
s := RenderSpec{Shape: ShapeList, List: &ListConfig{RowAction: "delete"}}
|
||||
if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Fatalf("unknown row_action must reject, got %v", err)
|
||||
}
|
||||
// Empty defaults to navigate at the renderer level — schema accepts.
|
||||
empty := RenderSpec{Shape: ShapeList, List: &ListConfig{}}
|
||||
if err := empty.Validate(); err != nil {
|
||||
t.Fatalf("empty row_action must validate (defaults to navigate): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderSpec_RoundTrip(t *testing.T) {
|
||||
original := RenderSpec{
|
||||
Shape: ShapeList,
|
||||
|
||||
@@ -101,8 +101,13 @@ func EventsSystemView() SystemView {
|
||||
}
|
||||
|
||||
// InboxSystemView returns the SystemView definition for /inbox — the
|
||||
// 4-eye approval surface (the "Zur Genehmigung" tab). The "Meine
|
||||
// Anfragen" tab is a sibling spec resolved by tab-state on the page.
|
||||
// 4-eye approval surface (the "Zur Genehmigung" view). The "Eigene
|
||||
// Anfragen" sibling view is selected via the bar's
|
||||
// approval_viewer_role axis (chip cluster on the same surface).
|
||||
//
|
||||
// RowAction = RowActionApprove → shape-list.ts renders the approval
|
||||
// row layout (entity title + diff + approve/reject/revoke buttons)
|
||||
// and the surface wires action handlers via the rendered data-attrs.
|
||||
func InboxSystemView() SystemView {
|
||||
return SystemView{
|
||||
Slug: "inbox",
|
||||
@@ -122,14 +127,17 @@ func InboxSystemView() SystemView {
|
||||
Render: RenderSpec{
|
||||
Shape: ShapeList,
|
||||
List: &ListConfig{
|
||||
Density: DensityComfortable,
|
||||
Sort: SortDateAsc,
|
||||
Density: DensityComfortable,
|
||||
Sort: SortDateAsc,
|
||||
RowAction: RowActionApprove,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InboxRequesterSystemView is the "Meine Anfragen" tab of /inbox.
|
||||
// InboxRequesterSystemView is the "Eigene Anfragen" sibling view of
|
||||
// /inbox. Reachable via the bar's approval_viewer_role chip ("Eigene
|
||||
// Anfragen") on the /inbox surface, or as its own URL on /views/inbox-mine.
|
||||
func InboxRequesterSystemView() SystemView {
|
||||
return SystemView{
|
||||
Slug: "inbox-mine",
|
||||
@@ -148,8 +156,9 @@ func InboxRequesterSystemView() SystemView {
|
||||
Render: RenderSpec{
|
||||
Shape: ShapeList,
|
||||
List: &ListConfig{
|
||||
Density: DensityComfortable,
|
||||
Sort: SortDateAsc,
|
||||
Density: DensityComfortable,
|
||||
Sort: SortDateAsc,
|
||||
RowAction: RowActionApprove,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user