diff --git a/crudview/crud.go b/crudview/crud.go index 81418dd..d1ddef4 100644 --- a/crudview/crud.go +++ b/crudview/crud.go @@ -45,6 +45,17 @@ type Config struct { // ListView) factory instead when the data wants a leading date/time // badge (view.Item's LeadTop/Main/Bottom) rather than a plain label. List func(selected *dom.SignalString, onSelect func(view.Item)) ListView + + // OnAfterReload se invoca al final de Reload(), justo después de que la lista + // se re-llena con los items del presenter (list.SetItems). Recibe el list + // concreto ya pintado, para que el consumidor acomode detalles que el widget no + // puede derivar — p. ej. type-assert a *targetdate.TargetDate / *targethour. + // TargetHour y setear sus campos (FreeSlots). nil = hook ausente. + // + // Un solo argumento a propósito: items[] no viajan (list.Items() / + // Presenter.Items() ya los dan); lo único que NO se puede conseguir de otro + // lado es el list concrete que Config.List construyó. + OnAfterReload func(list ListView) } // New builds the renderer around an already-constructed Presenter. It generates the form from @@ -82,7 +93,8 @@ func New(cfg Config) (*CrudView, error) { // List is passed through as-is, nil included: Init resolves the // same targetlist.TargetList default that a nil List would get // here, so there is exactly one place that decision lives. - List: cfg.List, + List: cfg.List, + OnAfterReload: cfg.OnAfterReload, } // Auto-save: every field commit (blur/change) persists immediately — see diff --git a/crudview/crudview.go b/crudview/crudview.go index 5982c5f..2cb83fa 100644 --- a/crudview/crudview.go +++ b/crudview/crudview.go @@ -135,6 +135,17 @@ type CrudView struct { OnUpdated func(ids []string, err error) OnCancel func() + // OnAfterReload se invoca al final de Reload(), justo después de que la lista + // se re-llena con los items del presenter (list.SetItems). Recibe el list + // concreto ya pintado, para que el consumidor acomode detalles que el widget no + // puede derivar — p. ej. type-assert a *targetdate.TargetDate / *targethour. + // TargetHour y setear sus campos (FreeSlots). nil = hook ausente. + // + // Un solo argumento a propósito: items[] no viajan (list.Items() / + // Presenter.Items() ya los dan); lo único que NO se puede conseguir de otro + // lado es el list concrete que Config.List construyó. + OnAfterReload func(list ListView) + // internal form *form.Form // typed handle set by New; nil when standalone list ListView // owns the row rendering + ⋮ menu @@ -329,6 +340,9 @@ func (v *CrudView) Reload() error { return err } v.filter() + if v.OnAfterReload != nil && v.list != nil { + v.OnAfterReload(v.list) + } return nil } diff --git a/crudview/crudview_test.go b/crudview/crudview_test.go index dfd1bdf..20d9042 100644 --- a/crudview/crudview_test.go +++ b/crudview/crudview_test.go @@ -5,6 +5,7 @@ package crudview import ( "testing" + "webtyp.com/dom" . "webtyp.com/fmt" "webtyp.com/fmt/lang" . "webtyp.com/html" @@ -136,3 +137,86 @@ func TestCrudView_DeleteConfirm_Language(t *testing.T) { } } } + +type stubList struct { + items []view.Item + selected *dom.SignalString +} + +func (s *stubList) GetID() string { return "" } +func (s *stubList) SetID(id string) {} +func (s *stubList) String() string { return "" } +func (s *stubList) Render() *dom.Element { return nil } +func (s *stubList) Children() []dom.Component { return nil } +func (s *stubList) SetItems(items []view.Item) { s.items = items } +func (s *stubList) Items() []view.Item { return s.items } +func (s *stubList) Count() int { return len(s.items) } +func (s *stubList) SetSelectMode(on bool) {} +func (s *stubList) SetDanger(on bool) {} +func (s *stubList) CheckedIDs() []string { return nil } +func (s *stubList) OnCheckedChange(fn func(n int)) {} + +func TestOnAfterReload_FiresWithList(t *testing.T) { + fb := fakeListBackend() + p := view.New(fb, &Device{}) + + var capturedList ListView + sList := &stubList{} + + v := &CrudView{ + Title: "OnAfterReload Test", + Presenter: p, + List: func(selected *dom.SignalString, onSelect func(view.Item)) ListView { + sList.selected = selected + return sList + }, + OnAfterReload: func(list ListView) { + capturedList = list + }, + } + v.Init(&mockCtx{}) + + if capturedList != ListView(sList) { + t.Errorf("expected capturedList to be sList, got %v", capturedList) + } +} + +func TestOnAfterReload_AfterSetItems(t *testing.T) { + fb := fakeListBackend() + p := view.New(fb, &Device{}) + + var countInHook int + sList := &stubList{} + + v := &CrudView{ + Title: "OnAfterReload Test", + Presenter: p, + List: func(selected *dom.SignalString, onSelect func(view.Item)) ListView { + sList.selected = selected + return sList + }, + OnAfterReload: func(list ListView) { + countInHook = len(list.Items()) + }, + } + v.Init(&mockCtx{}) + + if countInHook != len(fb.Rows) { + t.Errorf("expected countInHook == %d, got %d", len(fb.Rows), countInHook) + } +} + +func TestOnAfterReload_NilNoop(t *testing.T) { + fb := &conformance.FakeLister{} + p := view.New(fb, &Device{}) + + v := &CrudView{ + Title: "OnAfterReload Nil Test", + Presenter: p, + } + v.Init(&mockCtx{}) + + if err := v.Reload(); err != nil { + t.Fatalf("unexpected error on Reload: %v", err) + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3e42154..f17c7bf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -270,6 +270,7 @@ The high-level pattern for constructing a CRUD view is `crudview.New(Config)`. T - Saves are validated and synced via `form.SyncValues` before shipping to `Presenter.Save`. - `OnSave`/`OnDelete` are only wired when `Presenter.CanSave()`/`CanDelete()` are true. - Empty search string placeholders default to `"Search…"`, but can be customized via `Presenter.SearchPlaceholder()`. +- The `OnAfterReload func(list ListView)` hook runs at the end of `Reload()`, right after the list widget has been repopulated via `filter()`. It passes only the concrete `ListView` instance constructed by `Config.List`, adhering to the minimal API surface principle (items do not need to be passed separately as they are accessible directly via `list.Items()`). #### Principle: Standard-shaped tests diff --git a/docs/PLAN.md b/docs/PLAN.md index 58c3c0e..c4c8b76 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1,10 +1,11 @@ --- PLAN: "feat(crudview): OnAfterReload hook — consumer personalizes the list widget after every load" -TAG: v0.2.21 +TAG: v0.2.23 EXECUTOR: jules REVIEWER: none -STATUS: running +STATUS: review SESSION: 7064163444445900914 +PR: https://github.com/webtyp/layout/pull/34 --- # PLAN — `crudview.Config.OnAfterReload` (Etapa G del `DEMO_AGENDA_MASTER_PLAN`) diff --git a/docs/img/badges.svg b/docs/img/badges.svg index c023da6..4176845 100644 --- a/docs/img/badges.svg +++ b/docs/img/badges.svg @@ -51,7 +51,7 @@ text-anchor="middle" font-family="sans-serif" font-size="11" fill="white">Coverage 85.3% + text-anchor="middle" font-family="sans-serif" font-size="11" fill="white">85.4%