Compare commits
6 Commits
ee127f431c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
9c9caa409d
|
|||
|
bcf2f28a6b
|
|||
|
b512732a14
|
|||
|
3cd94e5999
|
|||
|
d5370220a4
|
|||
|
bfe49f65ec
|
21
apps/backend/Dockerfile
Normal file
21
apps/backend/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# Install dependencies only when needed
|
||||
FROM docker.io/node:lts-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /usr/src/app
|
||||
COPY dist/apps/backend/package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Production image, copy all the files and run nest
|
||||
FROM docker.io/node:lts-alpine AS runner
|
||||
RUN apk add --no-cache dumb-init
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=deps /usr/src/app/package.json ./package.json
|
||||
COPY dist/apps/backend .
|
||||
RUN chown -R node:node .
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
CMD ["dumb-init", "node", "main.js"]
|
||||
@@ -21,6 +21,24 @@
|
||||
"buildTarget": "backend:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"container": {
|
||||
"executor": "@nx-tools/nx-container:build",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"engine": "docker",
|
||||
"metadata": {
|
||||
"images": ["backend"],
|
||||
"load": true,
|
||||
"tags": [
|
||||
"type=schedule",
|
||||
"type=ref,event=branch",
|
||||
"type=ref,event=tag",
|
||||
"type=ref,event=pr",
|
||||
"type=sha,prefix=sha-"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
@@ -6,7 +7,6 @@ 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,15 +43,17 @@ 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({
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -20,28 +20,31 @@ 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([
|
||||
{
|
||||
@@ -49,18 +52,21 @@ 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,
|
||||
},
|
||||
@@ -73,13 +79,15 @@ 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,
|
||||
},
|
||||
@@ -170,7 +178,7 @@ export class FilesController {
|
||||
return;
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.FOUND)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("find")
|
||||
async findMany(
|
||||
@Query("limit", new DefaultValuePipe(20), ParseIntPipe) limit: number,
|
||||
@@ -180,7 +188,7 @@ export class FilesController {
|
||||
return this.filesService.search(limit, offset, search);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.FOUND)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("types")
|
||||
async getTypes() {
|
||||
console.log("Performing request");
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
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)
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.FOUND)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("types/:machineId")
|
||||
async getTypesOfMachine(@Param("machineId") machineId: string) {
|
||||
return await this.machineService.getFilesTypes(machineId);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.FOUND)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("files/:machineId")
|
||||
async getFilesForMachine(
|
||||
@Query("limit", new DefaultValuePipe(20), ParseIntPipe) limit: number,
|
||||
|
||||
@@ -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,8 +19,9 @@ 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;
|
||||
|
||||
@@ -2,7 +2,9 @@ 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() {
|
||||
@@ -11,9 +13,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();
|
||||
|
||||
@@ -23,6 +25,13 @@ 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);
|
||||
|
||||
|
||||
27
apps/frontend/Dockerfile
Normal file
27
apps/frontend/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
# This template uses Automatically Copying Traced Files feature
|
||||
# so you need to setup your Next Config file to use `output: 'standalone'`
|
||||
# Please read this for more information https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM docker.io/node:lts-alpine AS runner
|
||||
RUN apk add --no-cache dumb-init
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
WORKDIR /usr/src/app
|
||||
COPY apps/frontend/next.config.js ./
|
||||
COPY apps/frontend/public ./public
|
||||
COPY apps/frontend/.next/standalone/apps/frontend ./
|
||||
COPY apps/frontend/.next/standalone/package.json ./
|
||||
COPY apps/frontend/.next/standalone/node_modules ./node_modules
|
||||
COPY apps/frontend/.next/static ./.next/static
|
||||
# RUN npm i sharp
|
||||
RUN chown -R node:node .
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
# COPY --chown=node:node ./tools/scripts/entrypoints/api.sh /usr/local/bin/docker-entrypoint.sh
|
||||
# ENTRYPOINT [ "docker-entrypoint.sh" ]
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry.
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
CMD ["dumb-init", "node", "server.js"]
|
||||
@@ -4,6 +4,24 @@
|
||||
"sourceRoot": "apps/frontend",
|
||||
"projectType": "application",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project frontend --web",
|
||||
"targets": {}
|
||||
"targets": {
|
||||
"container": {
|
||||
"executor": "@nx-tools/nx-container:build",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"engine": "docker",
|
||||
"metadata": {
|
||||
"images": ["frontend"],
|
||||
"load": true,
|
||||
"tags": [
|
||||
"type=schedule",
|
||||
"type=ref,event=branch",
|
||||
"type=ref,event=tag",
|
||||
"type=ref,event=pr",
|
||||
"type=sha,prefix=sha-"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export async function GET(request: Request) {
|
||||
return new Response("Hello, from API!");
|
||||
}
|
||||
@@ -8,17 +8,19 @@ 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>
|
||||
);
|
||||
|
||||
@@ -6,30 +6,25 @@ 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 (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
329
apps/frontend/src/components/file-uploader.tsx
Normal file
329
apps/frontend/src/components/file-uploader.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import Dropzone, { DropzoneProps, FileRejection } from 'react-dropzone';
|
||||
import { toast } from 'sonner';
|
||||
import React from 'react';
|
||||
import { CrossIcon, FileTextIcon, UploadIcon } from 'lucide-react';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Progress } from '@radix-ui/react-progress';
|
||||
import { Button } from './ui/button';
|
||||
import { cn, formatBytes } from '../lib/utils';
|
||||
import { useControllableState } from '../hooks/use-controllable-state';
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
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}
|
||||
>
|
||||
<CrossIcon 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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
154
apps/frontend/src/components/forms/file-upload.tsx
Normal file
154
apps/frontend/src/components/forms/file-upload.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
50
apps/frontend/src/components/forms/machines-selector.tsx
Normal file
50
apps/frontend/src/components/forms/machines-selector.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'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>
|
||||
}
|
||||
/>);
|
||||
};
|
||||
8
apps/frontend/src/components/loading-spinner.tsx
Normal file
8
apps/frontend/src/components/loading-spinner.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
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>)
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent, DialogDescription,
|
||||
DialogHeader, DialogTitle,
|
||||
DialogContent, DialogHeader,
|
||||
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>
|
||||
@@ -22,13 +24,8 @@ export function NewFileModal(props: NewFileModalProps) {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<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>
|
||||
<DialogHeader>Ajouter un fichier</DialogHeader>
|
||||
<FileUploadForm/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ export enum ESubPage {
|
||||
|
||||
export function SubPageSelector(props: SubPageSelectorProps) {
|
||||
return (
|
||||
<div className={"w-full flex flex-col justify-center items-stretch pt-4 p-4 gap-2"}>
|
||||
<div className={"max-[20%]: flex flex-col justify-center items-stretch pt-4 p-4 gap-2"}>
|
||||
<Button
|
||||
onClick={()=>props.setCurrentSubPage(ESubPage.Home)}
|
||||
disabled={props.currentSubPage === ESubPage.Home}
|
||||
|
||||
@@ -3,6 +3,9 @@ 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 {
|
||||
@@ -11,28 +14,22 @@ 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"}>Page principal</h1>
|
||||
<h1 className={"text-2xl font-bold"}>Accueil</h1>
|
||||
</div>
|
||||
<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
|
||||
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>
|
||||
</section>)
|
||||
}
|
||||
@@ -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 (<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 }) => {
|
||||
return (<div className={"flex justify-center items-center"}>
|
||||
header: ({ column }) => (
|
||||
<div className={"flex justify-center items-center"}>
|
||||
Nom du fichier
|
||||
</div>)
|
||||
},
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "uploadedBy",
|
||||
header: ({ column }) => {
|
||||
return (<div className={"flex justify-center items-center"}>
|
||||
header: ({ column }) => (
|
||||
<div className={"flex justify-center items-center"}>
|
||||
Autheur(s)
|
||||
</div>)
|
||||
},
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "uploadedAt",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={"flex w-full"}
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Ajouté le
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
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()}`
|
||||
|
||||
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>)
|
||||
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 }) => {
|
||||
return (<div className={"flex justify-center items-center"}>
|
||||
header: ({ column }) => (
|
||||
<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 }) => {
|
||||
return (<div className={"flex justify-center items-center"}>
|
||||
header: ({ column }) => (
|
||||
<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>[]
|
||||
data: TData[]
|
||||
searchField: ISearchBarResult
|
||||
}
|
||||
|
||||
export function FilesDataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
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;
|
||||
|
||||
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="rounded-md border w-full">
|
||||
<Table>
|
||||
<div className="w-full">
|
||||
{isPending && isFetching && isPlaceholderData ? <LoadingSpinner/> : <Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
{headerGroup.headers.map((header) => (
|
||||
<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())}
|
||||
@@ -195,7 +235,10 @@ export function FilesDataTable<TData, TValue>({
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Table>}
|
||||
<div className={"flex w-full justify-end items-center"}>
|
||||
<TablePagination rowsCount={rowsCount} pagination={pagination} setPagination={setPagination}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
46
apps/frontend/src/components/tables/pagination.tsx
Normal file
46
apps/frontend/src/components/tables/pagination.tsx
Normal file
@@ -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<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>)
|
||||
}
|
||||
42
apps/frontend/src/components/tables/search-bar.tsx
Normal file
42
apps/frontend/src/components/tables/search-bar.tsx
Normal file
@@ -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<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>)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
608
apps/frontend/src/components/ui/multiple-selector.tsx
Normal file
608
apps/frontend/src/components/ui/multiple-selector.tsx
Normal file
@@ -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<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;
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
|
||||
27
apps/frontend/src/hooks/use-callback-ref.ts
Normal file
27
apps/frontend/src/hooks/use-callback-ref.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
|
||||
/**
|
||||
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
|
||||
* prop or avoid re-executing effects when passed as a dependency
|
||||
*/
|
||||
function useCallbackRef<T extends (...args: never[]) => unknown>(
|
||||
callback: T | undefined
|
||||
): T {
|
||||
const callbackRef = React.useRef(callback)
|
||||
|
||||
React.useEffect(() => {
|
||||
callbackRef.current = callback
|
||||
})
|
||||
|
||||
// https://github.com/facebook/react/issues/19240
|
||||
return React.useMemo(
|
||||
() => ((...args) => callbackRef.current?.(...args)) as T,
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
export { useCallbackRef }
|
||||
69
apps/frontend/src/hooks/use-controllable-state.ts
Normal file
69
apps/frontend/src/hooks/use-controllable-state.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { useCallbackRef } from "../hooks/use-callback-ref"
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* @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)
|
||||
|
||||
// @ts-ignore
|
||||
useEffect(() => {
|
||||
if (prevValueRef.current !== value) {
|
||||
handleChange(value as T)
|
||||
prevValueRef.current = value
|
||||
}
|
||||
}, [value, prevValueRef, handleChange])
|
||||
|
||||
return uncontrolledState
|
||||
}
|
||||
|
||||
export { useControllableState }
|
||||
@@ -4,3 +4,42 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
83
apps/frontend/src/requests/files.ts
Normal file
83
apps/frontend/src/requests/files.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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,
|
||||
}
|
||||
};
|
||||
33
apps/frontend/src/requests/machines.ts
Normal file
33
apps/frontend/src/requests/machines.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
0
apps/frontend/src/temp.tsx
Normal file
0
apps/frontend/src/temp.tsx
Normal file
20
apps/frontend/src/types/file.ts
Normal file
20
apps/frontend/src/types/file.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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>;
|
||||
}
|
||||
5
apps/frontend/src/types/machine.ts
Normal file
5
apps/frontend/src/types/machine.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type IMachine = {
|
||||
id: string;
|
||||
machineName: string;
|
||||
machineType: string;
|
||||
}
|
||||
12
package.json
12
package.json
@@ -12,6 +12,7 @@
|
||||
"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",
|
||||
@@ -36,6 +37,7 @@
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slider": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
@@ -45,6 +47,7 @@
|
||||
"@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",
|
||||
@@ -53,6 +56,7 @@
|
||||
"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",
|
||||
@@ -70,6 +74,8 @@
|
||||
"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",
|
||||
@@ -78,13 +84,16 @@
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"tslib": "^2.8.0"
|
||||
"tslib": "^2.8.0",
|
||||
"vaul": "^1.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.3",
|
||||
"@nestjs/cli": "^10.4.5",
|
||||
"@nestjs/schematics": "^10.2.0",
|
||||
"@nestjs/testing": "^10.4.5",
|
||||
"@nx-tools/nx-container": "^6.1.0",
|
||||
"@nx/cypress": "19.6.1",
|
||||
"@nx/eslint": "19.6.1",
|
||||
"@nx/eslint-plugin": "19.6.1",
|
||||
@@ -101,6 +110,7 @@
|
||||
"@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",
|
||||
|
||||
488
pnpm-lock.yaml
generated
488
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ 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)
|
||||
@@ -83,6 +86,9 @@ importers:
|
||||
'@radix-ui/react-scroll-area':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.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)
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.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)
|
||||
@@ -110,6 +116,9 @@ 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)
|
||||
@@ -134,6 +143,9 @@ 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
|
||||
@@ -185,6 +197,12 @@ 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)
|
||||
@@ -212,6 +230,12 @@ importers:
|
||||
tslib:
|
||||
specifier: ^2.8.0
|
||||
version: 2.8.0
|
||||
vaul:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.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)
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.23.8
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: ^1.9.3
|
||||
@@ -225,6 +249,9 @@ importers:
|
||||
'@nestjs/testing':
|
||||
specifier: ^10.4.5
|
||||
version: 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.1.14)(rxjs@7.8.1))(@nestjs/core@10.4.5))
|
||||
'@nx-tools/nx-container':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(@swc/helpers@0.5.13)(dotenv@16.4.5)(tslib@2.8.0)
|
||||
'@nx/cypress':
|
||||
specifier: 19.6.1
|
||||
version: 19.6.1(@babel/traverse@7.25.3)(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(cypress@13.15.0)(eslint@8.57.1)(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))(typescript@5.6.3)
|
||||
@@ -273,6 +300,9 @@ 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
|
||||
@@ -357,6 +387,18 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@actions/exec@1.1.1':
|
||||
resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==}
|
||||
|
||||
'@actions/github@6.0.0':
|
||||
resolution: {integrity: sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==}
|
||||
|
||||
'@actions/http-client@2.2.3':
|
||||
resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
|
||||
|
||||
'@actions/io@1.1.3':
|
||||
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
||||
|
||||
'@adobe/css-tools@4.4.0':
|
||||
resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
|
||||
|
||||
@@ -1051,6 +1093,10 @@ packages:
|
||||
'@babel/regjsgen@0.8.0':
|
||||
resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==}
|
||||
|
||||
'@babel/runtime-corejs3@7.25.9':
|
||||
resolution: {integrity: sha512-eHeq2HWhgn3aH6Gz4Dnajqp8U5DjBg3h933LlGJ52hAN6Kx34KAL7O3NzwTrldl9PrgKTyBcz0ScVIQ3A6e2fA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.25.0':
|
||||
resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1451,6 +1497,10 @@ packages:
|
||||
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@fastify/busboy@2.1.1':
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@floating-ui/core@1.6.7':
|
||||
resolution: {integrity: sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==}
|
||||
|
||||
@@ -1469,6 +1519,11 @@ 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'}
|
||||
@@ -1932,6 +1987,30 @@ packages:
|
||||
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@nx-tools/ci-context@6.1.0':
|
||||
resolution: {integrity: sha512-RUTbrSeqA9iA4PH/NEmWczDJ/VJmHRIPOtaJs39NbJCHy9bG93iyF5j5LCPrVgK26RFvMXXJy8kuyg2o7AqrXQ==}
|
||||
peerDependencies:
|
||||
tslib: ^2.5.0
|
||||
|
||||
'@nx-tools/container-metadata@6.1.0':
|
||||
resolution: {integrity: sha512-RzG3AxO4t+MJDwRYaTUjJXyGwWETSDef+2FWOaqV9D+bo98nAWNXbrz3PKajBCrDj6yo5LFbsBasqKt/0CHzGw==}
|
||||
peerDependencies:
|
||||
'@nx/devkit': ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||
tslib: ^2.5.0
|
||||
|
||||
'@nx-tools/core@6.1.0':
|
||||
resolution: {integrity: sha512-jVy3mR2G0/UpuW1oh46ZR/hut4qN8passzqPGYA8FwavtB/6dG1x+Syz6lE2feI8UbExFvLBU2hu4FUmIVvRSA==}
|
||||
peerDependencies:
|
||||
'@nx/devkit': ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||
tslib: ^2.5.0
|
||||
|
||||
'@nx-tools/nx-container@6.1.0':
|
||||
resolution: {integrity: sha512-5lI6NZesIvluElQ4b3kSB+fA1xCih+FMMHVvCP/rG+JVnAwmpd+F7LGncTIFFhWjjs3CrUlntMTcD8bGsn9Sgw==}
|
||||
peerDependencies:
|
||||
'@nx/devkit': ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0
|
||||
'@swc/helpers': ~0.5.11
|
||||
dotenv: '>=16.0.0'
|
||||
|
||||
'@nx/cypress@19.6.1':
|
||||
resolution: {integrity: sha512-9Qo7YD4gX9IqglGrHeyjAuLRwzOWNR9DaZWPooq5KMSfDDRtC05QtCc2egL/r4862vN4+tDwPp0U02nqb1OTeA==}
|
||||
peerDependencies:
|
||||
@@ -2154,6 +2233,54 @@ packages:
|
||||
'@nx/workspace@19.8.5':
|
||||
resolution: {integrity: sha512-SwvhYetHQMT4IroVku1OjboPnsOC9kv2TURrN4x7NKdYM/4aYAxC1sdabhpMFpqLXSJTUMgOT82hpD3peGbFQA==}
|
||||
|
||||
'@octokit/auth-token@4.0.0':
|
||||
resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/core@5.2.0':
|
||||
resolution: {integrity: sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/endpoint@9.0.5':
|
||||
resolution: {integrity: sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/graphql@7.1.0':
|
||||
resolution: {integrity: sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/openapi-types@20.0.0':
|
||||
resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==}
|
||||
|
||||
'@octokit/openapi-types@22.2.0':
|
||||
resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==}
|
||||
|
||||
'@octokit/plugin-paginate-rest@9.2.1':
|
||||
resolution: {integrity: sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '5'
|
||||
|
||||
'@octokit/plugin-rest-endpoint-methods@10.4.1':
|
||||
resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '5'
|
||||
|
||||
'@octokit/request-error@5.1.0':
|
||||
resolution: {integrity: sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/request@8.4.0':
|
||||
resolution: {integrity: sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/types@12.6.0':
|
||||
resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==}
|
||||
|
||||
'@octokit/types@13.6.1':
|
||||
resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==}
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@1.12.0':
|
||||
resolution: {integrity: sha512-wYe+dlF8npM7cwopOOxbdNjtmJp17e/xF5c0K2WooQXy5VOh74icydM33+Uh/SZDgwyum09/U1FVCX5GdeQk+A==}
|
||||
cpu: [arm64]
|
||||
@@ -2750,6 +2877,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-select@2.1.2':
|
||||
resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.0':
|
||||
resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==}
|
||||
peerDependencies:
|
||||
@@ -2987,6 +3127,9 @@ packages:
|
||||
'@radix-ui/rect@1.1.0':
|
||||
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
||||
|
||||
'@renovate/pep440@1.0.0':
|
||||
resolution: {integrity: sha512-k3pZVxGEGpU7rpH507/9vxfFjuxX7qx4MSj9Fk+6zBsf/uZmAy8x97dNtZacbge7gP9TazbW1d7SEb5vsOmKlw==}
|
||||
|
||||
'@rushstack/eslint-patch@1.10.4':
|
||||
resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==}
|
||||
|
||||
@@ -3196,6 +3339,14 @@ 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'}
|
||||
@@ -3260,6 +3411,9 @@ 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==}
|
||||
|
||||
@@ -3808,6 +3962,10 @@ 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}
|
||||
@@ -3916,6 +4074,9 @@ packages:
|
||||
bcrypt-pbkdf@1.0.2:
|
||||
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
||||
|
||||
before-after-hook@2.2.3:
|
||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||
|
||||
big.js@5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
|
||||
@@ -4090,6 +4251,10 @@ packages:
|
||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ci-info@4.0.0:
|
||||
resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
cjs-module-lexer@1.3.1:
|
||||
resolution: {integrity: sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==}
|
||||
|
||||
@@ -4293,6 +4458,9 @@ packages:
|
||||
core-js-compat@3.38.1:
|
||||
resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==}
|
||||
|
||||
core-js-pure@3.38.1:
|
||||
resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==}
|
||||
|
||||
core-util-is@1.0.2:
|
||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||
|
||||
@@ -4441,6 +4609,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
csv-parse@5.5.6:
|
||||
resolution: {integrity: sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==}
|
||||
|
||||
cypress@13.15.0:
|
||||
resolution: {integrity: sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==}
|
||||
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
|
||||
@@ -4634,6 +4805,9 @@ packages:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
deprecation@2.3.1:
|
||||
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5252,6 +5426,10 @@ 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}
|
||||
@@ -5539,6 +5717,11 @@ packages:
|
||||
handle-thing@2.0.1:
|
||||
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
|
||||
|
||||
handlebars@4.7.8:
|
||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
harmony-reflect@1.6.2:
|
||||
resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==}
|
||||
|
||||
@@ -6601,6 +6784,12 @@ packages:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
|
||||
moment-timezone@0.5.46:
|
||||
resolution: {integrity: sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==}
|
||||
|
||||
moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
@@ -7328,6 +7517,9 @@ packages:
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
properties-file@3.5.9:
|
||||
resolution: {integrity: sha512-04pqpblRKTG7S6Juyx2rYdAwJJ+hK9SIycmDwwic1T+m3Ek6nBSKvHalIoF10RNpx7Xi3891JZZkyIIVCJETaQ==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -7396,6 +7588,18 @@ 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==}
|
||||
|
||||
@@ -8260,6 +8464,10 @@ packages:
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
tunnel@0.0.6:
|
||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
||||
|
||||
tweetnacl@0.14.5:
|
||||
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
|
||||
|
||||
@@ -8320,6 +8528,11 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
hasBin: true
|
||||
|
||||
uid@2.0.2:
|
||||
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -8331,6 +8544,10 @@ packages:
|
||||
unbox-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
|
||||
undici@5.28.4:
|
||||
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.0:
|
||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -8351,6 +8568,9 @@ packages:
|
||||
resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
universal-user-agent@6.0.1:
|
||||
resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
|
||||
|
||||
universalify@0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
@@ -8440,6 +8660,12 @@ packages:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vaul@1.1.0:
|
||||
resolution: {integrity: sha512-YhO/bikcauk48hzhMhvIvT+U87cuCbNbKk9fF4Ou5UkI9t2KkBMernmdP37pCzF15hrv55fcny1YhexK8h6GVQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
|
||||
verror@1.10.0:
|
||||
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
@@ -8606,6 +8832,9 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wordwrap@1.0.0:
|
||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -8656,6 +8885,9 @@ packages:
|
||||
xmlchars@2.2.0:
|
||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||
|
||||
xregexp@4.4.1:
|
||||
resolution: {integrity: sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
@@ -8711,6 +8943,24 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@actions/exec@1.1.1':
|
||||
dependencies:
|
||||
'@actions/io': 1.1.3
|
||||
|
||||
'@actions/github@6.0.0':
|
||||
dependencies:
|
||||
'@actions/http-client': 2.2.3
|
||||
'@octokit/core': 5.2.0
|
||||
'@octokit/plugin-paginate-rest': 9.2.1(@octokit/core@5.2.0)
|
||||
'@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.0)
|
||||
|
||||
'@actions/http-client@2.2.3':
|
||||
dependencies:
|
||||
tunnel: 0.0.6
|
||||
undici: 5.28.4
|
||||
|
||||
'@actions/io@1.1.3': {}
|
||||
|
||||
'@adobe/css-tools@4.4.0': {}
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
@@ -9635,6 +9885,11 @@ snapshots:
|
||||
|
||||
'@babel/regjsgen@0.8.0': {}
|
||||
|
||||
'@babel/runtime-corejs3@7.25.9':
|
||||
dependencies:
|
||||
core-js-pure: 3.38.1
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/runtime@7.25.0':
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
@@ -9922,6 +10177,8 @@ snapshots:
|
||||
|
||||
'@eslint/js@8.57.1': {}
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
'@floating-ui/core@1.6.7':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.7
|
||||
@@ -9941,6 +10198,10 @@ 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
|
||||
@@ -10525,12 +10786,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- nx
|
||||
|
||||
'@nrwl/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))':
|
||||
dependencies:
|
||||
'@nx/devkit': 19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
transitivePeerDependencies:
|
||||
- nx
|
||||
|
||||
'@nrwl/devkit@19.8.5(nx@19.8.5(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))':
|
||||
dependencies:
|
||||
'@nx/devkit': 19.8.5(nx@19.8.5(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
@@ -10859,6 +11114,53 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@nx-tools/ci-context@6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)':
|
||||
dependencies:
|
||||
'@actions/github': 6.0.0
|
||||
'@nx-tools/core': 6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)
|
||||
'@octokit/openapi-types': 22.2.0
|
||||
ci-info: 4.0.0
|
||||
properties-file: 3.5.9
|
||||
tslib: 2.8.0
|
||||
transitivePeerDependencies:
|
||||
- '@nx/devkit'
|
||||
|
||||
'@nx-tools/container-metadata@6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)':
|
||||
dependencies:
|
||||
'@nx-tools/ci-context': 6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)
|
||||
'@nx-tools/core': 6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)
|
||||
'@nx/devkit': 19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
'@renovate/pep440': 1.0.0
|
||||
csv-parse: 5.5.6
|
||||
handlebars: 4.7.8
|
||||
moment-timezone: 0.5.46
|
||||
semver: 7.6.3
|
||||
tslib: 2.8.0
|
||||
|
||||
'@nx-tools/core@6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)':
|
||||
dependencies:
|
||||
'@actions/exec': 1.1.1
|
||||
'@actions/io': 1.1.3
|
||||
'@nx/devkit': 19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
chalk: 4.1.2
|
||||
ci-info: 4.0.0
|
||||
csv-parse: 5.5.6
|
||||
tslib: 2.8.0
|
||||
|
||||
'@nx-tools/nx-container@6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(@swc/helpers@0.5.13)(dotenv@16.4.5)(tslib@2.8.0)':
|
||||
dependencies:
|
||||
'@nx-tools/container-metadata': 6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)
|
||||
'@nx-tools/core': 6.1.0(@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))))(tslib@2.8.0)
|
||||
'@nx/devkit': 19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
'@swc/helpers': 0.5.13
|
||||
csv-parse: 5.5.6
|
||||
dotenv: 16.4.5
|
||||
handlebars: 4.7.8
|
||||
semver: 7.6.3
|
||||
tmp: 0.2.3
|
||||
transitivePeerDependencies:
|
||||
- tslib
|
||||
|
||||
'@nx/cypress@19.6.1(@babel/traverse@7.25.3)(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(cypress@13.15.0)(eslint@8.57.1)(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@nrwl/cypress': 19.6.1(@babel/traverse@7.25.3)(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@18.16.9)(@zkochan/js-yaml@0.0.7)(cypress@13.15.0)(eslint@8.57.1)(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))(typescript@5.6.3)
|
||||
@@ -10899,7 +11201,7 @@ snapshots:
|
||||
|
||||
'@nx/devkit@19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))':
|
||||
dependencies:
|
||||
'@nrwl/devkit': 19.8.5(nx@19.6.1(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
'@nrwl/devkit': 19.8.5(nx@19.8.5(@swc-node/register@1.10.9(@swc/core@1.7.36(@swc/helpers@0.5.13))(@swc/types@0.1.13)(typescript@5.6.3))(@swc/core@1.7.36(@swc/helpers@0.5.13)))
|
||||
ejs: 3.1.10
|
||||
enquirer: 2.3.6
|
||||
ignore: 5.3.2
|
||||
@@ -11610,6 +11912,64 @@ snapshots:
|
||||
- '@swc/core'
|
||||
- debug
|
||||
|
||||
'@octokit/auth-token@4.0.0': {}
|
||||
|
||||
'@octokit/core@5.2.0':
|
||||
dependencies:
|
||||
'@octokit/auth-token': 4.0.0
|
||||
'@octokit/graphql': 7.1.0
|
||||
'@octokit/request': 8.4.0
|
||||
'@octokit/request-error': 5.1.0
|
||||
'@octokit/types': 13.6.1
|
||||
before-after-hook: 2.2.3
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/endpoint@9.0.5':
|
||||
dependencies:
|
||||
'@octokit/types': 13.6.1
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/graphql@7.1.0':
|
||||
dependencies:
|
||||
'@octokit/request': 8.4.0
|
||||
'@octokit/types': 13.6.1
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/openapi-types@20.0.0': {}
|
||||
|
||||
'@octokit/openapi-types@22.2.0': {}
|
||||
|
||||
'@octokit/plugin-paginate-rest@9.2.1(@octokit/core@5.2.0)':
|
||||
dependencies:
|
||||
'@octokit/core': 5.2.0
|
||||
'@octokit/types': 12.6.0
|
||||
|
||||
'@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.0)':
|
||||
dependencies:
|
||||
'@octokit/core': 5.2.0
|
||||
'@octokit/types': 12.6.0
|
||||
|
||||
'@octokit/request-error@5.1.0':
|
||||
dependencies:
|
||||
'@octokit/types': 13.6.1
|
||||
deprecation: 2.3.1
|
||||
once: 1.4.0
|
||||
|
||||
'@octokit/request@8.4.0':
|
||||
dependencies:
|
||||
'@octokit/endpoint': 9.0.5
|
||||
'@octokit/request-error': 5.1.0
|
||||
'@octokit/types': 13.6.1
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/types@12.6.0':
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 20.0.0
|
||||
|
||||
'@octokit/types@13.6.1':
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 22.2.0
|
||||
|
||||
'@oxc-resolver/binding-darwin-arm64@1.12.0':
|
||||
optional: true
|
||||
|
||||
@@ -12213,6 +12573,35 @@ snapshots:
|
||||
'@types/react': 18.3.1
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/react-select@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.0
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
'@radix-ui/react-collection': 1.1.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)
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-focus-scope': 1.1.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)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-popper': 1.2.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)
|
||||
'@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.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)
|
||||
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-visually-hidden': 1.1.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)
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-remove-scroll: 2.6.0(@types/react@18.3.1)(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.1
|
||||
'@types/react-dom': 18.3.0
|
||||
|
||||
'@radix-ui/react-separator@1.1.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)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.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)
|
||||
@@ -12440,6 +12829,10 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.0': {}
|
||||
|
||||
'@renovate/pep440@1.0.0':
|
||||
dependencies:
|
||||
xregexp: 4.4.1
|
||||
|
||||
'@rushstack/eslint-patch@1.10.4': {}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
@@ -12655,6 +13048,13 @@ 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
|
||||
@@ -12727,6 +13127,10 @@ 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': {}
|
||||
@@ -13353,6 +13757,8 @@ 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
|
||||
@@ -13505,6 +13911,8 @@ snapshots:
|
||||
dependencies:
|
||||
tweetnacl: 0.14.5
|
||||
|
||||
before-after-hook@2.2.3: {}
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
bin-check@4.1.0:
|
||||
@@ -13696,6 +14104,8 @@ snapshots:
|
||||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
ci-info@4.0.0: {}
|
||||
|
||||
cjs-module-lexer@1.3.1: {}
|
||||
|
||||
class-transformer@0.5.1: {}
|
||||
@@ -13888,6 +14298,8 @@ snapshots:
|
||||
dependencies:
|
||||
browserslist: 4.23.3
|
||||
|
||||
core-js-pure@3.38.1: {}
|
||||
|
||||
core-util-is@1.0.2: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
@@ -14075,6 +14487,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
csv-parse@5.5.6: {}
|
||||
|
||||
cypress@13.15.0:
|
||||
dependencies:
|
||||
'@cypress/request': 3.0.5
|
||||
@@ -14271,6 +14685,8 @@ snapshots:
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
deprecation@2.3.1: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destroy@1.2.0: {}
|
||||
@@ -15065,6 +15481,10 @@ 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
|
||||
@@ -15421,6 +15841,15 @@ snapshots:
|
||||
|
||||
handle-thing@2.0.1: {}
|
||||
|
||||
handlebars@4.7.8:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
neo-async: 2.6.2
|
||||
source-map: 0.6.1
|
||||
wordwrap: 1.0.0
|
||||
optionalDependencies:
|
||||
uglify-js: 3.19.3
|
||||
|
||||
harmony-reflect@1.6.2: {}
|
||||
|
||||
has-bigints@1.0.2: {}
|
||||
@@ -16696,6 +17125,12 @@ snapshots:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
|
||||
moment-timezone@0.5.46:
|
||||
dependencies:
|
||||
moment: 2.30.1
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.2: {}
|
||||
@@ -17470,6 +17905,8 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
properties-file@3.5.9: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
@@ -17532,6 +17969,17 @@ 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: {}
|
||||
@@ -18515,6 +18963,8 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
tunnel@0.0.6: {}
|
||||
|
||||
tweetnacl@0.14.5: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
@@ -18574,6 +19024,9 @@ snapshots:
|
||||
|
||||
typescript@5.6.3: {}
|
||||
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
uid@2.0.2:
|
||||
dependencies:
|
||||
'@lukeed/csprng': 1.1.0
|
||||
@@ -18587,6 +19040,10 @@ snapshots:
|
||||
has-symbols: 1.0.3
|
||||
which-boxed-primitive: 1.0.2
|
||||
|
||||
undici@5.28.4:
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
||||
|
||||
unicode-match-property-ecmascript@2.0.0:
|
||||
@@ -18602,6 +19059,8 @@ snapshots:
|
||||
dependencies:
|
||||
qs: 6.13.0
|
||||
|
||||
universal-user-agent@6.0.1: {}
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
||||
universalify@0.2.0: {}
|
||||
@@ -18666,6 +19125,15 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vaul@1.1.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):
|
||||
dependencies:
|
||||
'@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
verror@1.10.0:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
@@ -18937,6 +19405,8 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wordwrap@1.0.0: {}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -18970,6 +19440,10 @@ snapshots:
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
xregexp@4.4.1:
|
||||
dependencies:
|
||||
'@babel/runtime-corejs3': 7.25.9
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
Reference in New Issue
Block a user