272 lines
6.3 KiB
TypeScript
272 lines
6.3 KiB
TypeScript
import { Workspace, TodoEntity, DocEntity, Status, LayoutNode } from "./types";
|
|
|
|
export function createDefaultWorkspace(): Workspace {
|
|
const now = Date.now();
|
|
|
|
const todoId = crypto.randomUUID();
|
|
const docId = crypto.randomUUID();
|
|
|
|
const defaultStatuses: Status[] = [
|
|
{ id: "status_todo", name: "To Do", order: 0, color: "#808080" },
|
|
{
|
|
id: "status_inprogress",
|
|
name: "In Progress",
|
|
order: 1,
|
|
color: "#0ea5e9",
|
|
},
|
|
{ id: "status_done", name: "Done", order: 2, color: "#10b981" },
|
|
];
|
|
|
|
const todoEntity: TodoEntity = {
|
|
id: todoId,
|
|
type: "todo",
|
|
name: "Tasks",
|
|
config: { statuses: defaultStatuses },
|
|
tags: {},
|
|
tasks: {},
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
|
|
const docEntity: DocEntity = {
|
|
id: docId,
|
|
type: "doc",
|
|
name: "Notes",
|
|
content: { version: "2.28.0", time: now, blocks: [] },
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
name: "My Workspace",
|
|
entities: {
|
|
[todoId]: todoEntity,
|
|
[docId]: docEntity,
|
|
},
|
|
layout: [
|
|
{
|
|
type: "group",
|
|
id: crypto.randomUUID(),
|
|
name: "Productivity",
|
|
children: [
|
|
{ entityId: todoId, type: "item" },
|
|
{ entityId: docId, type: "item" },
|
|
],
|
|
},
|
|
],
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
}
|
|
|
|
export function loadWorkspaceFromLocalStorage(id: string): Workspace {
|
|
try {
|
|
const raw = localStorage.getItem(id);
|
|
|
|
if (!raw) {
|
|
return createDefaultWorkspace();
|
|
}
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
// Basic shape validation
|
|
if (
|
|
typeof parsed !== "object" ||
|
|
parsed === null ||
|
|
typeof parsed.id !== "string"
|
|
) {
|
|
console.warn("Invalid workspace shape. Resetting.");
|
|
return createDefaultWorkspace();
|
|
}
|
|
|
|
return parsed as Workspace;
|
|
} catch (err) {
|
|
console.error("Failed to load workspace:", err);
|
|
return createDefaultWorkspace();
|
|
}
|
|
}
|
|
|
|
export function saveWorkspaceToLocalStorage(workspace: Workspace): void {
|
|
try {
|
|
const serialized = JSON.stringify(workspace);
|
|
// keep using raw id for backwards compatibility
|
|
localStorage.setItem(workspace.id, serialized);
|
|
} catch (err) {
|
|
console.error("Failed to save workspace:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scan localStorage and return all entries that appear to be workspaces.
|
|
* We ignore keys starting with "route_" which are used for routing state.
|
|
*/
|
|
export function listWorkspaces(): Workspace[] {
|
|
const workspaces: Workspace[] = [];
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (!key) continue;
|
|
if (key.startsWith("route_")) continue;
|
|
|
|
const raw = localStorage.getItem(key);
|
|
if (!raw) continue;
|
|
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
if (
|
|
parsed &&
|
|
typeof parsed === "object" &&
|
|
typeof parsed.id === "string" &&
|
|
typeof parsed.name === "string" &&
|
|
parsed.entities &&
|
|
parsed.layout
|
|
) {
|
|
workspaces.push(parsed as Workspace);
|
|
}
|
|
} catch {
|
|
// ignore non-json values
|
|
}
|
|
}
|
|
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,
|
|
};
|
|
}
|