Sidebar elements editor
This commit is contained in:
@@ -36,6 +36,7 @@ import { ChevronDown } from "lucide-react";
|
|||||||
import { Route, SetState, Workspace } from "@/lib/types";
|
import { Route, SetState, Workspace } from "@/lib/types";
|
||||||
import { listWorkspaces, saveWorkspaceToLocalStorage } from "@/lib/workspace";
|
import { listWorkspaces, saveWorkspaceToLocalStorage } from "@/lib/workspace";
|
||||||
import { openFileDialog } from "@/lib/import";
|
import { openFileDialog } from "@/lib/import";
|
||||||
|
import { useWorkspace } from "@/hooks/workspace";
|
||||||
|
|
||||||
// Simple ID generator
|
// Simple ID generator
|
||||||
const id = (prefix = "t") =>
|
const id = (prefix = "t") =>
|
||||||
@@ -55,6 +56,8 @@ export default function AppSidebar({
|
|||||||
setRoute,
|
setRoute,
|
||||||
setWorkspace,
|
setWorkspace,
|
||||||
}: AppSidebarProps) {
|
}: AppSidebarProps) {
|
||||||
|
const { addEntity, addGroup, renameEntity } = useWorkspace();
|
||||||
|
|
||||||
const [availableWorkspaces, setAvailableWorkspaces] = React.useState<
|
const [availableWorkspaces, setAvailableWorkspaces] = React.useState<
|
||||||
Workspace[]
|
Workspace[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -158,6 +161,51 @@ export default function AppSidebar({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
const name = window.prompt(
|
||||||
|
"Name for new list",
|
||||||
|
"New list",
|
||||||
|
);
|
||||||
|
const id = addEntity("todo", null);
|
||||||
|
if (id && name) {
|
||||||
|
// rename newly created entity
|
||||||
|
// obtain hook function
|
||||||
|
// import renameEntity above
|
||||||
|
renameEntity(id, name);
|
||||||
|
}
|
||||||
|
if (id) setRoute({ workspace: workspace.id, entity: id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New list (root)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
const name = window.prompt(
|
||||||
|
"Name for new document",
|
||||||
|
"New doc",
|
||||||
|
);
|
||||||
|
const id = addEntity("doc", null);
|
||||||
|
if (id && name) {
|
||||||
|
renameEntity(id, name);
|
||||||
|
}
|
||||||
|
if (id) setRoute({ workspace: workspace.id, entity: id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New doc (root)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
const name = window.prompt(
|
||||||
|
"Name for new group",
|
||||||
|
"New group",
|
||||||
|
);
|
||||||
|
addGroup(name || "New group", null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New group (root)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onSelect={importWorkspace}>
|
<DropdownMenuItem onSelect={importWorkspace}>
|
||||||
Import Workspace
|
Import Workspace
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -10,8 +10,27 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "../ui/sidebar";
|
} from "../ui/sidebar";
|
||||||
import { Collapsible, CollapsibleContent } from "../ui/collapsible";
|
import { Collapsible, CollapsibleContent } from "../ui/collapsible";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown, MoreHorizontal } from "lucide-react";
|
||||||
import { Route, Workspace, LayoutNode } from "@/lib/types";
|
import { Route, Workspace, LayoutNode } from "@/lib/types";
|
||||||
|
import { useWorkspace } from "@/hooks/workspace";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
node: LayoutNode;
|
node: LayoutNode;
|
||||||
@@ -20,36 +39,151 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EntitySwitch({ node, workspace, setRoute }: Props) {
|
export default function EntitySwitch({ node, workspace, setRoute }: Props) {
|
||||||
|
const {
|
||||||
|
renameEntity,
|
||||||
|
deleteEntity,
|
||||||
|
addEntity,
|
||||||
|
addGroup,
|
||||||
|
removeNode,
|
||||||
|
renameGroup,
|
||||||
|
} = useWorkspace();
|
||||||
|
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [dialogValue, setDialogValue] = useState("");
|
||||||
|
const [dialogMode, setDialogMode] = useState<
|
||||||
|
"rename-group" | "rename-item" | "add-item" | "add-group"
|
||||||
|
>("rename-item");
|
||||||
|
|
||||||
|
const openDialog = (mode: typeof dialogMode, initial = "") => {
|
||||||
|
setDialogMode(mode);
|
||||||
|
// supply default for add-item
|
||||||
|
if (mode === "add-item") {
|
||||||
|
setDialogValue("New list");
|
||||||
|
} else {
|
||||||
|
setDialogValue(initial);
|
||||||
|
}
|
||||||
|
setDialogOpen(true);
|
||||||
|
setMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogSubmit = () => {
|
||||||
|
if (dialogMode === "rename-group" && node.type === "group") {
|
||||||
|
renameGroup(node.id, dialogValue);
|
||||||
|
} else if (dialogMode === "rename-item" && node.type === "item") {
|
||||||
|
renameEntity(node.entityId, dialogValue);
|
||||||
|
} else if (dialogMode === "add-item") {
|
||||||
|
const parentId = node.type === "group" ? node.id : null;
|
||||||
|
const id = addEntity("todo", parentId);
|
||||||
|
// optionally could rename newly created entity name
|
||||||
|
if (id && dialogValue) {
|
||||||
|
renameEntity(id, dialogValue);
|
||||||
|
}
|
||||||
|
} else if (dialogMode === "add-group") {
|
||||||
|
const parentId = node.type === "group" ? node.id : null;
|
||||||
|
addGroup(dialogValue || "New group", parentId);
|
||||||
|
}
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
// ---- GROUP ----
|
// ---- GROUP ----
|
||||||
if (node.type === "group") {
|
if (node.type === "group") {
|
||||||
return (
|
return (
|
||||||
<Collapsible defaultOpen className="group/collapsible">
|
<>
|
||||||
<SidebarGroup>
|
<Collapsible defaultOpen className="group/collapsible">
|
||||||
<SidebarGroupLabel asChild>
|
<SidebarGroup>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarGroupLabel asChild>
|
||||||
<CollapsibleTrigger className="flex w-full items-center">
|
<div className="flex items-center justify-between w-full">
|
||||||
{node.name}
|
<SidebarMenuButton asChild>
|
||||||
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
<CollapsibleTrigger className="flex w-full items-center">
|
||||||
</CollapsibleTrigger>
|
{node.name}
|
||||||
</SidebarMenuButton>
|
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||||
</SidebarGroupLabel>
|
</CollapsibleTrigger>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="p-1 hover:bg-accent rounded">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onSelect={() => openDialog("add-item")}>
|
||||||
|
Add list
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => openDialog("add-group")}>
|
||||||
|
Add group
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => openDialog("rename-group", node.name)}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
if (
|
||||||
|
window.confirm("Delete this group and its children?")
|
||||||
|
) {
|
||||||
|
removeNode(node.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{node.children.map((child) => (
|
{node.children.map((child) => (
|
||||||
<EntitySwitch
|
<EntitySwitch
|
||||||
key={child.type === "group" ? child.id : child.entityId}
|
key={child.type === "group" ? child.id : child.entityId}
|
||||||
node={child}
|
node={child}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
setRoute={setRoute}
|
setRoute={setRoute}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
|
{/* dialog for add/rename */}
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{dialogMode.startsWith("rename")
|
||||||
|
? "Rename"
|
||||||
|
: dialogMode === "add-item"
|
||||||
|
? "Add new list"
|
||||||
|
: "Add new group"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{dialogMode === "add-item" &&
|
||||||
|
"A default todo list will be created."}
|
||||||
|
{dialogMode === "add-group" &&
|
||||||
|
"Enter the name for the new group."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Input
|
||||||
|
value={dialogValue}
|
||||||
|
onChange={(e) => setDialogValue(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDialogSubmit}>Save</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,16 +196,41 @@ export default function EntitySwitch({ node, workspace, setRoute }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<div className="flex items-center justify-between w-full">
|
||||||
onClick={() =>
|
<SidebarMenuButton
|
||||||
setRoute({
|
onClick={() =>
|
||||||
workspace: workspace.id,
|
setRoute({
|
||||||
entity: node.entityId,
|
workspace: workspace.id,
|
||||||
})
|
entity: node.entityId,
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
{workspaceEntity.name}
|
>
|
||||||
</SidebarMenuButton>
|
{workspaceEntity.name}
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="p-1 hover:bg-accent rounded">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => openDialog("rename-item", workspaceEntity.name)}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
if (window.confirm("Delete this list?")) {
|
||||||
|
deleteEntity(node.entityId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-2
@@ -1,7 +1,11 @@
|
|||||||
import { Entity, EntityId, Workspace } from "@/lib/types";
|
import { Entity, EntityId, LayoutNode, Workspace } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
loadWorkspaceFromLocalStorage,
|
loadWorkspaceFromLocalStorage,
|
||||||
saveWorkspaceToLocalStorage,
|
saveWorkspaceToLocalStorage,
|
||||||
|
addLayoutItem,
|
||||||
|
addLayoutGroup,
|
||||||
|
removeLayoutNode,
|
||||||
|
renameLayoutGroup,
|
||||||
} from "@/lib/workspace";
|
} from "@/lib/workspace";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -23,6 +27,105 @@ export function useWorkspace() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renameEntity = (entityId: EntityId, newName: string) => {
|
||||||
|
updateEntity(entityId, (e) => ({ ...e, name: newName }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEntity = (entityId: EntityId) => {
|
||||||
|
setWorkspace((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const { [entityId]: _, ...rest } = prev.entities;
|
||||||
|
// also remove from layout
|
||||||
|
const { layout: newLayout } = removeLayoutNode(prev.layout, entityId);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
entities: rest,
|
||||||
|
layout: newLayout,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEntity = (type: "todo" | "doc", parentGroupId: string | null) => {
|
||||||
|
if (!workspace) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
let entity: Entity;
|
||||||
|
if (type === "todo") {
|
||||||
|
entity = {
|
||||||
|
id,
|
||||||
|
type: "todo",
|
||||||
|
name: "New list",
|
||||||
|
config: { statuses: [] },
|
||||||
|
tags: {},
|
||||||
|
tasks: {},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
} as any;
|
||||||
|
} else {
|
||||||
|
entity = {
|
||||||
|
id,
|
||||||
|
type: "doc",
|
||||||
|
name: "New doc",
|
||||||
|
content: { version: "2.28.0", time: now, blocks: [] },
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
setWorkspace((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const newEntities = { ...prev.entities, [id]: entity };
|
||||||
|
const newLayout = addLayoutItem(prev.layout, parentGroupId, {
|
||||||
|
type: "item",
|
||||||
|
entityId: id,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
entities: newEntities,
|
||||||
|
layout: newLayout,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGroup = (name = "New group", parentGroupId: string | null) => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const group: LayoutNode = { type: "group", id, name, children: [] };
|
||||||
|
setWorkspace((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const newLayout = addLayoutGroup(prev.layout, parentGroupId, group);
|
||||||
|
return { ...prev, layout: newLayout, updatedAt: Date.now() };
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNode = (id: string) => {
|
||||||
|
setWorkspace((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const { layout: newLayout, removedEntityIds } = removeLayoutNode(
|
||||||
|
prev.layout,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
const newEntities = { ...prev.entities } as Record<string, Entity>;
|
||||||
|
removedEntityIds.forEach((eid) => delete newEntities[eid]);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
entities: newEntities,
|
||||||
|
layout: newLayout,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameGroup = (id: string, newName: string) => {
|
||||||
|
setWorkspace((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const newLayout = renameLayoutGroup(prev.layout, id, newName);
|
||||||
|
return { ...prev, layout: newLayout, updatedAt: Date.now() };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadWorkspace = (workspaceId: string | undefined) => {
|
const loadWorkspace = (workspaceId: string | undefined) => {
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
return setWorkspace(undefined);
|
return setWorkspace(undefined);
|
||||||
@@ -37,5 +140,16 @@ export function useWorkspace() {
|
|||||||
if (workspace) saveWorkspaceToLocalStorage(workspace);
|
if (workspace) saveWorkspaceToLocalStorage(workspace);
|
||||||
}, [workspace]);
|
}, [workspace]);
|
||||||
|
|
||||||
return { workspace, loadWorkspace, updateEntity, setWorkspace };
|
return {
|
||||||
|
workspace,
|
||||||
|
loadWorkspace,
|
||||||
|
updateEntity,
|
||||||
|
renameEntity,
|
||||||
|
deleteEntity,
|
||||||
|
addEntity,
|
||||||
|
addGroup,
|
||||||
|
removeNode,
|
||||||
|
renameGroup,
|
||||||
|
setWorkspace,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Workspace, TodoEntity, DocEntity, Status } from "./types";
|
import { Workspace, TodoEntity, DocEntity, Status, LayoutNode } from "./types";
|
||||||
|
|
||||||
export function createDefaultWorkspace(): Workspace {
|
export function createDefaultWorkspace(): Workspace {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -129,3 +129,143 @@ export function listWorkspaces(): Workspace[] {
|
|||||||
}
|
}
|
||||||
return workspaces;
|
return workspaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// layout helpers -------------------------------------------------------------------
|
||||||
|
|
||||||
|
type RemoveResult = { layout: LayoutNode[]; removedEntityIds: string[] };
|
||||||
|
|
||||||
|
function collectEntityIds(node: LayoutNode): string[] {
|
||||||
|
if (node.type === "item") return [node.entityId];
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (const child of node.children) {
|
||||||
|
ids.push(...collectEntityIds(child));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeLayoutNode(
|
||||||
|
layout: LayoutNode[],
|
||||||
|
id: string,
|
||||||
|
): RemoveResult {
|
||||||
|
let removed: string[] = [];
|
||||||
|
|
||||||
|
function recurse(nodes: LayoutNode[]): LayoutNode[] {
|
||||||
|
return nodes
|
||||||
|
.map((node) => {
|
||||||
|
if (node.type === "group") {
|
||||||
|
if (node.id === id) {
|
||||||
|
removed.push(...collectEntityIds(node));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const children = recurse(node.children);
|
||||||
|
if (children.length !== node.children.length) {
|
||||||
|
// some children removed
|
||||||
|
}
|
||||||
|
return { ...node, children } as LayoutNode;
|
||||||
|
} else {
|
||||||
|
if (node.entityId === id) {
|
||||||
|
removed.push(node.entityId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as LayoutNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLayout = recurse(layout);
|
||||||
|
return { layout: newLayout, removedEntityIds: removed };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renameLayoutGroup(
|
||||||
|
layout: LayoutNode[],
|
||||||
|
id: string,
|
||||||
|
newName: string,
|
||||||
|
): LayoutNode[] {
|
||||||
|
return layout.map((node) => {
|
||||||
|
if (node.type === "group") {
|
||||||
|
if (node.id === id) {
|
||||||
|
return { ...node, name: newName };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: renameLayoutGroup(node.children, id, newName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLayoutGroup(
|
||||||
|
layout: LayoutNode[],
|
||||||
|
parentId: string | null,
|
||||||
|
group: LayoutNode,
|
||||||
|
): LayoutNode[] {
|
||||||
|
if (!parentId) {
|
||||||
|
return [...layout, group];
|
||||||
|
}
|
||||||
|
return layout.map((node) => {
|
||||||
|
if (node.type === "group") {
|
||||||
|
if (node.id === parentId) {
|
||||||
|
return { ...node, children: [...node.children, group] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: addLayoutGroup(node.children, parentId, group),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addLayoutItem(
|
||||||
|
layout: LayoutNode[],
|
||||||
|
parentId: string | null,
|
||||||
|
item: LayoutNode,
|
||||||
|
): LayoutNode[] {
|
||||||
|
if (!parentId) {
|
||||||
|
return [...layout, item];
|
||||||
|
}
|
||||||
|
return layout.map((node) => {
|
||||||
|
if (node.type === "group") {
|
||||||
|
if (node.id === parentId) {
|
||||||
|
return { ...node, children: [...node.children, item] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: addLayoutItem(node.children, parentId, item),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// entity creation helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
export function createTodoEntity(name = "New list"): TodoEntity {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const now = Date.now();
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: "todo",
|
||||||
|
name,
|
||||||
|
config: { statuses: [] },
|
||||||
|
tags: {},
|
||||||
|
tasks: {},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDocEntity(name = "New doc"): DocEntity {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const now = Date.now();
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: "doc",
|
||||||
|
name,
|
||||||
|
content: { version: "2.28.0", time: now, blocks: [] },
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user