Working layout editor

This commit is contained in:
2026-02-28 13:41:51 +01:00
parent b543988f02
commit 78daab4cf4
4 changed files with 153 additions and 63 deletions
+87 -29
View File
@@ -36,7 +36,6 @@ import { ChevronDown } from "lucide-react";
import { Route, SetState, Workspace } from "@/lib/types";
import { listWorkspaces, saveWorkspaceToLocalStorage } from "@/lib/workspace";
import { openFileDialog } from "@/lib/import";
import { useWorkspace } from "@/hooks/workspace";
// Simple ID generator
const id = (prefix = "t") =>
@@ -47,7 +46,17 @@ type AppSidebarProps = {
workspace: Workspace;
setRoute: SetState<Route>;
/** optional setter for the current workspace, used for rename */
setWorkspace?: React.Dispatch<React.SetStateAction<Workspace | undefined>>;
setWorkspace: React.Dispatch<React.SetStateAction<Workspace | undefined>>;
addEntity: (
type: "todo" | "doc",
parentGroupId: string | null,
name?: string,
) => string | undefined;
addGroup: (name: string | undefined, parentGroupId: string | null) => string;
renameEntity: (entityId: string, newName: string) => void;
deleteEntity: (entityId: string) => void;
removeNode: (id: string) => void;
renameGroup: (id: string, name: string) => void;
};
export default function AppSidebar({
@@ -55,8 +64,14 @@ export default function AppSidebar({
workspace,
setRoute,
setWorkspace,
addEntity,
addGroup,
renameEntity,
deleteEntity,
removeNode,
renameGroup,
}: AppSidebarProps) {
const { addEntity, addGroup, renameEntity } = useWorkspace();
// actions are passed from parent `useWorkspace` instance
const [availableWorkspaces, setAvailableWorkspaces] = React.useState<
Workspace[]
@@ -64,6 +79,11 @@ export default function AppSidebar({
const [isRenameOpen, setIsRenameOpen] = React.useState(false);
const [proposedName, setProposedName] = React.useState(workspace.name);
const [isCreateOpen, setIsCreateOpen] = React.useState(false);
const [createMode, setCreateMode] = React.useState<"list" | "doc" | "group">(
"list",
);
const [createValue, setCreateValue] = React.useState("");
React.useEffect(() => {
setProposedName(workspace.name);
@@ -163,44 +183,27 @@ export default function AppSidebar({
<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 });
setCreateMode("list");
setCreateValue("New list");
setIsCreateOpen(true);
}}
>
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 });
setCreateMode("doc");
setCreateValue("New doc");
setIsCreateOpen(true);
}}
>
New doc (root)
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
const name = window.prompt(
"Name for new group",
"New group",
);
addGroup(name || "New group", null);
setCreateMode("group");
setCreateValue("New group");
setIsCreateOpen(true);
}}
>
New group (root)
@@ -231,6 +234,12 @@ export default function AppSidebar({
node={node}
workspace={workspace}
setRoute={setRoute}
addEntity={addEntity}
addGroup={addGroup}
renameEntity={renameEntity}
deleteEntity={deleteEntity}
removeNode={removeNode}
renameGroup={renameGroup}
/>
))}
</SidebarMenu>
@@ -270,6 +279,55 @@ export default function AppSidebar({
</DialogFooter>
</DialogContent>
</Dialog>
{/* create dialog */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{createMode === "group"
? "Create group"
: createMode === "doc"
? "Create document"
: "Create list"}
</DialogTitle>
<DialogDescription>
{createMode === "group" && "Provide a name for the new group."}
{createMode === "doc" && "Provide a name for the new document."}
{createMode === "list" && "Provide a name for the new list."}
</DialogDescription>
</DialogHeader>
<Input
value={createValue}
onChange={(e) => setCreateValue(e.target.value)}
className="w-full"
/>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateOpen(false)}>
Cancel
</Button>
<Button
onClick={() => {
if (createMode === "group") {
addGroup?.(createValue || "New group", null);
} else if (createMode === "doc") {
const id = addEntity?.("doc", null, createValue || "New doc");
if (id) setRoute({ workspace: workspace.id, entity: id });
} else {
const id = addEntity?.(
"todo",
null,
createValue || "New list",
);
if (id) setRoute({ workspace: workspace.id, entity: id });
}
setIsCreateOpen(false);
}}
>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</SidebarProvider>
);
}
+28 -16
View File
@@ -12,7 +12,6 @@ import {
import { Collapsible, CollapsibleContent } from "../ui/collapsible";
import { ChevronDown, MoreHorizontal } from "lucide-react";
import { Route, Workspace, LayoutNode } from "@/lib/types";
import { useWorkspace } from "@/hooks/workspace";
import {
DropdownMenu,
DropdownMenuContent,
@@ -36,18 +35,29 @@ type Props = {
node: LayoutNode;
workspace: Workspace;
setRoute: (route: Route) => void;
addEntity: (
type: "todo" | "doc",
parentGroupId: string | null,
name?: string,
) => string | undefined;
addGroup: (name: string | undefined, parentGroupId: string | null) => string;
renameEntity: (entityId: string, newName: string) => void;
deleteEntity: (entityId: string) => void;
removeNode: (id: string) => void;
renameGroup: (id: string, name: string) => void;
};
export default function EntitySwitch({ node, workspace, setRoute }: Props) {
const {
renameEntity,
deleteEntity,
addEntity,
addGroup,
removeNode,
renameGroup,
} = useWorkspace();
export default function EntitySwitch({
node,
workspace,
setRoute,
addEntity,
addGroup,
renameEntity,
deleteEntity,
removeNode,
renameGroup,
}: Props) {
const [menuOpen, setMenuOpen] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState("");
@@ -74,11 +84,7 @@ export default function EntitySwitch({ node, workspace, setRoute }: Props) {
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);
}
addEntity("todo", parentId, dialogValue || "New list");
} else if (dialogMode === "add-group") {
const parentId = node.type === "group" ? node.id : null;
addGroup(dialogValue || "New group", parentId);
@@ -144,6 +150,12 @@ export default function EntitySwitch({ node, workspace, setRoute }: Props) {
node={child}
workspace={workspace}
setRoute={setRoute}
addEntity={addEntity}
addGroup={addGroup}
renameEntity={renameEntity}
deleteEntity={deleteEntity}
removeNode={removeNode}
renameGroup={renameGroup}
/>
))}
</SidebarMenu>