From bc28194d3db50b9254c1e34f1051813d3bbf4f57 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Wed, 15 Apr 2026 09:23:06 +0200 Subject: [PATCH] refactor: move tablo view components to packages/tablo-views Moves kanban, gantt, section components, chat hooks and extracted EtapesSection/RoadmapSection from apps/main into the new shared packages/tablo-views package. Components that previously depended on app-specific providers (UserStoreProvider, useIsReadOnlyUser, etc.) are refactored to receive data/callbacks as props, keeping the package free of apps/main dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/tablo-views/package.json | 3 +- .../tablo-views/src}/ChatMessages.tsx | 0 packages/tablo-views/src/ClickOutside.tsx | 37 ++ packages/tablo-views/src/EtapesSection.tsx | 366 ++++++++++++++++++ packages/tablo-views/src/ImageColorPicker.tsx | 114 ++++++ packages/tablo-views/src/RoadmapSection.tsx | 23 ++ .../src}/TabloDiscussionSection.tsx | 25 +- .../tablo-views/src}/TabloEventsSection.tsx | 89 ++++- .../tablo-views/src}/TabloFilesSection.tsx | 168 +++++--- .../tablo-views/src}/TabloHeaderActions.tsx | 63 ++- .../tablo-views/src}/TabloTasksSection.tsx | 103 +++-- .../src/components/gantt/GanttChart.tsx | 3 +- .../components/kanban/InlineTaskCreate.tsx | 18 - .../src/components/kanban/KanbanBoard.tsx | 0 .../src/components/kanban/KanbanColumn.tsx | 0 .../src/components/kanban/KanbanTaskCard.tsx | 0 .../src/components/kanban/TaskModal.tsx | 64 ++- .../src/components/kanban/types.ts | 0 .../tablo-views}/src/hooks/useChat.ts | 0 .../tablo-views}/src/hooks/useChatUnread.ts | 0 packages/tablo-views/src/index.ts | 29 +- packages/tablo-views/src/vite-env.d.ts | 1 + packages/tablo-views/tsconfig.json | 3 +- pnpm-lock.yaml | 55 +++ 24 files changed, 961 insertions(+), 203 deletions(-) rename {apps/main/src/components => packages/tablo-views/src}/ChatMessages.tsx (100%) create mode 100644 packages/tablo-views/src/ClickOutside.tsx create mode 100644 packages/tablo-views/src/EtapesSection.tsx create mode 100644 packages/tablo-views/src/ImageColorPicker.tsx create mode 100644 packages/tablo-views/src/RoadmapSection.tsx rename {apps/main/src/components => packages/tablo-views/src}/TabloDiscussionSection.tsx (68%) rename {apps/main/src/components => packages/tablo-views/src}/TabloEventsSection.tsx (78%) rename {apps/main/src/components => packages/tablo-views/src}/TabloFilesSection.tsx (90%) rename {apps/main/src/components => packages/tablo-views/src}/TabloHeaderActions.tsx (90%) rename {apps/main/src/components => packages/tablo-views/src}/TabloTasksSection.tsx (75%) rename {apps/main => packages/tablo-views}/src/components/gantt/GanttChart.tsx (99%) rename {apps/main => packages/tablo-views}/src/components/kanban/InlineTaskCreate.tsx (87%) rename {apps/main => packages/tablo-views}/src/components/kanban/KanbanBoard.tsx (100%) rename {apps/main => packages/tablo-views}/src/components/kanban/KanbanColumn.tsx (100%) rename {apps/main => packages/tablo-views}/src/components/kanban/KanbanTaskCard.tsx (100%) rename {apps/main => packages/tablo-views}/src/components/kanban/TaskModal.tsx (82%) rename {apps/main => packages/tablo-views}/src/components/kanban/types.ts (100%) rename {apps/main => packages/tablo-views}/src/hooks/useChat.ts (100%) rename {apps/main => packages/tablo-views}/src/hooks/useChatUnread.ts (100%) create mode 100644 packages/tablo-views/src/vite-env.d.ts diff --git a/packages/tablo-views/package.json b/packages/tablo-views/package.json index 464a36f..5de94b5 100644 --- a/packages/tablo-views/package.json +++ b/packages/tablo-views/package.json @@ -35,6 +35,7 @@ "@biomejs/biome": "2.2.5", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "vite": "^6.2.2" } } diff --git a/apps/main/src/components/ChatMessages.tsx b/packages/tablo-views/src/ChatMessages.tsx similarity index 100% rename from apps/main/src/components/ChatMessages.tsx rename to packages/tablo-views/src/ChatMessages.tsx diff --git a/packages/tablo-views/src/ClickOutside.tsx b/packages/tablo-views/src/ClickOutside.tsx new file mode 100644 index 0000000..f39778d --- /dev/null +++ b/packages/tablo-views/src/ClickOutside.tsx @@ -0,0 +1,37 @@ +import { useClickOutside } from "@xtablo/shared/hooks/useClickOutside"; +import React from "react"; + +interface ClickOutsideProps { + children: React.ReactNode; + onClickOutside: () => void; + className?: string; + disabled?: boolean; +} + +/** + * Component that wraps children and detects clicks outside + * @param children - The content to wrap + * @param onClickOutside - Function to call when clicking outside + * @param className - Optional className for the wrapper + * @param disabled - Disable click outside detection + */ +export const ClickOutside: React.FC = ({ + children, + onClickOutside, + className, + disabled = false, +}) => { + const ref = useClickOutside( + disabled + ? () => { + // Do nothing + } + : onClickOutside + ); + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/tablo-views/src/EtapesSection.tsx b/packages/tablo-views/src/EtapesSection.tsx new file mode 100644 index 0000000..c671847 --- /dev/null +++ b/packages/tablo-views/src/EtapesSection.tsx @@ -0,0 +1,366 @@ +import { cn } from "@xtablo/shared"; +import type { Etape, KanbanTask } from "@xtablo/shared-types"; +import { Button } from "@xtablo/ui/components/button"; +import { Input } from "@xtablo/ui/components/input"; +import { + CalendarIcon, + ChevronDownIcon, + ChevronRightIcon, + CircleCheckIcon, + ListChecksIcon, + PlusIcon, +} from "lucide-react"; +import { useState } from "react"; + +interface EtapesSectionProps { + etapes: Etape[]; + tabloTasks: KanbanTask[]; + tabloId: string; + isAdmin: boolean; + onCreateTask: (task: { + tablo_id: string; + title: string; + status: string; + parent_task_id: string; + is_parent: boolean; + position: number; + }) => void; + onCreateEtape: (params: { tabloId: string; title: string; position: number }) => Promise; + isCreatingEtape?: boolean; +} + +export function EtapesSection({ + etapes, + tabloTasks, + tabloId, + isAdmin, + onCreateTask, + onCreateEtape, + isCreatingEtape = false, +}: EtapesSectionProps) { + const [expandedEtapes, setExpandedEtapes] = useState>( + new Set(etapes.map((e) => e.id)) + ); + const [addingTaskToEtape, setAddingTaskToEtape] = useState(null); + const [newEtapeTitle, setNewEtapeTitle] = useState(""); + const [newTaskTitle, setNewTaskTitle] = useState(""); + + const toggleEtape = (id: string) => { + setExpandedEtapes((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const handleAddTask = (etapeId: string) => { + const title = newTaskTitle.trim(); + if (!title || !tabloId) return; + onCreateTask({ + tablo_id: tabloId, + title, + status: "todo", + parent_task_id: etapeId, + is_parent: false, + position: tabloTasks.filter((t) => t.parent_task_id === etapeId).length, + }); + setNewTaskTitle(""); + setAddingTaskToEtape(null); + }; + + const handleAddEtape = async () => { + const title = newEtapeTitle.trim(); + if (!title || !tabloId) { + return; + } + + const nextPosition = etapes.reduce((max, etape) => Math.max(max, etape.position), -1) + 1; + + await onCreateEtape({ + tabloId, + title, + position: nextPosition, + }); + + setNewEtapeTitle(""); + }; + + const statusConfig: Record = { + todo: { + label: "À faire", + color: "bg-blue-100 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400", + }, + in_progress: { + label: "En cours", + color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-400", + }, + in_review: { + label: "Vérification", + color: "bg-purple-100 text-purple-700 dark:bg-purple-950/30 dark:text-purple-400", + }, + done: { + label: "Terminé", + color: "bg-green-100 text-green-700 dark:bg-green-950/30 dark:text-green-400", + }, + }; + + return ( +
+ {isAdmin && ( +
+ setNewEtapeTitle(event.target.value)} + placeholder="Nom de la nouvelle étape..." + onKeyDown={(event) => { + if (event.key === "Enter") { + void handleAddEtape(); + } + }} + className="h-11 sm:h-9 sm:w-80" + /> + +
+ )} + + {etapes.length === 0 ? ( +
+ +

Aucune étape

+

+ Les étapes permettent de structurer votre projet en grandes phases +

+
+ ) : ( + etapes.map((etape, index) => { + const childTasks = tabloTasks.filter((t) => t.parent_task_id === etape.id); + const doneCount = childTasks.filter((t) => t.status === "done").length; + const totalCount = childTasks.length; + const progressPct = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0; + const isExpanded = expandedEtapes.has(etape.id); + + // Derive status from child tasks instead of etape.status + const derivedStatus = + totalCount === 0 + ? "todo" + : doneCount === totalCount + ? "done" + : doneCount > 0 + ? "in_progress" + : "todo"; + const status = statusConfig[derivedStatus] ?? statusConfig.todo; + + return ( +
+ {/* Etape header */} + + + {/* Child tasks + add task */} + {isExpanded && ( +
+ {childTasks.length > 0 && ( +
+ {childTasks.map((task) => ( +
+ {task.status === "done" ? ( + + ) : ( +
+ )} + + {task.title} + + {task.due_date && ( +
+ + + {new Intl.DateTimeFormat("fr-FR", { + day: "2-digit", + month: "short", + }).format(new Date(task.due_date))} + +
+ )} + {task.status && ( + + {(statusConfig[task.status] ?? statusConfig.todo).label} + + )} +
+ ))} +
+ )} + + {childTasks.length === 0 && addingTaskToEtape !== etape.id && ( +
+ Aucune tâche dans cette étape +
+ )} + + {/* Inline add task */} + {addingTaskToEtape === etape.id ? ( +
+
+ setNewTaskTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleAddTask(etape.id); + if (e.key === "Escape") { + setAddingTaskToEtape(null); + setNewTaskTitle(""); + } + }} + placeholder="Nom de la tâche..." + className="flex-1 text-sm bg-transparent border-none outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400 min-w-0" + /> + + +
+ ) : ( + + )} +
+ )} +
+ ); + }) + )} +
+ ); +} diff --git a/packages/tablo-views/src/ImageColorPicker.tsx b/packages/tablo-views/src/ImageColorPicker.tsx new file mode 100644 index 0000000..27d8a6a --- /dev/null +++ b/packages/tablo-views/src/ImageColorPicker.tsx @@ -0,0 +1,114 @@ +interface ImageColorPickerProps { + creationMode: "image" | "color"; + setCreationMode: (mode: "image" | "color") => void; + selectedColor: string; + setSelectedColor: (color: string) => void; +} + +const AVAILABLE_COLORS = [ + "bg-blue-500", + "bg-green-500", + "bg-purple-500", + "bg-red-500", + "bg-yellow-500", + "bg-indigo-500", + "bg-pink-500", + "bg-teal-500", + "bg-orange-500", + "bg-cyan-500", +]; + +export const ImageColorPicker = ({ + creationMode, + setCreationMode, + selectedColor, + setSelectedColor, +}: ImageColorPickerProps) => { + return ( +
+ {/* Mode Toggle */} +
+ +
+ + +
+
+ + {/* Image Mode */} + {creationMode === "image" && ( +
+ {/* File Upload - Coming Soon */} +
+
+ + + +

+ Import d'images +

+

Bientôt disponible

+
+
+
+ )} + + {/* Color Mode */} + {creationMode === "color" && ( +
+ +
+ {AVAILABLE_COLORS.map((color) => ( + + ))} +
+
+ )} +
+ ); +}; diff --git a/packages/tablo-views/src/RoadmapSection.tsx b/packages/tablo-views/src/RoadmapSection.tsx new file mode 100644 index 0000000..6d6bcb9 --- /dev/null +++ b/packages/tablo-views/src/RoadmapSection.tsx @@ -0,0 +1,23 @@ +import type { KanbanTask, TaskStatus } from "@xtablo/shared-types"; +import { GanttChart } from "./components/gantt/GanttChart"; + +interface RoadmapSectionProps { + tabloTasks: KanbanTask[]; + onDateClick: (date: Date) => void; + onTaskStatusChange: (taskId: string, status: TaskStatus) => void; +} + +export function RoadmapSection({ + tabloTasks, + onDateClick, + onTaskStatusChange, +}: RoadmapSectionProps) { + return ( + + ); +} diff --git a/apps/main/src/components/TabloDiscussionSection.tsx b/packages/tablo-views/src/TabloDiscussionSection.tsx similarity index 68% rename from apps/main/src/components/TabloDiscussionSection.tsx rename to packages/tablo-views/src/TabloDiscussionSection.tsx index 4f9256b..8973a7f 100644 --- a/apps/main/src/components/TabloDiscussionSection.tsx +++ b/packages/tablo-views/src/TabloDiscussionSection.tsx @@ -1,17 +1,26 @@ -import { UserTablo } from "@xtablo/shared/types/tablos.types"; +import type { UserTablo } from "@xtablo/shared/types/tablos.types"; import { useEffect } from "react"; -import { useChat } from "../hooks/useChat"; -import { useTabloMembers } from "../hooks/tablos"; -import { useUser } from "../providers/UserStoreProvider"; +import { useChat } from "./hooks/useChat"; import { ChatMessages } from "./ChatMessages"; +interface Member { + id: string; + name: string; + avatar_url: string | null; +} + interface TabloDiscussionSectionProps { tablo: UserTablo; isAdmin: boolean; + currentUserId: string; + members?: Member[]; } -export const TabloDiscussionSection = ({ tablo }: TabloDiscussionSectionProps) => { - const user = useUser(); +export const TabloDiscussionSection = ({ + tablo, + currentUserId, + members = [], +}: TabloDiscussionSectionProps) => { const { messages, sendMessage, @@ -22,8 +31,6 @@ export const TabloDiscussionSection = ({ tablo }: TabloDiscussionSectionProps) = markAsRead, } = useChat(tablo.id); - const { data: members = [] } = useTabloMembers(tablo.id); - // Mark as read when opening the discussion useEffect(() => { if (messages.length > 0) { @@ -36,7 +43,7 @@ export const TabloDiscussionSection = ({ tablo }: TabloDiscussionSectionProps) =
void; + onUpdateTablo?: (data: { id: string; name?: string | null; color?: string | null }) => Promise; + onInviteUser?: (params: { email: string; tablo_id: string }) => void; + onCancelInvite?: (params: { tabloId: string; inviteId: string }) => void; } -export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps) => { - const navigate = useNavigate(); +export const TabloEventsSection = ({ + tablo, + isAdmin, + isReadOnly = false, + events, + isLoading, + error, + currentUser, + members, + pendingInvites, + isInvitingUser, + isCancellingInvite, + onCreateEvent, + onUpdateTablo, + onInviteUser, + onCancelInvite, +}: TabloEventsSectionProps) => { const { t } = useTranslation(); - const { data: events, isLoading, error } = useEventsByTablo(tablo.id); - const isReadOnly = useIsReadOnlyUser(); // Filter upcoming events (events in the future or today) const today = new Date(); @@ -34,10 +85,6 @@ export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps) return (a.start_time || "").localeCompare(b.start_time || ""); }); - const handleCreateEvent = () => { - navigate(`/planning/create?tablo_id=${tablo.id}`); - }; - const formatDate = (dateStr: string) => { const date = new Date(dateStr); return new Intl.DateTimeFormat("fr-FR", { @@ -50,7 +97,6 @@ export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps) const formatTime = (timeStr: string) => { if (!timeStr) return ""; - return timeStr.slice(0, 5); // HH:MM }; @@ -66,7 +112,7 @@ export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps) {!isReadOnly && ( )}
- +
{/* Events List */}
@@ -176,7 +233,7 @@ export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps)

{!isReadOnly && (
- +
{/* Error Banner */} @@ -987,7 +1045,7 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) => }} onSave={handleSaveFolder} folder={editingFolder} - isLoading={createFolder.isPending || updateFolder.isPending} + isLoading={isCreatingFolder || isUpdatingFolder} /> ); diff --git a/apps/main/src/components/TabloHeaderActions.tsx b/packages/tablo-views/src/TabloHeaderActions.tsx similarity index 90% rename from apps/main/src/components/TabloHeaderActions.tsx rename to packages/tablo-views/src/TabloHeaderActions.tsx index e4f1757..1b3e54c 100644 --- a/apps/main/src/components/TabloHeaderActions.tsx +++ b/packages/tablo-views/src/TabloHeaderActions.tsx @@ -1,5 +1,5 @@ import { toast } from "@xtablo/shared"; -import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types"; +import type { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types"; import { Avatar, AvatarFallback, AvatarImage } from "@xtablo/ui/components/avatar"; import { Button } from "@xtablo/ui/components/button"; import { @@ -13,21 +13,52 @@ import { Input } from "@xtablo/ui/components/input"; import { Popover, PopoverContent, PopoverTrigger } from "@xtablo/ui/components/popover"; import { Loader2, Settings, Share2, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; -import { useInviteUser } from "../hooks/invite"; -import { useCancelTabloInvite, usePendingTabloInvitesByTablo } from "../hooks/tablo_invites"; -import { useTabloMembers, useUpdateTablo } from "../hooks/tablos"; -import { useUser } from "../providers/UserStoreProvider"; import { ClickOutside } from "./ClickOutside"; import { ImageColorPicker } from "./ImageColorPicker"; +interface PendingInvite { + id: string; + invited_email: string; +} + +interface Member { + id: string; + name: string; + email?: string; + avatar_url?: string | null; + is_admin?: boolean; +} + +interface CurrentUser { + id: string; + avatar_url?: string | null; +} + interface TabloHeaderActionsProps { tablo: UserTablo; isAdmin: boolean; + currentUser: CurrentUser; + members?: Member[]; + pendingInvites?: PendingInvite[]; + isInvitingUser?: boolean; + isCancellingInvite?: boolean; + onUpdateTablo?: (data: TabloUpdate & { id: string }) => Promise; + onInviteUser?: (params: { email: string; tablo_id: string }) => void; + onCancelInvite?: (params: { tabloId: string; inviteId: string }) => void; } -export const TabloHeaderActions = ({ tablo, isAdmin }: TabloHeaderActionsProps) => { - const { mutateAsync: updateTablo } = useUpdateTablo(); - const currentUser = useUser(); +export const TabloHeaderActions = ({ + tablo, + isAdmin, + currentUser, + members = [], + pendingInvites = [], + isInvitingUser = false, + isCancellingInvite = false, + onUpdateTablo, + onInviteUser, + onCancelInvite, +}: TabloHeaderActionsProps) => { const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); @@ -39,12 +70,6 @@ export const TabloHeaderActions = ({ tablo, isAdmin }: TabloHeaderActionsProps) const nameInputRef = useRef(null); - // Fetch members and invites for share dialog - const { data: members } = useTabloMembers(tablo?.id || ""); - const { data: pendingInvites } = usePendingTabloInvitesByTablo(tablo?.id || ""); - const { mutate: cancelInvite, isPending: isCancellingInvite } = useCancelTabloInvite(); - const { mutate: inviteUser, isPending: isInvitingUser } = useInviteUser(); - useEffect(() => { setEditData(tablo); setSelectedColor(tablo.color || "bg-blue-500"); @@ -59,14 +84,14 @@ export const TabloHeaderActions = ({ tablo, isAdmin }: TabloHeaderActionsProps) }, [isEditingName]); const handleSaveSettings = async () => { - if (editData && tablo) { + if (editData && tablo && onUpdateTablo) { const updatedTablo: TabloUpdate & { id: string } = { id: editData.id, name: editData.name, color: creationMode === "color" ? selectedColor : null, }; try { - await updateTablo(updatedTablo); + await onUpdateTablo(updatedTablo); toast.add( { title: "Tablo mis à jour", @@ -89,8 +114,8 @@ export const TabloHeaderActions = ({ tablo, isAdmin }: TabloHeaderActionsProps) }; const handleSendInvite = () => { - if (inviteEmail.trim() && tablo) { - inviteUser({ email: inviteEmail, tablo_id: tablo.id }); + if (inviteEmail.trim() && tablo && onInviteUser) { + onInviteUser({ email: inviteEmail, tablo_id: tablo.id }); setInviteEmail(""); } }; @@ -278,7 +303,7 @@ export const TabloHeaderActions = ({ tablo, isAdmin }: TabloHeaderActionsProps) size="icon" variant="ghost" className="h-8 w-8" - onClick={() => cancelInvite({ tabloId: tablo.id, inviteId: invite.id })} + onClick={() => onCancelInvite?.({ tabloId: tablo.id, inviteId: invite.id })} disabled={isCancellingInvite} title="Retirer l'invitation" > diff --git a/apps/main/src/components/TabloTasksSection.tsx b/packages/tablo-views/src/TabloTasksSection.tsx similarity index 75% rename from apps/main/src/components/TabloTasksSection.tsx rename to packages/tablo-views/src/TabloTasksSection.tsx index eccf7b5..0743baa 100644 --- a/apps/main/src/components/TabloTasksSection.tsx +++ b/packages/tablo-views/src/TabloTasksSection.tsx @@ -1,37 +1,71 @@ import { pluralize, toast } from "@xtablo/shared"; -import { UserTablo } from "@xtablo/shared/types/tablos.types"; -import type { KanbanColumn, KanbanTask, KanbanTaskInsert, TaskStatus } from "@xtablo/shared-types"; +import type { UserTablo } from "@xtablo/shared/types/tablos.types"; +import type { + Etape, + KanbanColumn, + KanbanTask, + KanbanTaskInsert, + KanbanTaskUpdate, + TaskStatus, +} from "@xtablo/shared-types"; import { TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography"; import { AlertTriangle, ListChecks } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTabloMembers } from "../hooks/tablos"; -import { - useCreateTask, - useTabloEtapes, - useTasksByTablo, - useUpdateTaskPositions, -} from "../hooks/tasks"; -import { KanbanBoard } from "./kanban/KanbanBoard"; -import { TaskModal } from "./kanban/TaskModal"; +import { KanbanBoard } from "./components/kanban/KanbanBoard"; +import type { TabloMember } from "./components/kanban/types"; +import { TaskModal } from "./components/kanban/TaskModal"; import { TabloHeaderActions } from "./TabloHeaderActions"; +interface CurrentUser { + id: string; + avatar_url?: string | null; +} + +interface PendingInvite { + id: string; + invited_email: string; +} + interface TabloTasksSectionProps { tablo: UserTablo; isAdmin: boolean; + tasks?: KanbanTask[]; + members?: TabloMember[]; + etapes?: Etape[]; + currentUser: CurrentUser; + pendingInvites?: PendingInvite[]; + isInvitingUser?: boolean; + isCancellingInvite?: boolean; + onCreateTask?: (task: KanbanTaskInsert) => void; + onUpdateTask?: (task: KanbanTaskUpdate & { id: string; tablo_id: string }) => void; + onUpdateTaskPositions?: (updates: Array<{ id: string; position: number; status: TaskStatus }>) => void; + onUpdateTablo?: (data: { id: string; name?: string | null; color?: string | null }) => Promise; + onInviteUser?: (params: { email: string; tablo_id: string }) => void; + onCancelInvite?: (params: { tabloId: string; inviteId: string }) => void; } -export const TabloTasksSection = ({ tablo, isAdmin }: TabloTasksSectionProps) => { - const { data: members = [] } = useTabloMembers(tablo.id); +export const TabloTasksSection = ({ + tablo, + isAdmin, + tasks, + members = [], + etapes = [], + currentUser, + pendingInvites, + isInvitingUser, + isCancellingInvite, + onCreateTask, + onUpdateTask, + onUpdateTaskPositions, + onUpdateTablo, + onInviteUser, + onCancelInvite, +}: TabloTasksSectionProps) => { const [columns, setColumns] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedTask, setSelectedTask] = useState(null); const [modalStatus, setModalStatus] = useState("todo"); - const { data: tasks } = useTasksByTablo(tablo.id); - const { data: etapes = [] } = useTabloEtapes(tablo.id); - const { mutate: updateTaskPositions } = useUpdateTaskPositions(); - const { mutate: createTask } = useCreateTask(); - const memberById = useMemo( () => new Map(members.map((member) => [member.id, member])), [members] @@ -72,7 +106,6 @@ export const TabloTasksSection = ({ tablo, isAdmin }: TabloTasksSectionProps) => return tasksWithAssigneeFallback.filter((task) => !task.parent_task_id); }, [tasksWithAssigneeFallback]); - // Helper functions defined before use const initializeColumns = useCallback((tasks: KanbanTask[]): KanbanColumn[] => { const defaultColumns: KanbanColumn[] = [ { @@ -137,19 +170,7 @@ export const TabloTasksSection = ({ tablo, isAdmin }: TabloTasksSectionProps) => parent_task_id: taskData.parent_task_id ?? null, }; - createTask(newTask); - - // setColumns((prevColumns) => - // prevColumns.map((column: KanbanColumn) => { - // if (column.status === (taskData.status as TaskStatus)) { - // return { - // ...column, - // tasks: [newTask, ...column.tasks], - // }; - // } - // return column; - // }) - // ); + onCreateTask?.(newTask); toast.add( { @@ -162,7 +183,7 @@ export const TabloTasksSection = ({ tablo, isAdmin }: TabloTasksSectionProps) => }; const handleTaskMove = (taskId: string, newStatus: TaskStatus) => { - updateTaskPositions([ + onUpdateTaskPositions?.([ { id: taskId, position: columns.find((column) => column.status === newStatus)?.position ?? 0, @@ -198,7 +219,18 @@ export const TabloTasksSection = ({ tablo, isAdmin }: TabloTasksSectionProps) => Gérez vos tâches avec un tableau Kanban - + {/* Warning for orphaned tasks */} @@ -238,11 +270,14 @@ export const TabloTasksSection = ({ tablo, isAdmin }: TabloTasksSectionProps) => setIsModalOpen(false)} members={members} initialStatus={modalStatus} etapes={etapes} + onCreateTask={onCreateTask} + onUpdateTask={onUpdateTask} /> ); diff --git a/apps/main/src/components/gantt/GanttChart.tsx b/packages/tablo-views/src/components/gantt/GanttChart.tsx similarity index 99% rename from apps/main/src/components/gantt/GanttChart.tsx rename to packages/tablo-views/src/components/gantt/GanttChart.tsx index 49c90f8..9f24854 100644 --- a/apps/main/src/components/gantt/GanttChart.tsx +++ b/packages/tablo-views/src/components/gantt/GanttChart.tsx @@ -1,4 +1,3 @@ -import { LoadingSpinner } from "@ui/components/LoadingSpinner"; import type { KanbanTask, TaskStatus } from "@xtablo/shared-types"; import { Button } from "@xtablo/ui/components/button"; import { @@ -253,7 +252,7 @@ export function GanttChart({ tasks, isLoading, onDateClick, onTaskStatusChange } if (isLoading) { return (
- +
); } diff --git a/apps/main/src/components/kanban/InlineTaskCreate.tsx b/packages/tablo-views/src/components/kanban/InlineTaskCreate.tsx similarity index 87% rename from apps/main/src/components/kanban/InlineTaskCreate.tsx rename to packages/tablo-views/src/components/kanban/InlineTaskCreate.tsx index 0d96492..9d03af2 100644 --- a/apps/main/src/components/kanban/InlineTaskCreate.tsx +++ b/packages/tablo-views/src/components/kanban/InlineTaskCreate.tsx @@ -130,24 +130,6 @@ export const InlineTaskCreate = ({ status, members, etapes, onSubmit }: InlineTa {/* Type and Assignee */}
- {/*
- - -
*/} -
{/* Étape */} - {etapes.length > 0 && ( + {providedEtapes.length > 0 && (