From 415e4cafaaf00ca734eb34dcdef0167b3a841072 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 26 Oct 2025 09:12:21 +0100 Subject: [PATCH] Have notes in the tablos --- .../main/src/components/TabloNotesSection.tsx | 110 ++ apps/main/src/hooks/notes.ts | 61 +- apps/main/src/pages/tablo-details.tsx | 11 +- apps/main/stats.html | 2 +- packages/shared/src/types/database.types.ts | 1074 ++++++++--------- 5 files changed, 713 insertions(+), 545 deletions(-) create mode 100644 apps/main/src/components/TabloNotesSection.tsx diff --git a/apps/main/src/components/TabloNotesSection.tsx b/apps/main/src/components/TabloNotesSection.tsx new file mode 100644 index 0000000..6900663 --- /dev/null +++ b/apps/main/src/components/TabloNotesSection.tsx @@ -0,0 +1,110 @@ +import { UserTablo } from "@xtablo/shared/types/tablos.types"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@xtablo/ui/components/select"; +import { BookOpen } from "lucide-react"; +import { useState } from "react"; +import { useTabloNotes } from "../hooks/notes"; +import { LoadingSpinner } from "./LoadingSpinner"; +import { NotesEditor } from "./NotesEditor"; + +interface TabloNotesSectionProps { + tablo: UserTablo; + isAdmin: boolean; +} + +export const TabloNotesSection = ({ tablo }: TabloNotesSectionProps) => { + const { notes, isLoading } = useTabloNotes(tablo.id); + const [selectedNoteId, setSelectedNoteId] = useState(null); + + // Auto-select first note when notes are loaded + if (notes && notes.length > 0 && !selectedNoteId) { + setSelectedNoteId(notes[0].id); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + const selectedNote = notes?.find((note) => note.id === selectedNoteId); + + return ( +
+
+

Notes

+

Notes partagées avec ce tablo (lecture seule)

+
+ + {/* Empty State */} + {!notes || notes.length === 0 ? ( +
+
+ +
+

Aucune note partagée

+

+ Il n'y a actuellement aucune note partagée avec ce tablo. +

+
+ ) : ( +
+ {/* Note Selector */} +
+ + +
+ + {/* Selected Note Display */} + {selectedNote && ( +
+
+

+ {selectedNote.title || "Sans titre"} +

+

+ Dernière modification:{" "} + {selectedNote.updated_at && + new Date(selectedNote.updated_at).toLocaleDateString("fr-FR", { + day: "numeric", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + })} +

+
+
+ +
+
+ )} +
+ )} +
+ ); +}; diff --git a/apps/main/src/hooks/notes.ts b/apps/main/src/hooks/notes.ts index eed3a40..658f8ae 100644 --- a/apps/main/src/hooks/notes.ts +++ b/apps/main/src/hooks/notes.ts @@ -92,6 +92,7 @@ export function useUpdateNote() { .update({ title: input.title, content: input.content, + updated_at: new Date().toISOString(), }) .eq("id", input.id) .eq("user_id", user.id) @@ -102,6 +103,7 @@ export function useUpdateNote() { onSuccess: () => { // Invalidate both the specific note and notes list queryClient.invalidateQueries({ queryKey: ["notes"] }); + queryClient.invalidateQueries({ queryKey: ["tablo-notes"] }); }, }); } @@ -166,7 +168,11 @@ export function useNoteSharing(noteId: string | undefined) { }, enabled: !!noteId && !!user.id, }); - return { isPublic: data?.isPublic, isSharedWithAllTablos: data?.isSharedWithAllTablos, isLoading }; + return { + isPublic: data?.isPublic, + isSharedWithAllTablos: data?.isSharedWithAllTablos, + isLoading, + }; } /** @@ -248,6 +254,7 @@ export function useUpdateNoteSharing() { onSuccess: (_, variables) => { // Invalidate sharing queries queryClient.invalidateQueries({ queryKey: ["note-sharing", variables.noteId] }); + queryClient.invalidateQueries({ queryKey: ["tablo-notes"] }); }, }); } @@ -333,3 +340,55 @@ export function useSharedTabloNotes() { return { notes, isLoading }; } + +/** + * Hook to fetch notes shared with a specific tablo + */ +export function useTabloNotes(tabloId: string | undefined) { + const user = useUser(); + const { data: notes, isLoading } = useQuery({ + queryKey: ["tablo-notes", tabloId], + queryFn: async () => { + if (!tabloId) return []; + + // Find notes shared with this specific tablo or all tablos + const { data, error } = await supabase + .from("note_access") + .select( + ` + note_id, + notes!inner ( + id, + title, + content, + user_id, + created_at, + updated_at, + deleted_at + ) + ` + ) + .eq("is_active", true) + .or(`tablo_id.eq.${tabloId},tablo_id.is.null`) + .is("notes.deleted_at", null); + + if (error) throw error; + + // Extract notes from the join result and remove duplicates + type JoinedResult = { note_id: string; notes: Note | Note[] }; + const extractedNotes = (data as JoinedResult[]) + .map((item) => (Array.isArray(item.notes) ? item.notes[0] : item.notes)) + .filter((note) => note !== null && note !== undefined) as Note[]; + + // Remove duplicates by note id (in case a note is shared both with all tablos and this specific tablo) + const uniqueNotes = Array.from( + new Map(extractedNotes.map((note) => [note.id, note])).values() + ); + + return uniqueNotes; + }, + enabled: !!tabloId && !!user.id, + }); + + return { notes, isLoading }; +} diff --git a/apps/main/src/pages/tablo-details.tsx b/apps/main/src/pages/tablo-details.tsx index 632c079..37b16ce 100644 --- a/apps/main/src/pages/tablo-details.tsx +++ b/apps/main/src/pages/tablo-details.tsx @@ -1,16 +1,17 @@ import { toast } from "@xtablo/shared"; import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types"; import { Button } from "@xtablo/ui/components/button"; -import { ArrowLeft, FileText, MessageSquare, Settings } from "lucide-react"; +import { ArrowLeft, BookOpen, FileText, MessageSquare, Settings } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { LoadingSpinner } from "../components/LoadingSpinner"; import { TabloDiscussionSection } from "../components/TabloDiscussionSection"; import { TabloFilesSection } from "../components/TabloFilesSection"; +import { TabloNotesSection } from "../components/TabloNotesSection"; import { TabloSettingsSection } from "../components/TabloSettingsSection"; import { useTablosList, useUpdateTablo } from "../hooks/tablos"; -type TabSection = "files" | "discussion" | "settings"; +type TabSection = "files" | "discussion" | "notes" | "settings"; export const TabloDetailsPage = () => { const { tabloId } = useParams<{ tabloId: string }>(); @@ -93,6 +94,11 @@ export const TabloDetailsPage = () => { label: "Discussion", icon: , }, + { + id: "notes", + label: "Notes", + icon: , + }, { id: "settings", label: "Paramètres", @@ -170,6 +176,7 @@ export const TabloDetailsPage = () => { {activeSection === "discussion" && ( )} + {activeSection === "notes" && } {activeSection === "settings" && ( )} diff --git a/apps/main/stats.html b/apps/main/stats.html index 84b6825..8e3ac52 100644 --- a/apps/main/stats.html +++ b/apps/main/stats.html @@ -4929,7 +4929,7 @@ var drawChart = (function (exports) {