detached a2631c0

fix(favorites): refresh sidebar after bookmark changes

commit
a2631c033ea7ac95ba3573bc3a54cd765dab8879
author
brice · · 2026-06-28 16:28 UTC
parent
4c9a8b7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
131131 if err != nil {
132132 return err
133133 }
134+ if bw := a.lastFocusedBrowserWindow(); bw != nil && bw.favoritesSidebar != nil {
135+ bw.favoritesSidebar.RequestReloadIfVisible("current-page-favorite-toggle")
136+ }
134137 return nil
135138 }
@@ -131,5 +131,8 @@ func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
131 if err != nil {131 if err != nil {
132 return err132 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 nil137 return nil
135 }138 }
M internal/ui/component/favorites_sidebar.go +14 -0
@@ -116,6 +116,7 @@ func (fs *FavoritesSidebar) Show() {
116116 fs.outerBox.SetVisible(true)
117117 fs.visible = true
118118 fs.mu.Unlock()
119+ fs.startLoad()
119120 fs.focusSearch()
120121 }
121122
@@ -233,6 +234,19 @@ func (fs *FavoritesSidebar) focusSearch() {
233234 }))
234235 }
235236
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+
236250 func (fs *FavoritesSidebar) startLoad() {
237251 if fs == nil {
238252 return
@@ -116,6 +116,7 @@ func (fs *FavoritesSidebar) Show() {
116 fs.outerBox.SetVisible(true)116 fs.outerBox.SetVisible(true)
117 fs.visible = true117 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 return252 return
M internal/ui/component/favorites_sidebar_forms.go +1 -1
@@ -207,7 +207,7 @@ func (fs *FavoritesSidebar) submitForm() bool {
207207 fs.setNotice(err.Error())
208208 return true
209209 }
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})
211211 if err != nil {
212212 fs.setNotice(err.Error())
213213 return true
@@ -207,7 +207,7 @@ func (fs *FavoritesSidebar) submitForm() bool {
207 fs.setNotice(err.Error())207 fs.setNotice(err.Error())
208 return true208 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 true213 return true
M internal/ui/component/favorites_sidebar_test.go +38 -5
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "sync"
77 "testing"
8+ "time"
89
910 "github.com/bnema/dumber/internal/application/dto"
1011 "github.com/bnema/dumber/internal/domain/entity"
@@ -33,10 +34,17 @@ type fakeFavoritesSidebarUC struct {
3334 favID entity.FavoriteID
3435 tagID entity.TagID
3536 }
36- err error
37+ err error
38+ getAllCalled chan struct{}
3739 }
3840
3941 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+ }
4048 return f.favorites, nil
4149 }
4250 func (f *fakeFavoritesSidebarUC) GetAllTags(context.Context) ([]*entity.Tag, error) {
@@ -183,6 +191,25 @@ func TestFavoritesSidebarActivationCallbacks(t *testing.T) {
183191 assert.Equal(t, []string{"nav:https://a.test", "keep:https://b.test", "pane:https://c.test"}, called)
184192 }
185193
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+
186213 func TestFavoritesSidebarStaleLoadRejected(t *testing.T) {
187214 fs := newFavoritesSidebarHarness(nil, nil)
188215 fs.loadGen = 2
@@ -236,9 +263,6 @@ func TestFavoritesSidebarRenderingModelTagCallbackRetentionAndListRows(t *testin
236263 assert.False(t, fs.displayRows[1].Selectable)
237264 assert.Equal(t, []string{"saved"}, favoriteSidebarNoticeRows("saved", "", true))
238265 assert.Equal(t, []string{"No favorites"}, favoriteSidebarNoticeRows("", "", false))
239- fs.tagCallbacks = []interface{}{func() {}, func() {}}
240- fs.tagCallbacks = nil
241- assert.Empty(t, fs.tagCallbacks)
242266 }
243267
244268 func TestFavoritesSidebarSlashFocusBehavior(t *testing.T) {
@@ -284,7 +308,7 @@ func TestFavoritesSidebarFocusZoneCyclingSkipsAbsentZones(t *testing.T) {
284308 assert.Equal(t, []favoritesSidebarFocusZone{favoritesSidebarFocusConfirm}, fs.availableFocusZones())
285309 }
286310
287-func TestFavoritesSidebarSingleKeyCommandsAreGatedInTextEditContext(t *testing.T) {
311+func TestFavoritesSidebarSingleKeyCommandsStartManagementOutsideTextEditContext(t *testing.T) {
288312 fs := newFavoritesSidebarHarness(nil, nil)
289313 fs.searchEntry = nil
290314 fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}}
@@ -324,6 +348,15 @@ func TestFavoritesSidebarAddAndEditDTOs(t *testing.T) {
324348 assert.Equal(t, "Edited", uc.updateInputs[0].Title)
325349 require.NotNil(t, uc.updateInputs[0].ShortcutKey)
326350 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)
327360 }
328361
329362 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.FavoriteID34 favID entity.FavoriteID
34 tagID entity.TagID35 tagID entity.TagID
35 }36 }
36- err error37+ 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, nil48 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 = 2215 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 = nil313 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) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close