Add SQL view and use it
This commit is contained in:
parent
73dbd6d928
commit
b7a1815c62
9 changed files with 120 additions and 62 deletions
|
|
@ -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: []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
}
|
||||
|
|
|
|||
11
ui/src/types/removeNull.ts
Normal file
11
ui/src/types/removeNull.ts
Normal 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];
|
||||
};
|
||||
15
ui/src/types/tablos.types.ts
Normal file
15
ui/src/types/tablos.types.ts
Normal 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"
|
||||
>;
|
||||
Loading…
Reference in a new issue