Mega push vol 5, working on messaging now
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user