Compare commits

..

No commits in common. "3cd94e5999fcad177e68e60f157b0edeb70c310c" and "ee127f431c5386065b3fb6de07a463ad6dd23961" have entirely different histories.

35 changed files with 221 additions and 1845 deletions

View File

@ -1,7 +1,7 @@
import { Controller, Get } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { AppService } from "./app.service";
import { ApiTags } from '@nestjs/swagger';
@Controller()
@ApiTags("useless")

View File

@ -10,12 +10,12 @@ import {
UnauthorizedException,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { SignInDto, SignUpDto } from "apps/backend/src/app/auth/auth.dto";
import { AuthService } from "apps/backend/src/app/auth/auth.service";
import { UserGuard } from "./auth.guard";
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
@ApiTags("User authentification")
@ApiTags('User authentification')
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}

View File

@ -1,4 +1,3 @@
import { ApiProperty } from "@nestjs/swagger";
import {
IsEmail,
IsNotEmpty,
@ -7,6 +6,7 @@ import {
MaxLength,
MinLength,
} from "class-validator";
import { ApiProperty } from '@nestjs/swagger';
export class SignUpDto {
/*
@ -24,7 +24,7 @@ export class SignUpDto {
**/
@ApiProperty({
example: "jean@paul.fr",
example: 'jean@paul.fr',
})
@MaxLength(32)
@IsEmail()
@ -32,7 +32,7 @@ export class SignUpDto {
email: string;
@ApiProperty({
example: "zSEs-6ze$",
example: 'zSEs-6ze$',
})
@IsString()
@IsNotEmpty()
@ -43,17 +43,15 @@ export class SignUpDto {
}
export class SignInDto {
@MaxLength(32)
@ApiProperty({
example: "jean@paul.fr",
@MaxLength(32)@ApiProperty({
example: 'jean@paul.fr',
})
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@ApiProperty({
example: "zSEs-6ze$",
@IsString()@ApiProperty({
example: 'zSEs-6ze$',
})
@IsNotEmpty()
@IsStrongPassword({

View File

@ -9,11 +9,11 @@ import {
Query,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { AdminGuard } from "apps/backend/src/app/auth/auth.guard";
import { AuthorsService } from "apps/backend/src/app/authors/authors.service";
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
@ApiTags("File authors")
@ApiTags('File authors')
@Controller("authors")
export class AuthorsController {
constructor(private readonly authorService: AuthorsService) {}

View File

@ -20,31 +20,28 @@ import {
StreamableFile,
UseGuards,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiHeaders,
ApiOperation,
ApiSecurity,
ApiTags,
} from "@nestjs/swagger";
import { CreateFileTypeDto } from "apps/backend/src/app/files/files.dto";
import { AdminGuard, InsertAdminState } from "../auth/auth.guard";
import { FilesService } from "./files.service";
import {
ApiBearerAuth, ApiBody, ApiConsumes,
ApiHeaders, ApiOperation,
ApiSecurity,
ApiTags
} from '@nestjs/swagger';
@ApiTags("Files")
@ApiTags('Files')
@Controller("files")
export class FilesController {
constructor(private readonly filesService: FilesService) {}
@ApiOperation({ summary: "Uploader un fichier" })
@ApiOperation({ summary: "Uploader un fichier"})
@ApiConsumes("application/octet-stream")
@ApiBody({
schema: {
type: "string",
format: "binary",
},
type: 'string',
format: 'binary'
}
})
@ApiHeaders([
{
@ -52,21 +49,18 @@ export class FilesController {
description: "Nom du fichier",
example: "Logo marianne",
allowEmptyValue: false,
required: true,
required: true
},
{
name: "group_id",
description: "Groupe de fichier optionnel",
example: "dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c",
allowEmptyValue: true,
allowEmptyValue: true
},
{
name: "machine_id",
description: "Identifiant(s) de machine(s)",
example: [
"dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c",
"dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c",
],
example: ["dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c", "dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c"],
allowEmptyValue: false,
required: true,
},
@ -79,15 +73,13 @@ export class FilesController {
},
{
name: "is_documentation",
description:
"Admin uniquement, défini le fichier comme étant une documentation",
description: "Admin uniquement, défini le fichier comme étant une documentation",
enum: ["true", "false"],
allowEmptyValue: false,
},
{
name: "is_restricted",
description:
"Admin uniquement, rend impossible l'écrasement d'un fichier (non implémenté pour l'instant)",
description: "Admin uniquement, rend impossible l'écrasement d'un fichier (non implémenté pour l'instant)",
enum: ["true", "false"],
allowEmptyValue: false,
},
@ -178,7 +170,7 @@ export class FilesController {
return;
}
@HttpCode(HttpStatus.OK)
@HttpCode(HttpStatus.FOUND)
@Get("find")
async findMany(
@Query("limit", new DefaultValuePipe(20), ParseIntPipe) limit: number,
@ -188,7 +180,7 @@ export class FilesController {
return this.filesService.search(limit, offset, search);
}
@HttpCode(HttpStatus.OK)
@HttpCode(HttpStatus.FOUND)
@Get("types")
async getTypes() {
console.log("Performing request");

View File

@ -1,20 +1,25 @@
import { DefaultValuePipe } from "@nestjs/common";
import { ApiBearerAuth, ApiProperty } from "@nestjs/swagger";
import { IsUUID, MaxLength, MinLength } from "class-validator";
import { ApiBearerAuth, ApiProperty } from '@nestjs/swagger';
export class CreateFileTypeDto {
@ApiProperty({
description: "Admin uniquement, nom d'affichage.",
examples: [".scad", "jpg"],
examples: [
".scad",
"jpg"
]
})
@MaxLength(128)
@MinLength(3)
name: string;
@ApiProperty({
description:
"Admin uniquement, Multipurpose Internet Mail Extensions (MIME)",
examples: ["application/x-openscad", "image/jpeg"],
description: "Admin uniquement, Multipurpose Internet Mail Extensions (MIME)",
examples: [
"application/x-openscad",
"image/jpeg"
]
})
@MaxLength(64)
@MinLength(4)

View File

@ -10,13 +10,13 @@ import {
Query,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { AdminGuard } from "apps/backend/src/app/auth/auth.guard";
import { CreateGroupDto } from "apps/backend/src/app/groups/groups.dto";
import { ISearchQuery } from "apps/backend/src/app/groups/groups.types";
import { GroupsService } from "./groups.service";
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
@ApiTags("File groups")
@ApiTags('File groups')
@Controller("groups")
export class GroupsController {
constructor(private readonly groupsService: GroupsService) {}

View File

@ -1,10 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, MaxLength, MinLength } from "class-validator";
import { ApiProperty } from '@nestjs/swagger';
export class CreateGroupDto {
@ApiProperty({
description: "Nom unique.",
example: "Numérique en communs",
example: "Numérique en communs"
})
@IsString()
@MinLength(4)

View File

@ -13,16 +13,16 @@ import {
Query,
UseGuards,
} from "@nestjs/common";
import { ApiResponse, ApiTags } from "@nestjs/swagger";
import { AdminGuard } from "apps/backend/src/app/auth/auth.guard";
import { IMachinesTable } from "apps/backend/src/app/db/schema";
import {
CreateMachineDto,
TypeDto,
} from "apps/backend/src/app/machines/machines.dto";
import { MachinesService } from "apps/backend/src/app/machines/machines.service";
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { IMachinesTable } from 'apps/backend/src/app/db/schema';
@ApiTags("Machines")
@ApiTags('Machines')
@Controller("machines")
export class MachinesController {
constructor(private readonly machineService: MachinesService) {}
@ -68,13 +68,13 @@ export class MachinesController {
return await this.machineService.removeFileType(machineId, body.fileTypeId);
}
@HttpCode(HttpStatus.OK)
@HttpCode(HttpStatus.FOUND)
@Get("types/:machineId")
async getTypesOfMachine(@Param("machineId") machineId: string) {
return await this.machineService.getFilesTypes(machineId);
}
@HttpCode(HttpStatus.OK)
@HttpCode(HttpStatus.FOUND)
@Get("files/:machineId")
async getFilesForMachine(
@Query("limit", new DefaultValuePipe(20), ParseIntPipe) limit: number,

View File

@ -1,16 +1,16 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsUUID, MaxLength, MinLength } from "class-validator";
import { ApiProperty } from '@nestjs/swagger';
export class CreateMachineDto {
@ApiProperty({
example: "Découpeuse laser portable",
example: "Découpeuse laser portable"
})
@MaxLength(128)
@MinLength(4)
machineName: string;
@ApiProperty({
example: "Découpe au laser",
example: "Découpe au laser"
})
@MaxLength(64)
@MinLength(2)
@ -19,9 +19,8 @@ export class CreateMachineDto {
export class TypeDto {
@ApiProperty({
description:
"Un identifiant unique présent en base de donnée qui représente un MIME",
example: "dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c",
description: "Un identifiant unique présent en base de donnée qui représente un MIME",
example: "dfd0fbb1-2bf3-4dbe-b86d-89b1bff5106c"
})
@IsUUID()
fileTypeId: string;

View File

@ -2,9 +2,7 @@ import { Logger } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import cors from "cors";
import helmet from "helmet";
import { AppModule } from "./app/app.module";
async function bootstrap() {
@ -13,9 +11,9 @@ async function bootstrap() {
.setDescription("Définition de l'api du FabLab Explorer")
.setVersion("1.0")
.addBearerAuth({
type: "http",
scheme: "bearer",
in: "header",
type: 'http',
scheme: 'bearer',
in: 'header',
})
.build();
@ -25,13 +23,6 @@ async function bootstrap() {
app.use(helmet());
const port = process.env.PORT || 3333;
const corsOptions = {
origin: "http://localhost:3000",
methods: ["GET", "POST", "PUT", "DELETE"],
};
app.use(cors(corsOptions));
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api", app, document);

View File

@ -0,0 +1,3 @@
export async function GET(request: Request) {
return new Response("Hello, from API!");
}

View File

@ -8,19 +8,17 @@ export const metadata = {
description: 'Generated by create-nx-workspace',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={"h-screen w-screen bg-card flex flex-col justify-between items-center police-ubuntu"}>
<Header/>
{children}
<Footer/>
<Header/>
{children}
<Footer/>
</body>
</html>
);

View File

@ -6,25 +6,30 @@ import {
ResizablePanelGroup
} from 'apps/frontend/src/components/ui/resizable';
import { ESubPage, SubPage, SubPageSelector } from '../components/sub-pages';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({})
export default function HomePage() {
const [currentSubPage, setCurrentSubPage] = useState<ESubPage>(0)
return (
<QueryClientProvider client={queryClient}>
<main
className="w-full h-full bg-background border border-muted p-2 rounded-md flex flex-row justify-stretch items-stretch">
<div className={"h-full flex flex-col justify-start items-center"}>
<SubPageSelector
currentSubPage={currentSubPage}
setCurrentSubPage={setCurrentSubPage}
/>
</div>
<SubPage currentSubPage={currentSubPage} />
</main>
</QueryClientProvider>
<main className="w-full h-full bg-background border border-muted p-2 rounded-md flex flex-row justify-stretch items-stretch">
<ResizablePanelGroup
direction="horizontal">
<ResizablePanel
defaultSize={20}
minSize={15}
maxSize={25}
className={"w-1/5 h-full p-1 flex flex-col items-center"}>
<SubPageSelector
currentSubPage={currentSubPage}
setCurrentSubPage={setCurrentSubPage}
/>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
className={"w-full flex justify-center items-center m-3"}>
<SubPage currentSubPage={currentSubPage}/>
</ResizablePanel>
</ResizablePanelGroup>
</main>
);
}

View File

@ -1,327 +0,0 @@
import Dropzone, { DropzoneProps, FileRejection } from 'react-dropzone';
import { toast } from 'sonner';
import React from 'react';
import { FileTextIcon, UploadIcon } from 'lucide-react';
import { ScrollArea } from './ui/scroll-area';
import { Progress } from '@radix-ui/react-progress';
import { Button } from './ui/button';
import { formatBytes } from '../lib/utils';
interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Value of the uploader.
* @type File[]
* @default undefined
* @example value={files}
*/
value?: File[]
/**
* Function to be called when the value changes.
* @type (files: File[]) => void
* @default undefined
* @example onValueChange={(files) => setFiles(files)}
*/
onValueChange?: (files: File[]) => void
/**
* Function to be called when files are uploaded.
* @type (files: File[]) => Promise<void>
* @default undefined
* @example onUpload={(files) => uploadFiles(files)}
*/
onUpload?: (files: File[]) => Promise<void>
/**
* Progress of the uploaded files.
* @type Record<string, number> | undefined
* @default undefined
* @example progresses={{ "file1.png": 50 }}
*/
progresses?: Record<string, number>
/**
* Accepted file types for the uploader.
* @type { [key: string]: string[]}
* @default
* ```ts
* { "image/*": [] }
* ```
* @example accept={["image/png", "image/jpeg"]}
*/
accept?: DropzoneProps["accept"]
/**
* Maximum file size for the uploader.
* @type number | undefined
* @default 1024 * 1024 * 2 // 2MB
* @example maxSize={1024 * 1024 * 2} // 2MB
*/
maxSize?: DropzoneProps["maxSize"]
/**
* Maximum number of files for the uploader.
* @type number | undefined
* @default 1
* @example maxFileCount={4}
*/
maxFileCount?: DropzoneProps["maxFiles"]
/**
* Whether the uploader should accept multiple files.
* @type boolean
* @default false
* @example multiple
*/
multiple?: boolean
/**
* Whether the uploader is disabled.
* @type boolean
* @default false
* @example disabled
*/
disabled?: boolean
}
export function FileUploader(props: FileUploaderProps) {
const {
value: valueProp,
onValueChange,
onUpload,
progresses,
accept = {
"image/*": [],
},
maxSize = 1024 * 1024 * 2,
maxFileCount = 1,
multiple = false,
disabled = false,
className,
...dropzoneProps
} = props
const [files, setFiles] = useControllableState({
prop: valueProp,
onChange: onValueChange,
})
const onDrop = React.useCallback(
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
toast.error("Cannot upload more than 1 file at a time")
return
}
if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) {
toast.error(`Cannot upload more than ${maxFileCount} files`)
return
}
const newFiles = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
)
const updatedFiles = files ? [...files, ...newFiles] : newFiles
setFiles(updatedFiles)
if (rejectedFiles.length > 0) {
rejectedFiles.forEach(({ file }) => {
toast.error(`File ${file.name} was rejected`)
})
}
if (
onUpload &&
updatedFiles.length > 0 &&
updatedFiles.length <= maxFileCount
) {
const target =
updatedFiles.length > 0 ? `${updatedFiles.length} files` : `file`
toast.promise(onUpload(updatedFiles), {
loading: `Uploading ${target}...`,
success: () => {
setFiles([])
return `${target} uploaded`
},
error: `Failed to upload ${target}`,
})
}
},
[files, maxFileCount, multiple, onUpload, setFiles]
)
function onRemove(index: number) {
if (!files) return
const newFiles = files.filter((_, i) => i !== index)
setFiles(newFiles)
onValueChange?.(newFiles)
}
// Revoke preview url when component unmounts
React.useEffect(() => {
return () => {
if (!files) return
files.forEach((file) => {
if (isFileWithPreview(file)) {
URL.revokeObjectURL(file.preview)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount
return (
<div className="relative flex flex-col gap-6 overflow-hidden">
<Dropzone
onDrop={onDrop}
accept={accept}
maxSize={maxSize}
maxFiles={maxFileCount}
multiple={maxFileCount > 1 || multiple}
disabled={isDisabled}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div
{...getRootProps()}
className={cn(
"group relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-muted-foreground/25 px-5 py-2.5 text-center transition hover:bg-muted/25",
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isDragActive && "border-muted-foreground/50",
isDisabled && "pointer-events-none opacity-60",
className
)}
{...dropzoneProps}
>
<input {...getInputProps()} />
{isDragActive ? (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<UploadIcon
className="size-7 text-muted-foreground"
aria-hidden="true"
/>
</div>
<p className="font-medium text-muted-foreground">
Drop the files here
</p>
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<UploadIcon
className="size-7 text-muted-foreground"
aria-hidden="true"
/>
</div>
<div className="flex flex-col gap-px">
<p className="font-medium text-muted-foreground">
Drag {`'n'`} drop files here, or click to select files
</p>
<p className="text-sm text-muted-foreground/70">
You can upload
{maxFileCount > 1
? ` ${maxFileCount === Infinity ? "multiple" : maxFileCount}
files (up to ${formatBytes(maxSize)} each)`
: ` a file with ${formatBytes(maxSize)}`}
</p>
</div>
</div>
)}
</div>
)}
</Dropzone>
{files?.length ? (
<ScrollArea className="h-fit w-full px-3">
<div className="flex max-h-48 flex-col gap-4">
{files?.map((file, index) => (
<FileCard
key={index}
file={file}
onRemove={() => onRemove(index)}
progress={progresses?.[file.name]}
/>
))}
</div>
</ScrollArea>
) : null}
</div>
)
}
interface FileCardProps {
file: File
onRemove: () => void
progress?: number
}
function FileCard({ file, progress, onRemove }: FileCardProps) {
return (
<div className="relative flex items-center gap-2.5">
<div className="flex flex-1 gap-2.5">
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-px">
<p className="line-clamp-1 text-sm font-medium text-foreground/80">
{file.name}
</p>
<p className="text-xs text-muted-foreground">
{formatBytes(file.size)}
</p>
</div>
{progress ? <Progress value={progress} /> : null}
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="icon"
className="size-7"
onClick={onRemove}
>
<Cross2Icon className="size-4" aria-hidden="true" />
<span className="sr-only">Remove file</span>
</Button>
</div>
</div>
)
}
function isFileWithPreview(file: File): file is File & { preview: string } {
return "preview" in file && typeof file.preview === "string"
}
interface FilePreviewProps {
file: File & { preview: string }
}
function FilePreview({ file }: FilePreviewProps) {
if (file.type.startsWith("image/")) {
return (
<Image
src={file.preview}
alt={file.name}
width={48}
height={48}
loading="lazy"
className="aspect-square shrink-0 rounded-md object-cover"
/>
)
}
return (
<FileTextIcon
className="size-10 text-muted-foreground"
aria-hidden="true"
/>
)
}

View File

@ -1,154 +0,0 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "../ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form"
import { Input } from "../ui/input"
import { FilesApi } from 'apps/frontend/src/requests/files';
import {
MultipleMachinesSelector
} from 'apps/frontend/src/components/forms/machines-selector';
import MultipleSelector, { Option } from 'apps/frontend/src/components/ui/multiple-selector';
import { MachinesApi } from 'apps/frontend/src/requests/machines';
import { Loader } from 'lucide-react';
import React from 'react';
async function getMachines(value: string): Promise<Option[]> {
try {
const machines = await MachinesApi.get.all();
console.log(machines.length);
const filtered = machines.filter((machine) => machine.machineName && machine.machineName.toLowerCase().includes(value.toLowerCase()));
console.log(filtered.length);
const mapped = filtered.map((machine) => ({
label: machine.machineName,
value: machine.id,
}));
return mapped;
} catch (error) {
console.error('Erreur lors de la récupération des machines:', error);
return [];
}
}
const machinesSchema = z.object({
label: z.string(),
value: z.string(),
disable: z.boolean().optional(),
});
const fileUploadSchema = z.object({
fileName: z.string().min(2, {
message: "Le nom du fichier doit faire au moins faire 2 carractères.",
}).max(128, {
message: "Le nom du fichier ne peux pas faire plus de 128 carractères."
}),
author: z.string().min(2, {
message: "Votre pseudonyme doit faire au moins faire 2 carractères."
}).max(64, {
message: "Votre pseudonyme ne peux pas faire plus de 64 carractères."
}),
machinesId: z.array(machinesSchema).min(1, {
message: "Vous devez indiqué au moins une machine."
})
})
export function FileUploadForm() {
const executeUpload = FilesApi.post.upload;
const form = useForm<z.infer<typeof fileUploadSchema>>({
resolver: zodResolver(fileUploadSchema),
})
function onSubmit(data: z.infer<typeof fileUploadSchema>) {
console.log(data)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<FormField
control={form.control}
name="fileName"
render={({ field }) => (
<FormItem>
<FormLabel>Nom du fichier</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Le nom qui sera affiché lors d'une recherche.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="machinesId"
render={({ field }) => (
<FormItem>
<FormLabel>Associer à des machines</FormLabel>
<FormControl>
<MultipleSelector
{...field}
onSearch={async (value) => {
const res = await getMachines(value);
return res;
}}
triggerSearchOnFocus
placeholder="Cliquez pour chercher."
loadingIndicator={
<div>
<Loader className={"animate-spin h-4 w-4 mr-2"} />
<p className="py-2 text-center text-lg leading-10 text-muted-foreground">Chargement...</p>
</div>
}
emptyIndicator={
<p
className="w-full text-center text-lg leading-10 text-muted-foreground">
Auccun résultats
</p>
}
/>
</FormControl>
<FormDescription>
Machine(s) qui seront associé(s) à ce fichier.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="author"
render={({ field }) => (
<FormItem>
<FormLabel>Votre pseudonyme ou nom.</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Votre nom d'affichage qui sera lié au fichier.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}

View File

@ -1,50 +0,0 @@
'use client';
import React from 'react';
import MultipleSelector, { Option } from '../ui/multiple-selector';
import { MachinesApi } from '../../requests/machines';
import { Loader } from 'lucide-react';
import IntrinsicAttributes = React.JSX.IntrinsicAttributes;
async function getMachines(value: string): Promise<Option[]> {
try {
const machines = await MachinesApi.get.all();
console.log(machines.length);
const filtered = machines.filter((machine) => machine.machineName && machine.machineName.toLowerCase().includes(value.toLowerCase()));
console.log(filtered.length);
const mapped = filtered.map((machine) => ({
label: machine.machineName,
value: machine.id,
}));
return mapped;
} catch (error) {
console.error('Erreur lors de la récupération des machines:', error);
return [];
}
}
export function MultipleMachinesSelector(fields: IntrinsicAttributes) {
return (<MultipleSelector
onSearch={async (value) => {
const res = await getMachines(value);
return res;
}}
triggerSearchOnFocus
placeholder="Cliquez pour chercher."
loadingIndicator={
<div>
<Loader className={"animate-spin h-4 w-4 mr-2"} />
<p className="py-2 text-center text-lg leading-10 text-muted-foreground">Chargement...</p>
</div>
}
emptyIndicator={
<p
className="w-full text-center text-lg leading-10 text-muted-foreground">
Auccun résultats
</p>
}
/>);
};

View File

@ -1,8 +0,0 @@
import { Loader } from 'lucide-react';
export function LoadingSpinner() {
return (
<div className={"flex justify-center items-center gap-2 text-xl"}><Loader
className={"animate-spin w-10 h-10"} />Loading...</div>)
}

View File

@ -1,20 +1,18 @@
import { Button } from './ui/button';
import {
Dialog,
DialogContent, DialogHeader,
DialogContent, DialogDescription,
DialogHeader, DialogTitle,
DialogTrigger
} from './ui/dialog';
import { FileInput } from 'lucide-react';
import { FileUploadForm } from 'apps/frontend/src/components/forms/file-upload';
import {
MultipleMachinesSelector
} from 'apps/frontend/src/components/forms/machines-selector';
export interface NewFileModalProps {
classname?: string;
}
export function NewFileModal(props: NewFileModalProps) {
return (
<Dialog>
<DialogTrigger asChild>
@ -24,8 +22,13 @@ export function NewFileModal(props: NewFileModalProps) {
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>Ajouter un fichier</DialogHeader>
<FileUploadForm/>
<DialogHeader>
<DialogTitle>Ajout d'un fichier</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
)

View File

@ -21,7 +21,7 @@ export enum ESubPage {
export function SubPageSelector(props: SubPageSelectorProps) {
return (
<div className={"max-[20%]: flex flex-col justify-center items-stretch pt-4 p-4 gap-2"}>
<div className={"w-full flex flex-col justify-center items-stretch pt-4 p-4 gap-2"}>
<Button
onClick={()=>props.setCurrentSubPage(ESubPage.Home)}
disabled={props.currentSubPage === ESubPage.Home}

View File

@ -3,9 +3,6 @@ import { HomeIcon } from 'lucide-react';
import {
FilesDataTable, filesTableColumns
} from 'apps/frontend/src/components/tables/files-table';
import {
ISearchBarResult, SearchBar
} from 'apps/frontend/src/components/tables/search-bar';
export interface SubHomePageProps {
@ -14,22 +11,28 @@ export interface SubHomePageProps {
export function SubHomePage(props: SubHomePageProps) {
const [isLoaded, setIsLoaded] = useState<boolean>(false);
const [searchField, setSearchField] = useState<ISearchBarResult>({value: ""})
return (<section className={"w-full h-full rounded bg-card flex flex-col"}>
<div className={"flex flex-row justify-start items-center gap-2 m-2"}>
<HomeIcon
className={"w-8 h-8 text-secondary"}
/>
<h1 className={"text-2xl font-bold"}>Accueil</h1>
<h1 className={"text-2xl font-bold"}>Page principal</h1>
</div>
<div
className={"m-1 flex flex-col justify-start items-center w-5/6 self-center h-full"}>
<div className={"flex w-full justify-start items-center mb-4"}>
<SearchBar
stateSetter={setSearchField} />
</div>
<FilesDataTable columns={filesTableColumns} searchField={searchField} />
<div className={"m-1 flex flex-col justify-start items-center w-5/6 self-center h-full"}>
<FilesDataTable columns={filesTableColumns} data={[{
"uuid": "bbc17f8c-244d-4a44-8faf-c5e1ec0786bf",
"fileName": "test",
"checksum": "60d6473dc75edd2e885cc32c098f0379a5dd2d8175de0df1ef7526636b2a03f5",
"extension": "jpeg",
"groupId": null,
"fileSize": 483636,
"fileType": "2c1fb8eb-59b1-4bef-b50d-6bc854f46105",
"isRestricted": false,
"isDocumentation": false,
"uploadedAt": "2024-10-21T11:40:36.350Z",
"uploadedBy": "Avnyr"
}]}/>
</div>
</section>)
}

View File

@ -1,14 +1,12 @@
"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
SortingState,
getCoreRowModel, getPaginationRowModel, getSortedRowModel, SortingState,
useReactTable
} from '@tanstack/react-table';
import {
Table,
TableBody,
@ -19,207 +17,169 @@ import {
} from "../ui/table"
import { Button } from '../ui/button';
import { Badge } from '../ui/badge'
import { ArrowUpDown, Clock, Download, Trash, TriangleAlert } from 'lucide-react';
import { Dispatch, SetStateAction, useMemo, useReducer, useState } from 'react';
import { ArrowUpDown, Clock, Download, Trash } from 'lucide-react';
import { useState } from 'react';
import Link from 'next/link';
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';
// 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;
}
function ContextButtonForFile() {
return (
<div className={"scale-75"}>
<Button variant={"destructive"}>
<Trash />
</Button>
</div>
);
return (<div className={"scale-75"}>
<Button variant={"destructive"}><Trash/></Button>
</div>)
}
export const filesTableColumns: ColumnDef<IFile>[] = [
{
accessorKey: "fileName",
header: ({ column }) => (
<div className={"flex justify-center items-center"}>
header: ({ column }) => {
return (<div className={"flex justify-center items-center"}>
Nom du fichier
</div>
),
</div>)
},
},
{
accessorKey: "uploadedBy",
header: ({ column }) => (
<div className={"flex justify-center items-center"}>
header: ({ column }) => {
return (<div className={"flex justify-center items-center"}>
Autheur(s)
</div>
),
</div>)
},
},
{
accessorKey: "uploadedAt",
header: ({ column }) => (
<Button
variant="ghost"
className={"flex w-full"}
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Ajouté le
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => {
const date = new Date(row.getValue("uploadedAt"));
const formatted = `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} à ${date.getHours()}:${date.getMinutes()}`;
header: ({ column }) => {
return (
<div className={"flex justify-center items-center"}>
<Badge variant="outline" className={"gap-1 flex w-fit items-center"}>
<Clock className={"w-4 h-4"} />
<p className={"font-light"}>
{formatted}
</p>
</Badge>
</div>
);
<Button
variant="ghost"
className={"flex w-full"}
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Ajouté le
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const date = new Date(row.getValue("uploadedAt"))
const formatted = `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} à ${date.getHours()}:${date.getMinutes()}`
return (<div className={"flex justify-center items-center"}>
<Badge
variant="outline"
className={"gap-1 flex w-fit items-center"}>
<Clock className={"w-4 h-4"} />
<p className={"font-light"}>
{formatted}
</p>
</Badge>
</div>)
},
},
{
accessorKey: "extension",
header: ({ column }) => (
<div className={"flex justify-center items-center"}>
header: ({ column }) => {
return (<div className={"flex justify-center items-center"}>
Extension du fichier
</div>
),
</div>)
},
cell: ({ row }) => {
const extension = row.getValue("extension") as string;
return (
<div className={"flex justify-center items-center"}>
<code className={"bg-gray-300 p-1 px-2 rounded-full"}>{extension}</code>
</div>
);
return (<div className={"flex justify-center items-center"}>
<code className={"bg-gray-300 p-1 px-2 rounded-full"}>{extension}</code>
</div>)
},
},
{
id: "actions",
header: ({ column }) => (
<div className={"flex justify-center items-center"}>
header: ({ column }) => {
return (<div className={"flex justify-center items-center"}>
Actions
</div>
),
</div>)
},
cell: ({ row }) => {
const file = row.original;
return (
<div className={"flex gap"}>
<Button variant={"ghost"} asChild>
<Link href={`http://localhost:3333/api/files/${file.uuid}`}>
<Download />
Télécharger
</Link>
</Button>
<ContextButtonForFile />
</div>
);
const file = row.original
return (<div className={"flex gap"}>
<Button variant={"ghost"} asChild>
<Link
href={`http://localhost:3333/api/files/${file.uuid}`}
>
<Download />
Télécharger
</Link>
</Button>
<ContextButtonForFile/>
</div>)
},
},
];
]
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
searchField: ISearchBarResult
data: TData[]
}
export function FilesDataTable<TData, TValue>({
columns,
searchField
}: DataTableProps<TData, TValue>) {
const rerender = useReducer(() => ({}), {})[1];
const [sorting, setSorting] = useState<SortingState>([]);
const [data, setData] = useState<TData[]>([]);
const [rowsCount, setRowsCount] = useState(0);
const [pagination, setPagination] = useState<PaginationState>({
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;
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([])
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 (
<Alert variant="destructive">
<TriangleAlert />
<AlertTitle>Erreur</AlertTitle>
<AlertDescription>
{error.message}
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Stack trace</AccordionTrigger>
<AccordionContent>
<code className="text-sm font-mono border-l-2 border-l-destructive h-fit">{error.stack}</code>
</AccordionContent>
</AccordionItem>
</Accordion>
</AlertDescription>
</Alert>
);
}
})
return (
<div className="w-full">
{isPending && isFetching && isPlaceholderData ? <LoadingSpinner/> : <Table>
<div className="rounded-md border w-full">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@ -235,10 +195,7 @@ export function FilesDataTable<TData, TValue>({
</TableRow>
)}
</TableBody>
</Table>}
<div className={"flex w-full justify-end items-center"}>
<TablePagination rowsCount={rowsCount} pagination={pagination} setPagination={setPagination}/>
</div>
</Table>
</div>
);
)
}

View File

@ -1,46 +0,0 @@
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<SetStateAction<PaginationState>>
}
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 (<div className="flex items-center justify-end gap-4 space-x-2 py-4">
{isMonoPage ? (<h2>Il n'y as qu'une seule page.</h2>) : (<h2>Page <em>{currentPage}</em> sur <em>{totalPages}</em></h2>)}
<div className={"flex gap-2 justify-center items-center"}>
<Button
variant="outline"
size="sm"
onClick={() => {
}}
disabled={!hasPreviousPage}
>
Page précédente
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
}}
disabled={!hasNextPage}
>
Page suivante
</Button>
</div>
</div>)
}

View File

@ -1,42 +0,0 @@
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<SetStateAction<ISearchBarResult>>;
}
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 (<div className="flex gap-2">
<div className="grid w-full max-w-md items-center gap-1.5">
<Label htmlFor="searchFiled">Chercher un nom de fichier</Label>
<Input
type="search"
id="searchFiled"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Votre recherche..." />
</div>
{isLoading && <Loader className={"w-6 h-6 animate-spin"} />}
</div>)
}

View File

@ -12,8 +12,8 @@ import {
useFormContext,
} from "react-hook-form"
import { cn } from "../../lib/utils"
import { Label } from "./label"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider

View File

@ -1,608 +0,0 @@
'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<Option[]>;
/**
* sync search. This search will not showing loadingIndicator.
* The rest props are the same as async search.
* i.e.: creatable, groupBy, delay.
**/
onSearchSync?: (value: string) => Option[];
onChange?: (options: Option[]) => void;
/** Limit the maximum number of selected options. */
maxSelected?: number;
/** When the number of selected options exceeds the limit, the onMaxSelected will be called. */
onMaxSelected?: (maxLimit: number) => void;
/** Hide the placeholder when there are options selected. */
hidePlaceholderWhenSelected?: boolean;
disabled?: boolean;
/** Group the options base on provided key. */
groupBy?: string;
className?: string;
badgeClassName?: string;
/**
* First item selected is a default behavior by cmdk. That is why the default is true.
* This is a workaround solution by add a dummy item.
*
* @reference: https://github.com/pacocoursey/cmdk/issues/171
*/
selectFirstItem?: boolean;
/** Allow user to create option when there is no option matched. */
creatable?: boolean;
/** Props of `Command` */
commandProps?: React.ComponentPropsWithoutRef<typeof Command>;
/** Props of `CommandInput` */
inputProps?: Omit<
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>,
'value' | 'placeholder' | 'disabled'
>;
/** hide the clear all button. */
hideClearAllButton?: boolean;
}
export interface MultipleSelectorRef {
selectedValue: Option[];
input: HTMLInputElement;
focus: () => void;
reset: () => void;
}
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
function transToGroupOption(options: Option[], groupBy?: string) {
if (options.length === 0) {
return {};
}
if (!groupBy) {
return {
'': options,
};
}
const groupOption: GroupOption = {};
options.forEach((option) => {
const key = (option[groupBy] as string) || '';
if (!groupOption[key]) {
groupOption[key] = [];
}
groupOption[key].push(option);
});
return groupOption;
}
function removePickedOption(groupOption: GroupOption, picked: Option[]) {
const cloneOption = JSON.parse(JSON.stringify(groupOption)) as GroupOption;
for (const [key, value] of Object.entries(cloneOption)) {
cloneOption[key] = value.filter((val) => !picked.find((p) => p.value === val.value));
}
return cloneOption;
}
function isOptionsExist(groupOption: GroupOption, targetOption: Option[]) {
for (const [, value] of Object.entries(groupOption)) {
if (value.some((option) => targetOption.find((p) => p.value === option.value))) {
return true;
}
}
return false;
}
/**
* The `CommandEmpty` of shadcn/ui will cause the cmdk empty not rendering correctly.
* So we create one and copy the `Empty` implementation from `cmdk`.
*
* @reference: https://github.com/hsuanyi-chou/shadcn-ui-expansions/issues/34#issuecomment-1949561607
**/
const CommandEmpty = forwardRef<
HTMLDivElement,
React.ComponentProps<typeof CommandPrimitive.Empty>
>(({ className, ...props }, forwardedRef) => {
const render = useCommandState((state) => state.filtered.count === 0);
if (!render) return null;
return (
<div
ref={forwardedRef}
className={cn('py-6 text-center text-sm', className)}
cmdk-empty=""
role="presentation"
{...props}
/>
);
});
CommandEmpty.displayName = 'CommandEmpty';
const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorProps>(
(
{
value,
onChange,
placeholder,
defaultOptions: arrayDefaultOptions = [],
options: arrayOptions,
delay,
onSearch,
onSearchSync,
loadingIndicator,
emptyIndicator,
maxSelected = Number.MAX_SAFE_INTEGER,
onMaxSelected,
hidePlaceholderWhenSelected,
disabled,
groupBy,
className,
badgeClassName,
selectFirstItem = true,
creatable = false,
triggerSearchOnFocus = false,
commandProps,
inputProps,
hideClearAllButton = false,
}: MultipleSelectorProps,
ref: React.Ref<MultipleSelectorRef>,
) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const [open, setOpen] = React.useState(false);
const [onScrollbar, setOnScrollbar] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const dropdownRef = React.useRef<HTMLDivElement>(null); // Added this
const [selected, setSelected] = React.useState<Option[]>(value || []);
const [options, setOptions] = React.useState<GroupOption>(
transToGroupOption(arrayDefaultOptions, groupBy),
);
const [inputValue, setInputValue] = React.useState('');
const debouncedSearchTerm = useDebounce(inputValue, delay || 500);
React.useImperativeHandle(
ref,
() => ({
selectedValue: [...selected],
input: inputRef.current as HTMLInputElement,
focus: () => inputRef?.current?.focus(),
reset: () => setSelected([])
}),
[selected],
);
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
inputRef.current &&
!inputRef.current.contains(event.target as Node)
) {
setOpen(false);
inputRef.current.blur();
}
};
const handleUnselect = React.useCallback(
(option: Option) => {
const newOptions = selected.filter((s) => s.value !== option.value);
setSelected(newOptions);
onChange?.(newOptions);
},
[onChange, selected],
);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current;
if (input) {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (input.value === '' && selected.length > 0) {
const lastSelectOption = selected[selected.length - 1];
// If last item is fixed, we should not remove it.
if (!lastSelectOption.fixed) {
handleUnselect(selected[selected.length - 1]);
}
}
}
// This is not a default behavior of the <input /> field
if (e.key === 'Escape') {
input.blur();
}
}
},
[handleUnselect, selected],
);
useEffect(() => {
if (open) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchend', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchend', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchend', handleClickOutside);
};
}, [open]);
useEffect(() => {
if (value) {
setSelected(value);
}
}, [value]);
useEffect(() => {
/** If `onSearch` is provided, do not trigger options updated. */
if (!arrayOptions || onSearch) {
return;
}
const newOption = transToGroupOption(arrayOptions || [], groupBy);
if (JSON.stringify(newOption) !== JSON.stringify(options)) {
setOptions(newOption);
}
}, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]);
useEffect(() => {
/** sync search */
const doSearchSync = () => {
const res = onSearchSync?.(debouncedSearchTerm);
setOptions(transToGroupOption(res || [], groupBy));
};
const exec = async () => {
if (!onSearchSync || !open) return;
if (triggerSearchOnFocus) {
doSearchSync();
}
if (debouncedSearchTerm) {
doSearchSync();
}
};
void exec();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
useEffect(() => {
/** async search */
const doSearch = async () => {
setIsLoading(true);
const res = await onSearch?.(debouncedSearchTerm);
setOptions(transToGroupOption(res || [], groupBy));
setIsLoading(false);
};
const exec = async () => {
if (!onSearch || !open) return;
if (triggerSearchOnFocus) {
await doSearch();
}
if (debouncedSearchTerm) {
await doSearch();
}
};
void exec();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus]);
const CreatableItem = () => {
if (!creatable) return undefined;
if (
isOptionsExist(options, [{ value: inputValue, label: inputValue }]) ||
selected.find((s) => s.value === inputValue)
) {
return undefined;
}
const Item = (
<CommandItem
value={inputValue}
className="cursor-pointer"
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={(value: string) => {
if (selected.length >= maxSelected) {
onMaxSelected?.(selected.length);
return;
}
setInputValue('');
const newOptions = [...selected, { value, label: value }];
setSelected(newOptions);
onChange?.(newOptions);
}}
>
{`Create "${inputValue}"`}
</CommandItem>
);
// For normal creatable
if (!onSearch && inputValue.length > 0) {
return Item;
}
// For async search creatable. avoid showing creatable item before loading at first.
if (onSearch && debouncedSearchTerm.length > 0 && !isLoading) {
return Item;
}
return undefined;
};
const EmptyItem = React.useCallback(() => {
if (!emptyIndicator) return undefined;
// For async search that showing emptyIndicator
if (onSearch && !creatable && Object.keys(options).length === 0) {
return (
<CommandItem value="-" disabled>
{emptyIndicator}
</CommandItem>
);
}
return <CommandEmpty>{emptyIndicator}</CommandEmpty>;
}, [creatable, emptyIndicator, onSearch, options]);
const selectables = React.useMemo<GroupOption>(
() => removePickedOption(options, selected),
[options, selected],
);
/** Avoid Creatable Selector freezing or lagging when paste a long string. */
const commandFilter = React.useCallback(() => {
if (commandProps?.filter) {
return commandProps.filter;
}
if (creatable) {
return (value: string, search: string) => {
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1;
};
}
// Using default filter in `cmdk`. We don't have to provide it.
return undefined;
}, [creatable, commandProps?.filter]);
return (
<Command
ref={dropdownRef}
{...commandProps}
onKeyDown={(e) => {
handleKeyDown(e);
commandProps?.onKeyDown?.(e);
}}
className={cn('h-auto overflow-visible bg-transparent', commandProps?.className)}
shouldFilter={
commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch
} // When onSearch is provided, we don't want to filter the options. You can still override it.
filter={commandFilter()}
>
<div
className={cn(
'min-h-10 rounded-md border border-input text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
{
'px-3 py-2': selected.length !== 0,
'cursor-text': !disabled && selected.length !== 0,
},
className,
)}
onClick={() => {
if (disabled) return;
inputRef?.current?.focus();
}}
>
<div className="relative flex flex-wrap gap-1">
{selected.map((option) => {
return (
<Badge
key={option.value}
className={cn(
'data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground',
'data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground',
badgeClassName,
)}
data-fixed={option.fixed}
data-disabled={disabled || undefined}
>
{option.label}
<button
className={cn(
'ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2',
(disabled || option.fixed) && 'hidden',
)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUnselect(option);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(option)}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
);
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
{...inputProps}
ref={inputRef}
value={inputValue}
disabled={disabled}
onValueChange={(value) => {
setInputValue(value);
inputProps?.onValueChange?.(value);
}}
onBlur={(event) => {
if (!onScrollbar) {
setOpen(false);
}
inputProps?.onBlur?.(event);
}}
onFocus={(event) => {
setOpen(true);
triggerSearchOnFocus && onSearch?.(debouncedSearchTerm);
inputProps?.onFocus?.(event);
}}
placeholder={hidePlaceholderWhenSelected && selected.length !== 0 ? '' : placeholder}
className={cn(
'flex-1 bg-transparent outline-none placeholder:text-muted-foreground',
{
'w-full': hidePlaceholderWhenSelected,
'px-3 py-2': selected.length === 0,
'ml-1': selected.length !== 0,
},
inputProps?.className,
)}
/>
<button
type="button"
onClick={() => {
setSelected(selected.filter((s) => s.fixed));
onChange?.(selected.filter((s) => s.fixed));
}}
className={cn(
'absolute right-0 h-6 w-6 p-0',
(hideClearAllButton ||
disabled ||
selected.length < 1 ||
selected.filter((s) => s.fixed).length === selected.length) &&
'hidden',
)}
>
<X />
</button>
</div>
</div>
<div className="relative">
{open && (
<CommandList
className="absolute top-1 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in"
onMouseLeave={() => {
setOnScrollbar(false);
}}
onMouseEnter={() => {
setOnScrollbar(true);
}}
onMouseUp={() => {
inputRef?.current?.focus();
}}
>
{isLoading ? (
<>{loadingIndicator}</>
) : (
<>
{EmptyItem()}
{CreatableItem()}
{!selectFirstItem && <CommandItem value="-" className="hidden" />}
{Object.entries(selectables).map(([key, dropdowns]) => (
<CommandGroup key={key} heading={key} className="h-full overflow-auto">
<>
{dropdowns.map((option) => {
return (
<CommandItem
key={option.value}
value={option.value}
disabled={option.disable}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={() => {
if (selected.length >= maxSelected) {
onMaxSelected?.(selected.length);
return;
}
setInputValue('');
const newOptions = [...selected, option];
setSelected(newOptions);
onChange?.(newOptions);
}}
className={cn(
'cursor-pointer',
option.disable && 'cursor-default text-muted-foreground',
)}
>
{option.label}
</CommandItem>
);
})}
</>
</CommandGroup>
))}
</>
)}
</CommandList>
)}
</div>
</Command>
);
},
);
MultipleSelector.displayName = 'MultipleSelector';
export default MultipleSelector;

View File

@ -1,67 +0,0 @@
import * as React from "react"
import { useCallbackRef } from "@/hooks/use-callback-ref"
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx
*/
type UseControllableStateParams<T> = {
prop?: T | undefined
defaultProp?: T | undefined
onChange?: (state: T) => void
}
type SetStateFn<T> = (prevState?: T) => T
function useControllableState<T>({
prop,
defaultProp,
onChange = () => {},
}: UseControllableStateParams<T>) {
const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({
defaultProp,
onChange,
})
const isControlled = prop !== undefined
const value = isControlled ? prop : uncontrolledProp
const handleChange = useCallbackRef(onChange)
const setValue: React.Dispatch<React.SetStateAction<T | undefined>> =
React.useCallback(
(nextValue) => {
if (isControlled) {
const setter = nextValue as SetStateFn<T>
const value =
typeof nextValue === "function" ? setter(prop) : nextValue
if (value !== prop) handleChange(value as T)
} else {
setUncontrolledProp(nextValue)
}
},
[isControlled, prop, setUncontrolledProp, handleChange]
)
return [value, setValue] as const
}
function useUncontrolledState<T>({
defaultProp,
onChange,
}: Omit<UseControllableStateParams<T>, "prop">) {
const uncontrolledState = React.useState<T | undefined>(defaultProp)
const [value] = uncontrolledState
const prevValueRef = React.useRef(value)
const handleChange = useCallbackRef(onChange)
React.useEffect(() => {
if (prevValueRef.current !== value) {
handleChange(value as T)
prevValueRef.current = value
}
}, [value, prevValueRef, handleChange])
return uncontrolledState
}
export { useControllableState }

View File

@ -4,42 +4,3 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatBytes(
bytes: number,
opts: {
decimals?: number
sizeType?: "accurate" | "normal"
} = {}
) {
const { decimals = 0, sizeType = "normal" } = opts
const sizes = ["Octets", "Ko", "Mo", "Go", "To"]
const accurateSizes = ["Octets", "Kio", "Mio", "Gio", "Tio"]
if (bytes === 0) return "0 Byte"
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / (1024 ** i)).toFixed(decimals)} ${
sizeType === "accurate" ? accurateSizes[i] ?? "Bytest" : sizes[i] ?? "Bytes"
}`
}
/**
* Stole this from the @radix-ui/primitive
* @see https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx
*/
export function composeEventHandlers<E>(
originalEventHandler?: (event: E) => void,
ourEventHandler?: (event: E) => void,
{ checkForDefaultPrevented = true } = {}
) {
return function handleEvent(event: E) {
originalEventHandler?.(event)
if (
checkForDefaultPrevented === false ||
!(event as unknown as Event).defaultPrevented
) {
return ourEventHandler?.(event)
}
}
}

View File

@ -1,83 +0,0 @@
import { PaginationState } from "@tanstack/react-table";
import { IFile, IFilesResponse } from "../types/file";
import axios from 'axios';
const API_URL = "http://localhost:3333/";
const PAGE_LIMIT = 10;
function getFullRoute(route: string) {
return API_URL + route;
}
/**
* Fetches a list of files from the server based on pagination and an optional search field.
*
* @param {PaginationState} pagination - The current state of pagination, including page size and page index.
* @param {string} [searchField] - An optional field used to filter the files by a search term.
* @return {Promise<IFilesResponse>} A promise that resolves to an IFilesResponse object containing the list of files.
*/
async function getFiles(
pagination: PaginationState,
searchField?: string,
): Promise<IFilesResponse> {
const offset = pagination.pageSize * pagination.pageIndex;
const res = await fetch(
`${getFullRoute("api/files/find")}?limit=${PAGE_LIMIT}&offset=${offset}${searchField ? `&search=${searchField}` : "&search="}`,
);
return res.json();
}
/**
* Uploads a file to the server with specified metadata.
*
* @param {File} file - The file to be uploaded.
* @param {string} fileName - The name to be assigned to the uploaded file.
* @param {string} uploadedBy - The identifier for the user uploading the file.
* @param {Array<string>} machineIds - An array of machine IDs associated with the file.
* @param {string|null} groupId - The group ID for grouping files, if applicable.
* @param {boolean} [isDocumentation=false] - Flag indicating if the file is documentation.
* @param {boolean} [isRestricted=false] - Flag indicating if the file is restricted.
* @return {Promise<Object>} A promise resolving to the server's response data.
*/
async function uploadFile(
file: File,
fileName: string,
uploadedBy: string,
machineIds: Array<string>,
groupId: string|null = null,
isDocumentation = false,
isRestricted = false
) {
const headers = {
"Content-Type": "application/octet-stream",
file_name: fileName,
uploaded_by: uploadedBy,
//machine_id: machineIds.join(","), // Assuming machine IDs should be a comma-separated string
machine_id: machineIds,
is_documentation: isDocumentation.toString(),
is_restricted: isRestricted.toString(),
};
if (groupId) {
// @ts-ignore
headers["group_id"] = groupId;
}
try {
const response = await axios.post(getFullRoute("api/files/new"), file, { headers });
return response.data;
} catch (error) {
console.error("Error uploading file:", error);
throw error;
}
}
export const FilesApi = {
get: {
files: getFiles,
},
post: {
upload: uploadFile,
}
};

View File

@ -1,33 +0,0 @@
import { PaginationState } from "@tanstack/react-table";
import { IFile, IFilesResponse } from "../types/file";
import axios from 'axios';
import { IMachine } from 'apps/frontend/src/types/machine';
const API_URL = "http://localhost:3333/";
const PAGE_LIMIT = 10;
function getFullRoute(route: string) {
return API_URL + route;
}
/**
* Fetches a list of files from the server.
*
* @return {Promise<IMachine[]>} A promise that resolves to an IFilesResponse object containing the list of files.
*/
async function getMachines(): Promise<IMachine[]> {
const response = await axios.get<IMachine[]>(getFullRoute("api/machines/find"), {
params: {
limit: -1,
offset: 0,
},
});
return response.data;
}
export const MachinesApi = {
get: {
all: getMachines
}
}

View File

@ -1,20 +0,0 @@
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;
};
export interface IFilesResponse {
count: number;
limit: number;
currentOffset: number;
data: Array<IFile>;
}

View File

@ -1,5 +0,0 @@
export type IMachine = {
id: string;
machineName: string;
machineType: string;
}

View File

@ -12,7 +12,6 @@
"private": true,
"dependencies": {
"@fontsource/ubuntu": "^5.1.0",
"@hookform/resolvers": "^3.9.0",
"@nestjs/common": "^10.4.5",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.4.5",
@ -46,7 +45,6 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5",
"argon2": "^0.41.1",
"axios": "^1.7.7",
@ -55,7 +53,6 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"cors": "^2.8.5",
"drizzle-kit": "^0.24.2",
"drizzle-orm": "^0.33.0",
"drizzle-zod": "^0.5.1",
@ -73,8 +70,6 @@
"react": "18.3.1",
"react-day-picker": "^9.1.4",
"react-dom": "18.3.1",
"react-dropzone": "^14.2.10",
"react-hook-form": "^7.53.1",
"react-resizable-panels": "^2.1.4",
"recharts": "^2.13.0",
"reflect-metadata": "^0.1.14",
@ -83,8 +78,7 @@
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"ts-mockito": "^2.6.1",
"tslib": "^2.8.0",
"zod": "^3.23.8"
"tslib": "^2.8.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.3",
@ -107,7 +101,6 @@
"@swc/cli": "~0.3.14",
"@swc/core": "~1.7.36",
"@swc/helpers": "~0.5.13",
"@types/cors": "^2.8.17",
"@types/jest": "^29.5.13",
"@types/node": "18.16.9",
"@types/react": "18.3.1",

89
pnpm-lock.yaml generated
View File

@ -11,9 +11,6 @@ importers:
'@fontsource/ubuntu':
specifier: ^5.1.0
version: 5.1.0
'@hookform/resolvers':
specifier: ^3.9.0
version: 3.9.0(react-hook-form@7.53.1(react@18.3.1))
'@nestjs/common':
specifier: ^10.4.5
version: 10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1)
@ -113,9 +110,6 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-query':
specifier: ^5.59.15
version: 5.59.15(react@18.3.1)
'@tanstack/react-table':
specifier: ^8.20.5
version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -140,9 +134,6 @@ importers:
cmdk:
specifier: ^1.0.0
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
cors:
specifier: ^2.8.5
version: 2.8.5
drizzle-kit:
specifier: ^0.24.2
version: 0.24.2
@ -194,12 +185,6 @@ importers:
react-dom:
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
react-dropzone:
specifier: ^14.2.10
version: 14.2.10(react@18.3.1)
react-hook-form:
specifier: ^7.53.1
version: 7.53.1(react@18.3.1)
react-resizable-panels:
specifier: ^2.1.4
version: 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -227,9 +212,6 @@ importers:
tslib:
specifier: ^2.8.0
version: 2.8.0
zod:
specifier: ^3.23.8
version: 3.23.8
devDependencies:
'@biomejs/biome':
specifier: ^1.9.3
@ -291,9 +273,6 @@ importers:
'@swc/helpers':
specifier: ~0.5.13
version: 0.5.13
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
'@types/jest':
specifier: ^29.5.13
version: 29.5.13
@ -1490,11 +1469,6 @@ packages:
'@fontsource/ubuntu@5.1.0':
resolution: {integrity: sha512-0XG/HrFsfP1q3phf4QN8IO7tetd0zOZKHZSHcTnBuVoQedoo1wS/hXxY2FMZuqoG+mVfrXh+Q614MDVmQPJq2w==}
'@hookform/resolvers@3.9.0':
resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==}
peerDependencies:
react-hook-form: ^7.0.0
'@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'}
@ -3222,14 +3196,6 @@ packages:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
'@tanstack/query-core@5.59.13':
resolution: {integrity: sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==}
'@tanstack/react-query@5.59.15':
resolution: {integrity: sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==}
peerDependencies:
react: ^18 || ^19
'@tanstack/react-table@8.20.5':
resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==}
engines: {node: '>=12'}
@ -3294,9 +3260,6 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cors@2.8.17':
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@ -3845,10 +3808,6 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
attr-accept@2.2.4:
resolution: {integrity: sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==}
engines: {node: '>=4'}
autoprefixer@10.4.13:
resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
engines: {node: ^10 || ^12 || >=14}
@ -5293,10 +5252,6 @@ packages:
peerDependencies:
webpack: ^4.0.0 || ^5.0.0
file-selector@0.6.0:
resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==}
engines: {node: '>= 12'}
file-type@17.1.6:
resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -7441,18 +7396,6 @@ packages:
peerDependencies:
react: ^18.3.1
react-dropzone@14.2.10:
resolution: {integrity: sha512-Y98LOCYxGO2jOFWREeKJlL7gbrHcOlTBp+9DCM1dh9XQ8+P/8ThhZT7kFb05C+bPcTXq/rixpU+5+LzwYrFLUw==}
engines: {node: '>= 10.13'}
peerDependencies:
react: '>= 16.8 || 18.0.0'
react-hook-form@7.53.1:
resolution: {integrity: sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -9998,10 +9941,6 @@ snapshots:
'@fontsource/ubuntu@5.1.0': {}
'@hookform/resolvers@3.9.0(react-hook-form@7.53.1(react@18.3.1))':
dependencies:
react-hook-form: 7.53.1(react@18.3.1)
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@ -12716,13 +12655,6 @@ snapshots:
dependencies:
defer-to-connect: 2.0.1
'@tanstack/query-core@5.59.13': {}
'@tanstack/react-query@5.59.15(react@18.3.1)':
dependencies:
'@tanstack/query-core': 5.59.13
react: 18.3.1
'@tanstack/react-table@8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/table-core': 8.20.5
@ -12795,10 +12727,6 @@ snapshots:
dependencies:
'@types/node': 18.16.9
'@types/cors@2.8.17':
dependencies:
'@types/node': 18.16.9
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
@ -13425,8 +13353,6 @@ snapshots:
at-least-node@1.0.0: {}
attr-accept@2.2.4: {}
autoprefixer@10.4.13(postcss@8.4.38):
dependencies:
browserslist: 4.23.3
@ -15139,10 +15065,6 @@ snapshots:
schema-utils: 3.3.0
webpack: 5.93.0(@swc/core@1.7.36(@swc/helpers@0.5.13))(esbuild@0.19.12)(webpack-cli@5.1.4(webpack@5.93.0))
file-selector@0.6.0:
dependencies:
tslib: 2.8.0
file-type@17.1.6:
dependencies:
readable-web-to-node-stream: 3.0.2
@ -17610,17 +17532,6 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-dropzone@14.2.10(react@18.3.1):
dependencies:
attr-accept: 2.2.4
file-selector: 0.6.0
prop-types: 15.8.1
react: 18.3.1
react-hook-form@7.53.1(react@18.3.1):
dependencies:
react: 18.3.1
react-is@16.13.1: {}
react-is@18.3.1: {}