fix(favorites): refresh sidebar after bookmark changes
- commit
- a2631c033ea7ac95ba3573bc3a54cd765dab8879
- author
- brice · · 2026-06-28 16:28 UTC
- parent
- 4c9a8b7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +56 -6
M internal/ui/browser_window_favorites_sidebar.go +3 -0
| @@ -131,5 +131,8 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error { | |||
| 131 | 131 | if err != nil { | |
| 132 | 132 | return err | |
| 133 | 133 | } | |
| 134 | + | if bw := a.lastFocusedBrowserWindow(); bw != nil && bw.favoritesSidebar != nil { | |
| 135 | + | bw.favoritesSidebar.RequestReloadIfVisible("current-page-favorite-toggle") | |
| 136 | + | } | |
| 134 | 137 | return nil | |
| 135 | 138 | } | |
| @@ -131,5 +131,8 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error { | |||||
| 131 | if err != nil { | 131 | if err != nil { | ||
| 132 | return err | 132 | return err | ||
| 133 | } | 133 | } | ||
| 134 | + | if bw := a.lastFocusedBrowserWindow(); bw != nil && bw.favoritesSidebar != nil { | |||
| 135 | + | bw.favoritesSidebar.RequestReloadIfVisible("current-page-favorite-toggle") | |||
| 136 | + | } | |||
| 134 | return nil | 137 | return nil | ||
| 135 | } | 138 | } | ||
M internal/ui/component/favorites_sidebar.go +14 -0
| @@ -116,6 +116,7 @@ func (fs *FavoritesSidebar) Show() { | |||
| 116 | 116 | fs.outerBox.SetVisible(true) | |
| 117 | 117 | fs.visible = true | |
| 118 | 118 | fs.mu.Unlock() | |
| 119 | + | fs.startLoad() | |
| 119 | 120 | fs.focusSearch() | |
| 120 | 121 | } | |
| 121 | 122 | ||
| @@ -233,6 +234,19 @@ func (fs *FavoritesSidebar) focusSearch() { | |||
| 233 | 234 | })) | |
| 234 | 235 | } | |
| 235 | 236 | ||
| 237 | + | // RequestReloadIfVisible refreshes sidebar data when the sidebar is currently visible. | |
| 238 | + | func (fs *FavoritesSidebar) RequestReloadIfVisible(_ string) { | |
| 239 | + | if fs == nil { | |
| 240 | + | return | |
| 241 | + | } | |
| 242 | + | fs.mu.RLock() | |
| 243 | + | visible := fs.visible && !fs.destroyed | |
| 244 | + | fs.mu.RUnlock() | |
| 245 | + | if visible { | |
| 246 | + | fs.startLoad() | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | ||
| 236 | 250 | func (fs *FavoritesSidebar) startLoad() { | |
| 237 | 251 | if fs == nil { | |
| 238 | 252 | return | |
| @@ -116,6 +116,7 @@ func (fs *FavoritesSidebar) Show() { | |||||
| 116 | fs.outerBox.SetVisible(true) | 116 | fs.outerBox.SetVisible(true) | ||
| 117 | fs.visible = true | 117 | fs.visible = true | ||
| 118 | fs.mu.Unlock() | 118 | fs.mu.Unlock() | ||
| 119 | + | fs.startLoad() | |||
| 119 | fs.focusSearch() | 120 | fs.focusSearch() | ||
| 120 | } | 121 | } | ||
| 121 | 122 | ||||
| @@ -233,6 +234,19 @@ func (fs *FavoritesSidebar) focusSearch() { | |||||
| 233 | })) | 234 | })) | ||
| 234 | } | 235 | } | ||
| 235 | 236 | ||||
| 237 | + | // RequestReloadIfVisible refreshes sidebar data when the sidebar is currently visible. | |||
| 238 | + | func (fs *FavoritesSidebar) RequestReloadIfVisible(_ string) { | |||
| 239 | + | if fs == nil { | |||
| 240 | + | return | |||
| 241 | + | } | |||
| 242 | + | fs.mu.RLock() | |||
| 243 | + | visible := fs.visible && !fs.destroyed | |||
| 244 | + | fs.mu.RUnlock() | |||
| 245 | + | if visible { | |||
| 246 | + | fs.startLoad() | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | ||||
| 236 | func (fs *FavoritesSidebar) startLoad() { | 250 | func (fs *FavoritesSidebar) startLoad() { | ||
| 237 | if fs == nil { | 251 | if fs == nil { | ||
| 238 | return | 252 | return | ||
M internal/ui/component/favorites_sidebar_forms.go +1 -1
| @@ -207,7 +207,7 @@ func (fs *FavoritesSidebar) submitForm() bool { | |||
| 207 | 207 | fs.setNotice(err.Error()) | |
| 208 | 208 | return true | |
| 209 | 209 | } | |
| 210 | - | _, err = uc.UpdateFavorite(ctx, dto.FavoriteUpdateInput{ID: id, Title: title, ShortcutKey: key, ShortcutKeySet: shortcutText != ""}) | |
| 210 | + | _, err = uc.UpdateFavorite(ctx, dto.FavoriteUpdateInput{ID: id, Title: title, ShortcutKey: key, ShortcutKeySet: true}) | |
| 211 | 211 | if err != nil { | |
| 212 | 212 | fs.setNotice(err.Error()) | |
| 213 | 213 | return true | |
| @@ -207,7 +207,7 @@ func (fs *FavoritesSidebar) submitForm() bool { | |||||
| 207 | fs.setNotice(err.Error()) | 207 | fs.setNotice(err.Error()) | ||
| 208 | return true | 208 | return true | ||
| 209 | } | 209 | } | ||
| 210 | - | _, err = uc.UpdateFavorite(ctx, dto.FavoriteUpdateInput{ID: id, Title: title, ShortcutKey: key, ShortcutKeySet: shortcutText != ""}) | 210 | + | _, err = uc.UpdateFavorite(ctx, dto.FavoriteUpdateInput{ID: id, Title: title, ShortcutKey: key, ShortcutKeySet: true}) |
| 211 | if err != nil { | 211 | if err != nil { | ||
| 212 | fs.setNotice(err.Error()) | 212 | fs.setNotice(err.Error()) | ||
| 213 | return true | 213 | return true | ||
M internal/ui/component/favorites_sidebar_test.go +38 -5
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "sync" | |
| 7 | 7 | "testing" | |
| 8 | + | "time" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/dumber/internal/application/dto" | |
| 10 | 11 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -33,10 +34,17 @@ type fakeFavoritesSidebarUC struct { | |||
| 33 | 34 | favID entity.FavoriteID | |
| 34 | 35 | tagID entity.TagID | |
| 35 | 36 | } | |
| 36 | - | err error | |
| 37 | + | err error | |
| 38 | + | getAllCalled chan struct{} | |
| 37 | 39 | } | |
| 38 | 40 | ||
| 39 | 41 | func (f *fakeFavoritesSidebarUC) GetAll(context.Context) ([]*entity.Favorite, error) { | |
| 42 | + | if f.getAllCalled != nil { | |
| 43 | + | select { | |
| 44 | + | case f.getAllCalled <- struct{}{}: | |
| 45 | + | default: | |
| 46 | + | } | |
| 47 | + | } | |
| 40 | 48 | return f.favorites, nil | |
| 41 | 49 | } | |
| 42 | 50 | func (f *fakeFavoritesSidebarUC) GetAllTags(context.Context) ([]*entity.Tag, error) { | |
| @@ -183,6 +191,25 @@ func TestFavoritesSidebarActivationCallbacks(t *testing.T) { | |||
| 183 | 191 | assert.Equal(t, []string{"nav:https://a.test", "keep:https://b.test", "pane:https://c.test"}, called) | |
| 184 | 192 | } | |
| 185 | 193 | ||
| 194 | + | func TestFavoritesSidebarRequestReloadIfVisibleReloadsData(t *testing.T) { | |
| 195 | + | called := make(chan struct{}, 1) | |
| 196 | + | uc := &fakeFavoritesSidebarUC{getAllCalled: called} | |
| 197 | + | fs := newFavoritesSidebarHarness(nil, nil) | |
| 198 | + | fs.favoritesUC = uc | |
| 199 | + | fs.visible = true | |
| 200 | + | ||
| 201 | + | fs.RequestReloadIfVisible("test") | |
| 202 | + | ||
| 203 | + | require.Eventually(t, func() bool { | |
| 204 | + | select { | |
| 205 | + | case <-called: | |
| 206 | + | return true | |
| 207 | + | default: | |
| 208 | + | return false | |
| 209 | + | } | |
| 210 | + | }, time.Second, 10*time.Millisecond) | |
| 211 | + | } | |
| 212 | + | ||
| 186 | 213 | func TestFavoritesSidebarStaleLoadRejected(t *testing.T) { | |
| 187 | 214 | fs := newFavoritesSidebarHarness(nil, nil) | |
| 188 | 215 | fs.loadGen = 2 | |
| @@ -236,9 +263,6 @@ func TestFavoritesSidebarRenderingModelTagCallbackRetentionAndListRows(t *testin | |||
| 236 | 263 | assert.False(t, fs.displayRows[1].Selectable) | |
| 237 | 264 | assert.Equal(t, []string{"saved"}, favoriteSidebarNoticeRows("saved", "", true)) | |
| 238 | 265 | assert.Equal(t, []string{"No favorites"}, favoriteSidebarNoticeRows("", "", false)) | |
| 239 | - | fs.tagCallbacks = []interface{}{func() {}, func() {}} | |
| 240 | - | fs.tagCallbacks = nil | |
| 241 | - | assert.Empty(t, fs.tagCallbacks) | |
| 242 | 266 | } | |
| 243 | 267 | ||
| 244 | 268 | func TestFavoritesSidebarSlashFocusBehavior(t *testing.T) { | |
| @@ -284,7 +308,7 @@ func TestFavoritesSidebarFocusZoneCyclingSkipsAbsentZones(t *testing.T) { | |||
| 284 | 308 | assert.Equal(t, []favoritesSidebarFocusZone{favoritesSidebarFocusConfirm}, fs.availableFocusZones()) | |
| 285 | 309 | } | |
| 286 | 310 | ||
| 287 | - | func TestFavoritesSidebarSingleKeyCommandsAreGatedInTextEditContext(t *testing.T) { | |
| 311 | + | func TestFavoritesSidebarSingleKeyCommandsStartManagementOutsideTextEditContext(t *testing.T) { | |
| 288 | 312 | fs := newFavoritesSidebarHarness(nil, nil) | |
| 289 | 313 | fs.searchEntry = nil | |
| 290 | 314 | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}} | |
| @@ -324,6 +348,15 @@ func TestFavoritesSidebarAddAndEditDTOs(t *testing.T) { | |||
| 324 | 348 | assert.Equal(t, "Edited", uc.updateInputs[0].Title) | |
| 325 | 349 | require.NotNil(t, uc.updateInputs[0].ShortcutKey) | |
| 326 | 350 | assert.Equal(t, 4, *uc.updateInputs[0].ShortcutKey) | |
| 351 | + | ||
| 352 | + | fs.mode = favoritesSidebarModeEdit | |
| 353 | + | fs.editingID = 7 | |
| 354 | + | fs.formTitle = "Edited" | |
| 355 | + | fs.formShortcut = "" | |
| 356 | + | assert.True(t, fs.submitForm()) | |
| 357 | + | require.Len(t, uc.updateInputs, 2) | |
| 358 | + | assert.True(t, uc.updateInputs[1].ShortcutKeySet) | |
| 359 | + | assert.Nil(t, uc.updateInputs[1].ShortcutKey) | |
| 327 | 360 | } | |
| 328 | 361 | ||
| 329 | 362 | func TestFavoritesSidebarInvalidTagAndShortcutPreserveFormState(t *testing.T) { | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "sync" | 6 | "sync" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | + | "time" | |||
| 8 | 9 | ||||
| 9 | "github.com/bnema/dumber/internal/application/dto" | 10 | "github.com/bnema/dumber/internal/application/dto" | ||
| 10 | "github.com/bnema/dumber/internal/domain/entity" | 11 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -33,10 +34,17 @@ type fakeFavoritesSidebarUC struct { | |||||
| 33 | favID entity.FavoriteID | 34 | favID entity.FavoriteID | ||
| 34 | tagID entity.TagID | 35 | tagID entity.TagID | ||
| 35 | } | 36 | } | ||
| 36 | - | err error | 37 | + | err error |
| 38 | + | getAllCalled chan struct{} | |||
| 37 | } | 39 | } | ||
| 38 | 40 | ||||
| 39 | func (f *fakeFavoritesSidebarUC) GetAll(context.Context) ([]*entity.Favorite, error) { | 41 | func (f *fakeFavoritesSidebarUC) GetAll(context.Context) ([]*entity.Favorite, error) { | ||
| 42 | + | if f.getAllCalled != nil { | |||
| 43 | + | select { | |||
| 44 | + | case f.getAllCalled <- struct{}{}: | |||
| 45 | + | default: | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 40 | return f.favorites, nil | 48 | return f.favorites, nil | ||
| 41 | } | 49 | } | ||
| 42 | func (f *fakeFavoritesSidebarUC) GetAllTags(context.Context) ([]*entity.Tag, error) { | 50 | func (f *fakeFavoritesSidebarUC) GetAllTags(context.Context) ([]*entity.Tag, error) { | ||
| @@ -183,6 +191,25 @@ func TestFavoritesSidebarActivationCallbacks(t *testing.T) { | |||||
| 183 | assert.Equal(t, []string{"nav:https://a.test", "keep:https://b.test", "pane:https://c.test"}, called) | 191 | assert.Equal(t, []string{"nav:https://a.test", "keep:https://b.test", "pane:https://c.test"}, called) | ||
| 184 | } | 192 | } | ||
| 185 | 193 | ||||
| 194 | + | func TestFavoritesSidebarRequestReloadIfVisibleReloadsData(t *testing.T) { | |||
| 195 | + | called := make(chan struct{}, 1) | |||
| 196 | + | uc := &fakeFavoritesSidebarUC{getAllCalled: called} | |||
| 197 | + | fs := newFavoritesSidebarHarness(nil, nil) | |||
| 198 | + | fs.favoritesUC = uc | |||
| 199 | + | fs.visible = true | |||
| 200 | + | ||||
| 201 | + | fs.RequestReloadIfVisible("test") | |||
| 202 | + | ||||
| 203 | + | require.Eventually(t, func() bool { | |||
| 204 | + | select { | |||
| 205 | + | case <-called: | |||
| 206 | + | return true | |||
| 207 | + | default: | |||
| 208 | + | return false | |||
| 209 | + | } | |||
| 210 | + | }, time.Second, 10*time.Millisecond) | |||
| 211 | + | } | |||
| 212 | + | ||||
| 186 | func TestFavoritesSidebarStaleLoadRejected(t *testing.T) { | 213 | func TestFavoritesSidebarStaleLoadRejected(t *testing.T) { | ||
| 187 | fs := newFavoritesSidebarHarness(nil, nil) | 214 | fs := newFavoritesSidebarHarness(nil, nil) | ||
| 188 | fs.loadGen = 2 | 215 | fs.loadGen = 2 | ||
| @@ -236,9 +263,6 @@ func TestFavoritesSidebarRenderingModelTagCallbackRetentionAndListRows(t *testin | |||||
| 236 | assert.False(t, fs.displayRows[1].Selectable) | 263 | assert.False(t, fs.displayRows[1].Selectable) | ||
| 237 | assert.Equal(t, []string{"saved"}, favoriteSidebarNoticeRows("saved", "", true)) | 264 | assert.Equal(t, []string{"saved"}, favoriteSidebarNoticeRows("saved", "", true)) | ||
| 238 | assert.Equal(t, []string{"No favorites"}, favoriteSidebarNoticeRows("", "", false)) | 265 | assert.Equal(t, []string{"No favorites"}, favoriteSidebarNoticeRows("", "", false)) | ||
| 239 | - | fs.tagCallbacks = []interface{}{func() {}, func() {}} | |||
| 240 | - | fs.tagCallbacks = nil | |||
| 241 | - | assert.Empty(t, fs.tagCallbacks) | |||
| 242 | } | 266 | } | ||
| 243 | 267 | ||||
| 244 | func TestFavoritesSidebarSlashFocusBehavior(t *testing.T) { | 268 | func TestFavoritesSidebarSlashFocusBehavior(t *testing.T) { | ||
| @@ -284,7 +308,7 @@ func TestFavoritesSidebarFocusZoneCyclingSkipsAbsentZones(t *testing.T) { | |||||
| 284 | assert.Equal(t, []favoritesSidebarFocusZone{favoritesSidebarFocusConfirm}, fs.availableFocusZones()) | 308 | assert.Equal(t, []favoritesSidebarFocusZone{favoritesSidebarFocusConfirm}, fs.availableFocusZones()) | ||
| 285 | } | 309 | } | ||
| 286 | 310 | ||||
| 287 | - | func TestFavoritesSidebarSingleKeyCommandsAreGatedInTextEditContext(t *testing.T) { | 311 | + | func TestFavoritesSidebarSingleKeyCommandsStartManagementOutsideTextEditContext(t *testing.T) { |
| 288 | fs := newFavoritesSidebarHarness(nil, nil) | 312 | fs := newFavoritesSidebarHarness(nil, nil) | ||
| 289 | fs.searchEntry = nil | 313 | fs.searchEntry = nil | ||
| 290 | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}} | 314 | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}} | ||
| @@ -324,6 +348,15 @@ func TestFavoritesSidebarAddAndEditDTOs(t *testing.T) { | |||||
| 324 | assert.Equal(t, "Edited", uc.updateInputs[0].Title) | 348 | assert.Equal(t, "Edited", uc.updateInputs[0].Title) | ||
| 325 | require.NotNil(t, uc.updateInputs[0].ShortcutKey) | 349 | require.NotNil(t, uc.updateInputs[0].ShortcutKey) | ||
| 326 | assert.Equal(t, 4, *uc.updateInputs[0].ShortcutKey) | 350 | assert.Equal(t, 4, *uc.updateInputs[0].ShortcutKey) | ||
| 351 | + | ||||
| 352 | + | fs.mode = favoritesSidebarModeEdit | |||
| 353 | + | fs.editingID = 7 | |||
| 354 | + | fs.formTitle = "Edited" | |||
| 355 | + | fs.formShortcut = "" | |||
| 356 | + | assert.True(t, fs.submitForm()) | |||
| 357 | + | require.Len(t, uc.updateInputs, 2) | |||
| 358 | + | assert.True(t, uc.updateInputs[1].ShortcutKeySet) | |||
| 359 | + | assert.Nil(t, uc.updateInputs[1].ShortcutKey) | |||
| 327 | } | 360 | } | ||
| 328 | 361 | ||||
| 329 | func TestFavoritesSidebarInvalidTagAndShortcutPreserveFormState(t *testing.T) { | 362 | func TestFavoritesSidebarInvalidTagAndShortcutPreserveFormState(t *testing.T) { | ||