diff --git a/.circleci/config.yml b/.circleci/config.yml index ea82ebf..c40679e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: executor: name: node/default resource_class: small - tag: '16' + tag: 'lts' steps: - checkout - node/install-packages: @@ -22,15 +22,12 @@ jobs: - run: name: Run linting command: pnpm run lint - - run: - name: Check formatting - command: pnpm run format --check || echo "Format check complete" test-typecheck: executor: name: node/default resource_class: small - tag: '16' + tag: 'lts' steps: - checkout - node/install-packages: @@ -44,7 +41,7 @@ jobs: executor: name: node/default resource_class: medium - tag: '16' + tag: 'lts' steps: - checkout - node/install-packages: @@ -52,44 +49,27 @@ jobs: cache-path: ~/.pnpm-store - run: name: Run unit tests - command: pnpm run test - - store_test_results: - path: apps/main/coverage - - store_artifacts: - path: apps/main/coverage - destination: coverage + command: pnpm --filter @xtablo/main run test test-api: executor: name: node/default - tag: '16' + tag: 'lts' resource_class: small steps: - checkout - - restore_cache: - name: Restore npm API Cache - keys: - - npm-api-{{ checksum "api/package-lock.json" }} + - node/install-packages: + pkg-manager: pnpm + cache-path: ~/.pnpm-store - run: - name: Install API dependencies + name: Run API checks command: | - cd api - npm ci - - save_cache: - name: Save npm API Cache - key: npm-api-{{ checksum "api/package-lock.json" }} - paths: - - api/node_modules - - run: - name: Lint API - command: | - cd api - npm run lint - - run: - name: Run API tests - command: | - cd api - npm run test + if [ "${RUN_API_INTEGRATION_TESTS:-0}" = "1" ]; then + pnpm --filter @xtablo/api run test + else + echo "Skipping API integration tests (set RUN_API_INTEGRATION_TESTS=1 to enable)." + pnpm --filter @xtablo/api run build + fi # ============================================ # BUILD PHASE @@ -123,8 +103,6 @@ jobs: paths: - apps/main/dist - apps/external/dist - - packages/ui/dist - - packages/shared/dist - store_artifacts: path: apps/main/dist destination: main-app-<< parameters.environment >> @@ -138,26 +116,18 @@ jobs: resource_class: small steps: - checkout - - restore_cache: - name: Restore npm API Cache - keys: - - npm-api-{{ checksum "api/package-lock.json" }} - - run: - name: Install API dependencies - command: | - cd api - npm ci + - node/install-packages: + pkg-manager: pnpm + cache-path: ~/.pnpm-store - run: name: Build API - command: | - cd api - npm run build + command: pnpm --filter @xtablo/api run build - persist_to_workspace: root: . paths: - - api/dist + - apps/api/dist - store_artifacts: - path: api/dist + path: apps/api/dist destination: api # ============================================ @@ -174,9 +144,7 @@ jobs: at: . - run: name: Build API Docker image - command: | - cd api - docker build -t xtablo-api:${CIRCLE_SHA1} -t xtablo-api:latest . + command: docker build -f apps/api/Dockerfile -t xtablo-api:${CIRCLE_SHA1} -t xtablo-api:latest . - run: name: Save Docker image command: | diff --git a/apps/api/src/__tests__/routes/tablo_data.test.ts b/apps/api/src/__tests__/routes/tablo_data.test.ts index 780755d..b7033c5 100644 --- a/apps/api/src/__tests__/routes/tablo_data.test.ts +++ b/apps/api/src/__tests__/routes/tablo_data.test.ts @@ -6,8 +6,8 @@ import { PutObjectCommand, S3Client, } from "@aws-sdk/client-s3"; -import type { TabloFoldersMetadata } from "@xtablo/shared-types"; import { sdkStreamMixin } from "@smithy/util-stream"; +import type { TabloFoldersMetadata } from "@xtablo/shared-types"; import { mockClient } from "aws-sdk-client-mock"; import { testClient } from "hono/testing"; import { Readable } from "stream"; @@ -664,12 +664,7 @@ describe("TabloData Endpoint", () => { }); it("should return 400 if folder name is empty string", async () => { - const res = await createFolderRequest( - ownerUser, - client, - "test_tablo_owner_private", - " " - ); + const res = await createFolderRequest(ownerUser, client, "test_tablo_owner_private", " "); expect(res.status).toBe(400); const data = await res.json(); @@ -1110,9 +1105,7 @@ describe("TabloData Endpoint", () => { }); }); - describe( - "DELETE /tablo-data/:tabloId/file/:path - Delete File with Nested Path (Admin or Uploader)", - () => { + describe("DELETE /tablo-data/:tabloId/file/:path - Delete File with Nested Path (Admin or Uploader)", () => { it("should allow admin to delete file with nested path", async () => { const res = await deleteNestedFileRequest( ownerUser, diff --git a/apps/api/src/routers/tablo_data.ts b/apps/api/src/routers/tablo_data.ts index a356010..37c75b8 100644 --- a/apps/api/src/routers/tablo_data.ts +++ b/apps/api/src/routers/tablo_data.ts @@ -230,9 +230,7 @@ const deleteTabloFile = (middlewareManager: ReturnType
{icon} @@ -65,7 +65,7 @@ export function ActionCard({ {label} @@ -79,7 +79,7 @@ export function ActionCard({

{description} diff --git a/apps/main/src/components/DashboardTaskList.tsx b/apps/main/src/components/DashboardTaskList.tsx index c3f71c1..05cee3f 100644 --- a/apps/main/src/components/DashboardTaskList.tsx +++ b/apps/main/src/components/DashboardTaskList.tsx @@ -13,28 +13,21 @@ type TaskWithTablo = KanbanTask & { tablos: { id: string; name: string; color: string | null } | null; }; -const STATUS_BADGE: Record< - TaskStatus, - { className: string; labelKey: string } -> = { +const STATUS_BADGE: Record = { todo: { - className: - "bg-blue-50 text-blue-600 dark:bg-blue-950/30 dark:text-blue-400", + className: "bg-blue-50 text-blue-600 dark:bg-blue-950/30 dark:text-blue-400", labelKey: "dashboard.taskList.status.todo", }, in_progress: { - className: - "bg-yellow-50 text-yellow-600 dark:bg-yellow-950/30 dark:text-yellow-400", + className: "bg-yellow-50 text-yellow-600 dark:bg-yellow-950/30 dark:text-yellow-400", labelKey: "dashboard.taskList.status.inProgress", }, in_review: { - className: - "bg-purple-50 text-purple-600 dark:bg-purple-950/30 dark:text-purple-400", + className: "bg-purple-50 text-purple-600 dark:bg-purple-950/30 dark:text-purple-400", labelKey: "dashboard.taskList.status.inReview", }, done: { - className: - "bg-green-50 text-green-600 dark:bg-green-950/30 dark:text-green-400", + className: "bg-green-50 text-green-600 dark:bg-green-950/30 dark:text-green-400", labelKey: "dashboard.taskList.status.done", }, }; @@ -76,7 +69,7 @@ function TaskRow({ "w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0", isDone ? "bg-purple-600 border-purple-600" - : "border-gray-300 hover:border-purple-400 dark:border-gray-600 dark:hover:border-purple-500", + : "border-gray-300 hover:border-purple-400 dark:border-gray-600 dark:hover:border-purple-500" )} onClick={(e) => { e.stopPropagation(); @@ -92,7 +85,7 @@ function TaskRow({ "text-sm font-medium truncate", isDone ? "line-through text-gray-400 dark:text-gray-500" - : "text-gray-900 dark:text-gray-100", + : "text-gray-900 dark:text-gray-100" )} > {task.title} @@ -105,7 +98,7 @@ function TaskRow({

@@ -128,7 +121,7 @@ function TaskRow({ {t(badge.labelKey)} @@ -146,14 +139,10 @@ export function DashboardTaskList() { const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); // Filter to tasks assigned to the current user, limited to recent ones - const myTasks = - allTasks - ?.filter((task) => task.assignee_id === user.id) - .slice(0, 7) ?? []; + const myTasks = allTasks?.filter((task) => task.assignee_id === user.id).slice(0, 7) ?? []; const handleToggleDone = (task: TaskWithTablo) => { - const newStatus: TaskStatus = - task.status === "done" ? "todo" : "done"; + const newStatus: TaskStatus = task.status === "done" ? "todo" : "done"; updateTask.mutate({ id: task.id, status: newStatus }); }; @@ -177,11 +166,7 @@ export function DashboardTaskList() {
{myTasks.map((task) => ( - + ))}
diff --git a/apps/main/src/components/ExceptionModal.tsx b/apps/main/src/components/ExceptionModal.tsx index 65acae3..65aafd9 100644 --- a/apps/main/src/components/ExceptionModal.tsx +++ b/apps/main/src/components/ExceptionModal.tsx @@ -41,7 +41,7 @@ export const ExceptionModal = ({ }) => { const { t } = useTranslation("components"); const form = useForm>({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: zodResolver typing is incompatible with current zod type in this package. resolver: zodResolver(formSchema as any), defaultValues: { exceptionType: "day", diff --git a/apps/main/src/components/ProjectCard.tsx b/apps/main/src/components/ProjectCard.tsx index 84c04cd..3720cc7 100644 --- a/apps/main/src/components/ProjectCard.tsx +++ b/apps/main/src/components/ProjectCard.tsx @@ -64,12 +64,7 @@ export interface ProjectCardProps { className?: string; } -export function ProjectCard({ - tablo, - onClick, - onMenuClick, - className, -}: ProjectCardProps) { +export function ProjectCard({ tablo, onClick, onMenuClick, className }: ProjectCardProps) { const { t } = useTranslation("pages"); const statusConfig = useStatusConfig(tablo.status); const progress = getProgressFromStatus(tablo.status); @@ -84,7 +79,7 @@ export function ProjectCard({
onClick?.(tablo.id)} > @@ -93,7 +88,7 @@ export function ProjectCard({ {statusConfig.label} @@ -114,15 +109,11 @@ export function ProjectCard({
{tablo.image ? ( - {tablo.name} + {tablo.name} ) : ( {tablo.name.charAt(0).toUpperCase()} @@ -143,19 +134,12 @@ export function ProjectCard({ {/* Progress */}
- - {t("tablo.card.progress")}: - - - {progress}% - + {t("tablo.card.progress")}: + {progress}%
diff --git a/apps/main/src/components/TabloEventsSection.tsx b/apps/main/src/components/TabloEventsSection.tsx index 1be30a9..998172e 100644 --- a/apps/main/src/components/TabloEventsSection.tsx +++ b/apps/main/src/components/TabloEventsSection.tsx @@ -1,12 +1,12 @@ import { UserTablo } from "@xtablo/shared/types/tablos.types"; import { Button } from "@xtablo/ui/components/button"; +import { TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography"; import { Calendar, Clock, Plus } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useEventsByTablo } from "../hooks/events"; import { useIsReadOnlyUser } from "../providers/UserStoreProvider"; import { TabloHeaderActions } from "./TabloHeaderActions"; -import { TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography"; -import { useTranslation } from "react-i18next"; interface TabloEventsSectionProps { tablo: UserTablo; @@ -65,7 +65,10 @@ export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps) {t("tablo:events.description")} {!isReadOnly && ( - @@ -172,7 +175,10 @@ export const TabloEventsSection = ({ tablo, isAdmin }: TabloEventsSectionProps) Aucun événement à venir pour ce tablo

{!isReadOnly && ( - diff --git a/apps/main/src/components/TabloFilesSection.tsx b/apps/main/src/components/TabloFilesSection.tsx index 14744f1..b05aff7 100644 --- a/apps/main/src/components/TabloFilesSection.tsx +++ b/apps/main/src/components/TabloFilesSection.tsx @@ -455,7 +455,11 @@ const FolderSection = ({ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) => { const currentUser = useUser(); - const { data: fileData, isLoading: filesLoading, error: filesError } = useTabloFileNames(tablo.id); + const { + data: fileData, + isLoading: filesLoading, + error: filesError, + } = useTabloFileNames(tablo.id); const { data: foldersData, isLoading: foldersLoading, diff --git a/apps/main/src/components/TabloHeaderActions.tsx b/apps/main/src/components/TabloHeaderActions.tsx index 0160717..e4f1757 100644 --- a/apps/main/src/components/TabloHeaderActions.tsx +++ b/apps/main/src/components/TabloHeaderActions.tsx @@ -13,12 +13,12 @@ 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 { ClickOutside } from "./ClickOutside"; -import { ImageColorPicker } from "./ImageColorPicker"; 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 TabloHeaderActionsProps { tablo: UserTablo; @@ -303,7 +303,8 @@ export const TabloHeaderActions = ({ tablo, isAdmin }: TabloHeaderActionsProps)
{filteredMembers.map((member, index) => { const avatarUrl = - member.avatar_url ?? (member.id === currentUser.id ? currentUser.avatar_url : null); + member.avatar_url ?? + (member.id === currentUser.id ? currentUser.avatar_url : null); return (
- cancelInvite({ tabloId: tablo.id, inviteId: invite.id }) - } + onClick={() => cancelInvite({ tabloId: tablo.id, inviteId: invite.id })} disabled={isCancellingInvite} > Retirer diff --git a/apps/main/src/components/TabloTasksSection.tsx b/apps/main/src/components/TabloTasksSection.tsx index 759b404..054b5f1 100644 --- a/apps/main/src/components/TabloTasksSection.tsx +++ b/apps/main/src/components/TabloTasksSection.tsx @@ -1,6 +1,7 @@ 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 { 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"; @@ -13,7 +14,6 @@ import { import { KanbanBoard } from "./kanban/KanbanBoard"; import { TaskModal } from "./kanban/TaskModal"; import { TabloHeaderActions } from "./TabloHeaderActions"; -import { TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography"; interface TabloTasksSectionProps { tablo: UserTablo; diff --git a/apps/main/src/components/TopBar.tsx b/apps/main/src/components/TopBar.tsx index 80031aa..91b2fe9 100644 --- a/apps/main/src/components/TopBar.tsx +++ b/apps/main/src/components/TopBar.tsx @@ -1,4 +1,5 @@ import type { Database } from "@xtablo/shared-types"; +import { Avatar, AvatarFallback, AvatarImage } from "@xtablo/ui/components/avatar"; import { Badge } from "@xtablo/ui/components/badge"; import { Button } from "@xtablo/ui/components/button"; import { @@ -8,15 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@xtablo/ui/components/dropdown-menu"; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@xtablo/ui/components/avatar"; -import { - TypographyMuted, - TypographySmall, -} from "@xtablo/ui/components/typography"; +import { TypographyMuted, TypographySmall } from "@xtablo/ui/components/typography"; import { BellIcon, CalendarCheckIcon, @@ -36,10 +29,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { useLogout } from "../hooks/auth"; -import { - useNotifications, - useNotificationsSubscription, -} from "../hooks/notifications"; +import { useNotifications, useNotificationsSubscription } from "../hooks/notifications"; import { useAcceptTabloInvite, useCancelTabloInvite, @@ -171,8 +161,7 @@ function NotificationItem({ function NotificationDropdown() { const { t } = useTranslation("navigation"); - const { notifications, unreadCount, isLoading, markAsRead, markAllAsRead } = - useNotifications(); + const { notifications, unreadCount, isLoading, markAsRead, markAllAsRead } = useNotifications(); const { setupSubscription } = useNotificationsSubscription(); useEffect(() => { @@ -209,9 +198,7 @@ function NotificationDropdown() { {t("notifications.title", "Notifications")} {unreadCount > 0 && ( - - {unreadCount} - + {unreadCount} )}
{unreadCount > 0 && ( @@ -250,10 +237,7 @@ function NotificationDropdown() {
{notifications.map((notification) => (
- +
))}
@@ -298,12 +282,16 @@ function TabloInvitesDropdown() { sideOffset={8} >
- {t("invites.title")} + + {t("invites.title")} +
{invites.map((invite) => (
- {invite.tablo_name} + + {invite.tablo_name} +
)} - - +
- {/* Filters */} -
-
- {/* Search */} -
-
- - setSearchTerm(e.target.value)} - className="pl-10 h-10" - /> -
+ {/* Filters */} +
+
+ {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 h-10" + />
- - {/* Tablo Filter */} -
- -
- - {/* Status Filter */} - - {statusOptions.map((option) => ( - - ))} -
-
- {/* Events List */} -
- {tablosLoading || eventsLoading ? ( -
- -
- ) : paginatedEvents.length === 0 ? ( -
- -

- {t("pages:events.emptyState.title")} -

-

- {searchTerm || statusFilter !== "all" - ? t("pages:events.emptyState.noResults") - : t("pages:events.emptyState.noEvents")} -

-
- ) : ( -
- {paginatedEvents.map((event) => ( -
-
-
-
- - {event.title || "Événement sans titre"} - - {getEventStatusBadge(event)} -
- -
- - - {formatEventDateTime(event)} - - {event.tablo_name && ( - - {event.tablo_name} - + {/* Tablo Filter */} +
+ +
- {event.description && ( - - {event.description} - + {/* Status Filter */} + + {statusOptions.map((option) => ( + + ))} + +
+
+ + {/* Events List */} +
+ {tablosLoading || eventsLoading ? ( +
+ +
+ ) : paginatedEvents.length === 0 ? ( +
+ +

+ {t("pages:events.emptyState.title")} +

+

+ {searchTerm || statusFilter !== "all" + ? t("pages:events.emptyState.noResults") + : t("pages:events.emptyState.noEvents")} +

+
+ ) : ( +
+ {paginatedEvents.map((event) => ( +
+
+
+
+ + {event.title || "Événement sans titre"} + + {getEventStatusBadge(event)} +
+ +
+ + + {formatEventDateTime(event)} + + {event.tablo_name && ( + + {event.tablo_name} + )}
-
- -
+ {event.description && ( + + {event.description} + + )} +
+ +
+
- ))} -
- )} -
+
+ ))} +
+ )} +
- {/* Pagination Controls */} - {totalItems > 0 && ( -
-
-
- - {t("pages:events.pagination.showing", { - start: startIndex + 1, - end: Math.min(endIndex, totalItems), - total: totalItems, - })} + {/* Pagination Controls */} + {totalItems > 0 && ( +
+
+
+ + {t("pages:events.pagination.showing", { + start: startIndex + 1, + end: Math.min(endIndex, totalItems), + total: totalItems, + })} + +
+ + {t("pages:events.pagination.itemsPerPage")} -
- - {t("pages:events.pagination.itemsPerPage")} - - setItemsPerPage(Number(value))} + > + - - - - - 5 - 10 - 20 - 50 - - -
+ + + + 5 + 10 + 20 + 50 + +
- - {totalPages > 1 && ( -
- - -
- {Array.from({ length: totalPages }, (_, i) => i + 1) - .filter((page) => { - return ( - page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1 - ); - }) - .map((page, index, array) => { - const prevPage = array[index - 1]; - const showEllipsis = prevPage && page - prevPage > 1; - - return ( -
- {showEllipsis && ( - ... - )} - -
- ); - })} -
- - -
- )}
-
- )} - {/* Stats Summary */} - {filteredEvents.length > 0 && ( -
-
-
-
- {filteredEvents.length} -
-
- {t("pages:events.stats.found")} + {totalPages > 1 && ( +
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter((page) => { + return ( + page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1 + ); + }) + .map((page, index, array) => { + const prevPage = array[index - 1]; + const showEllipsis = prevPage && page - prevPage > 1; + + return ( +
+ {showEllipsis && ( + ... + )} + +
+ ); + })}
+ +
-
-
- { - filteredEvents.filter((e) => { - if (!e.start_date) return false; - const eventDate = new Date(e.start_date); - return eventDate >= new Date(); - }).length - } -
-
- {t("pages:events.stats.upcoming")} -
+ )} +
+
+ )} + + {/* Stats Summary */} + {filteredEvents.length > 0 && ( +
+
+
+
{filteredEvents.length}
+
+ {t("pages:events.stats.found")}
-
-
- { - filteredEvents.filter((e) => { - if (!e.start_date) return false; - const today = new Date(); - today.setHours(0, 0, 0, 0); - const eventDate = new Date(e.start_date); - eventDate.setHours(0, 0, 0, 0); - return eventDate.getTime() === today.getTime(); - }).length - } -
-
- {t("pages:events.stats.today")} -
+
+
+
+ { + filteredEvents.filter((e) => { + if (!e.start_date) return false; + const eventDate = new Date(e.start_date); + return eventDate >= new Date(); + }).length + } +
+
+ {t("pages:events.stats.upcoming")} +
+
+
+
+ { + filteredEvents.filter((e) => { + if (!e.start_date) return false; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const eventDate = new Date(e.start_date); + eventDate.setHours(0, 0, 0, 0); + return eventDate.getTime() === today.getTime(); + }).length + } +
+
+ {t("pages:events.stats.today")}
- )} +
+ )}
{/* Event Details Modal */} @@ -522,7 +511,6 @@ export function EventsPage() { onEdit={() => selectedEvent && handleEditEvent(selectedEvent)} canEdit={selectedEvent ? canEditEvent(selectedEvent) : false} /> - {/* Render child routes (e.g. EventModal) */} diff --git a/apps/main/src/pages/files.tsx b/apps/main/src/pages/files.tsx index 1d8b455..f99f2b8 100644 --- a/apps/main/src/pages/files.tsx +++ b/apps/main/src/pages/files.tsx @@ -1,12 +1,7 @@ -import { toast } from "@xtablo/shared"; import { LoadingSpinner } from "@ui/components/LoadingSpinner"; +import { toast } from "@xtablo/shared"; import { Button } from "@xtablo/ui/components/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@xtablo/ui/components/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog"; import { DropdownMenu, DropdownMenuContent, @@ -25,18 +20,18 @@ import { import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useSearchParams } from "react-router-dom"; -import { - extractFolderIdFromFileName, - getFileNameWithoutFolder, - getFolderFilePrefix, - useTabloFolders, -} from "../hooks/tablo_folders"; import { useAllTablosFileNames, useCreateTabloFile, useDeleteTabloFile, useDownloadTabloFile, } from "../hooks/tablo_data"; +import { + extractFolderIdFromFileName, + getFileNameWithoutFolder, + getFolderFilePrefix, + useTabloFolders, +} from "../hooks/tablo_folders"; import { useTablosList } from "../hooks/tablos"; // Derive icon color from file extension @@ -146,7 +141,9 @@ function UploadModal({ > {tablo.name.charAt(0).toUpperCase()} - {tablo.name} + + {tablo.name} + ))}
@@ -155,7 +152,9 @@ function UploadModal({ {/* Folder selector (optional) */} {folders.length > 0 && (
- +