From 20412fe6770f6b3be9e7e64b914b9fd1a5fa8b47 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:38:46 +0000 Subject: [PATCH 1/3] feat(crudview): add OnAfterReload hook to Config and CrudView Co-authored-by: cdvelop <44058491+cdvelop@users.noreply.github.com> --- crudview/crud.go | 14 ++++++- crudview/crudview.go | 14 +++++++ crudview/crudview_test.go | 84 +++++++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 1 + 4 files changed, 112 insertions(+), 1 deletion(-) 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 From c2a13b00fd6ac7c6ee0af6957687e8f89a0358f2 Mon Sep 17 00:00:00 2001 From: Cesar Solis <44058491+cdvelop@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:54:07 -0300 Subject: [PATCH 2/3] chore: status transition to review [pr https://github.com/webtyp/layout/pull/34] --- docs/PLAN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index 58c3c0e..f5f369c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -3,8 +3,9 @@ PLAN: "feat(crudview): OnAfterReload hook — consumer personalizes the list wid TAG: v0.2.21 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`) From 5db69ddd3e65308f1f2f4351bcfecfe0c126fa05 Mon Sep 17 00:00:00 2001 From: Cesar Solis <44058491+cdvelop@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:03:11 -0300 Subject: [PATCH 3/3] review: corrections before merge --- docs/PLAN.md | 2 +- docs/img/badges.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index f5f369c..c4c8b76 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1,6 +1,6 @@ --- 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: review 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%