diff --git a/frontend/src/client/views/types.ts b/frontend/src/client/views/types.ts index 6170401..47db8fe 100644 --- a/frontend/src/client/views/types.ts +++ b/frontend/src/client/views/types.ts @@ -71,10 +71,13 @@ export interface FilterSpec { export type RenderShape = "list" | "cards" | "calendar"; +export type ListRowAction = "navigate" | "complete_toggle" | "approve" | "none"; + export interface ListConfig { columns?: string[]; sort?: "date_asc" | "date_desc"; density?: "comfortable" | "compact"; + row_action?: ListRowAction; } export interface CardsConfig { diff --git a/internal/services/render_spec.go b/internal/services/render_spec.go index 4076d5f..e62ce36 100644 --- a/internal/services/render_spec.go +++ b/internal/services/render_spec.go @@ -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 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 } diff --git a/internal/services/render_spec_test.go b/internal/services/render_spec_test.go index f7efc8a..932e5ca 100644 --- a/internal/services/render_spec_test.go +++ b/internal/services/render_spec_test.go @@ -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,