Working layout editor
This commit is contained in:
@@ -14,8 +14,18 @@ import {
|
|||||||
} from "@/lib/workspace";
|
} from "@/lib/workspace";
|
||||||
|
|
||||||
export default function Workspace() {
|
export default function Workspace() {
|
||||||
const { workspace, loadWorkspace, updateEntity, setWorkspace } =
|
const {
|
||||||
useWorkspace();
|
workspace,
|
||||||
|
loadWorkspace,
|
||||||
|
updateEntity,
|
||||||
|
setWorkspace,
|
||||||
|
addEntity,
|
||||||
|
addGroup,
|
||||||
|
renameEntity,
|
||||||
|
deleteEntity,
|
||||||
|
removeNode,
|
||||||
|
renameGroup,
|
||||||
|
} = useWorkspace();
|
||||||
|
|
||||||
const [route, setRoute] = useState<Route>({});
|
const [route, setRoute] = useState<Route>({});
|
||||||
|
|
||||||
@@ -120,6 +130,12 @@ export default function Workspace() {
|
|||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
setRoute={setRoute}
|
setRoute={setRoute}
|
||||||
setWorkspace={setWorkspace}
|
setWorkspace={setWorkspace}
|
||||||
|
addEntity={addEntity}
|
||||||
|
addGroup={addGroup}
|
||||||
|
renameEntity={renameEntity}
|
||||||
|
deleteEntity={deleteEntity}
|
||||||
|
removeNode={removeNode}
|
||||||
|
renameGroup={renameGroup}
|
||||||
>
|
>
|
||||||
<SidebarTrigger className="w-full px-4">
|
<SidebarTrigger className="w-full px-4">
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ 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") =>
|
||||||
@@ -47,7 +46,17 @@ type AppSidebarProps = {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
setRoute: SetState<Route>;
|
setRoute: SetState<Route>;
|
||||||
/** optional setter for the current workspace, used for rename */
|
/** 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({
|
export default function AppSidebar({
|
||||||
@@ -55,8 +64,14 @@ export default function AppSidebar({
|
|||||||
workspace,
|
workspace,
|
||||||
setRoute,
|
setRoute,
|
||||||
setWorkspace,
|
setWorkspace,
|
||||||
|
addEntity,
|
||||||
|
addGroup,
|
||||||
|
renameEntity,
|
||||||
|
deleteEntity,
|
||||||
|
removeNode,
|
||||||
|
renameGroup,
|
||||||
}: AppSidebarProps) {
|
}: AppSidebarProps) {
|
||||||
const { addEntity, addGroup, renameEntity } = useWorkspace();
|
// actions are passed from parent `useWorkspace` instance
|
||||||
|
|
||||||
const [availableWorkspaces, setAvailableWorkspaces] = React.useState<
|
const [availableWorkspaces, setAvailableWorkspaces] = React.useState<
|
||||||
Workspace[]
|
Workspace[]
|
||||||
@@ -64,6 +79,11 @@ export default function AppSidebar({
|
|||||||
|
|
||||||
const [isRenameOpen, setIsRenameOpen] = React.useState(false);
|
const [isRenameOpen, setIsRenameOpen] = React.useState(false);
|
||||||
const [proposedName, setProposedName] = React.useState(workspace.name);
|
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(() => {
|
React.useEffect(() => {
|
||||||
setProposedName(workspace.name);
|
setProposedName(workspace.name);
|
||||||
@@ -163,44 +183,27 @@ export default function AppSidebar({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const name = window.prompt(
|
setCreateMode("list");
|
||||||
"Name for new list",
|
setCreateValue("New list");
|
||||||
"New list",
|
setIsCreateOpen(true);
|
||||||
);
|
|
||||||
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)
|
New list (root)
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const name = window.prompt(
|
setCreateMode("doc");
|
||||||
"Name for new document",
|
setCreateValue("New doc");
|
||||||
"New doc",
|
setIsCreateOpen(true);
|
||||||
);
|
|
||||||
const id = addEntity("doc", null);
|
|
||||||
if (id && name) {
|
|
||||||
renameEntity(id, name);
|
|
||||||
}
|
|
||||||
if (id) setRoute({ workspace: workspace.id, entity: id });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New doc (root)
|
New doc (root)
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const name = window.prompt(
|
setCreateMode("group");
|
||||||
"Name for new group",
|
setCreateValue("New group");
|
||||||
"New group",
|
setIsCreateOpen(true);
|
||||||
);
|
|
||||||
addGroup(name || "New group", null);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New group (root)
|
New group (root)
|
||||||
@@ -231,6 +234,12 @@ export default function AppSidebar({
|
|||||||
node={node}
|
node={node}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
setRoute={setRoute}
|
setRoute={setRoute}
|
||||||
|
addEntity={addEntity}
|
||||||
|
addGroup={addGroup}
|
||||||
|
renameEntity={renameEntity}
|
||||||
|
deleteEntity={deleteEntity}
|
||||||
|
removeNode={removeNode}
|
||||||
|
renameGroup={renameGroup}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -270,6 +279,55 @@ export default function AppSidebar({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import { Collapsible, CollapsibleContent } from "../ui/collapsible";
|
import { Collapsible, CollapsibleContent } from "../ui/collapsible";
|
||||||
import { ChevronDown, MoreHorizontal } 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 {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -36,18 +35,29 @@ type Props = {
|
|||||||
node: LayoutNode;
|
node: LayoutNode;
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
setRoute: (route: Route) => void;
|
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) {
|
export default function EntitySwitch({
|
||||||
const {
|
node,
|
||||||
renameEntity,
|
workspace,
|
||||||
deleteEntity,
|
setRoute,
|
||||||
addEntity,
|
addEntity,
|
||||||
addGroup,
|
addGroup,
|
||||||
|
renameEntity,
|
||||||
|
deleteEntity,
|
||||||
removeNode,
|
removeNode,
|
||||||
renameGroup,
|
renameGroup,
|
||||||
} = useWorkspace();
|
}: Props) {
|
||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogValue, setDialogValue] = useState("");
|
const [dialogValue, setDialogValue] = useState("");
|
||||||
@@ -74,11 +84,7 @@ export default function EntitySwitch({ node, workspace, setRoute }: Props) {
|
|||||||
renameEntity(node.entityId, dialogValue);
|
renameEntity(node.entityId, dialogValue);
|
||||||
} else if (dialogMode === "add-item") {
|
} else if (dialogMode === "add-item") {
|
||||||
const parentId = node.type === "group" ? node.id : null;
|
const parentId = node.type === "group" ? node.id : null;
|
||||||
const id = addEntity("todo", parentId);
|
addEntity("todo", parentId, dialogValue || "New list");
|
||||||
// optionally could rename newly created entity name
|
|
||||||
if (id && dialogValue) {
|
|
||||||
renameEntity(id, dialogValue);
|
|
||||||
}
|
|
||||||
} else if (dialogMode === "add-group") {
|
} else if (dialogMode === "add-group") {
|
||||||
const parentId = node.type === "group" ? node.id : null;
|
const parentId = node.type === "group" ? node.id : null;
|
||||||
addGroup(dialogValue || "New group", parentId);
|
addGroup(dialogValue || "New group", parentId);
|
||||||
@@ -144,6 +150,12 @@ export default function EntitySwitch({ node, workspace, setRoute }: Props) {
|
|||||||
node={child}
|
node={child}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
setRoute={setRoute}
|
setRoute={setRoute}
|
||||||
|
addEntity={addEntity}
|
||||||
|
addGroup={addGroup}
|
||||||
|
renameEntity={renameEntity}
|
||||||
|
deleteEntity={deleteEntity}
|
||||||
|
removeNode={removeNode}
|
||||||
|
renameGroup={renameGroup}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
+15
-11
@@ -13,18 +13,19 @@ export function useWorkspace() {
|
|||||||
const [workspace, setWorkspace] = useState<Workspace>();
|
const [workspace, setWorkspace] = useState<Workspace>();
|
||||||
|
|
||||||
const updateEntity = (entityId: EntityId, updater: (e: Entity) => Entity) => {
|
const updateEntity = (entityId: EntityId, updater: (e: Entity) => Entity) => {
|
||||||
setWorkspace((prev) =>
|
setWorkspace((prev) => {
|
||||||
prev
|
if (!prev) return prev;
|
||||||
? {
|
const current = prev.entities[entityId];
|
||||||
|
if (!current) return prev;
|
||||||
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
entities: {
|
entities: {
|
||||||
...prev.entities,
|
...prev.entities,
|
||||||
[entityId]: updater(prev.entities[entityId]),
|
[entityId]: updater(current),
|
||||||
},
|
},
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}
|
};
|
||||||
: undefined,
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renameEntity = (entityId: EntityId, newName: string) => {
|
const renameEntity = (entityId: EntityId, newName: string) => {
|
||||||
@@ -46,8 +47,11 @@ export function useWorkspace() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addEntity = (type: "todo" | "doc", parentGroupId: string | null) => {
|
const addEntity = (
|
||||||
if (!workspace) return;
|
type: "todo" | "doc",
|
||||||
|
parentGroupId: string | null,
|
||||||
|
name?: string,
|
||||||
|
) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
let entity: Entity;
|
let entity: Entity;
|
||||||
@@ -55,7 +59,7 @@ export function useWorkspace() {
|
|||||||
entity = {
|
entity = {
|
||||||
id,
|
id,
|
||||||
type: "todo",
|
type: "todo",
|
||||||
name: "New list",
|
name: name || "New list",
|
||||||
config: { statuses: [] },
|
config: { statuses: [] },
|
||||||
tags: {},
|
tags: {},
|
||||||
tasks: {},
|
tasks: {},
|
||||||
@@ -66,7 +70,7 @@ export function useWorkspace() {
|
|||||||
entity = {
|
entity = {
|
||||||
id,
|
id,
|
||||||
type: "doc",
|
type: "doc",
|
||||||
name: "New doc",
|
name: name || "New doc",
|
||||||
content: { version: "2.28.0", time: now, blocks: [] },
|
content: { version: "2.28.0", time: now, blocks: [] },
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
|
|||||||
Reference in New Issue
Block a user