Mega push vol 5, working on messaging now

This commit is contained in:
AlacrisDevs
2026-02-07 01:31:55 +02:00
parent d8bbfd9dc3
commit e55881b38b
77 changed files with 8478 additions and 1554 deletions

View File

@@ -15,6 +15,15 @@
import type { Document } from "$lib/supabase/types";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
import * as m from "$lib/paraglide/messages";
import { logActivity } from "$lib/api/activity";
import {
moveDocument,
updateDocument,
deleteDocument,
createDocument,
copyDocument,
} from "$lib/api/documents";
const log = createLogger("component.file-browser");
@@ -32,7 +41,7 @@
documents = $bindable(),
currentFolderId,
user,
title = "Files",
title = m.files_title(),
}: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
@@ -43,7 +52,19 @@
let editingDoc = $state<Document | null>(null);
let newDocName = $state("");
let newDocType = $state<"folder" | "document" | "kanban">("document");
let viewMode = $state<"list" | "grid">("grid");
let viewMode = $state<"list" | "grid">(
typeof localStorage !== "undefined" &&
localStorage.getItem("root:viewMode") === "list"
? "list"
: "grid",
);
function toggleViewMode() {
viewMode = viewMode === "list" ? "grid" : "list";
if (typeof localStorage !== "undefined") {
localStorage.setItem("root:viewMode", viewMode);
}
}
// Context menu state
let contextMenu = $state<{ x: number; y: number; doc: Document } | null>(
@@ -171,23 +192,11 @@
if (!contextMenu || !user) return;
const doc = contextMenu.doc;
closeContextMenu();
const { data: newDoc, error } = await supabase
.from("documents")
.insert({
org_id: org.id,
name: `${doc.name} (copy)`,
type: doc.type,
parent_id: doc.parent_id,
created_by: user.id,
content: doc.content,
})
.select()
.single();
if (!error && newDoc) {
documents = [...documents, newDoc as Document];
try {
const newDoc = await copyDocument(supabase, doc, org.id, user.id);
documents = [...documents, newDoc];
toasts.success(`Copied "${doc.name}"`);
} else if (error) {
log.error("Failed to copy document", { error });
} catch {
toasts.error("Failed to copy document");
}
}
@@ -294,22 +303,15 @@
documents = documents.map((d) =>
d.id === docId ? { ...d, parent_id: newParentId } : d,
);
const { error } = await supabase
.from("documents")
.update({
parent_id: newParentId,
updated_at: new Date().toISOString(),
})
.eq("id", docId);
if (error) {
log.error("Failed to move document", {
error,
data: { docId, newParentId },
});
try {
await moveDocument(supabase, docId, newParentId);
} catch {
toasts.error("Failed to move file");
const { data: freshDocs } = await supabase
.from("documents")
.select("*")
.select(
"id, name, type, parent_id, path, position, created_at, updated_at, created_by, org_id",
)
.eq("org_id", org.id)
.order("name");
if (freshDocs) documents = freshDocs as Document[];
@@ -319,67 +321,71 @@
async function handleCreate() {
if (!newDocName.trim() || !user) return;
if (newDocType === "kanban") {
const { data: newBoard, error: boardError } = await supabase
.from("kanban_boards")
.insert({ org_id: org.id, name: newDocName })
.select()
.single();
if (boardError || !newBoard) {
toasts.error("Failed to create kanban board");
return;
}
await supabase.from("kanban_columns").insert([
{ board_id: newBoard.id, name: "To Do", position: 0 },
{ board_id: newBoard.id, name: "In Progress", position: 1 },
{ board_id: newBoard.id, name: "Done", position: 2 },
]);
const { data: newDoc, error } = await supabase
.from("documents")
.insert({
id: newBoard.id,
org_id: org.id,
name: newDocName,
type: "kanban",
parent_id: currentFolderId,
created_by: user.id,
content: {
type: "kanban",
board_id: newBoard.id,
} as import("$lib/supabase/types").Json,
})
.select()
.single();
if (!error && newDoc) {
goto(getFileUrl(newDoc as Document));
} else if (error) {
toasts.error("Failed to create kanban document");
}
} else {
let content: any = null;
if (newDocType === "document") {
content = { type: "doc", content: [] };
}
const { data: newDoc, error } = await supabase
.from("documents")
.insert({
org_id: org.id,
name: newDocName,
type: newDocType as "folder" | "document",
parent_id: currentFolderId,
created_by: user.id,
content,
})
.select()
.single();
if (!error && newDoc) {
documents = [...documents, newDoc as Document];
if (newDocType === "document") {
goto(getFileUrl(newDoc as Document));
try {
if (newDocType === "kanban") {
// Create kanban board first, then link as document
const { data: newBoard, error: boardError } = await supabase
.from("kanban_boards")
.insert({ org_id: org.id, name: newDocName })
.select()
.single();
if (boardError || !newBoard) {
toasts.error("Failed to create kanban board");
return;
}
await supabase.from("kanban_columns").insert([
{ board_id: newBoard.id, name: "To Do", position: 0 },
{ board_id: newBoard.id, name: "In Progress", position: 1 },
{ board_id: newBoard.id, name: "Done", position: 2 },
]);
const newDoc = await createDocument(
supabase,
org.id,
newDocName,
"kanban",
currentFolderId,
user.id,
{
id: newBoard.id,
content: {
type: "kanban",
board_id: newBoard.id,
} as import("$lib/supabase/types").Json,
},
);
logActivity(supabase, {
orgId: org.id,
userId: user.id,
action: "create",
entityType: "kanban_board",
entityId: newDoc.id,
entityName: newDocName,
});
goto(getFileUrl(newDoc));
} else {
const newDoc = await createDocument(
supabase,
org.id,
newDocName,
newDocType,
currentFolderId,
user.id,
);
documents = [...documents, newDoc];
logActivity(supabase, {
orgId: org.id,
userId: user.id,
action: "create",
entityType: newDocType === "folder" ? "folder" : "document",
entityId: newDoc.id,
entityName: newDocName,
});
if (newDocType === "document") {
goto(getFileUrl(newDoc));
}
} else if (error) {
toasts.error("Failed to create document");
}
} catch {
toasts.error("Failed to create document");
}
showCreateModal = false;
@@ -389,28 +395,39 @@
async function handleSave(content: import("$lib/supabase/types").Json) {
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,
);
try {
await updateDocument(supabase, selectedDoc.id, { content });
documents = documents.map((d) =>
d.id === selectedDoc!.id ? { ...d, content } : d,
);
} catch {
toasts.error("Failed to save document");
}
}
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) {
try {
await updateDocument(supabase, editingDoc.id, { name: newDocName });
if (user) {
logActivity(supabase, {
orgId: org.id,
userId: user.id,
action: "rename",
entityType:
editingDoc.type === "folder" ? "folder" : "document",
entityId: editingDoc.id,
entityName: newDocName,
});
}
documents = documents.map((d) =>
d.id === editingDoc!.id ? { ...d, name: newDocName } : d,
);
if (selectedDoc?.id === editingDoc.id) {
selectedDoc = { ...selectedDoc, name: newDocName };
}
} catch {
toasts.error("Failed to rename document");
}
showEditModal = false;
editingDoc = null;
@@ -435,21 +452,30 @@
return ids;
}
if (doc.type === "folder") {
const descendantIds = collectDescendantIds(doc.id);
if (descendantIds.length > 0) {
await supabase
.from("documents")
.delete()
.in("id", descendantIds);
try {
if (doc.type === "folder") {
const descendantIds = collectDescendantIds(doc.id);
for (const id of descendantIds) {
await deleteDocument(supabase, id);
}
}
}
await deleteDocument(supabase, doc.id);
const { error } = await supabase
.from("documents")
.delete()
.eq("id", doc.id);
if (!error) {
if (user) {
logActivity(supabase, {
orgId: org.id,
userId: user.id,
action: "delete",
entityType:
doc.type === "folder"
? "folder"
: doc.type === "kanban"
? "kanban_board"
: "document",
entityId: doc.id,
entityName: doc.name,
});
}
const deletedIds = new Set([
doc.id,
...(doc.type === "folder" ? collectDescendantIds(doc.id) : []),
@@ -458,6 +484,8 @@
if (selectedDoc?.id === doc.id) {
selectedDoc = null;
}
} catch {
toasts.error("Failed to delete document");
}
}
</script>
@@ -471,12 +499,8 @@
<header class="flex items-center gap-2 p-1">
<Avatar name={title} size="md" />
<h1 class="flex-1 font-heading text-h1 text-white">{title}</h1>
<Button size="md" onclick={handleAdd}>+ New</Button>
<IconButton
title="Toggle view"
onclick={() =>
(viewMode = viewMode === "list" ? "grid" : "list")}
>
<Button size="md" onclick={handleAdd}>{m.btn_new()}</Button>
<IconButton title={m.files_toggle_view()} onclick={toggleViewMode}>
<Icon
name={viewMode === "list" ? "grid_view" : "view_list"}
size={24}
@@ -668,7 +692,7 @@
<Modal
isOpen={showCreateModal}
onClose={() => (showCreateModal = false)}
title="Create New"
title={m.files_create_title()}
>
<div class="space-y-4">
<div class="flex gap-2">
@@ -685,7 +709,7 @@
style="font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>description</span
>
Document
{m.files_type_document()}
</button>
<button
type="button"
@@ -700,7 +724,7 @@
style="font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>folder</span
>
Folder
{m.files_type_folder()}
</button>
<button
type="button"
@@ -715,24 +739,24 @@
style="font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>view_kanban</span
>
Kanban
{m.files_type_kanban()}
</button>
</div>
<Input
label="Name"
label={m.files_name_label()}
bind:value={newDocName}
placeholder={newDocType === "folder"
? "Folder name"
? m.files_folder_placeholder()
: newDocType === "kanban"
? "Kanban board name"
: "Document name"}
? m.files_kanban_placeholder()
: m.files_doc_placeholder()}
/>
<div class="flex justify-end gap-2 pt-2">
<Button variant="tertiary" onclick={() => (showCreateModal = false)}
>Cancel</Button
>{m.btn_cancel()}</Button
>
<Button onclick={handleCreate} disabled={!newDocName.trim()}
>Create</Button
>{m.btn_create()}</Button
>
</div>
</div>
@@ -757,7 +781,7 @@
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>edit</span
>
Rename
{m.files_context_rename()}
</button>
<button
type="button"
@@ -837,7 +861,7 @@
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>delete</span
>
Delete
{m.files_context_delete()}
</button>
</div>
{/if}
@@ -849,13 +873,13 @@
editingDoc = null;
newDocName = "";
}}
title="Rename"
title={m.files_rename_title()}
>
<div class="space-y-4">
<Input
label="Name"
label={m.files_name_label()}
bind:value={newDocName}
placeholder="Enter new name"
placeholder={m.files_name_label()}
/>
<div class="flex justify-end gap-2 pt-2">
<Button
@@ -864,10 +888,10 @@
showEditModal = false;
editingDoc = null;
newDocName = "";
}}>Cancel</Button
}}>{m.btn_cancel()}</Button
>
<Button onclick={handleRename} disabled={!newDocName.trim()}
>Save</Button
>{m.btn_save()}</Button
>
</div>
</div>