Have notes in the tablos
This commit is contained in:
parent
39ec616c03
commit
415e4cafaa
5 changed files with 713 additions and 545 deletions
110
apps/main/src/components/TabloNotesSection.tsx
Normal file
110
apps/main/src/components/TabloNotesSection.tsx
Normal file
|
|
@ -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<string | null>(null);
|
||||
|
||||
// Auto-select first note when notes are loaded
|
||||
if (notes && notes.length > 0 && !selectedNoteId) {
|
||||
setSelectedNoteId(notes[0].id);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedNote = notes?.find((note) => note.id === selectedNoteId);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Notes</h1>
|
||||
<p className="text-muted-foreground mt-1">Notes partagées avec ce tablo (lecture seule)</p>
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
{!notes || notes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
|
||||
<BookOpen className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">Aucune note partagée</h3>
|
||||
<p className="text-muted-foreground max-w-md">
|
||||
Il n'y a actuellement aucune note partagée avec ce tablo.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Note Selector */}
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="text-sm font-medium text-foreground whitespace-nowrap">
|
||||
Sélectionner une note:
|
||||
</label>
|
||||
<Select value={selectedNoteId || undefined} onValueChange={setSelectedNoteId}>
|
||||
<SelectTrigger className="w-full max-w-md">
|
||||
<SelectValue placeholder="Choisir une note..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{notes.map((note) => (
|
||||
<SelectItem key={note.id} value={note.id}>
|
||||
{note.title || "Sans titre"}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Selected Note Display */}
|
||||
{selectedNote && (
|
||||
<div className="border border-border rounded-lg bg-card overflow-hidden">
|
||||
<div className="border-b border-border bg-muted/30 px-6 py-4">
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
{selectedNote.title || "Sans titre"}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
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",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-background">
|
||||
<NotesEditor
|
||||
key={selectedNote.id}
|
||||
initialContent={selectedNote.content || ""}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<Note[]>({
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <MessageSquare className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: "notes",
|
||||
label: "Notes",
|
||||
icon: <BookOpen className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "Paramètres",
|
||||
|
|
@ -170,6 +176,7 @@ export const TabloDetailsPage = () => {
|
|||
{activeSection === "discussion" && (
|
||||
<TabloDiscussionSection tablo={tablo} isAdmin={isAdmin} />
|
||||
)}
|
||||
{activeSection === "notes" && <TabloNotesSection tablo={tablo} isAdmin={isAdmin} />}
|
||||
{activeSection === "settings" && (
|
||||
<TabloSettingsSection tablo={tablo} isAdmin={isAdmin} onEdit={handleEdit} />
|
||||
)}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue