Improve invited mode
This commit is contained in:
parent
e6dddc5734
commit
ba8615e062
15 changed files with 490 additions and 119 deletions
|
|
@ -2,10 +2,10 @@ import { SessionProvider } from "@xtablo/shared/contexts/SessionContext";
|
|||
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
|
||||
import { Toaster } from "@xtablo/ui/components/sonner";
|
||||
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
|
||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||
import { publicRoutes } from "./lib/publicRoutes";
|
||||
import { routes } from "./lib/routes";
|
||||
import { supabase } from "./lib/supabase";
|
||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||
import { DatadogRumProvider } from "./providers/DatadogRumProvider";
|
||||
import { UserStoreProvider } from "./providers/UserStoreProvider";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { Event, EventInsert } from "@xtablo/shared/types/events.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { DatePicker } from "@xtablo/ui/components/date-picker";
|
||||
|
|
@ -26,7 +27,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { useCreateEvents, useEvent, useUpdateEvent } from "../hooks/events";
|
||||
import { useTablosList } from "../hooks/tablos";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { useIsReadOnlyUser, useUser } from "../providers/UserStoreProvider";
|
||||
|
||||
export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
||||
const { t, i18n } = useTranslation("components");
|
||||
|
|
@ -34,6 +35,7 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
const { data: event } = useEvent(event_id as string);
|
||||
|
||||
const user = useUser();
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
const [searchParams] = useSearchParams();
|
||||
const tablo_id = searchParams.get("tablo_id");
|
||||
const dateFromParams = searchParams.get("date");
|
||||
|
|
@ -237,6 +239,18 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isReadOnly) {
|
||||
toast.add(
|
||||
{
|
||||
title: t("eventModal.errors.readOnly"),
|
||||
description:
|
||||
"Vous êtes en mode lecture seule. Vous ne pouvez pas modifier ou créer d'événement.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
const eventName = formEvent?.title.trim() || t("eventModal.untitled");
|
||||
if (mode === "edit" && event) {
|
||||
updateEvent.mutate(
|
||||
|
|
@ -247,7 +261,7 @@ export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
|
|||
createEvents({ ...formEvent, title: eventName }, { onSuccess: () => onClose() });
|
||||
}
|
||||
}}
|
||||
disabled={!formEvent?.tablo_id}
|
||||
disabled={!formEvent?.tablo_id || isReadOnly}
|
||||
>
|
||||
{mode === "edit" ? t("eventModal.buttons.edit") : t("eventModal.buttons.save")}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { Link as RouterLink, useLocation } from "react-router-dom";
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import { useLogout } from "../hooks/auth";
|
||||
import { isProd, isStaging } from "../lib/env";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { useIsReadOnlyUser, useUser } from "../providers/UserStoreProvider";
|
||||
import { getXtabloIcon } from "../utils/iconHelpers";
|
||||
import { ThemeSwitcher } from "./ThemeSwitcher";
|
||||
|
||||
|
|
@ -276,6 +276,7 @@ export const SideNavigation = ({ isMobileMenuOpen }: { isMobileMenuOpen: boolean
|
|||
|
||||
export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
||||
const location = useLocation();
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
const { t } = useTranslation("navigation");
|
||||
|
||||
type List<T> = T[];
|
||||
|
|
@ -291,47 +292,61 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
| {
|
||||
isHorizontalBar: boolean;
|
||||
}
|
||||
> = [
|
||||
{
|
||||
path: "/",
|
||||
label: t("projects"),
|
||||
icon: <PanelsTopLeft className="w-5 h-5" />,
|
||||
},
|
||||
{ isHorizontalBar: true },
|
||||
{
|
||||
path: "/events",
|
||||
label: t("myEvents"),
|
||||
icon: <CalendarCheckIcon className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/kanban",
|
||||
label: t("kanban"),
|
||||
icon: <Kanban className="w-5 h-5" />,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
path: "/chantiers",
|
||||
label: t("chantiers"),
|
||||
icon: <ConstructionIcon className="w-5 h-5" />,
|
||||
isDisabled: true,
|
||||
},
|
||||
{ isHorizontalBar: true },
|
||||
{
|
||||
path: "/planning",
|
||||
label: t("planning"),
|
||||
icon: <SquareKanban className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/chat",
|
||||
label: t("discussions"),
|
||||
icon: <MessageCircleIcon className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/notes",
|
||||
label: t("notes"),
|
||||
icon: <FileTextIcon className="w-5 h-5" />,
|
||||
},
|
||||
];
|
||||
> = isReadOnly
|
||||
? [
|
||||
{
|
||||
path: "/",
|
||||
label: t("projects"),
|
||||
icon: <PanelsTopLeft className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/planning",
|
||||
label: t("planning"),
|
||||
icon: <SquareKanban className="w-5 h-5" />,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
path: "/",
|
||||
label: t("projects"),
|
||||
icon: <PanelsTopLeft className="w-5 h-5" />,
|
||||
},
|
||||
{ isHorizontalBar: true },
|
||||
{
|
||||
path: "/events",
|
||||
label: t("myEvents"),
|
||||
icon: <CalendarCheckIcon className="w-5 h-5" />,
|
||||
isDisabled: isReadOnly,
|
||||
},
|
||||
{
|
||||
path: "/kanban",
|
||||
label: t("kanban"),
|
||||
icon: <Kanban className="w-5 h-5" />,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
path: "/chantiers",
|
||||
label: t("chantiers"),
|
||||
icon: <ConstructionIcon className="w-5 h-5" />,
|
||||
isDisabled: true,
|
||||
},
|
||||
{ isHorizontalBar: true },
|
||||
{
|
||||
path: "/planning",
|
||||
label: t("planning"),
|
||||
icon: <SquareKanban className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/chat",
|
||||
label: t("discussions"),
|
||||
icon: <MessageCircleIcon className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
path: "/notes",
|
||||
label: t("notes"),
|
||||
icon: <FileTextIcon className="w-5 h-5" />,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<nav className="flex flex-1 flex-col" aria-label="Primary navigation">
|
||||
<ul role="list" className={twMerge("grid py-3", isCollapsed ? "pl-2.5 pr-3" : "")}>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,19 @@ import { useMaybeUser } from "../providers/UserStoreProvider";
|
|||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
// Fallback URL
|
||||
fallback?: string;
|
||||
// Only allow regular users (not temporary)
|
||||
onlyRegularUser?: boolean;
|
||||
// Redirect to current page
|
||||
shouldRedirectToCurrentPage?: boolean;
|
||||
}
|
||||
|
||||
export const ProtectedRoute = ({ fallback, shouldRedirectToCurrentPage }: ProtectedRouteProps) => {
|
||||
export const ProtectedRoute = ({
|
||||
fallback,
|
||||
onlyRegularUser,
|
||||
shouldRedirectToCurrentPage,
|
||||
}: ProtectedRouteProps) => {
|
||||
const user = useMaybeUser();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
|
|
@ -30,15 +38,19 @@ export const ProtectedRoute = ({ fallback, shouldRedirectToCurrentPage }: Protec
|
|||
status = "should-land-user";
|
||||
} else if (!user) {
|
||||
status = "should-redirect";
|
||||
} else if (onlyRegularUser && user.is_temporary) {
|
||||
status = "should-redirect";
|
||||
} else {
|
||||
status = "should-pass";
|
||||
}
|
||||
|
||||
const fallbackUrl = fallback ?? "/login";
|
||||
|
||||
const redirectUrl = shouldRedirectToCurrentPage
|
||||
? `${fallback ?? "/login"}?redirect=${encodeURIComponent(
|
||||
? `${fallbackUrl}?redirect=${encodeURIComponent(
|
||||
`${window.location.pathname}${window.location.search}`
|
||||
)}`
|
||||
: (fallback ?? "/login");
|
||||
: fallbackUrl;
|
||||
|
||||
return match(status)
|
||||
.with("loading", () => <LoadingSpinner />)
|
||||
|
|
|
|||
176
apps/main/src/components/TabloEventsSection.tsx
Normal file
176
apps/main/src/components/TabloEventsSection.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { Calendar, Clock, Plus } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEventsByTablo } from "../hooks/events";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
||||
interface TabloEventsSectionProps {
|
||||
tablo: UserTablo;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export const TabloEventsSection = ({ tablo }: TabloEventsSectionProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { data: events, isLoading, error } = useEventsByTablo(tablo.id);
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
|
||||
// Filter upcoming events (events in the future or today)
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const upcomingEvents = events
|
||||
?.filter((event) => {
|
||||
const eventDate = new Date(event.start_date);
|
||||
return eventDate >= today;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const dateCompare = new Date(a.start_date).getTime() - new Date(b.start_date).getTime();
|
||||
if (dateCompare !== 0) return dateCompare;
|
||||
return (a.start_time || "").localeCompare(b.start_time || "");
|
||||
});
|
||||
|
||||
const handleCreateEvent = () => {
|
||||
navigate(`/events/create?tablo_id=${tablo.id}`);
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return new Intl.DateTimeFormat("fr-FR", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
if (!timeStr) return "";
|
||||
return timeStr.slice(0, 5); // HH:MM
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Événements à venir</h1>
|
||||
<p className="text-muted-foreground mt-1">Gérez les événements futurs de ce tablo</p>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<Button onClick={handleCreateEvent} className="flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Créer un événement
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Events List */}
|
||||
<div className="bg-card rounded-lg border border-border">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
||||
<span className="ml-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
Chargement des événements...
|
||||
</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-6">
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-red-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-red-700 dark:text-red-300">
|
||||
Erreur lors du chargement des événements
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : upcomingEvents && upcomingEvents.length > 0 ? (
|
||||
<div className="divide-y divide-border">
|
||||
{upcomingEvents.map((event) => (
|
||||
<div key={event.event_id} className="p-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
{/* Date Badge */}
|
||||
<div className="shrink-0 w-16 h-16 bg-primary/10 dark:bg-primary/20 rounded-lg flex flex-col items-center justify-center">
|
||||
<span className="text-xs font-medium text-primary uppercase">
|
||||
{new Date(event.start_date).toLocaleDateString("fr-FR", {
|
||||
month: "short",
|
||||
})}
|
||||
</span>
|
||||
<span className="text-2xl font-bold text-primary">
|
||||
{new Date(event.start_date).getDate()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">{event.title}</h3>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span className="capitalize">{formatDate(event.start_date)}</span>
|
||||
</div>
|
||||
|
||||
{event.start_time && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>
|
||||
{formatTime(event.start_time)}
|
||||
{event.end_time && ` - ${formatTime(event.end_time)}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{event.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
|
||||
{event.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<svg
|
||||
className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Aucun événement à venir pour ce tablo
|
||||
</p>
|
||||
{!isReadOnly && (
|
||||
<Button onClick={handleCreateEvent} variant="outline" className="mt-4">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Créer le premier événement
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,14 +2,14 @@ import { toast } from "@xtablo/shared";
|
|||
import { UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { DownloadIcon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { FileTrigger } from "react-aria-components";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
useCreateTabloFile,
|
||||
useDeleteTabloFile,
|
||||
useDownloadTabloFile,
|
||||
useTabloFileNames,
|
||||
} from "../hooks/tablo_data";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
||||
interface TabloFilesSectionProps {
|
||||
tablo: UserTablo;
|
||||
|
|
@ -28,13 +28,15 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
||||
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const createFile = useCreateTabloFile();
|
||||
const deleteFile = useDeleteTabloFile();
|
||||
const downloadFile = useDownloadTabloFile();
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
const file = files?.[0];
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file size (20MB limit)
|
||||
|
|
@ -70,6 +72,9 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
// Reset upload state
|
||||
setSelectedFile(null);
|
||||
setIsUploading(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
} catch (uploadError) {
|
||||
setIsUploading(false);
|
||||
console.error("Upload error:", uploadError);
|
||||
|
|
@ -104,6 +109,9 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
|
||||
const cancelFileUpload = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = async (fileName: string) => {
|
||||
|
|
@ -183,8 +191,8 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* File Upload Section - Only for Admins */}
|
||||
{isAdmin && (
|
||||
{/* File Upload Section - Only for Admins and non-read-only users */}
|
||||
{isAdmin && !isReadOnly && (
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<svg
|
||||
|
|
@ -205,33 +213,39 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-3">
|
||||
<FileTrigger allowsMultiple={false} onSelect={handleFileSelect}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-500 bg-gray-50 dark:bg-gray-800/50 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<svg
|
||||
className="w-8 h-8 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<div className="text-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Cliquez pour sélectionner un fichier
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
accept="*/*"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-full justify-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-500 bg-gray-50 dark:bg-gray-800/50 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors rounded-md cursor-pointer inline-flex items-center"
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<svg
|
||||
className="w-8 h-8 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<div className="text-center">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Cliquez pour sélectionner un fichier
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</FileTrigger>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -444,7 +458,7 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
<DownloadIcon className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
{isAdmin && !isReadOnly && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
@ -487,7 +501,7 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) =>
|
|||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Aucun fichier dans ce tablo</p>
|
||||
{isAdmin && (
|
||||
{isAdmin && !isReadOnly && (
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
Ajoutez votre premier fichier ci-dessus
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export const routes: RouteObject[] = [
|
|||
{
|
||||
path: "events",
|
||||
element: <EventsPage />,
|
||||
children: [{ index: true }, { path: "create", element: <EventModal mode="create" /> }],
|
||||
},
|
||||
{
|
||||
path: "feedback",
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { EventTypeCard } from "../components/EventTypeCard";
|
||||
import { EventTypeConfig, useEventTypes } from "../hooks/event-types";
|
||||
import { useEventsByTablo } from "../hooks/events";
|
||||
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
||||
type BookingStatus = "all" | "upcoming" | "past";
|
||||
|
||||
|
|
@ -40,6 +41,8 @@ interface BookingStatusOption {
|
|||
export function EventsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation(["pages", "common"]);
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
const [activeTab, setActiveTab] = useState<"events" | "event-types">("events");
|
||||
|
||||
const statusOptions: BookingStatusOption[] = [
|
||||
{ id: "upcoming", name: t("pages:events.filters.upcoming") },
|
||||
|
|
@ -137,6 +140,18 @@ export function EventsPage() {
|
|||
|
||||
// Event Types handlers
|
||||
const handleCreateEventType = () => {
|
||||
if (isReadOnly) {
|
||||
toast.add(
|
||||
{
|
||||
title: t("common:error"),
|
||||
description:
|
||||
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer de type d'événement.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
setEditingEventType(null);
|
||||
setEventTypeFormData({
|
||||
name: "",
|
||||
|
|
@ -238,10 +253,21 @@ export function EventsPage() {
|
|||
};
|
||||
|
||||
const handleCreateEvent = () => {
|
||||
if (isReadOnly) {
|
||||
toast.add(
|
||||
{
|
||||
title: t("common:error"),
|
||||
description: "Vous êtes en mode lecture seule. Vous ne pouvez pas créer d'événement.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
const today = new Date();
|
||||
const dateParam = today.toISOString();
|
||||
const tabloParam = selectedTabloId !== "all" ? `&tablo_id=${selectedTabloId}` : "";
|
||||
navigate(`/planning/create?date=${dateParam}${tabloParam}`);
|
||||
navigate(`/events/create?date=${dateParam}${tabloParam}`);
|
||||
};
|
||||
|
||||
const canEditEvent = (event: EventAndTablo) => {
|
||||
|
|
@ -275,21 +301,32 @@ export function EventsPage() {
|
|||
|
||||
{/* Main Content with Tabs */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<Tabs defaultValue="events" className="w-full">
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="events">{t("pages:events.tabs.events")}</TabsTrigger>
|
||||
<TabsTrigger value="event-types">{t("pages:events.tabs.eventTypes")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as "events" | "event-types")}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="events">{t("pages:events.tabs.events")}</TabsTrigger>
|
||||
<TabsTrigger value="event-types">{t("pages:events.tabs.eventTypes")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Events Tab */}
|
||||
<TabsContent value="events" className="space-y-6">
|
||||
<div className="flex items-center justify-end mb-4">
|
||||
<Button onClick={handleCreateEvent}>
|
||||
{activeTab === "events" ? (
|
||||
<Button onClick={handleCreateEvent} disabled={isReadOnly}>
|
||||
<CalendarIcon className="w-4 h-4 mr-2" />
|
||||
{t("pages:events.createEvent")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={handleCreateEventType} disabled={isReadOnly}>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
{t("pages:events.createEventType")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Events Tab */}
|
||||
<TabsContent value="events" className="space-y-6">
|
||||
{/* Filters */}
|
||||
<div className="bg-card rounded-lg shadow-sm border border-border p-6">
|
||||
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center">
|
||||
|
|
@ -573,12 +610,6 @@ export function EventsPage() {
|
|||
|
||||
{/* Event Types Tab */}
|
||||
<TabsContent value="event-types" className="space-y-6">
|
||||
<div className="flex items-center justify-end mb-4">
|
||||
<Button size="lg" variant="default" onClick={handleCreateEventType}>
|
||||
<PlusIcon className="w-4 h-4 mr-2" /> {t("pages:events.createEventType")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{eventTypesData?.map((eventType) => (
|
||||
<EventTypeCard
|
||||
|
|
@ -594,7 +625,12 @@ export function EventsPage() {
|
|||
<Text className="text-muted-foreground mb-4">
|
||||
{t("pages:events.eventTypes.emptyState.title")}
|
||||
</Text>
|
||||
<Button variant="default" size="lg" onClick={handleCreateEventType}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={handleCreateEventType}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />{" "}
|
||||
{t("pages:events.eventTypes.emptyState.button")}
|
||||
</Button>
|
||||
|
|
@ -625,6 +661,9 @@ export function EventsPage() {
|
|||
handleSaveEventType={handleSaveEventType}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{/* Render child routes (e.g. EventModal) */}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
useUpdateNote,
|
||||
useUpdateNoteSharing,
|
||||
} from "../hooks/notes";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
||||
const useNoteId = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -62,6 +63,7 @@ const useNoteId = () => {
|
|||
export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
||||
const { t } = useTranslation(["notes", "common"]);
|
||||
const { noteId, setNoteId, goCreateNote } = useNoteId();
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
|
|
@ -123,6 +125,15 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isReadOnly) {
|
||||
toast.add({
|
||||
title: t("common:error", "Error"),
|
||||
description:
|
||||
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer ou modifier de notes.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!content && !title) {
|
||||
toast.add({
|
||||
title: t("common:error", "Error"),
|
||||
|
|
@ -163,6 +174,14 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
};
|
||||
|
||||
const handleNewNote = () => {
|
||||
if (isReadOnly) {
|
||||
toast.add({
|
||||
title: t("common:error", "Error"),
|
||||
description: "Vous êtes en mode lecture seule. Vous ne pouvez pas créer de notes.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (hasUnsavedChanges) {
|
||||
const confirmed = window.confirm(t("unsavedChangesWarning"));
|
||||
if (!confirmed) return;
|
||||
|
|
@ -241,7 +260,12 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
)}
|
||||
</TypographyMuted>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleNewNote} className="mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleNewNote}
|
||||
className="mt-2"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<PlusIcon className="mr-2 size-4" />
|
||||
{t("createNewNote", "Create a new note")}
|
||||
</Button>
|
||||
|
|
@ -262,6 +286,7 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
setTitle(e.target.value);
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
className="border-0 text-2xl font-bold focus-visible:ring-0"
|
||||
/>
|
||||
{noteId && (
|
||||
|
|
@ -276,7 +301,7 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
},
|
||||
});
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
disabled={isDeleting || isReadOnly}
|
||||
title={t("deleteNote")}
|
||||
className="hover:text-red-500 hover:bg-red-50/10 dark:hover:bg-red-950/10"
|
||||
>
|
||||
|
|
@ -290,6 +315,7 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
key={editorKey}
|
||||
initialContent={note?.content ?? content}
|
||||
onChange={handleContentChange}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -406,7 +432,10 @@ export default function NotesPage({ mode }: { mode: "create" | "edit" }) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<Button onClick={handleSave} disabled={!hasUnsavedChanges || isCreating || isUpdating}>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasUnsavedChanges || isCreating || isUpdating || isReadOnly}
|
||||
>
|
||||
<SaveIcon className="mr-2 size-4" />
|
||||
{isCreating ? t("saving") : t("saveNote")}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ImportICSModal } from "@ui/components/ImportICSModal";
|
||||
import { WebcalModal } from "@ui/components/WebcalModal";
|
||||
import { downloadICSFile, generateICSFromEvents } from "@xtablo/shared";
|
||||
import { downloadICSFile, generateICSFromEvents, toast } from "@xtablo/shared";
|
||||
import { EventAndTablo } from "@xtablo/shared/types/events.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import {
|
||||
|
|
@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import { useDeleteEvent, useEventsByTablo } from "../hooks/events";
|
||||
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
||||
type ViewType = "month" | "week" | "day";
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ export const PlanningPage = () => {
|
|||
const [selectedTabloId, setSelectedTabloId] = useState<string>(tablo_id || "all");
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isWebcalModalOpen, setIsWebcalModalOpen] = useState(false);
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
|
||||
// Fetch tablos
|
||||
const { data: tablos, isLoading: tablosLoading } = useTablosList();
|
||||
|
|
@ -697,6 +699,18 @@ export const PlanningPage = () => {
|
|||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isReadOnly) {
|
||||
toast.add(
|
||||
{
|
||||
title: t("common:error"),
|
||||
description:
|
||||
"Vous êtes en mode lecture seule. Vous ne pouvez pas créer d'événement.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (selectedTabloId === "all") {
|
||||
navigate(`/planning/create?date=${currentDate.toISOString()}`);
|
||||
} else {
|
||||
|
|
@ -706,15 +720,31 @@ export const PlanningPage = () => {
|
|||
}
|
||||
}}
|
||||
className="w-full"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5 mr-2" />
|
||||
{t("planning:createEvent")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsImportModalOpen(true)}
|
||||
onClick={() => {
|
||||
if (isReadOnly) {
|
||||
toast.add(
|
||||
{
|
||||
title: t("common:error"),
|
||||
description:
|
||||
"Vous êtes en mode lecture seule. Vous ne pouvez pas importer de calendrier.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
setIsImportModalOpen(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full mt-2"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<FolderInputIcon className="w-5 h-5 mr-2" />
|
||||
{t("planning:importPlanning")}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { toast } from "@xtablo/shared";
|
||||
import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { ArrowLeft, BookOpen, FileText, MessageSquare, Settings } from "lucide-react";
|
||||
import { ArrowLeft, BookOpen, Calendar, 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 { TabloEventsSection } from "../components/TabloEventsSection";
|
||||
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" | "notes" | "settings";
|
||||
type TabSection = "files" | "discussion" | "notes" | "events" | "settings";
|
||||
|
||||
export const TabloDetailsPage = () => {
|
||||
const { tabloId } = useParams<{ tabloId: string }>();
|
||||
|
|
@ -99,6 +100,11 @@ export const TabloDetailsPage = () => {
|
|||
label: "Notes",
|
||||
icon: <BookOpen className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: "events",
|
||||
label: "Événements",
|
||||
icon: <Calendar className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "Paramètres",
|
||||
|
|
@ -177,6 +183,7 @@ export const TabloDetailsPage = () => {
|
|||
<TabloDiscussionSection tablo={tablo} isAdmin={isAdmin} />
|
||||
)}
|
||||
{activeSection === "notes" && <TabloNotesSection tablo={tablo} isAdmin={isAdmin} />}
|
||||
{activeSection === "events" && <TabloEventsSection tablo={tablo} isAdmin={isAdmin} />}
|
||||
{activeSection === "settings" && (
|
||||
<TabloSettingsSection tablo={tablo} isAdmin={isAdmin} onEdit={handleEdit} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { CreateTabloModal } from "@ui/components/CreateTabloModal";
|
||||
import { DeleteTabloModal } from "@ui/components/DeleteTabloModal";
|
||||
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { TabloInsert, UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import {
|
||||
|
|
@ -35,6 +36,7 @@ import { useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useCreateTablo, useDeleteTablo, useTablosList, useUpdateTablo } from "../hooks/tablos";
|
||||
import { useIsReadOnlyUser } from "../providers/UserStoreProvider";
|
||||
|
||||
type FilterOption = {
|
||||
id: "all" | "todo" | "inProgress" | "done";
|
||||
|
|
@ -54,6 +56,7 @@ export const TabloPage = () => {
|
|||
const [filterType, setFilterType] = useState<"all" | "todo" | "inProgress" | "done">("all");
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isReadOnly = useIsReadOnlyUser();
|
||||
|
||||
// Get view mode from URL params, default to "list"
|
||||
const viewMode = (searchParams.get("view") as "grid" | "list") || "list";
|
||||
|
|
@ -106,6 +109,17 @@ export const TabloPage = () => {
|
|||
];
|
||||
|
||||
const openCreateModal = () => {
|
||||
if (isReadOnly) {
|
||||
toast.add(
|
||||
{
|
||||
title: t("common:error"),
|
||||
description: "Vous êtes en mode lecture seule. Vous ne pouvez pas créer de tablo.",
|
||||
type: "error",
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
setIsCreateModalOpen(true);
|
||||
};
|
||||
|
||||
|
|
@ -238,7 +252,7 @@ export const TabloPage = () => {
|
|||
Gérez vos projets et collaborations
|
||||
</Text>
|
||||
</div>
|
||||
<Button onClick={openCreateModal}>
|
||||
<Button onClick={openCreateModal} disabled={isReadOnly}>
|
||||
<Plus /> Nouveau tablo
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -266,7 +280,7 @@ export const TabloPage = () => {
|
|||
Gérez vos projets et collaborations
|
||||
</Text>
|
||||
</div>
|
||||
<Button onClick={openCreateModal}>
|
||||
<Button onClick={openCreateModal} disabled={isReadOnly}>
|
||||
<Plus /> Nouveau tablo
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -693,7 +707,7 @@ export const TabloPage = () => {
|
|||
<Button
|
||||
id="create-tablo-button"
|
||||
onClick={openCreateModal}
|
||||
disabled={createTabloMutation.isPending}
|
||||
disabled={createTabloMutation.isPending || isReadOnly}
|
||||
>
|
||||
<Plus />
|
||||
{createTabloMutation.isPending
|
||||
|
|
@ -706,7 +720,7 @@ export const TabloPage = () => {
|
|||
</header>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
{/* KPI Section */}
|
||||
{kpis && (
|
||||
{kpis && !isReadOnly && (
|
||||
<div className="mb-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4">
|
||||
{/* Total Tablos */}
|
||||
|
|
@ -842,7 +856,12 @@ export const TabloPage = () => {
|
|||
</EmptyHeader>
|
||||
{filterType === "all" && (
|
||||
<EmptyContent>
|
||||
<Button variant="default" size="lg" onClick={openCreateModal}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={openCreateModal}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<Plus /> {t("pages:tablo.emptyState.button")}
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,14 @@ export const useMaybeUser = () => {
|
|||
return useStore(store);
|
||||
};
|
||||
|
||||
export const useIsReadOnlyUser = () => {
|
||||
const store = React.useContext(UserStoreContext);
|
||||
if (!store) {
|
||||
throw new Error("Missing UserStoreProvider");
|
||||
}
|
||||
return useStore(store).is_temporary;
|
||||
};
|
||||
|
||||
// TestUserStoreProvider component
|
||||
export const TestUserStoreProvider = ({
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -21,11 +21,18 @@ CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
|
|||
-- Enable Row Level Security
|
||||
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy to allow users to view their own notes
|
||||
CREATE POLICY "Users can view their own notes" ON notes
|
||||
-- Policy to allow users to view their own notes and public notes
|
||||
CREATE POLICY "Users can view their own notes and public notes" ON notes
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (user_id = (SELECT auth.uid()));
|
||||
TO authenticated, anon
|
||||
USING (
|
||||
user_id = (SELECT auth.uid())
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM shared_notes
|
||||
WHERE shared_notes.note_id = notes.id
|
||||
AND shared_notes.is_public = TRUE
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy to allow users to insert their own notes
|
||||
CREATE POLICY "Users can insert their own notes" ON notes
|
||||
|
|
@ -62,7 +69,7 @@ CREATE POLICY "Users can delete their own notes" ON notes
|
|||
|
||||
-- Add comments to document the table
|
||||
COMMENT ON TABLE notes IS
|
||||
'User notes with Row Level Security to ensure users can only access their own notes';
|
||||
'User notes with Row Level Security. Users can access their own notes and public notes (marked in shared_notes table)';
|
||||
|
||||
COMMENT ON COLUMN notes.id IS
|
||||
'Primary key: random 24-character alphanumeric string';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ CREATE POLICY "Users can view their own shared notes" ON shared_notes
|
|||
-- Policy to allow anonymous users to view public notes
|
||||
CREATE POLICY "Anyone can view public notes" ON shared_notes
|
||||
FOR SELECT
|
||||
TO anon
|
||||
TO anon, authenticated
|
||||
USING (is_public = TRUE);
|
||||
|
||||
-- Policy to allow users to insert their own shared_notes entries
|
||||
|
|
|
|||
Loading…
Reference in a new issue