From b7a1815c62ca2ec0a797e5d04d8ef1b737d37231 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 5 Jul 2025 19:47:57 +0200 Subject: [PATCH] Add SQL view and use it --- api/src/database.types.ts | 2 +- ui/src/components/CreateTabloModal.tsx | 5 +- ui/src/components/DeleteTabloModal.tsx | 4 +- ui/src/components/TabloModal.tsx | 3 +- ui/src/hooks/tablos.ts | 18 ++-- ui/src/pages/tablo.tsx | 122 ++++++++++++++++--------- ui/src/types/database.types.ts | 2 +- ui/src/types/removeNull.ts | 11 +++ ui/src/types/tablos.types.ts | 15 +++ 9 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 ui/src/types/removeNull.ts create mode 100644 ui/src/types/tablos.types.ts diff --git a/api/src/database.types.ts b/api/src/database.types.ts index e12cbaf..8398ee5 100644 --- a/api/src/database.types.ts +++ b/api/src/database.types.ts @@ -240,9 +240,9 @@ export type Database = { image: string | null is_admin: boolean | null name: string | null - owner_id: string | null position: number | null status: string | null + user_id: string | null } Relationships: [] } diff --git a/ui/src/components/CreateTabloModal.tsx b/ui/src/components/CreateTabloModal.tsx index 5f3b617..d8beade 100644 --- a/ui/src/components/CreateTabloModal.tsx +++ b/ui/src/components/CreateTabloModal.tsx @@ -10,10 +10,7 @@ type StatusType = "todo" | "in_progress" | "done"; interface CreateTabloModalProps { onClose: () => void; onCreate: ( - tabloData: Omit< - Tablo, - "id" | "owner_id" | "created_at" | "deleted_at" | "position" - > + tabloData: Pick ) => void; } diff --git a/ui/src/components/DeleteTabloModal.tsx b/ui/src/components/DeleteTabloModal.tsx index faea2be..78fb0cb 100644 --- a/ui/src/components/DeleteTabloModal.tsx +++ b/ui/src/components/DeleteTabloModal.tsx @@ -1,7 +1,5 @@ import { ClickOutside } from "./ClickOutside"; -import { Database } from "@ui/types/database.types"; - -type Tablo = Database["public"]["Tables"]["tablos"]["Row"]; +import { Tablo } from "@ui/types/tablos.types"; interface DeleteTabloModalProps { tablo: Tablo | null; diff --git a/ui/src/components/TabloModal.tsx b/ui/src/components/TabloModal.tsx index d406f14..644df40 100644 --- a/ui/src/components/TabloModal.tsx +++ b/ui/src/components/TabloModal.tsx @@ -2,10 +2,9 @@ import { ClickOutside } from "./ClickOutside"; import { useState } from "react"; import { ImageColorPicker } from "./ImageColorPicker"; import { StatusPicker } from "./StatusPicker"; -import { Database } from "@ui/types/database.types"; import { useInviteUser } from "@ui/hooks/invite"; +import { Tablo } from "@ui/types/tablos.types"; -type Tablo = Database["public"]["Tables"]["tablos"]["Row"]; type StatusType = "todo" | "in_progress" | "done"; interface TabloModalProps { diff --git a/ui/src/hooks/tablos.ts b/ui/src/hooks/tablos.ts index 91e3f5a..638e6ec 100644 --- a/ui/src/hooks/tablos.ts +++ b/ui/src/hooks/tablos.ts @@ -4,24 +4,26 @@ import { supabase } from "./auth"; import { useSession } from "@ui/contexts/SessionContext"; import { api } from "@ui/lib/api"; import { toast } from "@ui/ui-library/toast/toast-queue"; +import { RemoveNullFromObject } from "@ui/types/removeNull"; type Tablo = Database["public"]["Tables"]["tablos"]; type TabloInsert = Tablo["Insert"]; type TabloUpdate = Tablo["Update"]; +type UserTablo = RemoveNullFromObject< + Database["public"]["Views"]["user_tablos"]["Row"] +>; + // Fetch all tablos export const useTablosList = () => { return useQuery({ queryKey: ["tablos"], queryFn: async () => { - const { data, error } = await supabase - .from("tablos") - .select("*") - .is("deleted_at", null) - .order("position", { ascending: true }); + const { data, error } = await supabase.from("user_tablos").select("*"); if (error) throw error; - return data; + const tablos = data as UserTablo[]; + return tablos; }, }); }; @@ -48,7 +50,9 @@ export const useCreateTablo = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (tablo: Omit) => { + mutationFn: async ( + tablo: Pick + ) => { const { data } = await api.post("/api/v1/tablos/create", tablo, { headers: { Authorization: `Bearer ${session?.access_token}`, diff --git a/ui/src/pages/tablo.tsx b/ui/src/pages/tablo.tsx index cfb5190..7eee52f 100644 --- a/ui/src/pages/tablo.tsx +++ b/ui/src/pages/tablo.tsx @@ -9,11 +9,8 @@ import { useUpdateTablo, useDeleteTablo, } from "@ui/hooks/tablos"; -import { Database } from "@ui/types/database.types"; import { LoadingSpinner } from "@ui/components/LoadingSpinner"; -import { useSession } from "@ui/contexts/SessionContext"; - -type Tablo = Database["public"]["Tables"]["tablos"]["Row"]; +import { Tablo } from "@ui/types/tablos.types"; export const TabloPage = () => { const [contextMenuTablo, setContextMenuTablo] = useState(null); @@ -22,7 +19,6 @@ export const TabloPage = () => { const [deletingTablo, setDeletingTablo] = useState(null); const [isDeleting, setIsDeleting] = useState(false); - const { session } = useSession(); const { data: tablos, isLoading, error } = useTablosList(); const createTabloMutation = useCreateTablo(); const { mutateAsync: updateTablo } = useUpdateTablo(); @@ -43,10 +39,7 @@ export const TabloPage = () => { }; const createNewTablo = async ( - tabloData: Omit< - Tablo, - "id" | "owner_id" | "created_at" | "deleted_at" | "position" - > + tabloData: Pick ) => { try { await createTabloMutation.mutateAsync(tabloData); @@ -151,13 +144,11 @@ export const TabloPage = () => { }; const getUserRole = (tablo: Tablo) => { - if (!session?.user) return "Invité"; - return tablo.owner_id === session.user.id ? "Admin" : "Invité"; + return tablo.is_admin ? "Admin" : "Invité"; }; const getRoleColor = (tablo: Tablo) => { - if (!session?.user) return "text-gray-500 dark:text-gray-400"; - return tablo.owner_id === session.user.id + return tablo.is_admin ? "text-blue-600 dark:text-blue-400" : "text-gray-500 dark:text-gray-400"; }; @@ -256,21 +247,36 @@ export const TabloPage = () => { } const renderTablo = (tablo: Tablo) => { + const isAdmin = tablo.is_admin; + return (
{ e.preventDefault(); - setContextMenuTablo(contextMenuTablo === tablo.id ? null : tablo.id); + // Only show context menu if user is admin + if (isAdmin) { + setContextMenuTablo( + contextMenuTablo === tablo.id ? null : tablo.id + ); + } }} >
{ - e.stopPropagation(); - openTablo(tablo.id); - }} + className={`bg-white dark:bg-gray-800 rounded-lg shadow-lg transition-all duration-300 w-64 overflow-hidden border border-gray-200 dark:border-gray-700 ${ + isAdmin + ? "hover:shadow-xl cursor-pointer" + : "cursor-default opacity-75" + }`} + onClick={ + isAdmin + ? (e) => { + e.stopPropagation(); + openTablo(tablo.id); + } + : undefined + } > {/* Image or Color */}
@@ -292,30 +298,58 @@ export const TabloPage = () => {
)} - {/* Trash Icon */} - + + + + + )} + + {/* Read-only indicator for non-admins */} + {!isAdmin && ( +
+ + + + +
+ )}
{/* Content */} @@ -353,8 +387,8 @@ export const TabloPage = () => {
- {/* Contextual Menu */} - {contextMenuTablo === tablo.id && ( + {/* Contextual Menu - Only show for admins */} + {isAdmin && contextMenuTablo === tablo.id && (
e.stopPropagation()} diff --git a/ui/src/types/database.types.ts b/ui/src/types/database.types.ts index e12cbaf..8398ee5 100644 --- a/ui/src/types/database.types.ts +++ b/ui/src/types/database.types.ts @@ -240,9 +240,9 @@ export type Database = { image: string | null is_admin: boolean | null name: string | null - owner_id: string | null position: number | null status: string | null + user_id: string | null } Relationships: [] } diff --git a/ui/src/types/removeNull.ts b/ui/src/types/removeNull.ts new file mode 100644 index 0000000..eeccef3 --- /dev/null +++ b/ui/src/types/removeNull.ts @@ -0,0 +1,11 @@ +/** + * Utility type to remove null from a type + */ +export type RemoveNull = T extends null ? never : T; + +/** + * Utility type to remove null from all properties of an object type + */ +export type RemoveNullFromObject = { + [L in keyof T]: L extends K ? RemoveNull : T[L]; +}; diff --git a/ui/src/types/tablos.types.ts b/ui/src/types/tablos.types.ts new file mode 100644 index 0000000..79b87e4 --- /dev/null +++ b/ui/src/types/tablos.types.ts @@ -0,0 +1,15 @@ +import { Database } from "@ui/types/database.types"; +import { RemoveNullFromObject } from "@ui/types/removeNull"; + +export type Tablo = RemoveNullFromObject< + Database["public"]["Views"]["user_tablos"]["Row"], + | "id" + | "access_level" + | "is_admin" + | "created_at" + | "deleted_at" + | "position" + | "user_id" + | "name" + | "status" +>;