-
+
)
}
\ No newline at end of file
diff --git a/apps/frontend/src/components/tables/files-table.tsx b/apps/frontend/src/components/tables/files-table.tsx
index e816daa..abd057e 100644
--- a/apps/frontend/src/components/tables/files-table.tsx
+++ b/apps/frontend/src/components/tables/files-table.tsx
@@ -1,12 +1,14 @@
"use client"
-
import {
ColumnDef,
flexRender,
- getCoreRowModel, getPaginationRowModel, getSortedRowModel, SortingState,
+ getCoreRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ PaginationState,
+ SortingState,
useReactTable
} from '@tanstack/react-table';
-
import {
Table,
TableBody,
@@ -17,169 +19,207 @@ import {
} from "../ui/table"
import { Button } from '../ui/button';
import { Badge } from '../ui/badge'
-import { ArrowUpDown, Clock, Download, Trash } from 'lucide-react';
-import { useState } from 'react';
+import { ArrowUpDown, Clock, Download, Trash, TriangleAlert } from 'lucide-react';
+import { Dispatch, SetStateAction, useMemo, useReducer, useState } from 'react';
import Link from 'next/link';
-
-// This type is used to define the shape of our data.
-// You can use a Zod schema here if you want.
-export type IFile = {
- uuid: string;
- fileName: string;
- checksum: string;
- extension: string;
- groupId: string | null;
- fileSize: number;
- fileType: string;
- isRestricted: boolean;
- isDocumentation: boolean;
- uploadedAt: string;
- uploadedBy: string;
-}
+import { IFile } from '../../types/file';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
+import { FilesApi } from 'apps/frontend/src/requests/files';
+import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
+import { LoadingSpinner } from 'apps/frontend/src/components/loading-spinner';
+import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../ui/accordion';
+import {
+ ISearchBarResult,
+ SearchBar
+} from 'apps/frontend/src/components/tables/search-bar';
+import {
+ TablePagination
+} from 'apps/frontend/src/components/tables/pagination';
function ContextButtonForFile() {
- return (
-
-
)
+ return (
+
+
+
+ );
}
export const filesTableColumns: ColumnDef
[] = [
{
accessorKey: "fileName",
- header: ({ column }) => {
- return (
+ header: ({ column }) => (
+
Nom du fichier
-
)
- },
+
+ ),
},
{
accessorKey: "uploadedBy",
- header: ({ column }) => {
- return (
+ header: ({ column }) => (
+
Autheur(s)
-
)
- },
+
+ ),
},
{
accessorKey: "uploadedAt",
- header: ({ column }) => {
- return (
-
- )
- },
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
- const date = new Date(row.getValue("uploadedAt"))
- const formatted = `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} à ${date.getHours()}:${date.getMinutes()}`
-
- return (
-
-
-
- {formatted}
-
-
-
)
+ const date = new Date(row.getValue("uploadedAt"));
+ const formatted = `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} à ${date.getHours()}:${date.getMinutes()}`;
+ return (
+
+
+
+
+ {formatted}
+
+
+
+ );
},
},
{
accessorKey: "extension",
- header: ({ column }) => {
- return (
+ header: ({ column }) => (
+
Extension du fichier
-
)
- },
+
+ ),
cell: ({ row }) => {
const extension = row.getValue("extension") as string;
-
- return (
- {extension}
-
)
+ return (
+
+ {extension}
+
+ );
},
},
{
id: "actions",
- header: ({ column }) => {
- return (
+ header: ({ column }) => (
+
Actions
-
)
- },
+
+ ),
cell: ({ row }) => {
- const file = row.original
-
- return (
-
-
-
)
+ const file = row.original;
+ return (
+
+
+
+
+ );
},
},
-]
+];
interface DataTableProps {
columns: ColumnDef[]
- data: TData[]
+ searchField: ISearchBarResult
}
export function FilesDataTable({
- columns,
- data,
- }: DataTableProps) {
- const [sorting, setSorting] = useState([])
+ columns,
+ searchField
+ }: DataTableProps) {
+ const rerender = useReducer(() => ({}), {})[1];
+ const [sorting, setSorting] = useState([]);
+ const [data, setData] = useState([]);
+ const [rowsCount, setRowsCount] = useState(0);
+ const [pagination, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+
+ const queryResult = useQuery({
+ queryKey: ['files', pagination, searchField],
+ queryFn: async () => {
+ const response = await FilesApi.get.files(pagination, searchField.value);
+ setRowsCount(response.count);
+ setData(response.data as TData[]);
+ return response.data;
+ },
+ staleTime: 500,
+ placeholderData: keepPreviousData,
+ });
+
+ const { isPending, isError, error, isFetching, isPlaceholderData } = queryResult;
+
const table = useReactTable({
data,
columns,
+ pageCount: Math.ceil(rowsCount / pagination.pageSize),
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
+ manualPagination: true,
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
sorting,
+ pagination,
},
- })
+ });
+
+
+ if (isError) {
+
+ return (
+
+
+ Erreur
+
+ {error.message}
+
+
+ Stack trace
+
+ {error.stack}
+
+
+
+
+
+ );
+ }
return (
-
-
+
+ {isPending && isFetching && isPlaceholderData ?
:
{table.getHeaderGroups().map((headerGroup) => (
- {headerGroup.headers.map((header) => {
- return (
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
-
- )
- })}
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder ? null : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ ))}
))}
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
-
+
{row.getVisibleCells().map((cell) => (
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -195,7 +235,10 @@ export function FilesDataTable({
)}
-
+
}
+
- )
+ );
}
diff --git a/apps/frontend/src/components/tables/pagination.tsx b/apps/frontend/src/components/tables/pagination.tsx
new file mode 100644
index 0000000..d20fc0a
--- /dev/null
+++ b/apps/frontend/src/components/tables/pagination.tsx
@@ -0,0 +1,46 @@
+import { PaginationState, Table } from '@tanstack/react-table';
+import { Dispatch, SetStateAction } from 'react';
+import { Button } from 'apps/frontend/src/components/ui/button';
+
+
+export interface TablePaginationProps {
+ rowsCount: number;
+ pagination: PaginationState;
+ setPagination: Dispatch>
+}
+
+export function TablePagination(props: TablePaginationProps) {
+ const totalPages = Math.ceil(props.rowsCount / props.pagination.pageSize)
+ const currentPage = props.pagination.pageIndex
+ const isMonoPage = totalPages === 1
+ const hasNextPage = (props.pagination.pageIndex >= totalPages)
+ const hasPreviousPage = !(props.pagination.pageIndex === 0)
+
+ if (!props.rowsCount) return (<>>);
+
+ return (
+ {isMonoPage ? (
Il n'y as qu'une seule page.
) : (
Page {currentPage} sur {totalPages}
)}
+
+
+
+
+
)
+}
\ No newline at end of file
diff --git a/apps/frontend/src/components/tables/search-bar.tsx b/apps/frontend/src/components/tables/search-bar.tsx
new file mode 100644
index 0000000..49eb763
--- /dev/null
+++ b/apps/frontend/src/components/tables/search-bar.tsx
@@ -0,0 +1,42 @@
+import { Dispatch, SetStateAction, useEffect, useState } from 'react';
+import { Input } from '../ui/input';
+import { Label } from '../ui/label';
+import { LoadingSpinner } from 'apps/frontend/src/components/loading-spinner';
+import { Loader } from 'lucide-react';
+
+
+export interface SearchBarProps {
+ stateSetter: Dispatch>;
+}
+export interface ISearchBarResult {
+ value: string;
+}
+
+export function SearchBar(props: SearchBarProps) {
+ const [inputValue, setInputValue] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+
+ setIsLoading(true);
+ const timer = setTimeout(() => {
+ props.stateSetter({value: inputValue});
+ setIsLoading(false);
+ }, 750);
+
+ // Nettoyage du timer
+ return () => clearTimeout(timer);
+ }, [inputValue]);
+ return (
+
+
+ setInputValue(e.target.value)}
+ placeholder="Votre recherche..." />
+
+ {isLoading &&
}
+
)
+}
\ No newline at end of file
diff --git a/apps/frontend/src/components/ui/form.tsx b/apps/frontend/src/components/ui/form.tsx
index ce264ae..aebe0f9 100644
--- a/apps/frontend/src/components/ui/form.tsx
+++ b/apps/frontend/src/components/ui/form.tsx
@@ -12,8 +12,8 @@ import {
useFormContext,
} from "react-hook-form"
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
+import { cn } from "../../lib/utils"
+import { Label } from "./label"
const Form = FormProvider
diff --git a/apps/frontend/src/components/ui/multiple-selector.tsx b/apps/frontend/src/components/ui/multiple-selector.tsx
new file mode 100644
index 0000000..bdfc018
--- /dev/null
+++ b/apps/frontend/src/components/ui/multiple-selector.tsx
@@ -0,0 +1,608 @@
+'use client';
+
+import { Command as CommandPrimitive, useCommandState } from 'cmdk';
+import { X } from 'lucide-react';
+import * as React from 'react';
+import { forwardRef, useEffect } from 'react';
+
+import { Badge } from './badge';
+import { Command, CommandGroup, CommandItem, CommandList } from './command';
+import { cn } from '../../lib/utils';
+
+export interface Option {
+ value: string;
+ label: string;
+ disable?: boolean;
+ /** fixed option that can't be removed. */
+ fixed?: boolean;
+ /** Group the options by providing key. */
+ [key: string]: string | boolean | undefined;
+}
+interface GroupOption {
+ [key: string]: Option[];
+}
+
+interface MultipleSelectorProps {
+ value?: Option[];
+ defaultOptions?: Option[];
+ /** manually controlled options */
+ options?: Option[];
+ placeholder?: string;
+ /** Loading component. */
+ loadingIndicator?: React.ReactNode;
+ /** Empty component. */
+ emptyIndicator?: React.ReactNode;
+ /** Debounce time for async search. Only work with `onSearch`. */
+ delay?: number;
+ /**
+ * Only work with `onSearch` prop. Trigger search when `onFocus`.
+ * For example, when user click on the input, it will trigger the search to get initial options.
+ **/
+ triggerSearchOnFocus?: boolean;
+ /** async search */
+ onSearch?: (value: string) => Promise