Files
dbx/apps/desktop/src/lib/sidebarLayout.ts
T

266 lines
9.0 KiB
TypeScript

import type { ConnectionConfig, ConnectionGroup, SidebarLayout, SidebarOrderEntry, TreeNode } from "@/types/database";
import { uuid } from "@/lib/utils";
export function emptyLayout(): SidebarLayout {
return { groups: [], order: [] };
}
export function reconcileLayout(connectionIds: string[], layout: SidebarLayout | null): SidebarLayout {
if (!layout) {
return {
groups: [],
order: connectionIds.map((id) => ({ type: "connection" as const, id })),
};
}
const validIds = new Set(connectionIds);
const seen = new Set<string>();
const order: SidebarOrderEntry[] = [];
for (const entry of layout.order) {
if (entry.type === "group") {
const filtered = entry.connectionIds.filter((id) => {
if (!validIds.has(id) || seen.has(id)) return false;
seen.add(id);
return true;
});
order.push({ type: "group", id: entry.id, connectionIds: filtered });
} else {
if (validIds.has(entry.id) && !seen.has(entry.id)) {
seen.add(entry.id);
order.push(entry);
}
}
}
for (const id of connectionIds) {
if (!seen.has(id)) {
order.push({ type: "connection", id });
}
}
const usedGroupIds = new Set(order.filter((e) => e.type === "group").map((e) => e.id));
const groups = layout.groups.filter((g) => usedGroupIds.has(g.id));
return { groups, order };
}
function makeConnectionNode(config: ConnectionConfig, pinned: boolean): TreeNode {
return {
id: config.id,
label: config.name,
type: "connection",
connectionId: config.id,
isExpanded: false,
children: [],
pinned,
};
}
function orderPinnedFirst(nodes: TreeNode[]): TreeNode[] {
const pinned: TreeNode[] = [];
const unpinned: TreeNode[] = [];
for (const node of nodes) {
if (node.pinned) pinned.push(node);
else unpinned.push(node);
}
return [...pinned, ...unpinned];
}
export function buildTreeNodesFromLayout(layout: SidebarLayout, connections: ConnectionConfig[], pinnedIds: Set<string>): TreeNode[] {
const configMap = new Map(connections.map((c) => [c.id, c]));
const groupMap = new Map(layout.groups.map((g) => [g.id, g]));
const nodes: TreeNode[] = [];
for (const entry of layout.order) {
if (entry.type === "group") {
const group = groupMap.get(entry.id);
if (!group) continue;
const children: TreeNode[] = [];
for (const connId of entry.connectionIds) {
const config = configMap.get(connId);
if (config) children.push(makeConnectionNode(config, pinnedIds.has(connId)));
}
nodes.push({
id: group.id,
label: group.name,
type: "connection-group",
isExpanded: !group.collapsed,
children: orderPinnedFirst(children),
});
} else {
const config = configMap.get(entry.id);
if (config) nodes.push(makeConnectionNode(config, pinnedIds.has(entry.id)));
}
}
return orderPinnedFirst(nodes);
}
export function findConnectionLocation(layout: SidebarLayout, connectionId: string): { entryIndex: number; groupId?: string; innerIndex?: number } | null {
for (let i = 0; i < layout.order.length; i++) {
const entry = layout.order[i];
if (entry.type === "connection" && entry.id === connectionId) {
return { entryIndex: i };
}
if (entry.type === "group") {
const innerIndex = entry.connectionIds.indexOf(connectionId);
if (innerIndex >= 0) {
return { entryIndex: i, groupId: entry.id, innerIndex };
}
}
}
return null;
}
function removeConnectionFromLayout(order: SidebarOrderEntry[], connectionId: string): SidebarOrderEntry[] {
return order
.map((entry) => {
if (entry.type === "connection" && entry.id === connectionId) return null;
if (entry.type === "group") {
return { ...entry, connectionIds: entry.connectionIds.filter((id) => id !== connectionId) };
}
return entry;
})
.filter(Boolean) as SidebarOrderEntry[];
}
export function moveConnectionToGroup(layout: SidebarLayout, connectionId: string, targetGroupId: string | null): SidebarLayout {
const order = removeConnectionFromLayout([...layout.order], connectionId);
if (targetGroupId) {
const groupEntry = order.find((e) => e.type === "group" && e.id === targetGroupId);
if (groupEntry && groupEntry.type === "group") {
groupEntry.connectionIds.push(connectionId);
}
} else {
order.push({ type: "connection", id: connectionId });
}
return { ...layout, order };
}
export type DropPosition = "before" | "after" | "inside";
export function reorderEntry(layout: SidebarLayout, draggedId: string, targetId: string, position: DropPosition): SidebarLayout {
if (draggedId === targetId) return layout;
const isDraggedGroup = layout.order.some((e) => e.type === "group" && e.id === draggedId);
const isTargetGroup = layout.order.some((e) => e.type === "group" && e.id === targetId);
if (isDraggedGroup) {
return reorderGroup(layout, draggedId, targetId, position);
}
if (position === "inside" && isTargetGroup) {
return moveConnectionToGroup(layout, draggedId, targetId);
}
return reorderConnection(layout, draggedId, targetId, position);
}
function reorderGroup(layout: SidebarLayout, draggedId: string, targetId: string, position: DropPosition): SidebarLayout {
const order = [...layout.order];
const draggedIndex = order.findIndex((e) => e.type === "group" && e.id === draggedId);
if (draggedIndex < 0) return layout;
const [dragged] = order.splice(draggedIndex, 1);
let targetIndex = order.findIndex((e) => (e.type === "group" && e.id === targetId) || (e.type === "connection" && e.id === targetId));
if (targetIndex < 0) {
order.push(dragged);
} else {
if (position === "after") targetIndex++;
order.splice(targetIndex, 0, dragged);
}
return { ...layout, order };
}
function reorderConnection(layout: SidebarLayout, draggedId: string, targetId: string, position: DropPosition): SidebarLayout {
const order = removeConnectionFromLayout([...layout.order], draggedId);
const targetLoc = findConnectionLocation({ ...layout, order }, targetId);
if (!targetLoc) {
order.push({ type: "connection", id: draggedId });
return { ...layout, order };
}
if (targetLoc.groupId) {
const groupEntry = order[targetLoc.entryIndex];
if (groupEntry.type === "group") {
const insertAt = position === "after" ? targetLoc.innerIndex! + 1 : targetLoc.innerIndex!;
groupEntry.connectionIds.splice(insertAt, 0, draggedId);
}
} else {
const isTargetGroup = order[targetLoc.entryIndex]?.type === "group";
if (isTargetGroup && position === "inside") {
const entry = order[targetLoc.entryIndex];
if (entry.type === "group") entry.connectionIds.push(draggedId);
} else {
const insertAt = position === "after" ? targetLoc.entryIndex + 1 : targetLoc.entryIndex;
order.splice(insertAt, 0, { type: "connection", id: draggedId });
}
}
return { ...layout, order };
}
export function createGroup(layout: SidebarLayout, name: string): { layout: SidebarLayout; groupId: string } {
const groupId = uuid();
const group: ConnectionGroup = { id: groupId, name, collapsed: false };
return {
groupId,
layout: {
groups: [...layout.groups, group],
order: [...layout.order, { type: "group" as const, id: groupId, connectionIds: [] }],
},
};
}
export function renameGroup(layout: SidebarLayout, groupId: string, name: string): SidebarLayout {
return {
...layout,
groups: layout.groups.map((g) => (g.id === groupId ? { ...g, name } : g)),
};
}
export function deleteGroup(layout: SidebarLayout, groupId: string): SidebarLayout {
const order: SidebarOrderEntry[] = [];
for (const entry of layout.order) {
if (entry.type === "group" && entry.id === groupId) {
for (const connId of entry.connectionIds) {
order.push({ type: "connection", id: connId });
}
} else {
order.push(entry);
}
}
return {
groups: layout.groups.filter((g) => g.id !== groupId),
order,
};
}
export function toggleGroupCollapsed(layout: SidebarLayout, groupId: string): SidebarLayout {
return {
...layout,
groups: layout.groups.map((g) => (g.id === groupId ? { ...g, collapsed: !g.collapsed } : g)),
};
}
export function removeConnectionFromSidebarLayout(layout: SidebarLayout, connectionId: string): SidebarLayout {
return { ...layout, order: removeConnectionFromLayout(layout.order, connectionId) };
}
export function appendConnectionToLayout(layout: SidebarLayout, connectionId: string, groupId?: string | null): SidebarLayout {
if (groupId && layout.groups.some((group) => group.id === groupId)) {
return {
...layout,
groups: layout.groups.map((group) => (group.id === groupId ? { ...group, collapsed: false } : group)),
order: layout.order.map((entry) => (entry.type === "group" && entry.id === groupId ? { ...entry, connectionIds: [...entry.connectionIds, connectionId] } : entry)),
};
}
return { ...layout, order: [...layout.order, { type: "connection" as const, id: connectionId }] };
}