diff --git a/internal/httpapi/auth_handlers.go b/internal/httpapi/auth_handlers.go index 7443558..57d6508 100644 --- a/internal/httpapi/auth_handlers.go +++ b/internal/httpapi/auth_handlers.go @@ -27,17 +27,62 @@ type userJSON struct { Locale string `json:"locale"` IsAdmin bool `json:"is_admin"` TOTPEnabled bool `json:"totp_enabled"` - CreatedAt string `json:"created_at"` + // SidebarCollapsed is which sidebar headings this person has folded away. On + // the account rather than in the browser, because folding one is a statement + // about the work and not about the screen — the sidebar's width is the + // opposite case and stays in localStorage. + SidebarCollapsed []string `json:"sidebar_collapsed"` + CreatedAt string `json:"created_at"` } func toUserJSON(u *store.User) userJSON { + collapsed := u.SidebarCollapsed + // Never null in the response: a client that has to check for both an absent + // array and an empty one will one day check for only one of them. + if collapsed == nil { + collapsed = []string{} + } return userJSON{ ID: u.ID, Email: u.Email, Name: u.Name, AvatarColor: u.AvatarColor, Timezone: u.Timezone, Locale: u.Locale, IsAdmin: u.IsAdmin, - TOTPEnabled: u.TOTPEnabled, CreatedAt: u.CreatedAt.Format(time.RFC3339), + TOTPEnabled: u.TOTPEnabled, SidebarCollapsed: collapsed, + CreatedAt: u.CreatedAt.Format(time.RFC3339), } } +// handleSetSidebarSections records which sidebar headings are folded away. +// +// Its own route rather than a field on the profile: that one backs a form with a +// save button and validates three fields together, and this writes on every click +// of a chevron. Sharing the path would mean a fold sending a name and a timezone +// it was never asked to change — and a validation error on one of those failing a +// fold. +// +// The keys are the sidebar's and are not checked here. An unknown one costs +// nothing: the sidebar folds the headings it recognises and ignores the rest, so +// a heading that is renamed or removed leaves a harmless entry rather than a +// migration. +func (s *Server) handleSetSidebarSections(w http.ResponseWriter, r *http.Request) { + var req struct { + Sections []string `json:"sections"` + } + if err := decodeJSON(w, r, &req); err != nil { + return + } + // A cap, because this is a list the client decides the length of. Twenty is far + // more headings than the sidebar has ever had. + if len(req.Sections) > 20 { + writeError(w, http.StatusBadRequest, CodeBadRequest, "too many sections") + return + } + user := userFrom(r.Context()) + if err := s.db.SetSidebarCollapsed(r.Context(), user.ID, req.Sections); err != nil { + s.storeError(w, r, "set sidebar sections", err) + return + } + w.WriteHeader(http.StatusNoContent) +} + // --- sign in ----------------------------------------------------------------- type loginRequest struct { diff --git a/internal/store/migrations/0010_sidebar_sections.sql b/internal/store/migrations/0010_sidebar_sections.sql new file mode 100644 index 0000000..1120340 --- /dev/null +++ b/internal/store/migrations/0010_sidebar_sections.sql @@ -0,0 +1,16 @@ +-- Which of the sidebar's fixed headings a person has folded away. +-- +-- A project group already carries `collapsed` on its own row, and the reasoning +-- there applies here too: folding a heading is a statement about the work, not +-- about the screen. "I am not in Etiketter at the moment" is as true on the laptop +-- as on the desktop, which is what separates this from the sidebar's *width* — +-- that one is in localStorage, because a narrow laptop and a wide monitor deserve +-- different answers and syncing it would make one of them wrong every time you +-- moved. +-- +-- A JSON array of keys rather than a column per heading. The set is the sidebar's +-- to decide and it has already changed twice; a column each would mean a migration +-- every time a heading is added, and a row full of booleans nobody reads for the +-- ones that were removed. Unknown keys are ignored on the way out, so an old +-- client's folded state survives a heading being renamed rather than resetting it. +ALTER TABLE users ADD COLUMN sidebar_collapsed TEXT NOT NULL DEFAULT '[]'; diff --git a/web/src/lib/components/Sidebar.svelte b/web/src/lib/components/Sidebar.svelte index 7f772f4..0a14784 100644 --- a/web/src/lib/components/Sidebar.svelte +++ b/web/src/lib/components/Sidebar.svelte @@ -3,6 +3,7 @@ import { api } from '$lib/api.js'; import { TASK, PROJECT, GROUP, startDrag, carries, dragged, accept } from '$lib/dnd.js'; import { COLORS, colorVar } from '$lib/colors.js'; + import { focusOnMount } from '$lib/focus.js'; import { page } from '$app/stores'; import { goto } from '$app/navigation'; @@ -85,8 +86,6 @@ let newGroupName = $state(''); let renamingGroup = $state(null); let groupName = $state(''); - /** The group whose swatches are open, if any. */ - let coloringGroup = $state(null); async function createGroup(event) { event.preventDefault(); @@ -411,6 +410,36 @@ {/snippet} + + {#snippet foldHeading(key, label)} +
Ingen projekter endnu.
+ {#if own.length === 0 && !adding} +Ingen projekter endnu.
+ {/if} {/if}