feat(config): add search shortcuts to webui config with validation and persistence
- commit
- 42bc6731386a9e86345f9d280d5b6e48f251b4fb
- author
- bnema · · 2025-12-21 13:53 UTC
- parent
- a258d03
- signature
- unsigned
3 files · +42 -0
M internal/application/port/webui_config.go +6 -0
| @@ -7,6 +7,7 @@ type WebUIConfig struct { | |||
| 7 | 7 | Appearance WebUIAppearanceConfig `json:"appearance"` | |
| 8 | 8 | DefaultUIScale float64 `json:"default_ui_scale"` | |
| 9 | 9 | DefaultSearchEngine string `json:"default_search_engine"` | |
| 10 | + | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |
| 10 | 11 | } | |
| 11 | 12 | ||
| 12 | 13 | type WebUIAppearanceConfig struct { | |
| @@ -29,6 +30,11 @@ type ColorPalette struct { | |||
| 29 | 30 | Border string `json:"border"` | |
| 30 | 31 | } | |
| 31 | 32 | ||
| 33 | + | type SearchShortcut struct { | |
| 34 | + | URL string `json:"url"` | |
| 35 | + | Description string `json:"description"` | |
| 36 | + | } | |
| 37 | + | ||
| 32 | 38 | // WebUIConfigSaver persists WebUI configuration changes. | |
| 33 | 39 | type WebUIConfigSaver interface { | |
| 34 | 40 | SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error | |
| @@ -7,6 +7,7 @@ type WebUIConfig struct { | |||||
| 7 | Appearance WebUIAppearanceConfig `json:"appearance"` | 7 | Appearance WebUIAppearanceConfig `json:"appearance"` | ||
| 8 | DefaultUIScale float64 `json:"default_ui_scale"` | 8 | DefaultUIScale float64 `json:"default_ui_scale"` | ||
| 9 | DefaultSearchEngine string `json:"default_search_engine"` | 9 | DefaultSearchEngine string `json:"default_search_engine"` | ||
| 10 | + | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |||
| 10 | } | 11 | } | ||
| 11 | 12 | ||||
| 12 | type WebUIAppearanceConfig struct { | 13 | type WebUIAppearanceConfig struct { | ||
| @@ -29,6 +30,11 @@ type ColorPalette struct { | |||||
| 29 | Border string `json:"border"` | 30 | Border string `json:"border"` | ||
| 30 | } | 31 | } | ||
| 31 | 32 | ||||
| 33 | + | type SearchShortcut struct { | |||
| 34 | + | URL string `json:"url"` | |||
| 35 | + | Description string `json:"description"` | |||
| 36 | + | } | |||
| 37 | + | ||||
| 32 | // WebUIConfigSaver persists WebUI configuration changes. | 38 | // WebUIConfigSaver persists WebUI configuration changes. | ||
| 33 | type WebUIConfigSaver interface { | 39 | type WebUIConfigSaver interface { | ||
| 34 | SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error | 40 | SaveWebUIConfig(ctx context.Context, cfg WebUIConfig) error | ||
M internal/application/usecase/save_webui_config.go +24 -0
| @@ -36,6 +36,18 @@ func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig { | |||
| 36 | 36 | cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont) | |
| 37 | 37 | cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme) | |
| 38 | 38 | cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine) | |
| 39 | + | if len(cfg.SearchShortcuts) > 0 { | |
| 40 | + | normalized := make(map[string]port.SearchShortcut, len(cfg.SearchShortcuts)) | |
| 41 | + | for key, shortcut := range cfg.SearchShortcuts { | |
| 42 | + | trimmedKey := strings.TrimSpace(key) | |
| 43 | + | shortcut.URL = strings.TrimSpace(shortcut.URL) | |
| 44 | + | shortcut.Description = strings.TrimSpace(shortcut.Description) | |
| 45 | + | if trimmedKey != "" { | |
| 46 | + | normalized[trimmedKey] = shortcut | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | cfg.SearchShortcuts = normalized | |
| 50 | + | } | |
| 39 | 51 | return cfg | |
| 40 | 52 | } | |
| 41 | 53 | ||
| @@ -59,6 +71,18 @@ func validateWebUIConfig(cfg port.WebUIConfig) error { | |||
| 59 | 71 | errs = append(errs, "default_search_engine must contain %s placeholder for the search query") | |
| 60 | 72 | } | |
| 61 | 73 | ||
| 74 | + | for key, shortcut := range cfg.SearchShortcuts { | |
| 75 | + | for _, err := range validation.ValidateShortcutKey(key) { | |
| 76 | + | errs = append(errs, fmt.Sprintf("search_shortcuts.%s: %s", key, err)) | |
| 77 | + | } | |
| 78 | + | for _, err := range validation.ValidateShortcutURL(shortcut.URL) { | |
| 79 | + | errs = append(errs, fmt.Sprintf("search_shortcuts.%s.url: %s", key, err)) | |
| 80 | + | } | |
| 81 | + | for _, err := range validation.ValidateShortcutDescription(shortcut.Description) { | |
| 82 | + | errs = append(errs, fmt.Sprintf("search_shortcuts.%s.description: %s", key, err)) | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | ||
| 62 | 86 | switch cfg.Appearance.ColorScheme { | |
| 63 | 87 | case "default", "prefer-dark", "prefer-light": | |
| 64 | 88 | // ok | |
| @@ -36,6 +36,18 @@ func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig { | |||||
| 36 | cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont) | 36 | cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont) | ||
| 37 | cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme) | 37 | cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme) | ||
| 38 | cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine) | 38 | cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine) | ||
| 39 | + | if len(cfg.SearchShortcuts) > 0 { | |||
| 40 | + | normalized := make(map[string]port.SearchShortcut, len(cfg.SearchShortcuts)) | |||
| 41 | + | for key, shortcut := range cfg.SearchShortcuts { | |||
| 42 | + | trimmedKey := strings.TrimSpace(key) | |||
| 43 | + | shortcut.URL = strings.TrimSpace(shortcut.URL) | |||
| 44 | + | shortcut.Description = strings.TrimSpace(shortcut.Description) | |||
| 45 | + | if trimmedKey != "" { | |||
| 46 | + | normalized[trimmedKey] = shortcut | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | cfg.SearchShortcuts = normalized | |||
| 50 | + | } | |||
| 39 | return cfg | 51 | return cfg | ||
| 40 | } | 52 | } | ||
| 41 | 53 | ||||
| @@ -59,6 +71,18 @@ func validateWebUIConfig(cfg port.WebUIConfig) error { | |||||
| 59 | errs = append(errs, "default_search_engine must contain %s placeholder for the search query") | 71 | errs = append(errs, "default_search_engine must contain %s placeholder for the search query") | ||
| 60 | } | 72 | } | ||
| 61 | 73 | ||||
| 74 | + | for key, shortcut := range cfg.SearchShortcuts { | |||
| 75 | + | for _, err := range validation.ValidateShortcutKey(key) { | |||
| 76 | + | errs = append(errs, fmt.Sprintf("search_shortcuts.%s: %s", key, err)) | |||
| 77 | + | } | |||
| 78 | + | for _, err := range validation.ValidateShortcutURL(shortcut.URL) { | |||
| 79 | + | errs = append(errs, fmt.Sprintf("search_shortcuts.%s.url: %s", key, err)) | |||
| 80 | + | } | |||
| 81 | + | for _, err := range validation.ValidateShortcutDescription(shortcut.Description) { | |||
| 82 | + | errs = append(errs, fmt.Sprintf("search_shortcuts.%s.description: %s", key, err)) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | ||||
| 62 | switch cfg.Appearance.ColorScheme { | 86 | switch cfg.Appearance.ColorScheme { | ||
| 63 | case "default", "prefer-dark", "prefer-light": | 87 | case "default", "prefer-dark", "prefer-light": | ||
| 64 | // ok | 88 | // ok | ||
M internal/infrastructure/config/webui_gateway.go +12 -0
| @@ -47,6 +47,18 @@ func (g *WebUIConfigGateway) SaveWebUIConfig(ctx context.Context, cfg port.WebUI | |||
| 47 | 47 | } | |
| 48 | 48 | current.DefaultUIScale = cfg.DefaultUIScale | |
| 49 | 49 | current.DefaultSearchEngine = cfg.DefaultSearchEngine | |
| 50 | + | if len(cfg.SearchShortcuts) == 0 { | |
| 51 | + | current.SearchShortcuts = nil | |
| 52 | + | } else { | |
| 53 | + | shortcuts := make(map[string]SearchShortcut, len(cfg.SearchShortcuts)) | |
| 54 | + | for key, shortcut := range cfg.SearchShortcuts { | |
| 55 | + | shortcuts[key] = SearchShortcut{ | |
| 56 | + | URL: shortcut.URL, | |
| 57 | + | Description: shortcut.Description, | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | current.SearchShortcuts = shortcuts | |
| 61 | + | } | |
| 50 | 62 | ||
| 51 | 63 | return g.mgr.Save(current) | |
| 52 | 64 | } | |
| @@ -47,6 +47,18 @@ func (g *WebUIConfigGateway) SaveWebUIConfig(ctx context.Context, cfg port.WebUI | |||||
| 47 | } | 47 | } | ||
| 48 | current.DefaultUIScale = cfg.DefaultUIScale | 48 | current.DefaultUIScale = cfg.DefaultUIScale | ||
| 49 | current.DefaultSearchEngine = cfg.DefaultSearchEngine | 49 | current.DefaultSearchEngine = cfg.DefaultSearchEngine | ||
| 50 | + | if len(cfg.SearchShortcuts) == 0 { | |||
| 51 | + | current.SearchShortcuts = nil | |||
| 52 | + | } else { | |||
| 53 | + | shortcuts := make(map[string]SearchShortcut, len(cfg.SearchShortcuts)) | |||
| 54 | + | for key, shortcut := range cfg.SearchShortcuts { | |||
| 55 | + | shortcuts[key] = SearchShortcut{ | |||
| 56 | + | URL: shortcut.URL, | |||
| 57 | + | Description: shortcut.Description, | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | current.SearchShortcuts = shortcuts | |||
| 61 | + | } | |||
| 50 | 62 | ||||
| 51 | return g.mgr.Save(current) | 63 | return g.mgr.Save(current) | ||
| 52 | } | 64 | } | ||