Add SQL view and use it

This commit is contained in:
Arthur Belleville 2025-07-05 19:47:57 +02:00
parent 73dbd6d928
commit b7a1815c62
No known key found for this signature in database
9 changed files with 120 additions and 62 deletions

View file

@ -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: []
}

View file

@ -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<Tablo, "name" | "color" | "image" | "status">
) => void;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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<TabloInsert, "owner_id">) => {
mutationFn: async (
tablo: Pick<TabloInsert, "name" | "color" | "image" | "status">
) => {
const { data } = await api.post("/api/v1/tablos/create", tablo, {
headers: {
Authorization: `Bearer ${session?.access_token}`,

View file

@ -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<string | null>(null);
@ -22,7 +19,6 @@ export const TabloPage = () => {
const [deletingTablo, setDeletingTablo] = useState<Tablo | null>(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<Tablo, "name" | "color" | "image" | "status">
) => {
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 (
<div
key={tablo.id}
className="relative"
onContextMenu={(e) => {
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
);
}
}}
>
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer w-64 overflow-hidden border border-gray-200 dark:border-gray-700"
onClick={(e) => {
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 */}
<div className="relative h-56 group">
@ -292,30 +298,58 @@ export const TabloPage = () => {
</div>
)}
{/* Trash Icon */}
<button
className="absolute top-2 right-2 p-1.5 bg-red-500 hover:bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10"
onClick={(e) => {
e.stopPropagation();
handleDeleteTablo(tablo.id);
}}
title="Supprimer le tablo"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{/* Trash Icon - Only show for admins */}
{isAdmin && (
<button
className="absolute top-2 right-2 p-1.5 bg-red-500 hover:bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10"
onClick={(e) => {
e.stopPropagation();
handleDeleteTablo(tablo.id);
}}
title="Supprimer le tablo"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
</button>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
</button>
)}
{/* Read-only indicator for non-admins */}
{!isAdmin && (
<div className="absolute top-2 right-2 p-1.5 bg-gray-500 text-white rounded-full opacity-80">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
</div>
)}
</div>
{/* Content */}
@ -353,8 +387,8 @@ export const TabloPage = () => {
</div>
</div>
{/* Contextual Menu */}
{contextMenuTablo === tablo.id && (
{/* Contextual Menu - Only show for admins */}
{isAdmin && contextMenuTablo === tablo.id && (
<div
className="absolute top-2 -right-3 bg-gray-50 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-30 min-w-36"
onClick={(e) => e.stopPropagation()}

View file

@ -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: []
}

View file

@ -0,0 +1,11 @@
/**
* Utility type to remove null from a type
*/
export type RemoveNull<T> = T extends null ? never : T;
/**
* Utility type to remove null from all properties of an object type
*/
export type RemoveNullFromObject<T, K extends keyof T = keyof T> = {
[L in keyof T]: L extends K ? RemoveNull<T[L]> : T[L];
};

View file

@ -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"
>;