Mega push vol 4

This commit is contained in:
AlacrisDevs
2026-02-06 16:08:40 +02:00
parent b517bb975c
commit d8bbfd9dc3
95 changed files with 8019 additions and 3946 deletions

View File

@@ -1,15 +1,24 @@
import type { PageServerLoad } from './$types';
import { createLogger } from '$lib/utils/logger';
const log = createLogger('page.documents');
export const load: PageServerLoad = async ({ parent, locals }) => {
const { org } = await parent();
const { supabase } = locals;
const { data: documents } = await supabase
const { data: documents, error } = await supabase
.from('documents')
.select('*')
.eq('org_id', org.id)
.order('name');
if (error) {
log.error('Failed to load documents', { error, data: { orgId: org.id } });
}
log.debug('Documents loaded', { data: { count: documents?.length ?? 0 } });
return {
documents: documents ?? []
};

View File

@@ -1,11 +1,6 @@
<script lang="ts">
import { getContext } from "svelte";
import { Button, Modal, Input } from "$lib/components/ui";
import { FileTree, Editor } from "$lib/components/documents";
import { buildDocumentTree } from "$lib/api/documents";
import { FileBrowser } from "$lib/components/documents";
import type { Document } from "$lib/supabase/types";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
interface Props {
data: {
@@ -17,326 +12,21 @@
let { data }: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
let documents = $state(data.documents);
let selectedDoc = $state<Document | null>(null);
let showCreateModal = $state(false);
let showEditModal = $state(false);
let editingDoc = $state<Document | null>(null);
let newDocName = $state("");
let newDocType = $state<"folder" | "document">("document");
let parentFolderId = $state<string | null>(null);
let isEditing = $state(false);
const documentTree = $derived(buildDocumentTree(documents));
function handleSelect(doc: Document) {
if (doc.type === "document") {
selectedDoc = doc;
}
}
function handleDoubleClick(doc: Document) {
if (doc.type === "document") {
// Open document in new window
const url = `/${data.org.slug}/documents/${doc.id}`;
window.open(url, "_blank", "width=900,height=700");
}
}
function handleAdd(folderId: string | null) {
parentFolderId = folderId;
showCreateModal = true;
}
async function handleMove(docId: string, newParentId: string | null) {
const { error } = await supabase
.from("documents")
.update({
parent_id: newParentId,
updated_at: new Date().toISOString(),
})
.eq("id", docId);
if (!error) {
documents = documents.map((d) =>
d.id === docId ? { ...d, parent_id: newParentId } : d,
);
}
}
async function handleCreate() {
if (!newDocName.trim() || !data.user) return;
const { data: newDoc, error } = await supabase
.from("documents")
.insert({
org_id: data.org.id,
name: newDocName,
type: newDocType,
parent_id: parentFolderId,
created_by: data.user.id,
content:
newDocType === "document"
? { type: "doc", content: [] }
: null,
})
.select()
.single();
if (!error && newDoc) {
documents = [...documents, newDoc];
if (newDocType === "document") {
selectedDoc = newDoc;
}
}
showCreateModal = false;
newDocName = "";
newDocType = "document";
parentFolderId = null;
}
async function handleSave(content: unknown) {
if (!selectedDoc) return;
await supabase
.from("documents")
.update({ content, updated_at: new Date().toISOString() })
.eq("id", selectedDoc.id);
documents = documents.map((d) =>
d.id === selectedDoc!.id ? { ...d, content } : d,
);
}
function handleEdit(doc: Document) {
editingDoc = doc;
newDocName = doc.name;
showEditModal = true;
}
async function handleRename() {
if (!editingDoc || !newDocName.trim()) return;
const { error } = await supabase
.from("documents")
.update({ name: newDocName, updated_at: new Date().toISOString() })
.eq("id", editingDoc.id);
if (!error) {
documents = documents.map((d) =>
d.id === editingDoc!.id ? { ...d, name: newDocName } : d,
);
if (selectedDoc?.id === editingDoc.id) {
selectedDoc = { ...selectedDoc, name: newDocName };
}
}
showEditModal = false;
editingDoc = null;
newDocName = "";
}
async function handleDelete(doc: Document) {
const itemType =
doc.type === "folder" ? "folder and all its contents" : "document";
if (!confirm(`Delete this ${itemType}?`)) return;
// If deleting a folder, delete all children first
if (doc.type === "folder") {
const childIds = documents
.filter((d) => d.parent_id === doc.id)
.map((d) => d.id);
if (childIds.length > 0) {
await supabase.from("documents").delete().in("id", childIds);
}
}
const { error } = await supabase
.from("documents")
.delete()
.eq("id", doc.id);
if (!error) {
documents = documents.filter(
(d) => d.id !== doc.id && d.parent_id !== doc.id,
);
if (selectedDoc?.id === doc.id) {
selectedDoc = null;
}
}
}
$effect(() => {
documents = data.documents;
});
</script>
<svelte:head>
<title
>{selectedDoc ? `${selectedDoc.name} - ` : ""}Documents - {data.org
.name} | Root</title
>
<title>Files - {data.org.name} | Root</title>
</svelte:head>
<div class="flex h-full">
<aside class="w-72 border-r border-light/10 flex flex-col">
<div
class="p-4 border-b border-light/10 flex items-center justify-between"
>
<h2 class="font-semibold text-light">Documents</h2>
<Button size="sm" onclick={() => (showCreateModal = true)}>
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</Button>
</div>
<div class="flex-1 overflow-y-auto p-2">
{#if documentTree.length === 0}
<div class="text-center text-light/40 py-8 text-sm">
<p>No documents yet</p>
<p class="mt-1">Create your first document</p>
</div>
{:else}
<FileTree
items={documentTree}
selectedId={selectedDoc?.id ?? null}
onSelect={handleSelect}
onDoubleClick={handleDoubleClick}
onAdd={handleAdd}
onMove={handleMove}
onEdit={handleEdit}
onDelete={handleDelete}
/>
{/if}
</div>
</aside>
<main class="flex-1 overflow-hidden flex flex-col">
{#if selectedDoc}
<div
class="flex items-center justify-between p-4 border-b border-light/10"
>
<h2 class="text-lg font-semibold text-light">
{selectedDoc.name}
</h2>
<button
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {isEditing
? 'bg-primary text-white'
: 'bg-light/10 text-light hover:bg-light/20'}"
onclick={() => (isEditing = !isEditing)}
>
{isEditing ? "Preview" : "Edit"}
</button>
</div>
<div class="flex-1 overflow-auto">
<Editor
document={selectedDoc}
onSave={handleSave}
editable={isEditing}
/>
</div>
{:else}
<div class="h-full flex items-center justify-center text-light/40">
<div class="text-center">
<svg
class="w-16 h-16 mx-auto mb-4 opacity-50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
/>
<polyline points="14,2 14,8 20,8" />
</svg>
<p>Select a document to edit</p>
</div>
</div>
{/if}
</main>
<div class="h-full p-4 lg:p-5">
<FileBrowser
org={data.org}
bind:documents
currentFolderId={null}
user={data.user}
/>
</div>
<Modal
isOpen={showCreateModal}
onClose={() => (showCreateModal = false)}
title="Create New"
>
<div class="space-y-4">
<div class="flex gap-2">
<button
class="flex-1 py-2 px-4 rounded-lg border transition-colors {newDocType ===
'document'
? 'border-primary bg-primary/10'
: 'border-light/20'}"
onclick={() => (newDocType = "document")}
>
Document
</button>
<button
class="flex-1 py-2 px-4 rounded-lg border transition-colors {newDocType ===
'folder'
? 'border-primary bg-primary/10'
: 'border-light/20'}"
onclick={() => (newDocType = "folder")}
>
Folder
</button>
</div>
<Input
label="Name"
bind:value={newDocName}
placeholder={newDocType === "folder"
? "Folder name"
: "Document name"}
/>
<div class="flex justify-end gap-2 pt-2">
<Button variant="ghost" onclick={() => (showCreateModal = false)}
>Cancel</Button
>
<Button onclick={handleCreate} disabled={!newDocName.trim()}
>Create</Button
>
</div>
</div>
</Modal>
<Modal
isOpen={showEditModal}
onClose={() => {
showEditModal = false;
editingDoc = null;
newDocName = "";
}}
title="Rename"
>
<div class="space-y-4">
<Input
label="Name"
bind:value={newDocName}
placeholder="Enter new name"
/>
<div class="flex justify-end gap-2 pt-2">
<Button
variant="ghost"
onclick={() => {
showEditModal = false;
editingDoc = null;
newDocName = "";
}}>Cancel</Button
>
<Button onclick={handleRename} disabled={!newDocName.trim()}
>Save</Button
>
</div>
</div>
</Modal>

View File

@@ -0,0 +1,31 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { createLogger } from '$lib/utils/logger';
const log = createLogger('page.document');
export const load: PageServerLoad = async ({ parent, locals, params }) => {
const { org } = await parent() as { org: { id: string; slug: string } };
const { supabase } = locals;
const { id } = params;
log.debug('Redirecting document by ID', { data: { id, orgId: org.id } });
const { data: document, error: docError } = await supabase
.from('documents')
.select('type')
.eq('org_id', org.id)
.eq('id', id)
.single();
if (docError || !document) {
log.error('Document not found', { error: docError, data: { id, orgId: org.id } });
throw error(404, 'Document not found');
}
if (document.type === 'folder') {
throw redirect(302, `/${org.slug}/documents/folder/${id}`);
}
throw redirect(302, `/${org.slug}/documents/file/${id}`);
};

View File

@@ -0,0 +1,9 @@
<!-- This route redirects to /folder/[id] or /file/[id] via +page.server.ts -->
<div class="flex items-center justify-center h-full">
<span
class="material-symbols-rounded text-primary animate-spin"
style="font-size: 40px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 40;"
>
progress_activity
</span>
</div>

View File

@@ -0,0 +1,39 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { createLogger } from '$lib/utils/logger';
const log = createLogger('page.file');
export const load: PageServerLoad = async ({ parent, locals, params }) => {
const { org, user } = await parent() as { org: { id: string; slug: string }; user: { id: string } | null };
const { supabase } = locals;
const { id } = params;
log.debug('Loading file by ID', { data: { id, orgId: org.id } });
const { data: document, error: docError } = await supabase
.from('documents')
.select('*')
.eq('org_id', org.id)
.eq('id', id)
.single();
if (docError || !document) {
log.error('File not found', { error: docError, data: { id, orgId: org.id } });
throw error(404, 'File not found');
}
if (document.type === 'folder') {
throw redirect(302, `/${org.slug}/documents/folder/${id}`);
}
const isKanban = document.type === 'kanban';
return {
document,
isKanban,
isFolder: false,
children: [],
user
};
};

View File

@@ -0,0 +1,572 @@
<script lang="ts">
import { getContext, onDestroy, onMount } from "svelte";
import { Button, Modal, Input } from "$lib/components/ui";
import { DocumentViewer } from "$lib/components/documents";
import { KanbanBoard, CardDetailModal } from "$lib/components/kanban";
import {
fetchBoardWithColumns,
createColumn,
moveCard,
deleteCard,
deleteColumn,
subscribeToBoard,
} from "$lib/api/kanban";
import {
getLockInfo,
acquireLock,
releaseLock,
startHeartbeat,
type LockInfo,
} from "$lib/api/document-locks";
import { createLogger } from "$lib/utils/logger";
import { toasts } from "$lib/stores/toast.svelte";
import type {
RealtimeChannel,
SupabaseClient,
} from "@supabase/supabase-js";
import type { Database, KanbanCard, Document } from "$lib/supabase/types";
import type { BoardWithColumns } from "$lib/api/kanban";
const log = createLogger("page.file-viewer");
interface Props {
data: {
org: { id: string; name: string; slug: string };
document: Document;
isKanban: boolean;
isFolder: boolean;
children: any[];
user: { id: string } | null;
};
}
let { data }: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
let isSaving = $state(false);
// Document lock state
let lockInfo = $state<LockInfo>({
isLocked: false,
lockedBy: null,
lockedByName: null,
isOwnLock: false,
});
let hasLock = $state(false);
let stopHeartbeat: (() => void) | null = null;
// Acquire lock for document editing (not for kanban)
onMount(async () => {
if (data.isKanban || !data.user) return;
// Check current lock status
lockInfo = await getLockInfo(supabase, data.document.id, data.user.id);
if (lockInfo.isLocked && !lockInfo.isOwnLock) {
// Someone else is editing
return;
}
// Try to acquire lock
const acquired = await acquireLock(
supabase,
data.document.id,
data.user.id,
);
if (acquired) {
hasLock = true;
stopHeartbeat = startHeartbeat(
supabase,
data.document.id,
data.user.id,
);
} else {
// Refresh lock info to get who holds it
lockInfo = await getLockInfo(
supabase,
data.document.id,
data.user.id,
);
}
});
// Kanban state
let kanbanBoard = $state<BoardWithColumns | null>(null);
let realtimeChannel = $state<RealtimeChannel | null>(null);
let showCardModal = $state(false);
let selectedCard = $state<KanbanCard | null>(null);
let targetColumnId = $state<string | null>(null);
let cardModalMode = $state<"edit" | "create">("edit");
let showAddColumnModal = $state(false);
let newColumnName = $state("");
async function handleSave(content: import("$lib/supabase/types").Json) {
isSaving = true;
try {
await supabase
.from("documents")
.update({
content,
updated_at: new Date().toISOString(),
})
.eq("id", data.document.id);
} catch (err) {
log.error("Failed to save document", { error: err });
toasts.error("Failed to save document");
}
isSaving = false;
}
// Kanban functions
async function loadKanbanBoard() {
if (!data.isKanban) return;
try {
const content = data.document.content as Record<
string,
unknown
> | null;
const boardId = (content?.board_id as string) || data.document.id;
let board = await fetchBoardWithColumns(supabase, boardId).catch(
() => null,
);
if (!board) {
log.info("Auto-creating kanban_boards entry for document", {
data: { boardId, docId: data.document.id },
});
const { data: newBoard, error: createErr } = await supabase
.from("kanban_boards")
.insert({
id: data.document.id,
org_id: data.org.id,
name: data.document.name,
})
.select()
.single();
if (createErr) {
log.error("Failed to auto-create kanban board", {
error: createErr,
});
toasts.error("Failed to load kanban board");
return;
}
await supabase.from("kanban_columns").insert([
{ board_id: data.document.id, name: "To Do", position: 0 },
{
board_id: data.document.id,
name: "In Progress",
position: 1,
},
{ board_id: data.document.id, name: "Done", position: 2 },
]);
await supabase
.from("documents")
.update({
content: {
type: "kanban",
board_id: data.document.id,
} as import("$lib/supabase/types").Json,
})
.eq("id", data.document.id);
board = await fetchBoardWithColumns(
supabase,
data.document.id,
).catch(() => null);
}
kanbanBoard = board;
} catch (err) {
log.error("Failed to load kanban board", { error: err });
toasts.error("Failed to load kanban board");
}
}
$effect(() => {
if (data.isKanban) {
loadKanbanBoard();
}
});
$effect(() => {
if (!kanbanBoard) return;
const channel = subscribeToBoard(
supabase,
kanbanBoard.id,
() => loadKanbanBoard(),
() => loadKanbanBoard(),
);
realtimeChannel = channel;
return () => {
if (channel) {
supabase.removeChannel(channel);
}
};
});
onDestroy(() => {
if (realtimeChannel) {
supabase.removeChannel(realtimeChannel);
}
// Release document lock
if (hasLock && data.user) {
stopHeartbeat?.();
releaseLock(supabase, data.document.id, data.user.id);
}
});
async function handleCardMove(
cardId: string,
toColumnId: string,
toPosition: number,
) {
try {
await moveCard(supabase, cardId, toColumnId, toPosition);
} catch (err) {
log.error("Failed to move card", { error: err });
toasts.error("Failed to move card");
}
}
function handleCardClick(card: KanbanCard) {
selectedCard = card;
cardModalMode = "edit";
showCardModal = true;
}
function handleAddCard(columnId: string) {
targetColumnId = columnId;
selectedCard = null;
cardModalMode = "create";
showCardModal = true;
}
async function handleAddColumn() {
if (!kanbanBoard || !newColumnName.trim()) return;
try {
await createColumn(
supabase,
kanbanBoard.id,
newColumnName,
kanbanBoard.columns.length,
);
newColumnName = "";
showAddColumnModal = false;
await loadKanbanBoard();
} catch (err) {
log.error("Failed to add column", { error: err });
toasts.error("Failed to add column");
}
}
async function handleDeleteColumn(columnId: string) {
if (!confirm("Delete this column and all its cards?")) return;
try {
await deleteColumn(supabase, columnId);
await loadKanbanBoard();
} catch (err) {
log.error("Failed to delete column", { error: err });
toasts.error("Failed to delete column");
}
}
async function handleDeleteCard(cardId: string) {
try {
await deleteCard(supabase, cardId);
await loadKanbanBoard();
} catch (err) {
log.error("Failed to delete card", { error: err });
toasts.error("Failed to delete card");
}
}
// JSON Import for kanban board
let fileInput = $state<HTMLInputElement | null>(null);
let isImporting = $state(false);
function triggerImport() {
fileInput?.click();
}
async function handleJsonImport(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file || !kanbanBoard) return;
isImporting = true;
try {
const text = await file.text();
const json = JSON.parse(text);
// Support two formats:
// 1. Full board export: { columns: [{ name, cards: [{ title, description, ... }] }] }
// 2. Flat card list: [{ title, description, column?, ... }]
if (Array.isArray(json)) {
// Flat card list — add all to first column
const firstCol = kanbanBoard.columns[0];
if (!firstCol) {
toasts.error("No columns exist to import cards into");
return;
}
let pos = firstCol.cards.length;
for (const card of json) {
await supabase.from("kanban_cards").insert({
column_id: firstCol.id,
title: card.title || "Untitled",
description: card.description || null,
priority: card.priority || null,
due_date: card.due_date || null,
position: pos++,
created_by: data.user?.id ?? null,
});
}
toasts.success(`Imported ${json.length} cards`);
} else if (json.columns && Array.isArray(json.columns)) {
// Full board format with columns
let colPos = kanbanBoard.columns.length;
for (const col of json.columns) {
// Check if column already exists by name
let targetCol = kanbanBoard.columns.find(
(c) =>
c.name.toLowerCase() ===
(col.name || "").toLowerCase(),
);
if (!targetCol) {
const { data: newCol, error: colErr } = await supabase
.from("kanban_columns")
.insert({
board_id: kanbanBoard.id,
name: col.name || `Column ${colPos}`,
position: colPos++,
})
.select()
.single();
if (colErr || !newCol) continue;
targetCol = { ...newCol, cards: [] };
}
if (col.cards && Array.isArray(col.cards)) {
let cardPos = targetCol.cards?.length ?? 0;
for (const card of col.cards) {
await supabase.from("kanban_cards").insert({
column_id: targetCol.id,
title: card.title || "Untitled",
description: card.description || null,
priority: card.priority || null,
due_date: card.due_date || null,
color: card.color || null,
position: cardPos++,
created_by: data.user?.id ?? null,
});
}
}
}
const totalCards = json.columns.reduce(
(sum: number, c: any) => sum + (c.cards?.length ?? 0),
0,
);
toasts.success(
`Imported ${json.columns.length} columns, ${totalCards} cards`,
);
} else {
toasts.error(
"Unrecognized JSON format. Expected { columns: [...] } or [{ title, ... }]",
);
return;
}
await loadKanbanBoard();
} catch (err) {
log.error("JSON import failed", { error: err });
toasts.error("Failed to import JSON — check file format");
} finally {
isImporting = false;
input.value = "";
}
}
function handleExportJson() {
if (!kanbanBoard) return;
const exportData = {
board: kanbanBoard.name,
columns: kanbanBoard.columns.map((col) => ({
name: col.name,
cards: col.cards.map((card) => ({
title: card.title,
description: card.description,
priority: card.priority,
due_date: card.due_date,
color: card.color,
assignee_id: card.assignee_id,
})),
})),
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${kanbanBoard.name || "board"}.json`;
a.click();
URL.revokeObjectURL(url);
toasts.success("Board exported as JSON");
}
</script>
<svelte:head>
<title>{data.document.name} - {data.org.name} | Root</title>
</svelte:head>
<div class="flex flex-col h-full p-4 lg:p-5 gap-4">
{#if data.isKanban}
<!-- Kanban: needs its own header since DocumentViewer is for documents -->
<input
type="file"
accept=".json"
class="hidden"
bind:this={fileInput}
onchange={handleJsonImport}
/>
<header class="flex items-center gap-2 p-1">
<h1 class="flex-1 font-heading text-h1 text-white truncate">
{data.document.name}
</h1>
<Button
variant="tertiary"
size="sm"
icon="upload"
onclick={triggerImport}
loading={isImporting}
>
Import JSON
</Button>
<Button
variant="tertiary"
size="sm"
icon="download"
onclick={handleExportJson}
>
Export JSON
</Button>
</header>
<div class="flex-1 overflow-auto min-h-0">
<div class="h-full">
{#if kanbanBoard}
<KanbanBoard
columns={kanbanBoard.columns}
onCardClick={handleCardClick}
onCardMove={handleCardMove}
onAddCard={handleAddCard}
onAddColumn={() => (showAddColumnModal = true)}
onDeleteCard={handleDeleteCard}
onDeleteColumn={handleDeleteColumn}
canEdit={true}
/>
{:else}
<div class="flex items-center justify-center h-full">
<div class="text-center">
<span
class="material-symbols-rounded text-light/30 animate-spin mb-4"
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;"
>
progress_activity
</span>
<p class="text-light/50">Loading board...</p>
</div>
</div>
{/if}
</div>
</div>
{:else}
<!-- Document Editor: use shared DocumentViewer component -->
<DocumentViewer
document={data.document}
onSave={handleSave}
mode="edit"
locked={lockInfo.isLocked && !lockInfo.isOwnLock}
lockedByName={lockInfo.lockedByName}
/>
{/if}
<!-- Status Bar -->
{#if isSaving}
<div class="text-body-sm text-light/50">Saving...</div>
{/if}
</div>
<!-- Kanban Card Detail Modal -->
{#if showCardModal}
<CardDetailModal
isOpen={showCardModal}
card={selectedCard}
mode={cardModalMode}
onClose={() => {
showCardModal = false;
selectedCard = null;
targetColumnId = null;
}}
onUpdate={(updatedCard) => {
if (kanbanBoard) {
kanbanBoard = {
...kanbanBoard,
columns: kanbanBoard.columns.map((col) => ({
...col,
cards: col.cards.map((c) =>
c.id === updatedCard.id ? updatedCard : c,
),
})),
};
}
}}
onDelete={(cardId) => handleDeleteCard(cardId)}
columnId={targetColumnId ?? undefined}
userId={data.user?.id}
orgId={data.org.id}
onCreate={(newCard) => {
loadKanbanBoard();
showCardModal = false;
selectedCard = null;
targetColumnId = null;
}}
/>
{/if}
<!-- Add Column Modal -->
<Modal
isOpen={showAddColumnModal}
onClose={() => {
showAddColumnModal = false;
newColumnName = "";
}}
title="Add Column"
>
<div class="space-y-4">
<Input
label="Column Name"
bind:value={newColumnName}
placeholder="e.g., To Do, In Progress, Done"
/>
<div class="flex justify-end gap-2 pt-2">
<Button
variant="tertiary"
onclick={() => (showAddColumnModal = false)}
>
Cancel
</Button>
<Button onclick={handleAddColumn} disabled={!newColumnName.trim()}>
Add Column
</Button>
</div>
</div>
</Modal>

View File

@@ -0,0 +1,43 @@
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { createLogger } from '$lib/utils/logger';
const log = createLogger('page.folder');
export const load: PageServerLoad = async ({ parent, locals, params }) => {
const { org, user } = await parent() as { org: { id: string; slug: string }; user: { id: string } | null };
const { supabase } = locals;
const { id } = params;
log.debug('Loading folder by ID', { data: { id, orgId: org.id } });
const { data: document, error: docError } = await supabase
.from('documents')
.select('*')
.eq('org_id', org.id)
.eq('id', id)
.single();
if (docError || !document) {
log.error('Folder not found', { error: docError, data: { id, orgId: org.id } });
throw error(404, 'Folder not found');
}
if (document.type !== 'folder') {
log.error('Document is not a folder', { data: { id, type: document.type } });
throw error(404, 'Not a folder');
}
// Load all documents in this org (for breadcrumb building and file listing)
const { data: allDocuments } = await supabase
.from('documents')
.select('*')
.eq('org_id', org.id)
.order('name');
return {
folder: document,
documents: allDocuments ?? [],
user
};
};

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { FileBrowser } from "$lib/components/documents";
import type { Document } from "$lib/supabase/types";
interface Props {
data: {
org: { id: string; name: string; slug: string };
folder: Document;
documents: Document[];
user: { id: string } | null;
};
}
let { data }: Props = $props();
let documents = $state(data.documents);
$effect(() => {
documents = data.documents;
});
const currentFolderId = $derived(data.folder.id);
</script>
<svelte:head>
<title>{data.folder.name} - {data.org.name} | Root</title>
</svelte:head>
<div class="h-full p-4 lg:p-5">
<FileBrowser
org={data.org}
bind:documents
{currentFolderId}
user={data.user}
/>
</div>