feat: space sidebar drag-and-drop reordering with folder system
Add user_space_layout table and PUT /api/users/@me/space-layout endpoint for persisting per-user sidebar ordering. Spaces can be freely reordered via drag-and-drop, folders created by dragging one space onto another, and folders auto-dissolve when they have fewer than 2 members. Includes folder context menu (rename, color, ungroup), collapsed folder mini-grid icons, multi-tab sync via WebSocket, and localStorage collapse state. Removes the rigid native/federated split — federated spaces now intermix freely while keeping their globe badge.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Space, Channel, ChannelCategory, MemberWithUser, SpaceWithChannelsAndMembers, Role, SpaceFolder, DmChannel, User, UpdateSpaceRequest, CreateSpaceRequest } from '@backspace/shared';
|
||||
import type { Space, Channel, ChannelCategory, MemberWithUser, SpaceWithChannelsAndMembers, Role, SpaceFolder, SpaceLayoutItem, DmChannel, User, UpdateSpaceRequest, CreateSpaceRequest } from '@backspace/shared';
|
||||
import { api, BackspaceApiClient } from '../api/client';
|
||||
import { resolveAssetUrl, normalizeUserAssets } from '../utils/assetUrls';
|
||||
import { isSelf } from '../utils/identity';
|
||||
@@ -30,6 +30,7 @@ interface SpaceState {
|
||||
members: MemberWithUser[];
|
||||
roles: Role[];
|
||||
folders: SpaceFolder[];
|
||||
spaceLayout: SpaceLayoutItem[] | null;
|
||||
dmChannels: DmChannel[];
|
||||
channelToSpaceMap: Map<string, string>;
|
||||
channelLastMessageIds: Map<string, string>;
|
||||
@@ -72,7 +73,9 @@ interface SpaceState {
|
||||
updateUserEverywhere: (user: User) => void;
|
||||
addMember: (member: MemberWithUser) => void;
|
||||
removeMember: (userId: string) => void;
|
||||
populateFromReady: (origin: string, spaces: SpaceWithChannelsAndMembers[], folders?: SpaceFolder[], dmChannels?: DmChannel[]) => void;
|
||||
setSpaceLayout: (layout: SpaceLayoutItem[] | null) => void;
|
||||
updateSpaceLayout: (items: SpaceLayoutItem[], folders: Record<string, { name: string | null; color: string | null; spaceIds: string[] }>) => Promise<void>;
|
||||
populateFromReady: (origin: string, spaces: SpaceWithChannelsAndMembers[], folders?: SpaceFolder[], dmChannels?: DmChannel[], spaceLayout?: SpaceLayoutItem[] | null) => void;
|
||||
addSpaceFromReady: (origin: string, space: SpaceWithChannelsAndMembers) => void;
|
||||
removeInstanceSpaces: (origin: string) => void;
|
||||
transferOwnership: (spaceId: string, newOwnerId: string) => Promise<void>;
|
||||
@@ -87,6 +90,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
||||
members: [],
|
||||
roles: [],
|
||||
folders: [],
|
||||
spaceLayout: null,
|
||||
dmChannels: [],
|
||||
channelToSpaceMap: new Map(),
|
||||
channelLastMessageIds: new Map(),
|
||||
@@ -415,7 +419,22 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
||||
}));
|
||||
},
|
||||
|
||||
populateFromReady: (origin: string, spaces: SpaceWithChannelsAndMembers[], folders?: SpaceFolder[], dmChannels?: DmChannel[]) => {
|
||||
setSpaceLayout: (layout) => set({ spaceLayout: layout }),
|
||||
|
||||
updateSpaceLayout: async (items, folders) => {
|
||||
// Optimistic: apply the layout immediately
|
||||
set({ spaceLayout: items });
|
||||
|
||||
try {
|
||||
const result = await api.spaceLayout.update({ items, folders });
|
||||
// Server may have resolved new:* IDs
|
||||
set({ spaceLayout: result.items, folders: result.folders });
|
||||
} catch (err) {
|
||||
console.error('Failed to save space layout:', err);
|
||||
}
|
||||
},
|
||||
|
||||
populateFromReady: (origin: string, spaces: SpaceWithChannelsAndMembers[], folders?: SpaceFolder[], dmChannels?: DmChannel[], spaceLayout?: SpaceLayoutItem[] | null) => {
|
||||
const isHome = !origin;
|
||||
|
||||
// Tag all incoming servers with their instance origin
|
||||
@@ -533,9 +552,12 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
||||
categoryOriginMap,
|
||||
};
|
||||
|
||||
// Only set folders from home origin
|
||||
// Only set folders and layout from home origin
|
||||
if (isHome) {
|
||||
update.folders = folders || [];
|
||||
if (spaceLayout !== undefined) {
|
||||
update.spaceLayout = spaceLayout ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
set(update as any);
|
||||
|
||||
Reference in New Issue
Block a user