From 5a1b75d1cb5c54b10f42d7fe4a085fe123b57a02 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 30 Sep 2026 22:13:21 -0700 Subject: [PATCH] fix: preserve sidebar channel order in JSON exports --- CHANGELOG.md | 1 + apps/api/internal/store/postgres/export.go | 1 + apps/api/internal/store/sqlite/export.go | 1 + apps/api/internal/store/storetest/sidebar.go | 36 +++++++++++++++++++- docs/features/profiles.md | 3 ++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 329e74574..e3ca5fbc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ **Highlights:** Personal channel order follows your account across browsers and devices. - Personal sidebar channel order now follows your account across browsers and devices, with the existing local cache retained for offline use. Thanks @isaiahknight-va. +- Preserve saved sidebar channel order, including explicit clears, in SQLite and PostgreSQL JSON exports. - Signed and notarized macOS server archives with the Foundation Developer ID, alongside the desktop installers, and verify their notarization tickets on a clean runner before publication. - Allow macOS release automation to select its credential keychain explicitly on headless hosts. - Updated SQLite, virtual scrolling, Wrangler, and Oxc tooling while preserving runtime minimums and the 48-hour dependency release-age gate. diff --git a/apps/api/internal/store/postgres/export.go b/apps/api/internal/store/postgres/export.go index 7bd5b5347..8fecff034 100644 --- a/apps/api/internal/store/postgres/export.go +++ b/apps/api/internal/store/postgres/export.go @@ -22,6 +22,7 @@ func (s *Store) ExportJSON(ctx context.Context, writer io.Writer) error { out := map[string]any{} tables := []string{ "users", "user_notification_settings", "channel_notification_settings", "identities", "workspaces", "workspace_members", "channels", + "user_sidebar_channel_order", "messages", "pinned_messages", "thread_state", "reactions", "events", "event_recipients", "uploads", "channel_reads", "direct_reads", "message_attachments", "direct_conversations", "direct_conversation_members", diff --git a/apps/api/internal/store/sqlite/export.go b/apps/api/internal/store/sqlite/export.go index 9680222a9..0fb16473a 100644 --- a/apps/api/internal/store/sqlite/export.go +++ b/apps/api/internal/store/sqlite/export.go @@ -22,6 +22,7 @@ func (s *Store) ExportJSON(ctx context.Context, writer io.Writer) error { out := map[string]any{} tables := []string{ "users", "user_notification_settings", "channel_notification_settings", "identities", "workspaces", "workspace_members", "channels", + "user_sidebar_channel_order", "messages", "pinned_messages", "thread_state", "reactions", "events", "event_recipients", "uploads", "channel_reads", "direct_reads", "message_attachments", "direct_conversations", "direct_conversation_members", diff --git a/apps/api/internal/store/storetest/sidebar.go b/apps/api/internal/store/storetest/sidebar.go index 23cc059d5..b35e0927c 100644 --- a/apps/api/internal/store/storetest/sidebar.go +++ b/apps/api/internal/store/storetest/sidebar.go @@ -1,9 +1,12 @@ package storetest import ( + "bytes" "context" + "encoding/json" "errors" "fmt" + "io" "reflect" "testing" "time" @@ -12,7 +15,10 @@ import ( ) // SidebarPreferences exercises the same persistence contract on both databases. -func SidebarPreferences(t *testing.T, st store.Store) { +func SidebarPreferences(t *testing.T, st interface { + store.Store + ExportJSON(context.Context, io.Writer) error +}) { t.Helper() ctx := context.Background() suffix := fmt.Sprint(time.Now().UnixNano()) @@ -81,4 +87,32 @@ func SidebarPreferences(t *testing.T, st store.Store) { t.Fatal("accepted oversized order") } check() + var output bytes.Buffer + if err := st.ExportJSON(ctx, &output); err != nil { + t.Fatal(err) + } + var snapshot struct { + Orders []struct { + UserID string `json:"user_id"` + WorkspaceID string `json:"workspace_id"` + ChannelIDs string `json:"channel_ids"` + } `json:"user_sidebar_channel_order"` + } + if err := json.Unmarshal(output.Bytes(), &snapshot); err != nil { + t.Fatal(err) + } + exported := map[string][]string{} + for _, row := range snapshot.Orders { + if row.UserID != user.ID { + continue + } + var order []string + if err := json.Unmarshal([]byte(row.ChannelIDs), &order); err != nil { + t.Fatal(err) + } + exported[row.WorkspaceID] = order + } + if !reflect.DeepEqual(exported, want) { + t.Fatalf("exported sidebar preferences: %v; want: %v", exported, want) + } } diff --git a/docs/features/profiles.md b/docs/features/profiles.md index 833557085..994e848e6 100644 --- a/docs/features/profiles.md +++ b/docs/features/profiles.md @@ -100,3 +100,6 @@ dropped and duplicates keep their first position. Up to 100 workspaces and 500 channel IDs per workspace can be saved per request. Larger local orders retain their tail on the browser where they were arranged. Appearance and sidebar preferences update independently. + +SQLite and PostgreSQL JSON exports include the saved per-workspace channel order, +including explicit clears, in `user_sidebar_channel_order`.