detached 1cda60e

refactor(systemviews): manage favorites with tags only

commit
1cda60e267dd1942dd4112b31b2650b298cab3fd
author
brice · · 2026-06-28 07:46 UTC
parent
45b8a06
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +426 -1412
M cmd/systemviews/bootstrap_test.go +0 -16
@@ -64,7 +64,6 @@ func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
6464
6565 bridge := &bridgeServiceRecorder{
6666 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},
67- folders: []*entity.Folder{{Name: "Read Later"}},
6867 tags: []*entity.Tag{{Name: "Go"}},
6968 }
7069 app := newBridgeApp(&recordingDOM{}, "dumb://favorites", bridge)
@@ -141,7 +140,6 @@ type bridgeServiceRecorder struct {
141140
142141 historyEntries []*entity.HistoryEntry
143142 favorites []*entity.Favorite
144- folders []*entity.Folder
145143 tags []*entity.Tag
146144 currentConfig dto.SystemviewConfigPayload
147145 keybindings port.KeybindingsConfig
@@ -199,26 +197,12 @@ func (*bridgeServiceRecorder) UpdateFavorite(context.Context, dto.FavoriteUpdate
199197
200198 func (*bridgeServiceRecorder) DeleteFavorite(context.Context, int64) error { return nil }
201199
202-func (f *bridgeServiceRecorder) ListFolders(context.Context) ([]*entity.Folder, error) {
203- return f.folders, nil
204-}
205-
206200 func (f *bridgeServiceRecorder) ListTags(context.Context) ([]*entity.Tag, error) {
207201 return f.tags, nil
208202 }
209203
210204 func (*bridgeServiceRecorder) SetShortcut(context.Context, int64, *int) error { return nil }
211205
212-func (*bridgeServiceRecorder) SetFolder(context.Context, int64, *int64) error { return nil }
213-
214-func (*bridgeServiceRecorder) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {
215- return nil, nil
216-}
217-
218-func (*bridgeServiceRecorder) UpdateFolder(context.Context, int64, string, string) error { return nil }
219-
220-func (*bridgeServiceRecorder) DeleteFolder(context.Context, int64) error { return nil }
221-
222206 func (*bridgeServiceRecorder) CreateTag(context.Context, string, string) (*entity.Tag, error) {
223207 return nil, nil
224208 }
@@ -64,7 +64,6 @@ func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
64 64
65 bridge := &bridgeServiceRecorder{65 bridge := &bridgeServiceRecorder{
66 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},66 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},
67- folders: []*entity.Folder{{Name: "Read Later"}},
68 tags: []*entity.Tag{{Name: "Go"}},67 tags: []*entity.Tag{{Name: "Go"}},
69 }68 }
70 app := newBridgeApp(&recordingDOM{}, "dumb://favorites", bridge)69 app := newBridgeApp(&recordingDOM{}, "dumb://favorites", bridge)
@@ -141,7 +140,6 @@ type bridgeServiceRecorder struct {
141 140
142 historyEntries []*entity.HistoryEntry141 historyEntries []*entity.HistoryEntry
143 favorites []*entity.Favorite142 favorites []*entity.Favorite
144- folders []*entity.Folder
145 tags []*entity.Tag143 tags []*entity.Tag
146 currentConfig dto.SystemviewConfigPayload144 currentConfig dto.SystemviewConfigPayload
147 keybindings port.KeybindingsConfig145 keybindings port.KeybindingsConfig
@@ -199,26 +197,12 @@ func (*bridgeServiceRecorder) UpdateFavorite(context.Context, dto.FavoriteUpdate
199 197
200 func (*bridgeServiceRecorder) DeleteFavorite(context.Context, int64) error { return nil }198 func (*bridgeServiceRecorder) DeleteFavorite(context.Context, int64) error { return nil }
201 199
202-func (f *bridgeServiceRecorder) ListFolders(context.Context) ([]*entity.Folder, error) {
203- return f.folders, nil
204-}
205-
206 func (f *bridgeServiceRecorder) ListTags(context.Context) ([]*entity.Tag, error) {200 func (f *bridgeServiceRecorder) ListTags(context.Context) ([]*entity.Tag, error) {
207 return f.tags, nil201 return f.tags, nil
208 }202 }
209 203
210 func (*bridgeServiceRecorder) SetShortcut(context.Context, int64, *int) error { return nil }204 func (*bridgeServiceRecorder) SetShortcut(context.Context, int64, *int) error { return nil }
211 205
212-func (*bridgeServiceRecorder) SetFolder(context.Context, int64, *int64) error { return nil }
213-
214-func (*bridgeServiceRecorder) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {
215- return nil, nil
216-}
217-
218-func (*bridgeServiceRecorder) UpdateFolder(context.Context, int64, string, string) error { return nil }
219-
220-func (*bridgeServiceRecorder) DeleteFolder(context.Context, int64) error { return nil }
221-
222 func (*bridgeServiceRecorder) CreateTag(context.Context, string, string) (*entity.Tag, error) {206 func (*bridgeServiceRecorder) CreateTag(context.Context, string, string) (*entity.Tag, error) {
223 return nil, nil207 return nil, nil
224 }208 }
M internal/infrastructure/handlers/homepage/favorites.go +0 -63
@@ -12,17 +12,6 @@ import (
1212 "github.com/bnema/dumber/internal/logging"
1313 )
1414
15-func folderIDFromInt64(id *int64) (*entity.FolderID, error) {
16- if id == nil {
17- return nil, nil
18- }
19- if *id <= 0 {
20- return nil, fmt.Errorf("folder id must be positive")
21- }
22- folderID := entity.FolderID(*id)
23- return &folderID, nil
24-}
25-
2615 func tagIDsFromInt64s(ids []int64) ([]entity.TagID, error) {
2716 out := make([]entity.TagID, 0, len(ids))
2817 for _, id := range ids {
@@ -69,7 +58,6 @@ type favoriteCreateRequest struct {
6958 URL string `json:"url"`
7059 Title string `json:"title"`
7160 FaviconURL string `json:"favicon_url"`
72- FolderID *int64 `json:"folder_id"`
7361 Tags []int64 `json:"tags"`
7462 }
7563
@@ -91,10 +79,6 @@ func (h *FavoritesHandlers) HandleCreate() port.WebUIMessageHandler {
9179 if trimmedURL == "" {
9280 return NewErrorResponse(req.RequestID, fmt.Errorf("URL is required")), nil
9381 }
94- folderID, err := folderIDFromInt64(req.FolderID)
95- if err != nil {
96- return NewErrorResponse(req.RequestID, err), nil
97- }
9882 tags, err := tagIDsFromInt64s(req.Tags)
9983 if err != nil {
10084 return NewErrorResponse(req.RequestID, err), nil
@@ -104,7 +88,6 @@ func (h *FavoritesHandlers) HandleCreate() port.WebUIMessageHandler {
10488 URL: trimmedURL,
10589 Title: req.Title,
10690 FaviconURL: req.FaviconURL,
107- FolderID: folderID,
10891 Tags: tags,
10992 }
11093 favorite, err := h.favoritesUC.AddFavorite(ctx, input)
@@ -121,7 +104,6 @@ type favoriteUpdateRequest struct {
121104 ID int64 `json:"id"`
122105 Title string `json:"title"`
123106 FaviconURL string `json:"favicon_url"`
124- FolderID *int64 `json:"folder_id"`
125107 ShortcutKey *int `json:"shortcut_key"`
126108 }
127109
@@ -144,16 +126,10 @@ func (h *FavoritesHandlers) HandleUpdate() port.WebUIMessageHandler {
144126 return NewErrorResponse(req.RequestID, fmt.Errorf("favorite id must be positive")), nil
145127 }
146128
147- folderID, err := folderIDFromInt64(req.FolderID)
148- if err != nil {
149- return NewErrorResponse(req.RequestID, err), nil
150- }
151-
152129 favorite, err := h.favoritesUC.UpdateFavorite(ctx, dto.FavoriteUpdateInput{
153130 ID: entity.FavoriteID(req.ID),
154131 Title: req.Title,
155132 FaviconURL: req.FaviconURL,
156- FolderID: folderID,
157133 ShortcutKey: req.ShortcutKey,
158134 })
159135 if err != nil {
@@ -259,42 +235,3 @@ func (h *FavoritesHandlers) HandleGetByShortcut() port.WebUIMessageHandler {
259235 return NewSuccessResponse(req.RequestID, favorite), nil
260236 })
261237 }
262-
263-// setFolderRequest is the payload for favorite_set_folder messages.
264-type setFolderRequest struct {
265- RequestID string `json:"requestId"`
266- FavoriteID int64 `json:"favorite_id"`
267- FolderID *int64 `json:"folder_id"` // null to move to root
268-}
269-
270-// HandleSetFolder handles favorite_set_folder messages.
271-func (h *FavoritesHandlers) HandleSetFolder() port.WebUIMessageHandler {
272- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
273- log := logging.FromContext(ctx)
274-
275- var req setFolderRequest
276- if err := json.Unmarshal(payload, &req); err != nil {
277- return NewErrorResponse("", err), nil
278- }
279-
280- log.Debug().
281- Str("request_id", req.RequestID).
282- Int64("favorite_id", req.FavoriteID).
283- Msg("handling favorite_set_folder")
284-
285- if req.FavoriteID <= 0 {
286- return NewErrorResponse(req.RequestID, fmt.Errorf("invalid favorite_id")), nil
287- }
288-
289- folderID, err := folderIDFromInt64(req.FolderID)
290- if err != nil {
291- return NewErrorResponse(req.RequestID, err), nil
292- }
293-
294- if err := h.favoritesUC.Move(ctx, entity.FavoriteID(req.FavoriteID), folderID); err != nil {
295- return NewErrorResponse(req.RequestID, err), nil
296- }
297-
298- return NewSuccessResponse(req.RequestID, nil), nil
299- })
300-}
@@ -12,17 +12,6 @@ import (
12 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
13 )13 )
14 14
15-func folderIDFromInt64(id *int64) (*entity.FolderID, error) {
16- if id == nil {
17- return nil, nil
18- }
19- if *id <= 0 {
20- return nil, fmt.Errorf("folder id must be positive")
21- }
22- folderID := entity.FolderID(*id)
23- return &folderID, nil
24-}
25-
26 func tagIDsFromInt64s(ids []int64) ([]entity.TagID, error) {15 func tagIDsFromInt64s(ids []int64) ([]entity.TagID, error) {
27 out := make([]entity.TagID, 0, len(ids))16 out := make([]entity.TagID, 0, len(ids))
28 for _, id := range ids {17 for _, id := range ids {
@@ -69,7 +58,6 @@ type favoriteCreateRequest struct {
69 URL string `json:"url"`58 URL string `json:"url"`
70 Title string `json:"title"`59 Title string `json:"title"`
71 FaviconURL string `json:"favicon_url"`60 FaviconURL string `json:"favicon_url"`
72- FolderID *int64 `json:"folder_id"`
73 Tags []int64 `json:"tags"`61 Tags []int64 `json:"tags"`
74 }62 }
75 63
@@ -91,10 +79,6 @@ func (h *FavoritesHandlers) HandleCreate() port.WebUIMessageHandler {
91 if trimmedURL == "" {79 if trimmedURL == "" {
92 return NewErrorResponse(req.RequestID, fmt.Errorf("URL is required")), nil80 return NewErrorResponse(req.RequestID, fmt.Errorf("URL is required")), nil
93 }81 }
94- folderID, err := folderIDFromInt64(req.FolderID)
95- if err != nil {
96- return NewErrorResponse(req.RequestID, err), nil
97- }
98 tags, err := tagIDsFromInt64s(req.Tags)82 tags, err := tagIDsFromInt64s(req.Tags)
99 if err != nil {83 if err != nil {
100 return NewErrorResponse(req.RequestID, err), nil84 return NewErrorResponse(req.RequestID, err), nil
@@ -104,7 +88,6 @@ func (h *FavoritesHandlers) HandleCreate() port.WebUIMessageHandler {
104 URL: trimmedURL,88 URL: trimmedURL,
105 Title: req.Title,89 Title: req.Title,
106 FaviconURL: req.FaviconURL,90 FaviconURL: req.FaviconURL,
107- FolderID: folderID,
108 Tags: tags,91 Tags: tags,
109 }92 }
110 favorite, err := h.favoritesUC.AddFavorite(ctx, input)93 favorite, err := h.favoritesUC.AddFavorite(ctx, input)
@@ -121,7 +104,6 @@ type favoriteUpdateRequest struct {
121 ID int64 `json:"id"`104 ID int64 `json:"id"`
122 Title string `json:"title"`105 Title string `json:"title"`
123 FaviconURL string `json:"favicon_url"`106 FaviconURL string `json:"favicon_url"`
124- FolderID *int64 `json:"folder_id"`
125 ShortcutKey *int `json:"shortcut_key"`107 ShortcutKey *int `json:"shortcut_key"`
126 }108 }
127 109
@@ -144,16 +126,10 @@ func (h *FavoritesHandlers) HandleUpdate() port.WebUIMessageHandler {
144 return NewErrorResponse(req.RequestID, fmt.Errorf("favorite id must be positive")), nil126 return NewErrorResponse(req.RequestID, fmt.Errorf("favorite id must be positive")), nil
145 }127 }
146 128
147- folderID, err := folderIDFromInt64(req.FolderID)
148- if err != nil {
149- return NewErrorResponse(req.RequestID, err), nil
150- }
151-
152 favorite, err := h.favoritesUC.UpdateFavorite(ctx, dto.FavoriteUpdateInput{129 favorite, err := h.favoritesUC.UpdateFavorite(ctx, dto.FavoriteUpdateInput{
153 ID: entity.FavoriteID(req.ID),130 ID: entity.FavoriteID(req.ID),
154 Title: req.Title,131 Title: req.Title,
155 FaviconURL: req.FaviconURL,132 FaviconURL: req.FaviconURL,
156- FolderID: folderID,
157 ShortcutKey: req.ShortcutKey,133 ShortcutKey: req.ShortcutKey,
158 })134 })
159 if err != nil {135 if err != nil {
@@ -259,42 +235,3 @@ func (h *FavoritesHandlers) HandleGetByShortcut() port.WebUIMessageHandler {
259 return NewSuccessResponse(req.RequestID, favorite), nil235 return NewSuccessResponse(req.RequestID, favorite), nil
260 })236 })
261 }237 }
262-
263-// setFolderRequest is the payload for favorite_set_folder messages.
264-type setFolderRequest struct {
265- RequestID string `json:"requestId"`
266- FavoriteID int64 `json:"favorite_id"`
267- FolderID *int64 `json:"folder_id"` // null to move to root
268-}
269-
270-// HandleSetFolder handles favorite_set_folder messages.
271-func (h *FavoritesHandlers) HandleSetFolder() port.WebUIMessageHandler {
272- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
273- log := logging.FromContext(ctx)
274-
275- var req setFolderRequest
276- if err := json.Unmarshal(payload, &req); err != nil {
277- return NewErrorResponse("", err), nil
278- }
279-
280- log.Debug().
281- Str("request_id", req.RequestID).
282- Int64("favorite_id", req.FavoriteID).
283- Msg("handling favorite_set_folder")
284-
285- if req.FavoriteID <= 0 {
286- return NewErrorResponse(req.RequestID, fmt.Errorf("invalid favorite_id")), nil
287- }
288-
289- folderID, err := folderIDFromInt64(req.FolderID)
290- if err != nil {
291- return NewErrorResponse(req.RequestID, err), nil
292- }
293-
294- if err := h.favoritesUC.Move(ctx, entity.FavoriteID(req.FavoriteID), folderID); err != nil {
295- return NewErrorResponse(req.RequestID, err), nil
296- }
297-
298- return NewSuccessResponse(req.RequestID, nil), nil
299- })
300-}
A internal/infrastructure/handlers/homepage/favorites_test.go +73 -0
@@ -0,0 +1,73 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/application/dto"
9+ "github.com/bnema/dumber/internal/application/port"
10+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
11+ "github.com/bnema/dumber/internal/domain/entity"
12+ "github.com/stretchr/testify/mock"
13+ "github.com/stretchr/testify/require"
14+)
15+
16+func TestHandleFavoriteCreatePassesTagsOnlyPayload(t *testing.T) {
17+ t.Parallel()
18+
19+ favorites := portmocks.NewMockHomepageFavorites(t)
20+ favorites.EXPECT().
21+ AddFavorite(mock.Anything, dto.FavoriteCreateInput{
22+ URL: "https://example.com",
23+ Title: "Example",
24+ Tags: []entity.TagID{7, 9},
25+ }).
26+ Return(&entity.Favorite{ID: 1, URL: "https://example.com", Title: "Example"}, nil).
27+ Once()
28+
29+ handler := NewFavoritesHandlers(favorites).HandleCreate()
30+ got, err := handler.Handle(context.Background(), port.WebViewID(0), json.RawMessage(`{"requestId":"req-1","url":" https://example.com ","title":"Example","tags":[7,9]}`))
31+ require.NoError(t, err)
32+
33+ resp, ok := got.(Response)
34+ require.True(t, ok)
35+ require.True(t, resp.Success)
36+}
37+
38+func TestHandleFavoriteCreateRejectsInvalidTagIDs(t *testing.T) {
39+ t.Parallel()
40+
41+ handler := NewFavoritesHandlers(portmocks.NewMockHomepageFavorites(t)).HandleCreate()
42+ got, err := handler.Handle(context.Background(), port.WebViewID(0), json.RawMessage(`{"requestId":"req-2","url":"https://example.com","tags":[0]}`))
43+ require.NoError(t, err)
44+
45+ resp, ok := got.(Response)
46+ require.True(t, ok)
47+ require.False(t, resp.Success)
48+ require.Contains(t, resp.Error, "tag id must be positive")
49+}
50+
51+func TestHandleFavoriteUpdatePassesTagsOnlyPayload(t *testing.T) {
52+ t.Parallel()
53+
54+ shortcut := 4
55+ favorites := portmocks.NewMockHomepageFavorites(t)
56+ favorites.EXPECT().
57+ UpdateFavorite(mock.Anything, dto.FavoriteUpdateInput{
58+ ID: entity.FavoriteID(42),
59+ Title: "Updated",
60+ FaviconURL: "https://example.com/favicon.ico",
61+ ShortcutKey: &shortcut,
62+ }).
63+ Return(&entity.Favorite{ID: 42, URL: "https://example.com", Title: "Updated", ShortcutKey: &shortcut}, nil).
64+ Once()
65+
66+ handler := NewFavoritesHandlers(favorites).HandleUpdate()
67+ got, err := handler.Handle(context.Background(), port.WebViewID(0), json.RawMessage(`{"requestId":"req-3","id":42,"title":"Updated","favicon_url":"https://example.com/favicon.ico","shortcut_key":4}`))
68+ require.NoError(t, err)
69+
70+ resp, ok := got.(Response)
71+ require.True(t, ok)
72+ require.True(t, resp.Success)
73+}
@@ -0,0 +1,73 @@
1+package homepage
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/application/dto"
9+ "github.com/bnema/dumber/internal/application/port"
10+ portmocks "github.com/bnema/dumber/internal/application/port/mocks"
11+ "github.com/bnema/dumber/internal/domain/entity"
12+ "github.com/stretchr/testify/mock"
13+ "github.com/stretchr/testify/require"
14+)
15+
16+func TestHandleFavoriteCreatePassesTagsOnlyPayload(t *testing.T) {
17+ t.Parallel()
18+
19+ favorites := portmocks.NewMockHomepageFavorites(t)
20+ favorites.EXPECT().
21+ AddFavorite(mock.Anything, dto.FavoriteCreateInput{
22+ URL: "https://example.com",
23+ Title: "Example",
24+ Tags: []entity.TagID{7, 9},
25+ }).
26+ Return(&entity.Favorite{ID: 1, URL: "https://example.com", Title: "Example"}, nil).
27+ Once()
28+
29+ handler := NewFavoritesHandlers(favorites).HandleCreate()
30+ got, err := handler.Handle(context.Background(), port.WebViewID(0), json.RawMessage(`{"requestId":"req-1","url":" https://example.com ","title":"Example","tags":[7,9]}`))
31+ require.NoError(t, err)
32+
33+ resp, ok := got.(Response)
34+ require.True(t, ok)
35+ require.True(t, resp.Success)
36+}
37+
38+func TestHandleFavoriteCreateRejectsInvalidTagIDs(t *testing.T) {
39+ t.Parallel()
40+
41+ handler := NewFavoritesHandlers(portmocks.NewMockHomepageFavorites(t)).HandleCreate()
42+ got, err := handler.Handle(context.Background(), port.WebViewID(0), json.RawMessage(`{"requestId":"req-2","url":"https://example.com","tags":[0]}`))
43+ require.NoError(t, err)
44+
45+ resp, ok := got.(Response)
46+ require.True(t, ok)
47+ require.False(t, resp.Success)
48+ require.Contains(t, resp.Error, "tag id must be positive")
49+}
50+
51+func TestHandleFavoriteUpdatePassesTagsOnlyPayload(t *testing.T) {
52+ t.Parallel()
53+
54+ shortcut := 4
55+ favorites := portmocks.NewMockHomepageFavorites(t)
56+ favorites.EXPECT().
57+ UpdateFavorite(mock.Anything, dto.FavoriteUpdateInput{
58+ ID: entity.FavoriteID(42),
59+ Title: "Updated",
60+ FaviconURL: "https://example.com/favicon.ico",
61+ ShortcutKey: &shortcut,
62+ }).
63+ Return(&entity.Favorite{ID: 42, URL: "https://example.com", Title: "Updated", ShortcutKey: &shortcut}, nil).
64+ Once()
65+
66+ handler := NewFavoritesHandlers(favorites).HandleUpdate()
67+ got, err := handler.Handle(context.Background(), port.WebViewID(0), json.RawMessage(`{"requestId":"req-3","id":42,"title":"Updated","favicon_url":"https://example.com/favicon.ico","shortcut_key":4}`))
68+ require.NoError(t, err)
69+
70+ resp, ok := got.(Response)
71+ require.True(t, ok)
72+ require.True(t, resp.Success)
73+}
M internal/infrastructure/handlers/homepage/folders.go +1 -144
@@ -1,146 +1,3 @@
11 package homepage
22
3-import (
4- "context"
5- "encoding/json"
6-
7- "github.com/bnema/dumber/internal/application/port"
8- "github.com/bnema/dumber/internal/domain/entity"
9- "github.com/bnema/dumber/internal/logging"
10-)
11-
12-// FolderHandlers handles folder-related messages from the homepage.
13-type FolderHandlers struct {
14- favoritesUC port.HomepageFavorites
15-}
16-
17-// NewFolderHandlers creates a new FolderHandlers instance.
18-func NewFolderHandlers(favoritesUC port.HomepageFavorites) *FolderHandlers {
19- return &FolderHandlers{favoritesUC: favoritesUC}
20-}
21-
22-// HandleList handles folder_list messages.
23-func (h *FolderHandlers) HandleList() port.WebUIMessageHandler {
24- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
25- log := logging.FromContext(ctx)
26-
27- requestID := ParseRequestID(payload)
28-
29- log.Debug().
30- Str("request_id", requestID).
31- Msg("handling folder_list")
32-
33- folders, err := h.favoritesUC.GetAllFolders(ctx)
34- if err != nil {
35- return NewErrorResponse(requestID, err), nil
36- }
37-
38- return NewSuccessResponse(requestID, folders), nil
39- })
40-}
41-
42-// createFolderRequest is the payload for folder_create messages.
43-type createFolderRequest struct {
44- RequestID string `json:"requestId"`
45- Name string `json:"name"`
46- Icon *string `json:"icon"`
47- ParentID *int64 `json:"parent_id"`
48-}
49-
50-// HandleCreate handles folder_create messages.
51-func (h *FolderHandlers) HandleCreate() port.WebUIMessageHandler {
52- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
53- log := logging.FromContext(ctx)
54-
55- var req createFolderRequest
56- if err := json.Unmarshal(payload, &req); err != nil {
57- return NewErrorResponse("", err), nil
58- }
59-
60- log.Debug().
61- Str("request_id", req.RequestID).
62- Str("name", req.Name).
63- Msg("handling folder_create")
64-
65- parentID, err := folderIDFromInt64(req.ParentID)
66- if err != nil {
67- return NewErrorResponse(req.RequestID, err), nil
68- }
69-
70- folder, err := h.favoritesUC.CreateFolder(ctx, req.Name, optionalStringValue(req.Icon), parentID)
71- if err != nil {
72- return NewErrorResponse(req.RequestID, err), nil
73- }
74-
75- return NewSuccessResponse(req.RequestID, folder), nil
76- })
77-}
78-
79-// deleteFolderRequest is the payload for folder_delete messages.
80-type deleteFolderRequest struct {
81- RequestID string `json:"requestId"`
82- ID int64 `json:"id"`
83-}
84-
85-// HandleDelete handles folder_delete messages.
86-func (h *FolderHandlers) HandleDelete() port.WebUIMessageHandler {
87- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
88- log := logging.FromContext(ctx)
89-
90- var req deleteFolderRequest
91- if err := json.Unmarshal(payload, &req); err != nil {
92- return NewErrorResponse("", err), nil
93- }
94-
95- log.Debug().
96- Str("request_id", req.RequestID).
97- Int64("id", req.ID).
98- Msg("handling folder_delete")
99-
100- if err := h.favoritesUC.DeleteFolder(ctx, entity.FolderID(req.ID)); err != nil {
101- return NewErrorResponse(req.RequestID, err), nil
102- }
103-
104- return NewSuccessResponse(req.RequestID, nil), nil
105- })
106-}
107-
108-// updateFolderRequest is the payload for folder_update messages.
109-type updateFolderRequest struct {
110- RequestID string `json:"requestId"`
111- ID int64 `json:"id"`
112- Name string `json:"name"`
113- Icon *string `json:"icon"`
114-}
115-
116-// HandleUpdate handles folder_update messages.
117-// It delegates to the port.HomepageFavorites interface's UpdateFolder method.
118-func optionalStringValue(value *string) string {
119- if value == nil {
120- return ""
121- }
122- return *value
123-}
124-
125-func (h *FolderHandlers) HandleUpdate() port.WebUIMessageHandler {
126- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
127- log := logging.FromContext(ctx)
128-
129- var req updateFolderRequest
130- if err := json.Unmarshal(payload, &req); err != nil {
131- return NewErrorResponse("", err), nil
132- }
133-
134- log.Debug().
135- Str("request_id", req.RequestID).
136- Int64("id", req.ID).
137- Str("name", req.Name).
138- Msg("handling folder_update")
139-
140- if err := h.favoritesUC.UpdateFolder(ctx, entity.FolderID(req.ID), req.Name, optionalStringValue(req.Icon)); err != nil {
141- return NewErrorResponse(req.RequestID, err), nil
142- }
143-
144- return NewSuccessResponse(req.RequestID, nil), nil
145- })
146-}
3+// Folder homepage handlers were removed with tags-only favorites.
@@ -1,146 +1,3 @@
1 package homepage1 package homepage
2 2
3-import (3+// Folder homepage handlers were removed with tags-only favorites.
4- "context"
5- "encoding/json"
6-
7- "github.com/bnema/dumber/internal/application/port"
8- "github.com/bnema/dumber/internal/domain/entity"
9- "github.com/bnema/dumber/internal/logging"
10-)
11-
12-// FolderHandlers handles folder-related messages from the homepage.
13-type FolderHandlers struct {
14- favoritesUC port.HomepageFavorites
15-}
16-
17-// NewFolderHandlers creates a new FolderHandlers instance.
18-func NewFolderHandlers(favoritesUC port.HomepageFavorites) *FolderHandlers {
19- return &FolderHandlers{favoritesUC: favoritesUC}
20-}
21-
22-// HandleList handles folder_list messages.
23-func (h *FolderHandlers) HandleList() port.WebUIMessageHandler {
24- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
25- log := logging.FromContext(ctx)
26-
27- requestID := ParseRequestID(payload)
28-
29- log.Debug().
30- Str("request_id", requestID).
31- Msg("handling folder_list")
32-
33- folders, err := h.favoritesUC.GetAllFolders(ctx)
34- if err != nil {
35- return NewErrorResponse(requestID, err), nil
36- }
37-
38- return NewSuccessResponse(requestID, folders), nil
39- })
40-}
41-
42-// createFolderRequest is the payload for folder_create messages.
43-type createFolderRequest struct {
44- RequestID string `json:"requestId"`
45- Name string `json:"name"`
46- Icon *string `json:"icon"`
47- ParentID *int64 `json:"parent_id"`
48-}
49-
50-// HandleCreate handles folder_create messages.
51-func (h *FolderHandlers) HandleCreate() port.WebUIMessageHandler {
52- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
53- log := logging.FromContext(ctx)
54-
55- var req createFolderRequest
56- if err := json.Unmarshal(payload, &req); err != nil {
57- return NewErrorResponse("", err), nil
58- }
59-
60- log.Debug().
61- Str("request_id", req.RequestID).
62- Str("name", req.Name).
63- Msg("handling folder_create")
64-
65- parentID, err := folderIDFromInt64(req.ParentID)
66- if err != nil {
67- return NewErrorResponse(req.RequestID, err), nil
68- }
69-
70- folder, err := h.favoritesUC.CreateFolder(ctx, req.Name, optionalStringValue(req.Icon), parentID)
71- if err != nil {
72- return NewErrorResponse(req.RequestID, err), nil
73- }
74-
75- return NewSuccessResponse(req.RequestID, folder), nil
76- })
77-}
78-
79-// deleteFolderRequest is the payload for folder_delete messages.
80-type deleteFolderRequest struct {
81- RequestID string `json:"requestId"`
82- ID int64 `json:"id"`
83-}
84-
85-// HandleDelete handles folder_delete messages.
86-func (h *FolderHandlers) HandleDelete() port.WebUIMessageHandler {
87- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
88- log := logging.FromContext(ctx)
89-
90- var req deleteFolderRequest
91- if err := json.Unmarshal(payload, &req); err != nil {
92- return NewErrorResponse("", err), nil
93- }
94-
95- log.Debug().
96- Str("request_id", req.RequestID).
97- Int64("id", req.ID).
98- Msg("handling folder_delete")
99-
100- if err := h.favoritesUC.DeleteFolder(ctx, entity.FolderID(req.ID)); err != nil {
101- return NewErrorResponse(req.RequestID, err), nil
102- }
103-
104- return NewSuccessResponse(req.RequestID, nil), nil
105- })
106-}
107-
108-// updateFolderRequest is the payload for folder_update messages.
109-type updateFolderRequest struct {
110- RequestID string `json:"requestId"`
111- ID int64 `json:"id"`
112- Name string `json:"name"`
113- Icon *string `json:"icon"`
114-}
115-
116-// HandleUpdate handles folder_update messages.
117-// It delegates to the port.HomepageFavorites interface's UpdateFolder method.
118-func optionalStringValue(value *string) string {
119- if value == nil {
120- return ""
121- }
122- return *value
123-}
124-
125-func (h *FolderHandlers) HandleUpdate() port.WebUIMessageHandler {
126- return port.WebUIMessageHandlerFunc(func(ctx context.Context, _ port.WebViewID, payload json.RawMessage) (any, error) {
127- log := logging.FromContext(ctx)
128-
129- var req updateFolderRequest
130- if err := json.Unmarshal(payload, &req); err != nil {
131- return NewErrorResponse("", err), nil
132- }
133-
134- log.Debug().
135- Str("request_id", req.RequestID).
136- Int64("id", req.ID).
137- Str("name", req.Name).
138- Msg("handling folder_update")
139-
140- if err := h.favoritesUC.UpdateFolder(ctx, entity.FolderID(req.ID), req.Name, optionalStringValue(req.Icon)); err != nil {
141- return NewErrorResponse(req.RequestID, err), nil
142- }
143-
144- return NewSuccessResponse(req.RequestID, nil), nil
145- })
146-}
M internal/infrastructure/handlers/homepage/registry.go +0 -9
@@ -48,15 +48,6 @@ func RegisterHandlers(ctx context.Context, router port.WebUIHandlerRouter, cfg C
4848 handlers["favorite_delete"] = favoritesHandlers.HandleDelete()
4949 handlers["favorite_set_shortcut"] = favoritesHandlers.HandleSetShortcut()
5050 handlers["favorite_get_by_shortcut"] = favoritesHandlers.HandleGetByShortcut()
51- handlers["favorite_set_folder"] = favoritesHandlers.HandleSetFolder()
52-
53- // Folder handlers
54- folderHandlers := NewFolderHandlers(cfg.FavoritesUC)
55- handlers["folder_list"] = folderHandlers.HandleList()
56- handlers["folder_create"] = folderHandlers.HandleCreate()
57- handlers["folder_delete"] = folderHandlers.HandleDelete()
58- handlers["folder_update"] = folderHandlers.HandleUpdate() // Requires UpdateFolder()
59-
6051 // Tag handlers
6152 tagHandlers := NewTagHandlers(cfg.FavoritesUC)
6253 handlers["tag_list"] = tagHandlers.HandleList()
@@ -48,15 +48,6 @@ func RegisterHandlers(ctx context.Context, router port.WebUIHandlerRouter, cfg C
48 handlers["favorite_delete"] = favoritesHandlers.HandleDelete()48 handlers["favorite_delete"] = favoritesHandlers.HandleDelete()
49 handlers["favorite_set_shortcut"] = favoritesHandlers.HandleSetShortcut()49 handlers["favorite_set_shortcut"] = favoritesHandlers.HandleSetShortcut()
50 handlers["favorite_get_by_shortcut"] = favoritesHandlers.HandleGetByShortcut()50 handlers["favorite_get_by_shortcut"] = favoritesHandlers.HandleGetByShortcut()
51- handlers["favorite_set_folder"] = favoritesHandlers.HandleSetFolder()
52-
53- // Folder handlers
54- folderHandlers := NewFolderHandlers(cfg.FavoritesUC)
55- handlers["folder_list"] = folderHandlers.HandleList()
56- handlers["folder_create"] = folderHandlers.HandleCreate()
57- handlers["folder_delete"] = folderHandlers.HandleDelete()
58- handlers["folder_update"] = folderHandlers.HandleUpdate() // Requires UpdateFolder()
59-
60 // Tag handlers51 // Tag handlers
61 tagHandlers := NewTagHandlers(cfg.FavoritesUC)52 tagHandlers := NewTagHandlers(cfg.FavoritesUC)
62 handlers["tag_list"] = tagHandlers.HandleList()53 handlers["tag_list"] = tagHandlers.HandleList()
M internal/infrastructure/systemviewsbridge/browser_transport_js.go +2 -2
@@ -25,8 +25,8 @@ func NewBrowserClient() *Client {
2525 func callbackPlanForMessage(msgType string) (callbackPlan, bool) {
2626 switch msgType {
2727 case "history_timeline", "history_timeline_by_domain", "history_timeline_window", "history_search_fts", "history_delete_entry", "history_delete_range", "history_stats", "history_analytics",
28- "history_domain_stats", "history_delete_domain", "favorite_list", "favorite_create", "favorite_update", "favorite_delete", "folder_list", "tag_list",
29- "favorite_set_shortcut", "favorite_set_folder", "folder_create", "folder_update", "folder_delete",
28+ "history_domain_stats", "history_delete_domain", "favorite_list", "favorite_create", "favorite_update", "favorite_delete", "tag_list",
29+ "favorite_set_shortcut",
3030 "tag_create", "tag_update", "tag_delete", "tag_assign", "tag_remove":
3131 return callbackPlan{success: "__dumber_homepage_response", failure: "__dumber_error"}, true
3232 case "save_config":
@@ -25,8 +25,8 @@ func NewBrowserClient() *Client {
25 func callbackPlanForMessage(msgType string) (callbackPlan, bool) {25 func callbackPlanForMessage(msgType string) (callbackPlan, bool) {
26 switch msgType {26 switch msgType {
27 case "history_timeline", "history_timeline_by_domain", "history_timeline_window", "history_search_fts", "history_delete_entry", "history_delete_range", "history_stats", "history_analytics",27 case "history_timeline", "history_timeline_by_domain", "history_timeline_window", "history_search_fts", "history_delete_entry", "history_delete_range", "history_stats", "history_analytics",
28- "history_domain_stats", "history_delete_domain", "favorite_list", "favorite_create", "favorite_update", "favorite_delete", "folder_list", "tag_list",28+ "history_domain_stats", "history_delete_domain", "favorite_list", "favorite_create", "favorite_update", "favorite_delete", "tag_list",
29- "favorite_set_shortcut", "favorite_set_folder", "folder_create", "folder_update", "folder_delete",29+ "favorite_set_shortcut",
30 "tag_create", "tag_update", "tag_delete", "tag_assign", "tag_remove":30 "tag_create", "tag_update", "tag_delete", "tag_assign", "tag_remove":
31 return callbackPlan{success: "__dumber_homepage_response", failure: "__dumber_error"}, true31 return callbackPlan{success: "__dumber_homepage_response", failure: "__dumber_error"}, true
32 case "save_config":32 case "save_config":
M internal/infrastructure/systemviewsbridge/client.go +0 -79
@@ -272,14 +272,12 @@ func favoriteCreatePayload(input dto.FavoriteCreateInput) any {
272272 URL string `json:"url"`
273273 Title string `json:"title"`
274274 FaviconURL string `json:"favicon_url"`
275- FolderID *int64 `json:"folder_id"`
276275 Tags []int64 `json:"tags"`
277276 }{
278277 RequestID: nextRequestID(),
279278 URL: input.URL,
280279 Title: input.Title,
281280 FaviconURL: input.FaviconURL,
282- FolderID: folderIDPayload(input.FolderID),
283281 Tags: tagIDPayloads(input.Tags),
284282 }
285283 }
@@ -290,26 +288,16 @@ func favoriteUpdatePayload(input dto.FavoriteUpdateInput) any {
290288 ID int64 `json:"id"`
291289 Title string `json:"title"`
292290 FaviconURL string `json:"favicon_url"`
293- FolderID *int64 `json:"folder_id"`
294291 ShortcutKey *int `json:"shortcut_key"`
295292 }{
296293 RequestID: nextRequestID(),
297294 ID: int64(input.ID),
298295 Title: input.Title,
299296 FaviconURL: input.FaviconURL,
300- FolderID: folderIDPayload(input.FolderID),
301297 ShortcutKey: input.ShortcutKey,
302298 }
303299 }
304300
305-func folderIDPayload(id *entity.FolderID) *int64 {
306- if id == nil {
307- return nil
308- }
309- value := int64(*id)
310- return &value
311-}
312-
313301 func tagIDPayloads(ids []entity.TagID) []int64 {
314302 out := make([]int64, 0, len(ids))
315303 for _, id := range ids {
@@ -318,12 +306,6 @@ func tagIDPayloads(ids []entity.TagID) []int64 {
318306 return out
319307 }
320308
321-func (c *Client) ListFolders(ctx context.Context) ([]*entity.Folder, error) {
322- return request[[]*entity.Folder](c, ctx, "folder_list", struct {
323- RequestID string `json:"requestId"`
324- }{RequestID: nextRequestID()})
325-}
326-
327309 func (c *Client) ListTags(ctx context.Context) ([]*entity.Tag, error) {
328310 return request[[]*entity.Tag](c, ctx, "tag_list", struct {
329311 RequestID string `json:"requestId"`
@@ -342,67 +324,6 @@ func (c *Client) SetShortcut(ctx context.Context, favoriteID int64, shortcutKey
342324 return err
343325 }
344326
345-func (c *Client) SetFolder(ctx context.Context, favoriteID int64, folderID *int64) error {
346- if favoriteID <= 0 {
347- return fmt.Errorf("set folder favorite id must be positive, got %d", favoriteID)
348- }
349- _, err := request[struct{}](c, ctx, "favorite_set_folder", struct {
350- RequestID string `json:"requestId"`
351- FavoriteID int64 `json:"favorite_id"`
352- FolderID *int64 `json:"folder_id"`
353- }{RequestID: nextRequestID(), FavoriteID: favoriteID, FolderID: folderID})
354- return err
355-}
356-
357-func (c *Client) CreateFolder(ctx context.Context, name, icon string, parentID *int64) (*entity.Folder, error) {
358- name = strings.TrimSpace(name)
359- if name == "" {
360- return nil, fmt.Errorf("folder name is required")
361- }
362- var iconPtr *string
363- if trimmedIcon := strings.TrimSpace(icon); trimmedIcon != "" {
364- iconPtr = &trimmedIcon
365- }
366- return request[*entity.Folder](c, ctx, "folder_create", struct {
367- RequestID string `json:"requestId"`
368- Name string `json:"name"`
369- Icon *string `json:"icon"`
370- ParentID *int64 `json:"parent_id,omitempty"`
371- }{RequestID: nextRequestID(), Name: name, Icon: iconPtr, ParentID: parentID})
372-}
373-
374-func (c *Client) UpdateFolder(ctx context.Context, id int64, name, icon string) error {
375- if id <= 0 {
376- return fmt.Errorf("update folder id must be positive, got %d", id)
377- }
378- name = strings.TrimSpace(name)
379- if name == "" {
380- return fmt.Errorf("folder name is required")
381- }
382- var iconPtr *string
383- if trimmed := strings.TrimSpace(icon); trimmed != "" {
384- iconPtr = &trimmed
385- }
386- _, err := request[struct{}](c, ctx, "folder_update", struct {
387- RequestID string `json:"requestId"`
388- ID int64 `json:"id"`
389- Name string `json:"name"`
390- Icon *string `json:"icon"`
391- }{RequestID: nextRequestID(), ID: id, Name: name, Icon: iconPtr})
392- return err
393-}
394-
395-func (c *Client) DeleteFolder(ctx context.Context, id int64) error {
396- if id <= 0 {
397- return fmt.Errorf("delete folder id must be positive, got %d", id)
398- }
399- _, err := request[struct{}](c, ctx, "folder_delete", struct {
400- RequestID string `json:"requestId"`
401- ID int64 `json:"id"`
402- }{RequestID: nextRequestID(), ID: id})
403- return err
404-}
405-
406327 func (c *Client) CreateTag(ctx context.Context, name, color string) (*entity.Tag, error) {
407328 name = strings.TrimSpace(name)
408329 if name == "" {
@@ -272,14 +272,12 @@ func favoriteCreatePayload(input dto.FavoriteCreateInput) any {
272 URL string `json:"url"`272 URL string `json:"url"`
273 Title string `json:"title"`273 Title string `json:"title"`
274 FaviconURL string `json:"favicon_url"`274 FaviconURL string `json:"favicon_url"`
275- FolderID *int64 `json:"folder_id"`
276 Tags []int64 `json:"tags"`275 Tags []int64 `json:"tags"`
277 }{276 }{
278 RequestID: nextRequestID(),277 RequestID: nextRequestID(),
279 URL: input.URL,278 URL: input.URL,
280 Title: input.Title,279 Title: input.Title,
281 FaviconURL: input.FaviconURL,280 FaviconURL: input.FaviconURL,
282- FolderID: folderIDPayload(input.FolderID),
283 Tags: tagIDPayloads(input.Tags),281 Tags: tagIDPayloads(input.Tags),
284 }282 }
285 }283 }
@@ -290,26 +288,16 @@ func favoriteUpdatePayload(input dto.FavoriteUpdateInput) any {
290 ID int64 `json:"id"`288 ID int64 `json:"id"`
291 Title string `json:"title"`289 Title string `json:"title"`
292 FaviconURL string `json:"favicon_url"`290 FaviconURL string `json:"favicon_url"`
293- FolderID *int64 `json:"folder_id"`
294 ShortcutKey *int `json:"shortcut_key"`291 ShortcutKey *int `json:"shortcut_key"`
295 }{292 }{
296 RequestID: nextRequestID(),293 RequestID: nextRequestID(),
297 ID: int64(input.ID),294 ID: int64(input.ID),
298 Title: input.Title,295 Title: input.Title,
299 FaviconURL: input.FaviconURL,296 FaviconURL: input.FaviconURL,
300- FolderID: folderIDPayload(input.FolderID),
301 ShortcutKey: input.ShortcutKey,297 ShortcutKey: input.ShortcutKey,
302 }298 }
303 }299 }
304 300
305-func folderIDPayload(id *entity.FolderID) *int64 {
306- if id == nil {
307- return nil
308- }
309- value := int64(*id)
310- return &value
311-}
312-
313 func tagIDPayloads(ids []entity.TagID) []int64 {301 func tagIDPayloads(ids []entity.TagID) []int64 {
314 out := make([]int64, 0, len(ids))302 out := make([]int64, 0, len(ids))
315 for _, id := range ids {303 for _, id := range ids {
@@ -318,12 +306,6 @@ func tagIDPayloads(ids []entity.TagID) []int64 {
318 return out306 return out
319 }307 }
320 308
321-func (c *Client) ListFolders(ctx context.Context) ([]*entity.Folder, error) {
322- return request[[]*entity.Folder](c, ctx, "folder_list", struct {
323- RequestID string `json:"requestId"`
324- }{RequestID: nextRequestID()})
325-}
326-
327 func (c *Client) ListTags(ctx context.Context) ([]*entity.Tag, error) {309 func (c *Client) ListTags(ctx context.Context) ([]*entity.Tag, error) {
328 return request[[]*entity.Tag](c, ctx, "tag_list", struct {310 return request[[]*entity.Tag](c, ctx, "tag_list", struct {
329 RequestID string `json:"requestId"`311 RequestID string `json:"requestId"`
@@ -342,67 +324,6 @@ func (c *Client) SetShortcut(ctx context.Context, favoriteID int64, shortcutKey
342 return err324 return err
343 }325 }
344 326
345-func (c *Client) SetFolder(ctx context.Context, favoriteID int64, folderID *int64) error {
346- if favoriteID <= 0 {
347- return fmt.Errorf("set folder favorite id must be positive, got %d", favoriteID)
348- }
349- _, err := request[struct{}](c, ctx, "favorite_set_folder", struct {
350- RequestID string `json:"requestId"`
351- FavoriteID int64 `json:"favorite_id"`
352- FolderID *int64 `json:"folder_id"`
353- }{RequestID: nextRequestID(), FavoriteID: favoriteID, FolderID: folderID})
354- return err
355-}
356-
357-func (c *Client) CreateFolder(ctx context.Context, name, icon string, parentID *int64) (*entity.Folder, error) {
358- name = strings.TrimSpace(name)
359- if name == "" {
360- return nil, fmt.Errorf("folder name is required")
361- }
362- var iconPtr *string
363- if trimmedIcon := strings.TrimSpace(icon); trimmedIcon != "" {
364- iconPtr = &trimmedIcon
365- }
366- return request[*entity.Folder](c, ctx, "folder_create", struct {
367- RequestID string `json:"requestId"`
368- Name string `json:"name"`
369- Icon *string `json:"icon"`
370- ParentID *int64 `json:"parent_id,omitempty"`
371- }{RequestID: nextRequestID(), Name: name, Icon: iconPtr, ParentID: parentID})
372-}
373-
374-func (c *Client) UpdateFolder(ctx context.Context, id int64, name, icon string) error {
375- if id <= 0 {
376- return fmt.Errorf("update folder id must be positive, got %d", id)
377- }
378- name = strings.TrimSpace(name)
379- if name == "" {
380- return fmt.Errorf("folder name is required")
381- }
382- var iconPtr *string
383- if trimmed := strings.TrimSpace(icon); trimmed != "" {
384- iconPtr = &trimmed
385- }
386- _, err := request[struct{}](c, ctx, "folder_update", struct {
387- RequestID string `json:"requestId"`
388- ID int64 `json:"id"`
389- Name string `json:"name"`
390- Icon *string `json:"icon"`
391- }{RequestID: nextRequestID(), ID: id, Name: name, Icon: iconPtr})
392- return err
393-}
394-
395-func (c *Client) DeleteFolder(ctx context.Context, id int64) error {
396- if id <= 0 {
397- return fmt.Errorf("delete folder id must be positive, got %d", id)
398- }
399- _, err := request[struct{}](c, ctx, "folder_delete", struct {
400- RequestID string `json:"requestId"`
401- ID int64 `json:"id"`
402- }{RequestID: nextRequestID(), ID: id})
403- return err
404-}
405-
406 func (c *Client) CreateTag(ctx context.Context, name, color string) (*entity.Tag, error) {327 func (c *Client) CreateTag(ctx context.Context, name, color string) (*entity.Tag, error) {
407 name = strings.TrimSpace(name)328 name = strings.TrimSpace(name)
408 if name == "" {329 if name == "" {
M internal/infrastructure/systemviewsbridge/client_test.go +0 -139
@@ -268,32 +268,6 @@ func TestClientListDecodesFavorites(t *testing.T) {
268268 }
269269 }
270270
271-func TestClientListFoldersDecodesFolders(t *testing.T) {
272- t.Parallel()
273-
274- native := newTransportRecorder(t, true, []byte(`{"requestId":"req-12","success":true,"data":[{"id":9,"name":"Read Later"}]}`))
275- client := NewClient(native, nil)
276-
277- folders, err := client.ListFolders(context.Background())
278- if err != nil {
279- t.Fatalf("ListFolders() error = %v", err)
280- }
281- if len(folders) != 1 {
282- t.Fatalf("ListFolders() len = %d, want 1", len(folders))
283- }
284- if folders[0].Name != "Read Later" {
285- t.Fatalf("ListFolders() folder = %+v", folders[0])
286- }
287-
288- var msg port.WebUIMessage
289- if err := json.Unmarshal(native.last, &msg); err != nil {
290- t.Fatalf("unmarshal sent envelope: %v", err)
291- }
292- if msg.Type != "folder_list" {
293- t.Fatalf("sent type = %q, want %q", msg.Type, "folder_list")
294- }
295-}
296-
297271 func TestClientListTagsDecodesTags(t *testing.T) {
298272 t.Parallel()
299273
@@ -470,61 +444,6 @@ func TestClientRequestUsesDefaultTimeoutWhenCallerHasNoDeadline(t *testing.T) {
470444 }
471445 }
472446
473-func TestClientCreateFolderIncludesParentID(t *testing.T) {
474- t.Parallel()
475-
476- parentID := int64(42)
477- transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-16","success":true,"data":{"id":9,"name":"Nested","icon":"📁","parent_id":42}}`))
478- client := NewClient(transport, nil)
479-
480- folder, err := client.CreateFolder(context.Background(), "Nested", " 📁 ", &parentID)
481- if err != nil {
482- t.Fatalf("CreateFolder() error = %v", err)
483- }
484- if folder == nil {
485- t.Fatal("CreateFolder() returned nil folder")
486- }
487- if folder.Name != "Nested" {
488- t.Fatalf("CreateFolder() folder name = %q, want %q", folder.Name, "Nested")
489- }
490- if folder.Icon != "📁" {
491- t.Fatalf("CreateFolder() folder icon = %q, want 📁", folder.Icon)
492- }
493- if folder.ParentID == nil || int64(*folder.ParentID) != parentID {
494- t.Fatalf("CreateFolder() folder parent_id = %#v, want %d", folder.ParentID, parentID)
495- }
496-
497- var msg port.WebUIMessage
498- if err := json.Unmarshal(transport.last, &msg); err != nil {
499- t.Fatalf("unmarshal sent envelope: %v", err)
500- }
501- if msg.Type != "folder_create" {
502- t.Fatalf("sent type = %q, want %q", msg.Type, "folder_create")
503- }
504-
505- var payload struct {
506- RequestID string `json:"requestId"`
507- Name string `json:"name"`
508- Icon *string `json:"icon"`
509- ParentID *int64 `json:"parent_id"`
510- }
511- if err := json.Unmarshal(msg.Payload, &payload); err != nil {
512- t.Fatalf("unmarshal sent payload: %v", err)
513- }
514- if payload.ParentID == nil || *payload.ParentID != parentID {
515- t.Fatalf("sent parent_id = %#v, want %d", payload.ParentID, parentID)
516- }
517- if payload.Name != "Nested" {
518- t.Fatalf("sent name = %q, want %q", payload.Name, "Nested")
519- }
520- if payload.Icon == nil || *payload.Icon != "📁" {
521- t.Fatalf("sent icon = %#v, want 📁", payload.Icon)
522- }
523- if payload.RequestID == "" {
524- t.Fatal("sent requestId was empty")
525- }
526-}
527-
528447 func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
529448 t.Parallel()
530449
@@ -555,59 +474,6 @@ func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
555474 }
556475 }
557476
558-func TestClientUpdateFolderTrimsIcon(t *testing.T) {
559- t.Parallel()
560-
561- transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-18","success":true}`))
562- client := NewClient(transport, nil)
563-
564- if err := client.UpdateFolder(context.Background(), 9, "Read", " 📚 "); err != nil {
565- t.Fatalf("UpdateFolder() error = %v", err)
566- }
567-
568- var msg port.WebUIMessage
569- if err := json.Unmarshal(transport.last, &msg); err != nil {
570- t.Fatalf("unmarshal sent envelope: %v", err)
571- }
572- if msg.Type != "folder_update" {
573- t.Fatalf("sent type = %q, want folder_update", msg.Type)
574- }
575- var payload struct {
576- Icon *string `json:"icon"`
577- }
578- if err := json.Unmarshal(msg.Payload, &payload); err != nil {
579- t.Fatalf("unmarshal sent payload: %v", err)
580- }
581- if payload.Icon == nil || *payload.Icon != "📚" {
582- t.Fatalf("sent icon = %#v, want 📚", payload.Icon)
583- }
584-}
585-
586-func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
587- t.Parallel()
588-
589- transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-19","success":true}`))
590- client := NewClient(transport, nil)
591-
592- if err := client.UpdateFolder(context.Background(), 9, "Read", " "); err != nil {
593- t.Fatalf("UpdateFolder() error = %v", err)
594- }
595-
596- var msg port.WebUIMessage
597- if err := json.Unmarshal(transport.last, &msg); err != nil {
598- t.Fatalf("unmarshal sent envelope: %v", err)
599- }
600- var payload struct {
601- Icon *string `json:"icon"`
602- }
603- if err := json.Unmarshal(msg.Payload, &payload); err != nil {
604- t.Fatalf("unmarshal sent payload: %v", err)
605- }
606- if payload.Icon != nil {
607- t.Fatalf("sent icon = %#v, want nil", payload.Icon)
608- }
609-}
610-
611477 func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
612478 t.Parallel()
613479
@@ -719,11 +585,6 @@ func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
719585 }{
720586 {name: "delete favorite", call: func() error { return client.DeleteFavorite(context.Background(), 0) }},
721587 {name: "set shortcut", call: func() error { return client.SetShortcut(context.Background(), 0, nil) }},
722- {name: "set folder", call: func() error { return client.SetFolder(context.Background(), 0, nil) }},
723- {name: "create folder", call: func() error { _, err := client.CreateFolder(context.Background(), " ", "", nil); return err }},
724- {name: "update folder id", call: func() error { return client.UpdateFolder(context.Background(), 0, "Folder", "") }},
725- {name: "update folder name", call: func() error { return client.UpdateFolder(context.Background(), 1, " ", "") }},
726- {name: "delete folder", call: func() error { return client.DeleteFolder(context.Background(), 0) }},
727588 {name: "create tag", call: func() error { _, err := client.CreateTag(context.Background(), " ", ""); return err }},
728589 {name: "update tag", call: func() error { return client.UpdateTag(context.Background(), 0, "Tag", "") }},
729590 {name: "update tag name", call: func() error { return client.UpdateTag(context.Background(), 1, " ", "") }},
@@ -268,32 +268,6 @@ func TestClientListDecodesFavorites(t *testing.T) {
268 }268 }
269 }269 }
270 270
271-func TestClientListFoldersDecodesFolders(t *testing.T) {
272- t.Parallel()
273-
274- native := newTransportRecorder(t, true, []byte(`{"requestId":"req-12","success":true,"data":[{"id":9,"name":"Read Later"}]}`))
275- client := NewClient(native, nil)
276-
277- folders, err := client.ListFolders(context.Background())
278- if err != nil {
279- t.Fatalf("ListFolders() error = %v", err)
280- }
281- if len(folders) != 1 {
282- t.Fatalf("ListFolders() len = %d, want 1", len(folders))
283- }
284- if folders[0].Name != "Read Later" {
285- t.Fatalf("ListFolders() folder = %+v", folders[0])
286- }
287-
288- var msg port.WebUIMessage
289- if err := json.Unmarshal(native.last, &msg); err != nil {
290- t.Fatalf("unmarshal sent envelope: %v", err)
291- }
292- if msg.Type != "folder_list" {
293- t.Fatalf("sent type = %q, want %q", msg.Type, "folder_list")
294- }
295-}
296-
297 func TestClientListTagsDecodesTags(t *testing.T) {271 func TestClientListTagsDecodesTags(t *testing.T) {
298 t.Parallel()272 t.Parallel()
299 273
@@ -470,61 +444,6 @@ func TestClientRequestUsesDefaultTimeoutWhenCallerHasNoDeadline(t *testing.T) {
470 }444 }
471 }445 }
472 446
473-func TestClientCreateFolderIncludesParentID(t *testing.T) {
474- t.Parallel()
475-
476- parentID := int64(42)
477- transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-16","success":true,"data":{"id":9,"name":"Nested","icon":"📁","parent_id":42}}`))
478- client := NewClient(transport, nil)
479-
480- folder, err := client.CreateFolder(context.Background(), "Nested", " 📁 ", &parentID)
481- if err != nil {
482- t.Fatalf("CreateFolder() error = %v", err)
483- }
484- if folder == nil {
485- t.Fatal("CreateFolder() returned nil folder")
486- }
487- if folder.Name != "Nested" {
488- t.Fatalf("CreateFolder() folder name = %q, want %q", folder.Name, "Nested")
489- }
490- if folder.Icon != "📁" {
491- t.Fatalf("CreateFolder() folder icon = %q, want 📁", folder.Icon)
492- }
493- if folder.ParentID == nil || int64(*folder.ParentID) != parentID {
494- t.Fatalf("CreateFolder() folder parent_id = %#v, want %d", folder.ParentID, parentID)
495- }
496-
497- var msg port.WebUIMessage
498- if err := json.Unmarshal(transport.last, &msg); err != nil {
499- t.Fatalf("unmarshal sent envelope: %v", err)
500- }
501- if msg.Type != "folder_create" {
502- t.Fatalf("sent type = %q, want %q", msg.Type, "folder_create")
503- }
504-
505- var payload struct {
506- RequestID string `json:"requestId"`
507- Name string `json:"name"`
508- Icon *string `json:"icon"`
509- ParentID *int64 `json:"parent_id"`
510- }
511- if err := json.Unmarshal(msg.Payload, &payload); err != nil {
512- t.Fatalf("unmarshal sent payload: %v", err)
513- }
514- if payload.ParentID == nil || *payload.ParentID != parentID {
515- t.Fatalf("sent parent_id = %#v, want %d", payload.ParentID, parentID)
516- }
517- if payload.Name != "Nested" {
518- t.Fatalf("sent name = %q, want %q", payload.Name, "Nested")
519- }
520- if payload.Icon == nil || *payload.Icon != "📁" {
521- t.Fatalf("sent icon = %#v, want 📁", payload.Icon)
522- }
523- if payload.RequestID == "" {
524- t.Fatal("sent requestId was empty")
525- }
526-}
527-
528 func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {447 func TestClientDomainStatsRejectsNegativeLimit(t *testing.T) {
529 t.Parallel()448 t.Parallel()
530 449
@@ -555,59 +474,6 @@ func TestClientDeleteDomainRejectsEmptyDomain(t *testing.T) {
555 }474 }
556 }475 }
557 476
558-func TestClientUpdateFolderTrimsIcon(t *testing.T) {
559- t.Parallel()
560-
561- transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-18","success":true}`))
562- client := NewClient(transport, nil)
563-
564- if err := client.UpdateFolder(context.Background(), 9, "Read", " 📚 "); err != nil {
565- t.Fatalf("UpdateFolder() error = %v", err)
566- }
567-
568- var msg port.WebUIMessage
569- if err := json.Unmarshal(transport.last, &msg); err != nil {
570- t.Fatalf("unmarshal sent envelope: %v", err)
571- }
572- if msg.Type != "folder_update" {
573- t.Fatalf("sent type = %q, want folder_update", msg.Type)
574- }
575- var payload struct {
576- Icon *string `json:"icon"`
577- }
578- if err := json.Unmarshal(msg.Payload, &payload); err != nil {
579- t.Fatalf("unmarshal sent payload: %v", err)
580- }
581- if payload.Icon == nil || *payload.Icon != "📚" {
582- t.Fatalf("sent icon = %#v, want 📚", payload.Icon)
583- }
584-}
585-
586-func TestClientUpdateFolderOmitsBlankIcon(t *testing.T) {
587- t.Parallel()
588-
589- transport := newTransportRecorder(t, true, []byte(`{"requestId":"req-19","success":true}`))
590- client := NewClient(transport, nil)
591-
592- if err := client.UpdateFolder(context.Background(), 9, "Read", " "); err != nil {
593- t.Fatalf("UpdateFolder() error = %v", err)
594- }
595-
596- var msg port.WebUIMessage
597- if err := json.Unmarshal(transport.last, &msg); err != nil {
598- t.Fatalf("unmarshal sent envelope: %v", err)
599- }
600- var payload struct {
601- Icon *string `json:"icon"`
602- }
603- if err := json.Unmarshal(msg.Payload, &payload); err != nil {
604- t.Fatalf("unmarshal sent payload: %v", err)
605- }
606- if payload.Icon != nil {
607- t.Fatalf("sent icon = %#v, want nil", payload.Icon)
608- }
609-}
610-
611 func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {477 func TestClientDeleteEntryRejectsInvalidID(t *testing.T) {
612 t.Parallel()478 t.Parallel()
613 479
@@ -719,11 +585,6 @@ func TestClientRejectsInvalidFavoriteMutationIDs(t *testing.T) {
719 }{585 }{
720 {name: "delete favorite", call: func() error { return client.DeleteFavorite(context.Background(), 0) }},586 {name: "delete favorite", call: func() error { return client.DeleteFavorite(context.Background(), 0) }},
721 {name: "set shortcut", call: func() error { return client.SetShortcut(context.Background(), 0, nil) }},587 {name: "set shortcut", call: func() error { return client.SetShortcut(context.Background(), 0, nil) }},
722- {name: "set folder", call: func() error { return client.SetFolder(context.Background(), 0, nil) }},
723- {name: "create folder", call: func() error { _, err := client.CreateFolder(context.Background(), " ", "", nil); return err }},
724- {name: "update folder id", call: func() error { return client.UpdateFolder(context.Background(), 0, "Folder", "") }},
725- {name: "update folder name", call: func() error { return client.UpdateFolder(context.Background(), 1, " ", "") }},
726- {name: "delete folder", call: func() error { return client.DeleteFolder(context.Background(), 0) }},
727 {name: "create tag", call: func() error { _, err := client.CreateTag(context.Background(), " ", ""); return err }},588 {name: "create tag", call: func() error { _, err := client.CreateTag(context.Background(), " ", ""); return err }},
728 {name: "update tag", call: func() error { return client.UpdateTag(context.Background(), 0, "Tag", "") }},589 {name: "update tag", call: func() error { return client.UpdateTag(context.Background(), 0, "Tag", "") }},
729 {name: "update tag name", call: func() error { return client.UpdateTag(context.Background(), 1, " ", "") }},590 {name: "update tag name", call: func() error { return client.UpdateTag(context.Background(), 1, " ", "") }},
M internal/ui/systemviews/app.go +45 -56
@@ -24,44 +24,43 @@ type Dependencies struct {
2424 type App struct {
2525 // mu protects rendered route state; actionMu protects worker channels.
2626 // The two locks must not be held at the same time.
27- mu sync.Mutex
28- actionMu sync.Mutex
29- deps Dependencies
30- currentRoute Route
31- shellTheme shellTheme
32- historyEntries []*entity.HistoryEntry
33- historyStats *entity.HistoryStats
34- historyAnalytics *entity.HistoryAnalytics
35- historyDomainStats []*entity.DomainStat
36- historyQuery string
37- historyDomainFilter string
38- historyOffset int
39- historyWindowBefore time.Time
40- historyWindowAfter time.Time
41- historyCursorID int64
42- historyHasMore bool
43- historyNotice string
44- historyError string
45- favorites []*entity.Favorite
46- folders []*entity.Folder
47- tags []*entity.Tag
48- favoriteFolderFilter *entity.FolderID
49- favoriteTagFilter *entity.TagID
50- favoritesNotice string
51- favoritesError string
52- config *dto.SystemviewConfigPayload
53- keybindings port.KeybindingsConfig
54- configNotice string
55- configError string
56- renderedHTML string
57- renderGeneration uint64
58- closed bool
59- actionQueue chan DOMAction
60- actionErrorQueue chan error
61- actionCtx context.Context
62- actionCancel context.CancelFunc
63- actionClosed bool
64- actionWG sync.WaitGroup
27+ mu sync.Mutex
28+ actionMu sync.Mutex
29+ deps Dependencies
30+ currentRoute Route
31+ shellTheme shellTheme
32+ historyEntries []*entity.HistoryEntry
33+ historyStats *entity.HistoryStats
34+ historyAnalytics *entity.HistoryAnalytics
35+ historyDomainStats []*entity.DomainStat
36+ historyQuery string
37+ historyDomainFilter string
38+ historyOffset int
39+ historyWindowBefore time.Time
40+ historyWindowAfter time.Time
41+ historyCursorID int64
42+ historyHasMore bool
43+ historyNotice string
44+ historyError string
45+ favorites []*entity.Favorite
46+ tags []*entity.Tag
47+ favoriteTagFilter *entity.TagID
48+ favoriteUntaggedFilter bool
49+ favoritesNotice string
50+ favoritesError string
51+ config *dto.SystemviewConfigPayload
52+ keybindings port.KeybindingsConfig
53+ configNotice string
54+ configError string
55+ renderedHTML string
56+ renderGeneration uint64
57+ closed bool
58+ actionQueue chan DOMAction
59+ actionErrorQueue chan error
60+ actionCtx context.Context
61+ actionCancel context.CancelFunc
62+ actionClosed bool
63+ actionWG sync.WaitGroup
6564 }
6665
6766 const (
@@ -513,7 +512,6 @@ func routeSubtitle(route Route) string {
513512
514513 func (a *App) loadHistoryLoadingRoute() {
515514 a.favorites = nil
516- a.folders = nil
517515 a.tags = nil
518516 a.historyEntries = nil
519517 a.historyStats = nil
@@ -698,7 +696,6 @@ func loadHistoryWindowSnapshot(ctx context.Context, snapshot historyRouteSnapsho
698696
699697 func (a *App) commitHistoryRouteResultLocked(result historyRouteResult) {
700698 a.favorites = nil
701- a.folders = nil
702699 a.tags = nil
703700 a.historyEntries = result.entries
704701 a.historyStats = result.stats
@@ -727,28 +724,22 @@ func (a *App) loadFavoritesRoute(ctx context.Context) error {
727724 if err != nil {
728725 return err
729726 }
730- folders, err := a.deps.Favorites.ListFolders(ctx)
731- if err != nil {
732- return err
733- }
734727 tags, err := a.deps.Favorites.ListTags(ctx)
735728 if err != nil {
736729 return err
737730 }
738731
739732 a.historyEntries = nil
740- favorites = filterFavorites(favorites, a.favoriteFolderFilter, a.favoriteTagFilter)
733+ favorites = filterFavorites(favorites, a.favoriteTagFilter, a.favoriteUntaggedFilter)
741734 a.favorites = favorites
742- a.folders = folders
743735 a.tags = tags
744736 data := favoritesRenderData{
745- Favorites: favorites,
746- Folders: folders,
747- Tags: tags,
748- FolderFilter: a.favoriteFolderFilter,
749- TagFilter: a.favoriteTagFilter,
750- Notice: a.favoritesNotice,
751- Error: a.favoritesError,
737+ Favorites: favorites,
738+ Tags: tags,
739+ TagFilter: a.favoriteTagFilter,
740+ UntaggedFilter: a.favoriteUntaggedFilter,
741+ Notice: a.favoritesNotice,
742+ Error: a.favoritesError,
752743 }
753744 a.renderedHTML = renderAppFrame(renderedPage{
754745 route: RouteFavorites,
@@ -782,7 +773,6 @@ func (a *App) loadConfigRoute(ctx context.Context) error {
782773
783774 a.historyEntries = nil
784775 a.favorites = nil
785- a.folders = nil
786776 a.tags = nil
787777 a.config = &config
788778 a.keybindings = keybindings
@@ -835,10 +825,9 @@ func (a *App) resetRouteState() {
835825 a.historyNotice = ""
836826 a.historyError = ""
837827 a.favorites = nil
838- a.folders = nil
839828 a.tags = nil
840- a.favoriteFolderFilter = nil
841829 a.favoriteTagFilter = nil
830+ a.favoriteUntaggedFilter = false
842831 a.favoritesNotice = ""
843832 a.favoritesError = ""
844833 a.config = nil
@@ -24,44 +24,43 @@ type Dependencies struct {
24 type App struct {24 type App struct {
25 // mu protects rendered route state; actionMu protects worker channels.25 // mu protects rendered route state; actionMu protects worker channels.
26 // The two locks must not be held at the same time.26 // The two locks must not be held at the same time.
27- mu sync.Mutex27+ mu sync.Mutex
28- actionMu sync.Mutex28+ actionMu sync.Mutex
29- deps Dependencies29+ deps Dependencies
30- currentRoute Route30+ currentRoute Route
31- shellTheme shellTheme31+ shellTheme shellTheme
32- historyEntries []*entity.HistoryEntry32+ historyEntries []*entity.HistoryEntry
33- historyStats *entity.HistoryStats33+ historyStats *entity.HistoryStats
34- historyAnalytics *entity.HistoryAnalytics34+ historyAnalytics *entity.HistoryAnalytics
35- historyDomainStats []*entity.DomainStat35+ historyDomainStats []*entity.DomainStat
36- historyQuery string36+ historyQuery string
37- historyDomainFilter string37+ historyDomainFilter string
38- historyOffset int38+ historyOffset int
39- historyWindowBefore time.Time39+ historyWindowBefore time.Time
40- historyWindowAfter time.Time40+ historyWindowAfter time.Time
41- historyCursorID int6441+ historyCursorID int64
42- historyHasMore bool42+ historyHasMore bool
43- historyNotice string43+ historyNotice string
44- historyError string44+ historyError string
45- favorites []*entity.Favorite45+ favorites []*entity.Favorite
46- folders []*entity.Folder46+ tags []*entity.Tag
47- tags []*entity.Tag47+ favoriteTagFilter *entity.TagID
48- favoriteFolderFilter *entity.FolderID48+ favoriteUntaggedFilter bool
49- favoriteTagFilter *entity.TagID49+ favoritesNotice string
50- favoritesNotice string50+ favoritesError string
51- favoritesError string51+ config *dto.SystemviewConfigPayload
52- config *dto.SystemviewConfigPayload52+ keybindings port.KeybindingsConfig
53- keybindings port.KeybindingsConfig53+ configNotice string
54- configNotice string54+ configError string
55- configError string55+ renderedHTML string
56- renderedHTML string56+ renderGeneration uint64
57- renderGeneration uint6457+ closed bool
58- closed bool58+ actionQueue chan DOMAction
59- actionQueue chan DOMAction59+ actionErrorQueue chan error
60- actionErrorQueue chan error60+ actionCtx context.Context
61- actionCtx context.Context61+ actionCancel context.CancelFunc
62- actionCancel context.CancelFunc62+ actionClosed bool
63- actionClosed bool63+ actionWG sync.WaitGroup
64- actionWG sync.WaitGroup
65 }64 }
66 65
67 const (66 const (
@@ -513,7 +512,6 @@ func routeSubtitle(route Route) string {
513 512
514 func (a *App) loadHistoryLoadingRoute() {513 func (a *App) loadHistoryLoadingRoute() {
515 a.favorites = nil514 a.favorites = nil
516- a.folders = nil
517 a.tags = nil515 a.tags = nil
518 a.historyEntries = nil516 a.historyEntries = nil
519 a.historyStats = nil517 a.historyStats = nil
@@ -698,7 +696,6 @@ func loadHistoryWindowSnapshot(ctx context.Context, snapshot historyRouteSnapsho
698 696
699 func (a *App) commitHistoryRouteResultLocked(result historyRouteResult) {697 func (a *App) commitHistoryRouteResultLocked(result historyRouteResult) {
700 a.favorites = nil698 a.favorites = nil
701- a.folders = nil
702 a.tags = nil699 a.tags = nil
703 a.historyEntries = result.entries700 a.historyEntries = result.entries
704 a.historyStats = result.stats701 a.historyStats = result.stats
@@ -727,28 +724,22 @@ func (a *App) loadFavoritesRoute(ctx context.Context) error {
727 if err != nil {724 if err != nil {
728 return err725 return err
729 }726 }
730- folders, err := a.deps.Favorites.ListFolders(ctx)
731- if err != nil {
732- return err
733- }
734 tags, err := a.deps.Favorites.ListTags(ctx)727 tags, err := a.deps.Favorites.ListTags(ctx)
735 if err != nil {728 if err != nil {
736 return err729 return err
737 }730 }
738 731
739 a.historyEntries = nil732 a.historyEntries = nil
740- favorites = filterFavorites(favorites, a.favoriteFolderFilter, a.favoriteTagFilter)733+ favorites = filterFavorites(favorites, a.favoriteTagFilter, a.favoriteUntaggedFilter)
741 a.favorites = favorites734 a.favorites = favorites
742- a.folders = folders
743 a.tags = tags735 a.tags = tags
744 data := favoritesRenderData{736 data := favoritesRenderData{
745- Favorites: favorites,737+ Favorites: favorites,
746- Folders: folders,738+ Tags: tags,
747- Tags: tags,739+ TagFilter: a.favoriteTagFilter,
748- FolderFilter: a.favoriteFolderFilter,740+ UntaggedFilter: a.favoriteUntaggedFilter,
749- TagFilter: a.favoriteTagFilter,741+ Notice: a.favoritesNotice,
750- Notice: a.favoritesNotice,742+ Error: a.favoritesError,
751- Error: a.favoritesError,
752 }743 }
753 a.renderedHTML = renderAppFrame(renderedPage{744 a.renderedHTML = renderAppFrame(renderedPage{
754 route: RouteFavorites,745 route: RouteFavorites,
@@ -782,7 +773,6 @@ func (a *App) loadConfigRoute(ctx context.Context) error {
782 773
783 a.historyEntries = nil774 a.historyEntries = nil
784 a.favorites = nil775 a.favorites = nil
785- a.folders = nil
786 a.tags = nil776 a.tags = nil
787 a.config = &config777 a.config = &config
788 a.keybindings = keybindings778 a.keybindings = keybindings
@@ -835,10 +825,9 @@ func (a *App) resetRouteState() {
835 a.historyNotice = ""825 a.historyNotice = ""
836 a.historyError = ""826 a.historyError = ""
837 a.favorites = nil827 a.favorites = nil
838- a.folders = nil
839 a.tags = nil828 a.tags = nil
840- a.favoriteFolderFilter = nil
841 a.favoriteTagFilter = nil829 a.favoriteTagFilter = nil
830+ a.favoriteUntaggedFilter = false
842 a.favoritesNotice = ""831 a.favoritesNotice = ""
843 a.favoritesError = ""832 a.favoritesError = ""
844 a.config = nil833 a.config = nil
M internal/ui/systemviews/app_test.go +13 -67
@@ -517,7 +517,6 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
517517
518518 favorites := &recordingFavoritesService{
519519 favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}},
520- folders: []*entity.Folder{{Name: "Read Later"}},
521520 tags: []*entity.Tag{{Name: "Go"}},
522521 }
523522
@@ -529,14 +528,11 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
529528 require.NoError(t, app.LoadInitial(context.Background()))
530529 assert.Equal(t, RouteFavorites, app.CurrentRoute())
531530 assert.Len(t, app.favorites, 1)
532- assert.Len(t, app.folders, 1)
533531 assert.Len(t, app.tags, 1)
534532 assert.Contains(t, app.renderedHTML, "Favorites")
535533 assert.Contains(t, app.renderedHTML, "Example")
536- assert.Contains(t, app.renderedHTML, "Read Later")
537534 assert.Contains(t, app.renderedHTML, "Go")
538535 assert.True(t, favorites.calledList)
539- assert.True(t, favorites.calledFolders)
540536 assert.True(t, favorites.calledTags)
541537
542538 // Shell frame present, no full document wrapper.
@@ -553,11 +549,9 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
553549 func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
554550 t.Parallel()
555551
556- folderID := entity.FolderID(1)
557552 shortcut := 3
558553 favorites := &recordingFavoritesService{
559- favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", FolderID: &folderID, ShortcutKey: &shortcut}},
560- folders: []*entity.Folder{{ID: folderID, Name: "Read Later", Icon: "📚"}},
554+ favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", ShortcutKey: &shortcut}},
561555 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
562556 }
563557
@@ -567,16 +561,16 @@ func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
567561 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.create"`)
568562 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.update"`)
569563 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.delete"`)
570- assert.Contains(t, app.renderedHTML, `data-sv-action="folder.create"`)
571- assert.Contains(t, app.renderedHTML, `data-sv-action="folder.update"`)
572- assert.Contains(t, app.renderedHTML, `data-sv-action="folder.delete"`)
564+ assert.NotContains(t, app.renderedHTML, `data-sv-action="folder.create"`)
565+ assert.NotContains(t, app.renderedHTML, `data-sv-action="folder.update"`)
566+ assert.NotContains(t, app.renderedHTML, `data-sv-action="folder.delete"`)
573567 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.create"`)
574568 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.update"`)
575569 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.delete"`)
576570 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.assign"`)
571+ assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.filterUntagged"`)
577572 assert.Contains(t, app.renderedHTML, `name="tags"`)
578573 assert.Contains(t, app.renderedHTML, `value="7"`)
579- assert.Contains(t, app.renderedHTML, "Read Later")
580574 assert.Contains(t, app.renderedHTML, "Go")
581575 assert.Contains(t, app.renderedHTML, "Shortcut 3")
582576 }
@@ -585,18 +579,15 @@ func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {
585579 dom := &recordingDOM{}
586580 favorites := &recordingFavoritesService{
587581 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example"}},
588- folders: []*entity.Folder{{ID: 5, Name: "Read Later"}},
589582 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
590583 }
591584 app := NewApp(Dependencies{DOM: dom, Favorites: favorites, LocationURI: "dumb://favorites"})
592585
593586 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
594587 Action: favoriteActionCreate,
595- Data: map[string]string{"url": "https://new.example", "title": "New", "folder_id": "5", "tags": "7"},
588+ Data: map[string]string{"url": "https://new.example", "title": "New", "tags": "7"},
596589 }))
597- require.NotNil(t, favorites.createdFavorite.FolderID)
598590 assert.Equal(t, "https://new.example", favorites.createdFavorite.URL)
599- assert.Equal(t, entity.FolderID(5), *favorites.createdFavorite.FolderID)
600591 assert.Equal(t, []entity.TagID{7}, favorites.createdFavorite.Tags)
601592 assert.Contains(t, dom.HTML(), "Added favorite New")
602593
@@ -617,29 +608,13 @@ func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {
617608 assert.Equal(t, int64(42), favorites.assignedFavoriteID)
618609 assert.Equal(t, int64(7), favorites.assignedTagID)
619610
620- require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
621- Action: favoriteActionFilterFolder,
622- Data: map[string]string{"folderId": "5"},
623- }))
624- require.NotNil(t, app.favoriteFolderFilter)
625- assert.Equal(t, entity.FolderID(5), *app.favoriteFolderFilter)
626- assert.Nil(t, app.favoriteTagFilter)
627-
628611 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
629612 Action: favoriteActionFilterTag,
630613 Data: map[string]string{"tagId": "7"},
631614 }))
632615 require.NotNil(t, app.favoriteTagFilter)
633616 assert.Equal(t, entity.TagID(7), *app.favoriteTagFilter)
634- assert.Nil(t, app.favoriteFolderFilter)
635-
636- require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
637- Action: folderActionCreate,
638- Data: map[string]string{"name": "Reading", "icon": "📚"},
639- }))
640- assert.Equal(t, "Reading", favorites.createdFolder)
641- assert.Equal(t, "📚", favorites.createdFolderIcon)
642- assert.Contains(t, dom.HTML(), "Created folder 📚 Reading")
617+ assert.False(t, app.favoriteUntaggedFilter)
643618
644619 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
645620 Action: favoriteActionDelete,
@@ -1199,20 +1174,14 @@ func (s *recordingHistoryService) DeleteDomain(_ context.Context, domain string)
11991174
12001175 // Recording fixture to capture favorites state for stateful render assertions.
12011176 type recordingFavoritesService struct {
1202- calledList bool
1203- calledFolders bool
1204- calledTags bool
1205- favorites []*entity.Favorite
1206- folders []*entity.Folder
1207- tags []*entity.Tag
1177+ calledList bool
1178+ calledTags bool
1179+ favorites []*entity.Favorite
1180+ tags []*entity.Tag
12081181
12091182 createdFavorite dto.FavoriteCreateInput
12101183 updatedFavorite dto.FavoriteUpdateInput
12111184 deletedFavorite int64
1212- createdFolder string
1213- createdFolderIcon string
1214- updatedFolderID int64
1215- deletedFolderID int64
12161185 createdTag string
12171186 updatedTagID int64
12181187 deletedTagID int64
@@ -1227,11 +1196,6 @@ func (s *recordingFavoritesService) List(context.Context) ([]*entity.Favorite, e
12271196 return s.favorites, nil
12281197 }
12291198
1230-func (s *recordingFavoritesService) ListFolders(context.Context) ([]*entity.Folder, error) {
1231- s.calledFolders = true
1232- return s.folders, nil
1233-}
1234-
12351199 func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {
12361200 s.calledTags = true
12371201 return s.tags, nil
@@ -1239,12 +1203,12 @@ func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, er
12391203
12401204 func (s *recordingFavoritesService) CreateFavorite(_ context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) {
12411205 s.createdFavorite = input
1242- return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title, FolderID: input.FolderID}, nil
1206+ return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title}, nil
12431207 }
12441208
12451209 func (s *recordingFavoritesService) UpdateFavorite(_ context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) {
12461210 s.updatedFavorite = input
1247- return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, FolderID: input.FolderID, ShortcutKey: input.ShortcutKey}, nil
1211+ return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, ShortcutKey: input.ShortcutKey}, nil
12481212 }
12491213
12501214 func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64) error {
@@ -1254,24 +1218,6 @@ func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64)
12541218
12551219 func (s *recordingFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }
12561220
1257-func (s *recordingFavoritesService) SetFolder(context.Context, int64, *int64) error { return nil }
1258-
1259-func (s *recordingFavoritesService) CreateFolder(_ context.Context, name, icon string, _ *int64) (*entity.Folder, error) {
1260- s.createdFolder = name
1261- s.createdFolderIcon = icon
1262- return &entity.Folder{ID: 77, Name: name, Icon: icon}, nil
1263-}
1264-
1265-func (s *recordingFavoritesService) UpdateFolder(_ context.Context, id int64, _, _ string) error {
1266- s.updatedFolderID = id
1267- return nil
1268-}
1269-
1270-func (s *recordingFavoritesService) DeleteFolder(_ context.Context, id int64) error {
1271- s.deletedFolderID = id
1272- return nil
1273-}
1274-
12751221 func (s *recordingFavoritesService) CreateTag(_ context.Context, name, color string) (*entity.Tag, error) {
12761222 s.createdTag = name
12771223 return &entity.Tag{ID: 55, Name: name, Color: color}, nil
@@ -517,7 +517,6 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
517 517
518 favorites := &recordingFavoritesService{518 favorites := &recordingFavoritesService{
519 favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}},519 favorites: []*entity.Favorite{{URL: "https://example.com", Title: "Example"}},
520- folders: []*entity.Folder{{Name: "Read Later"}},
521 tags: []*entity.Tag{{Name: "Go"}},520 tags: []*entity.Tag{{Name: "Go"}},
522 }521 }
523 522
@@ -529,14 +528,11 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
529 require.NoError(t, app.LoadInitial(context.Background()))528 require.NoError(t, app.LoadInitial(context.Background()))
530 assert.Equal(t, RouteFavorites, app.CurrentRoute())529 assert.Equal(t, RouteFavorites, app.CurrentRoute())
531 assert.Len(t, app.favorites, 1)530 assert.Len(t, app.favorites, 1)
532- assert.Len(t, app.folders, 1)
533 assert.Len(t, app.tags, 1)531 assert.Len(t, app.tags, 1)
534 assert.Contains(t, app.renderedHTML, "Favorites")532 assert.Contains(t, app.renderedHTML, "Favorites")
535 assert.Contains(t, app.renderedHTML, "Example")533 assert.Contains(t, app.renderedHTML, "Example")
536- assert.Contains(t, app.renderedHTML, "Read Later")
537 assert.Contains(t, app.renderedHTML, "Go")534 assert.Contains(t, app.renderedHTML, "Go")
538 assert.True(t, favorites.calledList)535 assert.True(t, favorites.calledList)
539- assert.True(t, favorites.calledFolders)
540 assert.True(t, favorites.calledTags)536 assert.True(t, favorites.calledTags)
541 537
542 // Shell frame present, no full document wrapper.538 // Shell frame present, no full document wrapper.
@@ -553,11 +549,9 @@ func TestAppLoadInitialFavoritesRouteRendersData(t *testing.T) {
553 func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {549 func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
554 t.Parallel()550 t.Parallel()
555 551
556- folderID := entity.FolderID(1)
557 shortcut := 3552 shortcut := 3
558 favorites := &recordingFavoritesService{553 favorites := &recordingFavoritesService{
559- favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", FolderID: &folderID, ShortcutKey: &shortcut}},554+ favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example", ShortcutKey: &shortcut}},
560- folders: []*entity.Folder{{ID: folderID, Name: "Read Later", Icon: "📚"}},
561 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},555 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
562 }556 }
563 557
@@ -567,16 +561,16 @@ func TestAppLoadInitialFavoritesRouteRendersCRUDControls(t *testing.T) {
567 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.create"`)561 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.create"`)
568 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.update"`)562 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.update"`)
569 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.delete"`)563 assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.delete"`)
570- assert.Contains(t, app.renderedHTML, `data-sv-action="folder.create"`)564+ assert.NotContains(t, app.renderedHTML, `data-sv-action="folder.create"`)
571- assert.Contains(t, app.renderedHTML, `data-sv-action="folder.update"`)565+ assert.NotContains(t, app.renderedHTML, `data-sv-action="folder.update"`)
572- assert.Contains(t, app.renderedHTML, `data-sv-action="folder.delete"`)566+ assert.NotContains(t, app.renderedHTML, `data-sv-action="folder.delete"`)
573 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.create"`)567 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.create"`)
574 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.update"`)568 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.update"`)
575 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.delete"`)569 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.delete"`)
576 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.assign"`)570 assert.Contains(t, app.renderedHTML, `data-sv-action="tag.assign"`)
571+ assert.Contains(t, app.renderedHTML, `data-sv-action="favorite.filterUntagged"`)
577 assert.Contains(t, app.renderedHTML, `name="tags"`)572 assert.Contains(t, app.renderedHTML, `name="tags"`)
578 assert.Contains(t, app.renderedHTML, `value="7"`)573 assert.Contains(t, app.renderedHTML, `value="7"`)
579- assert.Contains(t, app.renderedHTML, "Read Later")
580 assert.Contains(t, app.renderedHTML, "Go")574 assert.Contains(t, app.renderedHTML, "Go")
581 assert.Contains(t, app.renderedHTML, "Shortcut 3")575 assert.Contains(t, app.renderedHTML, "Shortcut 3")
582 }576 }
@@ -585,18 +579,15 @@ func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {
585 dom := &recordingDOM{}579 dom := &recordingDOM{}
586 favorites := &recordingFavoritesService{580 favorites := &recordingFavoritesService{
587 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example"}},581 favorites: []*entity.Favorite{{ID: 42, URL: "https://example.com", Title: "Example"}},
588- folders: []*entity.Folder{{ID: 5, Name: "Read Later"}},
589 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},582 tags: []*entity.Tag{{ID: 7, Name: "Go", Color: "#00add8"}},
590 }583 }
591 app := NewApp(Dependencies{DOM: dom, Favorites: favorites, LocationURI: "dumb://favorites"})584 app := NewApp(Dependencies{DOM: dom, Favorites: favorites, LocationURI: "dumb://favorites"})
592 585
593 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{586 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
594 Action: favoriteActionCreate,587 Action: favoriteActionCreate,
595- Data: map[string]string{"url": "https://new.example", "title": "New", "folder_id": "5", "tags": "7"},588+ Data: map[string]string{"url": "https://new.example", "title": "New", "tags": "7"},
596 }))589 }))
597- require.NotNil(t, favorites.createdFavorite.FolderID)
598 assert.Equal(t, "https://new.example", favorites.createdFavorite.URL)590 assert.Equal(t, "https://new.example", favorites.createdFavorite.URL)
599- assert.Equal(t, entity.FolderID(5), *favorites.createdFavorite.FolderID)
600 assert.Equal(t, []entity.TagID{7}, favorites.createdFavorite.Tags)591 assert.Equal(t, []entity.TagID{7}, favorites.createdFavorite.Tags)
601 assert.Contains(t, dom.HTML(), "Added favorite New")592 assert.Contains(t, dom.HTML(), "Added favorite New")
602 593
@@ -617,29 +608,13 @@ func TestAppHandleFavoriteActionsRefreshesDOM(t *testing.T) {
617 assert.Equal(t, int64(42), favorites.assignedFavoriteID)608 assert.Equal(t, int64(42), favorites.assignedFavoriteID)
618 assert.Equal(t, int64(7), favorites.assignedTagID)609 assert.Equal(t, int64(7), favorites.assignedTagID)
619 610
620- require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
621- Action: favoriteActionFilterFolder,
622- Data: map[string]string{"folderId": "5"},
623- }))
624- require.NotNil(t, app.favoriteFolderFilter)
625- assert.Equal(t, entity.FolderID(5), *app.favoriteFolderFilter)
626- assert.Nil(t, app.favoriteTagFilter)
627-
628 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{611 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
629 Action: favoriteActionFilterTag,612 Action: favoriteActionFilterTag,
630 Data: map[string]string{"tagId": "7"},613 Data: map[string]string{"tagId": "7"},
631 }))614 }))
632 require.NotNil(t, app.favoriteTagFilter)615 require.NotNil(t, app.favoriteTagFilter)
633 assert.Equal(t, entity.TagID(7), *app.favoriteTagFilter)616 assert.Equal(t, entity.TagID(7), *app.favoriteTagFilter)
634- assert.Nil(t, app.favoriteFolderFilter)617+ assert.False(t, app.favoriteUntaggedFilter)
635-
636- require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
637- Action: folderActionCreate,
638- Data: map[string]string{"name": "Reading", "icon": "📚"},
639- }))
640- assert.Equal(t, "Reading", favorites.createdFolder)
641- assert.Equal(t, "📚", favorites.createdFolderIcon)
642- assert.Contains(t, dom.HTML(), "Created folder 📚 Reading")
643 618
644 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{619 require.NoError(t, app.HandleDOMAction(context.Background(), DOMAction{
645 Action: favoriteActionDelete,620 Action: favoriteActionDelete,
@@ -1199,20 +1174,14 @@ func (s *recordingHistoryService) DeleteDomain(_ context.Context, domain string)
1199 1174
1200 // Recording fixture to capture favorites state for stateful render assertions.1175 // Recording fixture to capture favorites state for stateful render assertions.
1201 type recordingFavoritesService struct {1176 type recordingFavoritesService struct {
1202- calledList bool1177+ calledList bool
1203- calledFolders bool1178+ calledTags bool
1204- calledTags bool1179+ favorites []*entity.Favorite
1205- favorites []*entity.Favorite1180+ tags []*entity.Tag
1206- folders []*entity.Folder
1207- tags []*entity.Tag
1208 1181
1209 createdFavorite dto.FavoriteCreateInput1182 createdFavorite dto.FavoriteCreateInput
1210 updatedFavorite dto.FavoriteUpdateInput1183 updatedFavorite dto.FavoriteUpdateInput
1211 deletedFavorite int641184 deletedFavorite int64
1212- createdFolder string
1213- createdFolderIcon string
1214- updatedFolderID int64
1215- deletedFolderID int64
1216 createdTag string1185 createdTag string
1217 updatedTagID int641186 updatedTagID int64
1218 deletedTagID int641187 deletedTagID int64
@@ -1227,11 +1196,6 @@ func (s *recordingFavoritesService) List(context.Context) ([]*entity.Favorite, e
1227 return s.favorites, nil1196 return s.favorites, nil
1228 }1197 }
1229 1198
1230-func (s *recordingFavoritesService) ListFolders(context.Context) ([]*entity.Folder, error) {
1231- s.calledFolders = true
1232- return s.folders, nil
1233-}
1234-
1235 func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {1199 func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, error) {
1236 s.calledTags = true1200 s.calledTags = true
1237 return s.tags, nil1201 return s.tags, nil
@@ -1239,12 +1203,12 @@ func (s *recordingFavoritesService) ListTags(context.Context) ([]*entity.Tag, er
1239 1203
1240 func (s *recordingFavoritesService) CreateFavorite(_ context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) {1204 func (s *recordingFavoritesService) CreateFavorite(_ context.Context, input dto.FavoriteCreateInput) (*entity.Favorite, error) {
1241 s.createdFavorite = input1205 s.createdFavorite = input
1242- return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title, FolderID: input.FolderID}, nil1206+ return &entity.Favorite{ID: 99, URL: input.URL, Title: input.Title}, nil
1243 }1207 }
1244 1208
1245 func (s *recordingFavoritesService) UpdateFavorite(_ context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) {1209 func (s *recordingFavoritesService) UpdateFavorite(_ context.Context, input dto.FavoriteUpdateInput) (*entity.Favorite, error) {
1246 s.updatedFavorite = input1210 s.updatedFavorite = input
1247- return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, FolderID: input.FolderID, ShortcutKey: input.ShortcutKey}, nil1211+ return &entity.Favorite{ID: input.ID, URL: "https://example.com", Title: input.Title, FaviconURL: input.FaviconURL, ShortcutKey: input.ShortcutKey}, nil
1248 }1212 }
1249 1213
1250 func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64) error {1214 func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64) error {
@@ -1254,24 +1218,6 @@ func (s *recordingFavoritesService) DeleteFavorite(_ context.Context, id int64)
1254 1218
1255 func (s *recordingFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }1219 func (s *recordingFavoritesService) SetShortcut(context.Context, int64, *int) error { return nil }
1256 1220
1257-func (s *recordingFavoritesService) SetFolder(context.Context, int64, *int64) error { return nil }
1258-
1259-func (s *recordingFavoritesService) CreateFolder(_ context.Context, name, icon string, _ *int64) (*entity.Folder, error) {
1260- s.createdFolder = name
1261- s.createdFolderIcon = icon
1262- return &entity.Folder{ID: 77, Name: name, Icon: icon}, nil
1263-}
1264-
1265-func (s *recordingFavoritesService) UpdateFolder(_ context.Context, id int64, _, _ string) error {
1266- s.updatedFolderID = id
1267- return nil
1268-}
1269-
1270-func (s *recordingFavoritesService) DeleteFolder(_ context.Context, id int64) error {
1271- s.deletedFolderID = id
1272- return nil
1273-}
1274-
1275 func (s *recordingFavoritesService) CreateTag(_ context.Context, name, color string) (*entity.Tag, error) {1221 func (s *recordingFavoritesService) CreateTag(_ context.Context, name, color string) (*entity.Tag, error) {
1276 s.createdTag = name1222 s.createdTag = name
1277 return &entity.Tag{ID: 55, Name: name, Color: color}, nil1223 return &entity.Tag{ID: 55, Name: name, Color: color}, nil
M internal/ui/systemviews/favorite_actions.go +20 -95
@@ -12,20 +12,17 @@ import (
1212 )
1313
1414 const (
15- favoriteActionCreate = "favorite.create"
16- favoriteActionUpdate = "favorite.update"
17- favoriteActionDelete = "favorite.delete"
18- favoriteActionFilterFolder = "favorite.filterFolder"
19- favoriteActionFilterTag = "favorite.filterTag"
20- favoriteActionClearFilters = "favorite.clearFilters"
21- folderActionCreate = "folder.create"
22- folderActionUpdate = "folder.update"
23- folderActionDelete = "folder.delete"
24- tagActionCreate = "tag.create"
25- tagActionUpdate = "tag.update"
26- tagActionDelete = "tag.delete"
27- tagActionAssign = "tag.assign"
28- tagActionRemove = "tag.remove"
15+ favoriteActionCreate = "favorite.create"
16+ favoriteActionUpdate = "favorite.update"
17+ favoriteActionDelete = "favorite.delete"
18+ favoriteActionFilterTag = "favorite.filterTag"
19+ favoriteActionFilterUntagged = "favorite.filterUntagged"
20+ favoriteActionClearFilters = "favorite.clearFilters"
21+ tagActionCreate = "tag.create"
22+ tagActionUpdate = "tag.update"
23+ tagActionDelete = "tag.delete"
24+ tagActionAssign = "tag.assign"
25+ tagActionRemove = "tag.remove"
2926 )
3027
3128 //nolint:gocyclo,funlen // Mechanical dispatcher keeps action routing in one place.
@@ -36,10 +33,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
3633 data := event.Data
3734 switch event.Action {
3835 case favoriteActionCreate:
39- folderID, err := optionalFolderID(data["folder_id"])
40- if err != nil {
41- return err
42- }
4336 favoriteURL, err := validateFavoriteURL(data["url"])
4437 if err != nil {
4538 return err
@@ -49,10 +42,9 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
4942 return err
5043 }
5144 favorite, err := a.deps.Favorites.CreateFavorite(ctx, dto.FavoriteCreateInput{
52- URL: favoriteURL,
53- Title: strings.TrimSpace(data["title"]),
54- FolderID: folderID,
55- Tags: tags,
45+ URL: favoriteURL,
46+ Title: strings.TrimSpace(data["title"]),
47+ Tags: tags,
5648 })
5749 if err != nil {
5850 return err
@@ -63,10 +55,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
6355 if err != nil {
6456 return err
6557 }
66- folderID, err := optionalFolderID(data["folder_id"])
67- if err != nil {
68- return err
69- }
7058 shortcut, err := optionalShortcut(data["shortcut_key"])
7159 if err != nil {
7260 return err
@@ -75,7 +63,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
7563 ID: entity.FavoriteID(id),
7664 Title: strings.TrimSpace(data["title"]),
7765 FaviconURL: strings.TrimSpace(data["favicon_url"]),
78- FolderID: folderID,
7966 ShortcutKey: shortcut,
8067 })
8168 if err != nil {
@@ -91,14 +78,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
9178 return err
9279 }
9380 a.favoritesNotice = "Deleted favorite"
94- case favoriteActionFilterFolder:
95- folderID, err := filterFolderID(firstActionValue(data, "folderId", "folder_id"))
96- if err != nil {
97- return err
98- }
99- a.favoriteFolderFilter = folderID
100- a.favoriteTagFilter = nil
101- a.favoritesNotice = ""
10281 case favoriteActionFilterTag:
10382 tagID, err := parsePositiveInt64(firstActionValue(data, "tagId", "tag_id"), "tag id")
10483 if err != nil {
@@ -106,45 +85,16 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
10685 }
10786 id := entity.TagID(tagID)
10887 a.favoriteTagFilter = &id
109- a.favoriteFolderFilter = nil
88+ a.favoriteUntaggedFilter = false
89+ a.favoritesNotice = ""
90+ case favoriteActionFilterUntagged:
91+ a.favoriteTagFilter = nil
92+ a.favoriteUntaggedFilter = true
11093 a.favoritesNotice = ""
11194 case favoriteActionClearFilters:
112- a.favoriteFolderFilter = nil
11395 a.favoriteTagFilter = nil
96+ a.favoriteUntaggedFilter = false
11497 a.favoritesNotice = ""
115- case folderActionCreate:
116- name := strings.TrimSpace(data["name"])
117- if name == "" {
118- return fmt.Errorf("folder name is required")
119- }
120- folder, err := a.deps.Favorites.CreateFolder(ctx, name, strings.TrimSpace(data["icon"]), nil)
121- if err != nil {
122- return err
123- }
124- a.favoritesNotice = "Created folder " + folderDisplayName(folder)
125- case folderActionUpdate:
126- id, err := parsePositiveInt64(data["id"], "folder id")
127- if err != nil {
128- return err
129- }
130- name := strings.TrimSpace(data["name"])
131- if name == "" {
132- return fmt.Errorf("folder name is required")
133- }
134- if err := a.deps.Favorites.UpdateFolder(ctx, id, name, strings.TrimSpace(data["icon"])); err != nil {
135- return err
136- }
137- a.favoritesNotice = "Saved folder " + name
138- case folderActionDelete:
139- id, err := parsePositiveInt64(data["id"], "folder id")
140- if err != nil {
141- return err
142- }
143- if err := a.deps.Favorites.DeleteFolder(ctx, id); err != nil {
144- return err
145- }
146- a.favoriteFolderFilter = nil
147- a.favoritesNotice = "Deleted folder"
14898 case tagActionCreate:
14999 name := strings.TrimSpace(data["name"])
150100 if name == "" {
@@ -272,31 +222,6 @@ func optionalTagIDs(raw string) ([]entity.TagID, error) {
272222 return ids, nil
273223 }
274224
275-// optionalFolderID returns nil for empty or "root" input, meaning no folder constraint;
276-// otherwise it parses a positive folder id into a non-nil *entity.FolderID.
277-func optionalFolderID(raw string) (*entity.FolderID, error) {
278- raw = strings.TrimSpace(raw)
279- if raw == "" || raw == "root" {
280- return nil, nil
281- }
282- id, err := parsePositiveInt64(raw, "folder id")
283- if err != nil {
284- return nil, err
285- }
286- folderID := entity.FolderID(id)
287- return &folderID, nil
288-}
289-
290-// filterFolderID returns *entity.FolderID(0) for "root" to explicitly filter
291-// favorites in the root folder; other inputs delegate to optionalFolderID.
292-func filterFolderID(raw string) (*entity.FolderID, error) {
293- if strings.TrimSpace(raw) == "root" {
294- root := entity.FolderID(0)
295- return &root, nil
296- }
297- return optionalFolderID(raw)
298-}
299-
300225 // optionalShortcut accepts favorite keyboard shortcuts 1–9; empty input clears the shortcut.
301226 func optionalShortcut(raw string) (*int, error) {
302227 raw = strings.TrimSpace(raw)
@@ -12,20 +12,17 @@ import (
12 )12 )
13 13
14 const (14 const (
15- favoriteActionCreate = "favorite.create"15+ favoriteActionCreate = "favorite.create"
16- favoriteActionUpdate = "favorite.update"16+ favoriteActionUpdate = "favorite.update"
17- favoriteActionDelete = "favorite.delete"17+ favoriteActionDelete = "favorite.delete"
18- favoriteActionFilterFolder = "favorite.filterFolder"18+ favoriteActionFilterTag = "favorite.filterTag"
19- favoriteActionFilterTag = "favorite.filterTag"19+ favoriteActionFilterUntagged = "favorite.filterUntagged"
20- favoriteActionClearFilters = "favorite.clearFilters"20+ favoriteActionClearFilters = "favorite.clearFilters"
21- folderActionCreate = "folder.create"21+ tagActionCreate = "tag.create"
22- folderActionUpdate = "folder.update"22+ tagActionUpdate = "tag.update"
23- folderActionDelete = "folder.delete"23+ tagActionDelete = "tag.delete"
24- tagActionCreate = "tag.create"24+ tagActionAssign = "tag.assign"
25- tagActionUpdate = "tag.update"25+ tagActionRemove = "tag.remove"
26- tagActionDelete = "tag.delete"
27- tagActionAssign = "tag.assign"
28- tagActionRemove = "tag.remove"
29 )26 )
30 27
31 //nolint:gocyclo,funlen // Mechanical dispatcher keeps action routing in one place.28 //nolint:gocyclo,funlen // Mechanical dispatcher keeps action routing in one place.
@@ -36,10 +33,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
36 data := event.Data33 data := event.Data
37 switch event.Action {34 switch event.Action {
38 case favoriteActionCreate:35 case favoriteActionCreate:
39- folderID, err := optionalFolderID(data["folder_id"])
40- if err != nil {
41- return err
42- }
43 favoriteURL, err := validateFavoriteURL(data["url"])36 favoriteURL, err := validateFavoriteURL(data["url"])
44 if err != nil {37 if err != nil {
45 return err38 return err
@@ -49,10 +42,9 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
49 return err42 return err
50 }43 }
51 favorite, err := a.deps.Favorites.CreateFavorite(ctx, dto.FavoriteCreateInput{44 favorite, err := a.deps.Favorites.CreateFavorite(ctx, dto.FavoriteCreateInput{
52- URL: favoriteURL,45+ URL: favoriteURL,
53- Title: strings.TrimSpace(data["title"]),46+ Title: strings.TrimSpace(data["title"]),
54- FolderID: folderID,47+ Tags: tags,
55- Tags: tags,
56 })48 })
57 if err != nil {49 if err != nil {
58 return err50 return err
@@ -63,10 +55,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
63 if err != nil {55 if err != nil {
64 return err56 return err
65 }57 }
66- folderID, err := optionalFolderID(data["folder_id"])
67- if err != nil {
68- return err
69- }
70 shortcut, err := optionalShortcut(data["shortcut_key"])58 shortcut, err := optionalShortcut(data["shortcut_key"])
71 if err != nil {59 if err != nil {
72 return err60 return err
@@ -75,7 +63,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
75 ID: entity.FavoriteID(id),63 ID: entity.FavoriteID(id),
76 Title: strings.TrimSpace(data["title"]),64 Title: strings.TrimSpace(data["title"]),
77 FaviconURL: strings.TrimSpace(data["favicon_url"]),65 FaviconURL: strings.TrimSpace(data["favicon_url"]),
78- FolderID: folderID,
79 ShortcutKey: shortcut,66 ShortcutKey: shortcut,
80 })67 })
81 if err != nil {68 if err != nil {
@@ -91,14 +78,6 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
91 return err78 return err
92 }79 }
93 a.favoritesNotice = "Deleted favorite"80 a.favoritesNotice = "Deleted favorite"
94- case favoriteActionFilterFolder:
95- folderID, err := filterFolderID(firstActionValue(data, "folderId", "folder_id"))
96- if err != nil {
97- return err
98- }
99- a.favoriteFolderFilter = folderID
100- a.favoriteTagFilter = nil
101- a.favoritesNotice = ""
102 case favoriteActionFilterTag:81 case favoriteActionFilterTag:
103 tagID, err := parsePositiveInt64(firstActionValue(data, "tagId", "tag_id"), "tag id")82 tagID, err := parsePositiveInt64(firstActionValue(data, "tagId", "tag_id"), "tag id")
104 if err != nil {83 if err != nil {
@@ -106,45 +85,16 @@ func (a *App) handleFavoriteAction(ctx context.Context, event DOMAction) error {
106 }85 }
107 id := entity.TagID(tagID)86 id := entity.TagID(tagID)
108 a.favoriteTagFilter = &id87 a.favoriteTagFilter = &id
109- a.favoriteFolderFilter = nil88+ a.favoriteUntaggedFilter = false
89+ a.favoritesNotice = ""
90+ case favoriteActionFilterUntagged:
91+ a.favoriteTagFilter = nil
92+ a.favoriteUntaggedFilter = true
110 a.favoritesNotice = ""93 a.favoritesNotice = ""
111 case favoriteActionClearFilters:94 case favoriteActionClearFilters:
112- a.favoriteFolderFilter = nil
113 a.favoriteTagFilter = nil95 a.favoriteTagFilter = nil
96+ a.favoriteUntaggedFilter = false
114 a.favoritesNotice = ""97 a.favoritesNotice = ""
115- case folderActionCreate:
116- name := strings.TrimSpace(data["name"])
117- if name == "" {
118- return fmt.Errorf("folder name is required")
119- }
120- folder, err := a.deps.Favorites.CreateFolder(ctx, name, strings.TrimSpace(data["icon"]), nil)
121- if err != nil {
122- return err
123- }
124- a.favoritesNotice = "Created folder " + folderDisplayName(folder)
125- case folderActionUpdate:
126- id, err := parsePositiveInt64(data["id"], "folder id")
127- if err != nil {
128- return err
129- }
130- name := strings.TrimSpace(data["name"])
131- if name == "" {
132- return fmt.Errorf("folder name is required")
133- }
134- if err := a.deps.Favorites.UpdateFolder(ctx, id, name, strings.TrimSpace(data["icon"])); err != nil {
135- return err
136- }
137- a.favoritesNotice = "Saved folder " + name
138- case folderActionDelete:
139- id, err := parsePositiveInt64(data["id"], "folder id")
140- if err != nil {
141- return err
142- }
143- if err := a.deps.Favorites.DeleteFolder(ctx, id); err != nil {
144- return err
145- }
146- a.favoriteFolderFilter = nil
147- a.favoritesNotice = "Deleted folder"
148 case tagActionCreate:98 case tagActionCreate:
149 name := strings.TrimSpace(data["name"])99 name := strings.TrimSpace(data["name"])
150 if name == "" {100 if name == "" {
@@ -272,31 +222,6 @@ func optionalTagIDs(raw string) ([]entity.TagID, error) {
272 return ids, nil222 return ids, nil
273 }223 }
274 224
275-// optionalFolderID returns nil for empty or "root" input, meaning no folder constraint;
276-// otherwise it parses a positive folder id into a non-nil *entity.FolderID.
277-func optionalFolderID(raw string) (*entity.FolderID, error) {
278- raw = strings.TrimSpace(raw)
279- if raw == "" || raw == "root" {
280- return nil, nil
281- }
282- id, err := parsePositiveInt64(raw, "folder id")
283- if err != nil {
284- return nil, err
285- }
286- folderID := entity.FolderID(id)
287- return &folderID, nil
288-}
289-
290-// filterFolderID returns *entity.FolderID(0) for "root" to explicitly filter
291-// favorites in the root folder; other inputs delegate to optionalFolderID.
292-func filterFolderID(raw string) (*entity.FolderID, error) {
293- if strings.TrimSpace(raw) == "root" {
294- root := entity.FolderID(0)
295- return &root, nil
296- }
297- return optionalFolderID(raw)
298-}
299-
300 // optionalShortcut accepts favorite keyboard shortcuts 1–9; empty input clears the shortcut.225 // optionalShortcut accepts favorite keyboard shortcuts 1–9; empty input clears the shortcut.
301 func optionalShortcut(raw string) (*int, error) {226 func optionalShortcut(raw string) (*int, error) {
302 raw = strings.TrimSpace(raw)227 raw = strings.TrimSpace(raw)
M internal/ui/systemviews/favorites.templ +8 -64
@@ -19,22 +19,7 @@ templ FavoritesView(data favoritesRenderData) {
1919 <ul class="sv-list">
2020 for _, favorite := range data.Favorites {
2121 if favorite != nil {
22- @FavoriteItem(favorite, data.Folders, data.Tags)
23- }
24- }
25- </ul>
26- }
27- }
28- @Section("sv-folders-list", "Folders") {
29- @Meta(foldersSummary(data))
30- @FolderCreateForm()
31- if len(data.Folders) == 0 {
32- @EmptyState("No folders")
33- } else {
34- <ul class="sv-list">
35- for _, folder := range data.Folders {
36- if folder != nil {
37- @FolderItem(folder)
22+ @FavoriteItem(favorite, data.Tags)
3823 }
3924 }
4025 </ul>
@@ -61,23 +46,14 @@ templ FavoritesControls(data favoritesRenderData) {
6146 <form class="sv-card-form" data-sv-action="favorite.create">
6247 <label><span>URL</span><input name="url" type="url" placeholder="https://example.com" required/></label>
6348 <label><span>Title</span><input name="title" type="text" placeholder="Optional title"/></label>
64- @FolderSelect("folder_id", data.Folders, nil, "Folder")
6549 @FavoriteCreateTagPicker(data.Tags)
6650 <button class="sv-button" type="submit">@ButtonIconLabel(iconPlus, "Add favorite")</button>
6751 </form>
6852 <div class="sv-filter-panel">
69- <div class="sv-button-row">
70- <span class="sv-meta">Folders</span>
71- <button type="button" class={ favoriteFilterClass(favoriteAllFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteAllFilterActive(data)) } data-sv-action="favorite.clearFilters">All</button>
72- <button type="button" class={ favoriteFilterClass(favoriteFolderFilterActive(data, entity.FolderID(0))) } aria-pressed={ fmt.Sprintf("%t", favoriteFolderFilterActive(data, entity.FolderID(0))) } data-sv-action="favorite.filterFolder" data-folder-id="root">Unfiled</button>
73- for _, folder := range data.Folders {
74- if folder != nil {
75- <button type="button" class={ favoriteFilterClass(favoriteFolderFilterActive(data, folder.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteFolderFilterActive(data, folder.ID)) } data-sv-action="favorite.filterFolder" data-folder-id={ fmt.Sprintf("%d", folder.ID) }>{ folderDisplayName(folder) }</button>
76- }
77- }
78- </div>
7953 <div class="sv-button-row">
8054 <span class="sv-meta">Tags</span>
55+ <button type="button" class={ favoriteFilterClass(favoriteAllFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteAllFilterActive(data)) } data-sv-action="favorite.clearFilters">All</button>
56+ <button type="button" class={ favoriteFilterClass(favoriteUntaggedFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteUntaggedFilterActive(data)) } data-sv-action="favorite.filterUntagged">Untagged</button>
8157 for _, tag := range data.Tags {
8258 if tag != nil {
8359 <button type="button" class={ favoriteFilterClass(favoriteTagFilterActive(data, tag.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteTagFilterActive(data, tag.ID)) } data-sv-action="favorite.filterTag" data-tag-id={ fmt.Sprintf("%d", tag.ID) }>
@@ -108,15 +84,15 @@ templ FavoriteCreateTagPicker(tags []*entity.Tag) {
10884 }
10985 }
11086
111-templ FavoriteItem(favorite *entity.Favorite, folders []*entity.Folder, tags []*entity.Tag) {
87+templ FavoriteItem(favorite *entity.Favorite, tags []*entity.Tag) {
11288 <li class="sv-list-row">
11389 <article class="sv-favorite-item" data-sv-favorite-row data-favorite-id={ fmt.Sprintf("%d", favorite.ID) }>
11490 <div class="sv-favorite-header">
11591 <div>
11692 <a class="sv-link sv-favorite-open" href={ templ.SafeURL(sanitizeHref(favorite.URL)) }>{ favoriteItemLabel(favorite) }</a>
11793 <p class="sv-history-url">{ favorite.URL }</p>
118- if favoriteMetaText(favorite, folders) != "" {
119- <p class="sv-meta">{ favoriteMetaText(favorite, folders) }</p>
94+ if favoriteMetaText(favorite) != "" {
95+ <p class="sv-meta">{ favoriteMetaText(favorite) }</p>
12096 }
12197 </div>
12298 <div class="sv-history-row-actions">
@@ -124,18 +100,17 @@ templ FavoriteItem(favorite *entity.Favorite, folders []*entity.Folder, tags []*
124100 <button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="favorite.delete" data-id={ fmt.Sprintf("%d", favorite.ID) } data-sv-confirm="Delete this favorite?" aria-label="Delete this favorite">@IconOnly(iconTrash, "Delete this favorite")</button>
125101 </div>
126102 </div>
127- @FavoriteEditForm(favorite, folders)
103+ @FavoriteEditForm(favorite)
128104 @FavoriteTagControls(favorite, tags)
129105 </article>
130106 </li>
131107 }
132108
133-templ FavoriteEditForm(favorite *entity.Favorite, folders []*entity.Folder) {
109+templ FavoriteEditForm(favorite *entity.Favorite) {
134110 <form class="sv-inline-form" data-sv-action="favorite.update">
135111 <input type="hidden" name="id" value={ fmt.Sprintf("%d", favorite.ID) }/>
136112 <label><span>Title</span><input name="title" type="text" value={ favorite.Title }/></label>
137113 <label><span>Favicon</span><input name="favicon_url" type="url" value={ favorite.FaviconURL }/></label>
138- @FolderSelect("folder_id", folders, favorite.FolderID, "Folder")
139114 @ShortcutSelect(favorite.ShortcutKey)
140115 <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
141116 </form>
@@ -156,26 +131,6 @@ templ FavoriteTagControls(favorite *entity.Favorite, tags []*entity.Tag) {
156131 }
157132 }
158133
159-templ FolderCreateForm() {
160- <form class="sv-inline-form" data-sv-action="folder.create">
161- <label><span>Name</span><input name="name" type="text" required/></label>
162- <label><span>Icon</span><input name="icon" type="text" placeholder="Optional"/></label>
163- <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconFolder, "Create folder")</button>
164- </form>
165-}
166-
167-templ FolderItem(folder *entity.Folder) {
168- <li class="sv-list-row">
169- <form class="sv-inline-form" data-sv-action="folder.update">
170- <input type="hidden" name="id" value={ fmt.Sprintf("%d", folder.ID) }/>
171- <label><span>Name</span><input name="name" type="text" value={ folder.Name } required/></label>
172- <label><span>Icon</span><input name="icon" type="text" value={ folder.Icon }/></label>
173- <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
174- <button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="folder.delete" data-id={ fmt.Sprintf("%d", folder.ID) } data-sv-confirm="Delete this folder? Favorites will move out of it." aria-label="Delete this folder">@IconOnly(iconTrash, "Delete this folder")</button>
175- </form>
176- </li>
177-}
178-
179134 templ TagCreateForm() {
180135 <form class="sv-inline-form" data-sv-action="tag.create">
181136 <label><span>Name</span><input name="name" type="text" required/></label>
@@ -197,17 +152,6 @@ templ TagItem(tag *entity.Tag) {
197152 </li>
198153 }
199154
200-templ FolderSelect(name string, folders []*entity.Folder, selected *entity.FolderID, label string) {
201- <label><span>{ label }</span><select name={ name }>
202- <option value="">Unfiled</option>
203- for _, folder := range folders {
204- if folder != nil {
205- <option value={ fmt.Sprintf("%d", folder.ID) } selected?={ folderSelected(selected, folder.ID) }>{ folderDisplayName(folder) }</option>
206- }
207- }
208- </select></label>
209-}
210-
211155 templ ShortcutSelect(selected *int) {
212156 <label><span>Shortcut</span><select name="shortcut_key">
213157 <option value="">None</option>
@@ -19,22 +19,7 @@ templ FavoritesView(data favoritesRenderData) {
19 <ul class="sv-list">19 <ul class="sv-list">
20 for _, favorite := range data.Favorites {20 for _, favorite := range data.Favorites {
21 if favorite != nil {21 if favorite != nil {
22- @FavoriteItem(favorite, data.Folders, data.Tags)22+ @FavoriteItem(favorite, data.Tags)
23- }
24- }
25- </ul>
26- }
27- }
28- @Section("sv-folders-list", "Folders") {
29- @Meta(foldersSummary(data))
30- @FolderCreateForm()
31- if len(data.Folders) == 0 {
32- @EmptyState("No folders")
33- } else {
34- <ul class="sv-list">
35- for _, folder := range data.Folders {
36- if folder != nil {
37- @FolderItem(folder)
38 }23 }
39 }24 }
40 </ul>25 </ul>
@@ -61,23 +46,14 @@ templ FavoritesControls(data favoritesRenderData) {
61 <form class="sv-card-form" data-sv-action="favorite.create">46 <form class="sv-card-form" data-sv-action="favorite.create">
62 <label><span>URL</span><input name="url" type="url" placeholder="https://example.com" required/></label>47 <label><span>URL</span><input name="url" type="url" placeholder="https://example.com" required/></label>
63 <label><span>Title</span><input name="title" type="text" placeholder="Optional title"/></label>48 <label><span>Title</span><input name="title" type="text" placeholder="Optional title"/></label>
64- @FolderSelect("folder_id", data.Folders, nil, "Folder")
65 @FavoriteCreateTagPicker(data.Tags)49 @FavoriteCreateTagPicker(data.Tags)
66 <button class="sv-button" type="submit">@ButtonIconLabel(iconPlus, "Add favorite")</button>50 <button class="sv-button" type="submit">@ButtonIconLabel(iconPlus, "Add favorite")</button>
67 </form>51 </form>
68 <div class="sv-filter-panel">52 <div class="sv-filter-panel">
69- <div class="sv-button-row">
70- <span class="sv-meta">Folders</span>
71- <button type="button" class={ favoriteFilterClass(favoriteAllFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteAllFilterActive(data)) } data-sv-action="favorite.clearFilters">All</button>
72- <button type="button" class={ favoriteFilterClass(favoriteFolderFilterActive(data, entity.FolderID(0))) } aria-pressed={ fmt.Sprintf("%t", favoriteFolderFilterActive(data, entity.FolderID(0))) } data-sv-action="favorite.filterFolder" data-folder-id="root">Unfiled</button>
73- for _, folder := range data.Folders {
74- if folder != nil {
75- <button type="button" class={ favoriteFilterClass(favoriteFolderFilterActive(data, folder.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteFolderFilterActive(data, folder.ID)) } data-sv-action="favorite.filterFolder" data-folder-id={ fmt.Sprintf("%d", folder.ID) }>{ folderDisplayName(folder) }</button>
76- }
77- }
78- </div>
79 <div class="sv-button-row">53 <div class="sv-button-row">
80 <span class="sv-meta">Tags</span>54 <span class="sv-meta">Tags</span>
55+ <button type="button" class={ favoriteFilterClass(favoriteAllFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteAllFilterActive(data)) } data-sv-action="favorite.clearFilters">All</button>
56+ <button type="button" class={ favoriteFilterClass(favoriteUntaggedFilterActive(data)) } aria-pressed={ fmt.Sprintf("%t", favoriteUntaggedFilterActive(data)) } data-sv-action="favorite.filterUntagged">Untagged</button>
81 for _, tag := range data.Tags {57 for _, tag := range data.Tags {
82 if tag != nil {58 if tag != nil {
83 <button type="button" class={ favoriteFilterClass(favoriteTagFilterActive(data, tag.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteTagFilterActive(data, tag.ID)) } data-sv-action="favorite.filterTag" data-tag-id={ fmt.Sprintf("%d", tag.ID) }>59 <button type="button" class={ favoriteFilterClass(favoriteTagFilterActive(data, tag.ID)) } aria-pressed={ fmt.Sprintf("%t", favoriteTagFilterActive(data, tag.ID)) } data-sv-action="favorite.filterTag" data-tag-id={ fmt.Sprintf("%d", tag.ID) }>
@@ -108,15 +84,15 @@ templ FavoriteCreateTagPicker(tags []*entity.Tag) {
108 }84 }
109 }85 }
110 86
111-templ FavoriteItem(favorite *entity.Favorite, folders []*entity.Folder, tags []*entity.Tag) {87+templ FavoriteItem(favorite *entity.Favorite, tags []*entity.Tag) {
112 <li class="sv-list-row">88 <li class="sv-list-row">
113 <article class="sv-favorite-item" data-sv-favorite-row data-favorite-id={ fmt.Sprintf("%d", favorite.ID) }>89 <article class="sv-favorite-item" data-sv-favorite-row data-favorite-id={ fmt.Sprintf("%d", favorite.ID) }>
114 <div class="sv-favorite-header">90 <div class="sv-favorite-header">
115 <div>91 <div>
116 <a class="sv-link sv-favorite-open" href={ templ.SafeURL(sanitizeHref(favorite.URL)) }>{ favoriteItemLabel(favorite) }</a>92 <a class="sv-link sv-favorite-open" href={ templ.SafeURL(sanitizeHref(favorite.URL)) }>{ favoriteItemLabel(favorite) }</a>
117 <p class="sv-history-url">{ favorite.URL }</p>93 <p class="sv-history-url">{ favorite.URL }</p>
118- if favoriteMetaText(favorite, folders) != "" {94+ if favoriteMetaText(favorite) != "" {
119- <p class="sv-meta">{ favoriteMetaText(favorite, folders) }</p>95+ <p class="sv-meta">{ favoriteMetaText(favorite) }</p>
120 }96 }
121 </div>97 </div>
122 <div class="sv-history-row-actions">98 <div class="sv-history-row-actions">
@@ -124,18 +100,17 @@ templ FavoriteItem(favorite *entity.Favorite, folders []*entity.Folder, tags []*
124 <button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="favorite.delete" data-id={ fmt.Sprintf("%d", favorite.ID) } data-sv-confirm="Delete this favorite?" aria-label="Delete this favorite">@IconOnly(iconTrash, "Delete this favorite")</button>100 <button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="favorite.delete" data-id={ fmt.Sprintf("%d", favorite.ID) } data-sv-confirm="Delete this favorite?" aria-label="Delete this favorite">@IconOnly(iconTrash, "Delete this favorite")</button>
125 </div>101 </div>
126 </div>102 </div>
127- @FavoriteEditForm(favorite, folders)103+ @FavoriteEditForm(favorite)
128 @FavoriteTagControls(favorite, tags)104 @FavoriteTagControls(favorite, tags)
129 </article>105 </article>
130 </li>106 </li>
131 }107 }
132 108
133-templ FavoriteEditForm(favorite *entity.Favorite, folders []*entity.Folder) {109+templ FavoriteEditForm(favorite *entity.Favorite) {
134 <form class="sv-inline-form" data-sv-action="favorite.update">110 <form class="sv-inline-form" data-sv-action="favorite.update">
135 <input type="hidden" name="id" value={ fmt.Sprintf("%d", favorite.ID) }/>111 <input type="hidden" name="id" value={ fmt.Sprintf("%d", favorite.ID) }/>
136 <label><span>Title</span><input name="title" type="text" value={ favorite.Title }/></label>112 <label><span>Title</span><input name="title" type="text" value={ favorite.Title }/></label>
137 <label><span>Favicon</span><input name="favicon_url" type="url" value={ favorite.FaviconURL }/></label>113 <label><span>Favicon</span><input name="favicon_url" type="url" value={ favorite.FaviconURL }/></label>
138- @FolderSelect("folder_id", folders, favorite.FolderID, "Folder")
139 @ShortcutSelect(favorite.ShortcutKey)114 @ShortcutSelect(favorite.ShortcutKey)
140 <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>115 <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
141 </form>116 </form>
@@ -156,26 +131,6 @@ templ FavoriteTagControls(favorite *entity.Favorite, tags []*entity.Tag) {
156 }131 }
157 }132 }
158 133
159-templ FolderCreateForm() {
160- <form class="sv-inline-form" data-sv-action="folder.create">
161- <label><span>Name</span><input name="name" type="text" required/></label>
162- <label><span>Icon</span><input name="icon" type="text" placeholder="Optional"/></label>
163- <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconFolder, "Create folder")</button>
164- </form>
165-}
166-
167-templ FolderItem(folder *entity.Folder) {
168- <li class="sv-list-row">
169- <form class="sv-inline-form" data-sv-action="folder.update">
170- <input type="hidden" name="id" value={ fmt.Sprintf("%d", folder.ID) }/>
171- <label><span>Name</span><input name="name" type="text" value={ folder.Name } required/></label>
172- <label><span>Icon</span><input name="icon" type="text" value={ folder.Icon }/></label>
173- <button class="sv-button sv-button-secondary" type="submit">@ButtonIconLabel(iconSave, "Save")</button>
174- <button type="button" class="sv-button sv-button-secondary sv-button-danger sv-button-icon-only" data-sv-action="folder.delete" data-id={ fmt.Sprintf("%d", folder.ID) } data-sv-confirm="Delete this folder? Favorites will move out of it." aria-label="Delete this folder">@IconOnly(iconTrash, "Delete this folder")</button>
175- </form>
176- </li>
177-}
178-
179 templ TagCreateForm() {134 templ TagCreateForm() {
180 <form class="sv-inline-form" data-sv-action="tag.create">135 <form class="sv-inline-form" data-sv-action="tag.create">
181 <label><span>Name</span><input name="name" type="text" required/></label>136 <label><span>Name</span><input name="name" type="text" required/></label>
@@ -197,17 +152,6 @@ templ TagItem(tag *entity.Tag) {
197 </li>152 </li>
198 }153 }
199 154
200-templ FolderSelect(name string, folders []*entity.Folder, selected *entity.FolderID, label string) {
201- <label><span>{ label }</span><select name={ name }>
202- <option value="">Unfiled</option>
203- for _, folder := range folders {
204- if folder != nil {
205- <option value={ fmt.Sprintf("%d", folder.ID) } selected?={ folderSelected(selected, folder.ID) }>{ folderDisplayName(folder) }</option>
206- }
207- }
208- </select></label>
209-}
210-
211 templ ShortcutSelect(selected *int) {155 templ ShortcutSelect(selected *int) {
212 <label><span>Shortcut</span><select name="shortcut_key">156 <label><span>Shortcut</span><select name="shortcut_key">
213 <option value="">None</option>157 <option value="">None</option>
M internal/ui/systemviews/favorites_templ.go +245 -616 generated file: collapsed
M internal/ui/systemviews/favorites_view.go +19 -62
@@ -10,13 +10,12 @@ import (
1010 const defaultTagColor = "#808080"
1111
1212 type favoritesRenderData struct {
13- Favorites []*entity.Favorite
14- Folders []*entity.Folder
15- Tags []*entity.Tag
16- FolderFilter *entity.FolderID
17- TagFilter *entity.TagID
18- Notice string
19- Error string
13+ Favorites []*entity.Favorite
14+ Tags []*entity.Tag
15+ TagFilter *entity.TagID
16+ UntaggedFilter bool
17+ Notice string
18+ Error string
2019 }
2120
2221 func favoritesHTML(data favoritesRenderData) string {
@@ -25,7 +24,7 @@ func favoritesHTML(data favoritesRenderData) string {
2524
2625 func favoritesDocumentTitle(data favoritesRenderData) string {
2726 count := len(data.Favorites)
28- if data.FolderFilter != nil || data.TagFilter != nil {
27+ if data.TagFilter != nil || data.UntaggedFilter {
2928 if count == 1 {
3029 return "Favorites — 1 filtered bookmark"
3130 }
@@ -38,11 +37,7 @@ func favoritesDocumentTitle(data favoritesRenderData) string {
3837 }
3938
4039 func favoritesSummary(data favoritesRenderData) string {
41- return fmt.Sprintf("%d favorites · %d folders · %d tags", len(data.Favorites), len(data.Folders), len(data.Tags))
42-}
43-
44-func foldersSummary(data favoritesRenderData) string {
45- return fmt.Sprintf("%d folders", len(data.Folders))
40+ return fmt.Sprintf("%d favorites · %d tags", len(data.Favorites), len(data.Tags))
4641 }
4742
4843 func tagsSummary(data favoritesRenderData) string {
@@ -57,12 +52,12 @@ func favoriteFilterClass(active bool) string {
5752 return classes
5853 }
5954
60-func favoriteFolderFilterActive(data favoritesRenderData, folderID entity.FolderID) bool {
61- return data.FolderFilter != nil && *data.FolderFilter == folderID
55+func favoriteUntaggedFilterActive(data favoritesRenderData) bool {
56+ return data.UntaggedFilter
6257 }
6358
6459 func favoriteAllFilterActive(data favoritesRenderData) bool {
65- return data.FolderFilter == nil && data.TagFilter == nil
60+ return data.TagFilter == nil && !data.UntaggedFilter
6661 }
6762
6863 func favoriteTagFilterActive(data favoritesRenderData, tagID entity.TagID) bool {
@@ -80,18 +75,11 @@ func favoriteItemLabel(favorite *entity.Favorite) string {
8075 return label
8176 }
8277
83-func favoriteMetaText(favorite *entity.Favorite, folders []*entity.Folder) string {
84- if favorite == nil {
78+func favoriteMetaText(favorite *entity.Favorite) string {
79+ if favorite == nil || favorite.ShortcutKey == nil {
8580 return ""
8681 }
87- parts := make([]string, 0, 2)
88- if favorite.ShortcutKey != nil {
89- parts = append(parts, fmt.Sprintf("Shortcut %d", *favorite.ShortcutKey))
90- }
91- if name := folderNameForID(folders, favorite.FolderID); name != "" {
92- parts = append(parts, "Folder "+name)
93- }
94- return strings.Join(parts, " · ")
82+ return fmt.Sprintf("Shortcut %d", *favorite.ShortcutKey)
9583 }
9684
9785 func favoriteTagButtonAction(favorite *entity.Favorite, tag *entity.Tag) string {
@@ -136,38 +124,12 @@ func safeTagColor(raw string) string {
136124 return value
137125 }
138126
139-func folderDisplayName(folder *entity.Folder) string {
140- if folder == nil {
141- return ""
142- }
143- if strings.TrimSpace(folder.Icon) != "" {
144- return folder.Icon + " " + folder.Name
145- }
146- return folder.Name
147-}
148-
149-func folderNameForID(folders []*entity.Folder, id *entity.FolderID) string {
150- if id == nil {
151- return "Unfiled"
152- }
153- for _, folder := range folders {
154- if folder != nil && folder.ID == *id {
155- return folderDisplayName(folder)
156- }
157- }
158- return "Unknown"
159-}
160-
161-func folderSelected(selected *entity.FolderID, folderID entity.FolderID) bool {
162- return selected != nil && *selected == folderID
163-}
164-
165127 func shortcutSelected(selected *int, value int) bool {
166128 return selected != nil && *selected == value
167129 }
168130
169-func filterFavorites(favorites []*entity.Favorite, folderID *entity.FolderID, tagID *entity.TagID) []*entity.Favorite {
170- if folderID == nil && tagID == nil {
131+func filterFavorites(favorites []*entity.Favorite, tagID *entity.TagID, untagged bool) []*entity.Favorite {
132+ if tagID == nil && !untagged {
171133 return favorites
172134 }
173135 out := make([]*entity.Favorite, 0, len(favorites))
@@ -175,16 +137,11 @@ func filterFavorites(favorites []*entity.Favorite, folderID *entity.FolderID, ta
175137 if favorite == nil {
176138 continue
177139 }
178- if folderID != nil {
179- if *folderID == 0 {
180- if favorite.FolderID != nil {
181- continue
182- }
183- } else if favorite.FolderID == nil || *favorite.FolderID != *folderID {
140+ if untagged {
141+ if len(favorite.Tags) != 0 {
184142 continue
185143 }
186- }
187- if tagID != nil && !favorite.HasTag(*tagID) {
144+ } else if tagID != nil && !favorite.HasTag(*tagID) {
188145 continue
189146 }
190147 out = append(out, favorite)
@@ -10,13 +10,12 @@ import (
10 const defaultTagColor = "#808080"10 const defaultTagColor = "#808080"
11 11
12 type favoritesRenderData struct {12 type favoritesRenderData struct {
13- Favorites []*entity.Favorite13+ Favorites []*entity.Favorite
14- Folders []*entity.Folder14+ Tags []*entity.Tag
15- Tags []*entity.Tag15+ TagFilter *entity.TagID
16- FolderFilter *entity.FolderID16+ UntaggedFilter bool
17- TagFilter *entity.TagID17+ Notice string
18- Notice string18+ Error string
19- Error string
20 }19 }
21 20
22 func favoritesHTML(data favoritesRenderData) string {21 func favoritesHTML(data favoritesRenderData) string {
@@ -25,7 +24,7 @@ func favoritesHTML(data favoritesRenderData) string {
25 24
26 func favoritesDocumentTitle(data favoritesRenderData) string {25 func favoritesDocumentTitle(data favoritesRenderData) string {
27 count := len(data.Favorites)26 count := len(data.Favorites)
28- if data.FolderFilter != nil || data.TagFilter != nil {27+ if data.TagFilter != nil || data.UntaggedFilter {
29 if count == 1 {28 if count == 1 {
30 return "Favorites — 1 filtered bookmark"29 return "Favorites — 1 filtered bookmark"
31 }30 }
@@ -38,11 +37,7 @@ func favoritesDocumentTitle(data favoritesRenderData) string {
38 }37 }
39 38
40 func favoritesSummary(data favoritesRenderData) string {39 func favoritesSummary(data favoritesRenderData) string {
41- return fmt.Sprintf("%d favorites · %d folders · %d tags", len(data.Favorites), len(data.Folders), len(data.Tags))40+ return fmt.Sprintf("%d favorites · %d tags", len(data.Favorites), len(data.Tags))
42-}
43-
44-func foldersSummary(data favoritesRenderData) string {
45- return fmt.Sprintf("%d folders", len(data.Folders))
46 }41 }
47 42
48 func tagsSummary(data favoritesRenderData) string {43 func tagsSummary(data favoritesRenderData) string {
@@ -57,12 +52,12 @@ func favoriteFilterClass(active bool) string {
57 return classes52 return classes
58 }53 }
59 54
60-func favoriteFolderFilterActive(data favoritesRenderData, folderID entity.FolderID) bool {55+func favoriteUntaggedFilterActive(data favoritesRenderData) bool {
61- return data.FolderFilter != nil && *data.FolderFilter == folderID56+ return data.UntaggedFilter
62 }57 }
63 58
64 func favoriteAllFilterActive(data favoritesRenderData) bool {59 func favoriteAllFilterActive(data favoritesRenderData) bool {
65- return data.FolderFilter == nil && data.TagFilter == nil60+ return data.TagFilter == nil && !data.UntaggedFilter
66 }61 }
67 62
68 func favoriteTagFilterActive(data favoritesRenderData, tagID entity.TagID) bool {63 func favoriteTagFilterActive(data favoritesRenderData, tagID entity.TagID) bool {
@@ -80,18 +75,11 @@ func favoriteItemLabel(favorite *entity.Favorite) string {
80 return label75 return label
81 }76 }
82 77
83-func favoriteMetaText(favorite *entity.Favorite, folders []*entity.Folder) string {78+func favoriteMetaText(favorite *entity.Favorite) string {
84- if favorite == nil {79+ if favorite == nil || favorite.ShortcutKey == nil {
85 return ""80 return ""
86 }81 }
87- parts := make([]string, 0, 2)82+ return fmt.Sprintf("Shortcut %d", *favorite.ShortcutKey)
88- if favorite.ShortcutKey != nil {
89- parts = append(parts, fmt.Sprintf("Shortcut %d", *favorite.ShortcutKey))
90- }
91- if name := folderNameForID(folders, favorite.FolderID); name != "" {
92- parts = append(parts, "Folder "+name)
93- }
94- return strings.Join(parts, " · ")
95 }83 }
96 84
97 func favoriteTagButtonAction(favorite *entity.Favorite, tag *entity.Tag) string {85 func favoriteTagButtonAction(favorite *entity.Favorite, tag *entity.Tag) string {
@@ -136,38 +124,12 @@ func safeTagColor(raw string) string {
136 return value124 return value
137 }125 }
138 126
139-func folderDisplayName(folder *entity.Folder) string {
140- if folder == nil {
141- return ""
142- }
143- if strings.TrimSpace(folder.Icon) != "" {
144- return folder.Icon + " " + folder.Name
145- }
146- return folder.Name
147-}
148-
149-func folderNameForID(folders []*entity.Folder, id *entity.FolderID) string {
150- if id == nil {
151- return "Unfiled"
152- }
153- for _, folder := range folders {
154- if folder != nil && folder.ID == *id {
155- return folderDisplayName(folder)
156- }
157- }
158- return "Unknown"
159-}
160-
161-func folderSelected(selected *entity.FolderID, folderID entity.FolderID) bool {
162- return selected != nil && *selected == folderID
163-}
164-
165 func shortcutSelected(selected *int, value int) bool {127 func shortcutSelected(selected *int, value int) bool {
166 return selected != nil && *selected == value128 return selected != nil && *selected == value
167 }129 }
168 130
169-func filterFavorites(favorites []*entity.Favorite, folderID *entity.FolderID, tagID *entity.TagID) []*entity.Favorite {131+func filterFavorites(favorites []*entity.Favorite, tagID *entity.TagID, untagged bool) []*entity.Favorite {
170- if folderID == nil && tagID == nil {132+ if tagID == nil && !untagged {
171 return favorites133 return favorites
172 }134 }
173 out := make([]*entity.Favorite, 0, len(favorites))135 out := make([]*entity.Favorite, 0, len(favorites))
@@ -175,16 +137,11 @@ func filterFavorites(favorites []*entity.Favorite, folderID *entity.FolderID, ta
175 if favorite == nil {137 if favorite == nil {
176 continue138 continue
177 }139 }
178- if folderID != nil {140+ if untagged {
179- if *folderID == 0 {141+ if len(favorite.Tags) != 0 {
180- if favorite.FolderID != nil {
181- continue
182- }
183- } else if favorite.FolderID == nil || *favorite.FolderID != *folderID {
184 continue142 continue
185 }143 }
186- }144+ } else if tagID != nil && !favorite.HasTag(*tagID) {
187- if tagID != nil && !favorite.HasTag(*tagID) {
188 continue145 continue
189 }146 }
190 out = append(out, favorite)147 out = append(out, favorite)

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