diff --git a/.lintstagedrc b/.lintstagedrc index 7912ffc7..e9ce4e48 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,5 +1,4 @@ { "**/*.{ts,tsx,js,jsx}": ["prettier --write"], - "**/*.{md,mdx,yml,json}": ["prettier --write"], - "commit-msg": ["node scripts/js/commitEmoji.js"] + "**/*.{md,mdx,yml,json}": ["prettier --write"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 30014628..330cdc61 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -107,8 +107,8 @@ "stopOnEntry": false, "autoAttachChildProcesses": true, "runtimeVersion": "21.7.0", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["--filter", "backend", "run", "start"], + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "backend:start"], "presentation": { "group": "3" } }, { diff --git a/README.md b/README.md index dee80ca1..a013e18d 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,11 @@ Our goal is to provide an easy setup and deployment process, allowing developers ^7.2.0 Provides rate limiting to protect against brute force attacks + + flatted + ^3.3.1 + - + helmet ^7.1.0 @@ -485,6 +490,11 @@ Our goal is to provide an easy setup and deployment process, allowing developers ^6.22.3 Provides routing functionality for the React frontend application + + react-use + ^17.5.0 + - + diff --git a/package.json b/package.json index 9258119a..eb064bcb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "typedoc": "typedoc && pnpm run script:customizeTypedocOutput", "lint": "npx eslint . --fix", "backend:build": "pnpm --filter backend run build", + "backend:start": "npm run start --prefix packages/backend", "script:customizeTypedocOutput": "bash scripts/sh/customizeTypedocOutput.sh", "script:writeDependenciesMarkdown": "node scripts/js/writeDependenciesMarkdown.js", "script:writeReadmeMarkdown": "node scripts/js/writeReadmeMarkdown.js", @@ -53,5 +54,8 @@ "mongodb", "express", "node" - ] + ], + "dependencies": { + "react-use": "^17.5.0" + } } diff --git a/packages/backend/package.json b/packages/backend/package.json index 4cdc11e2..09dc83b8 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -29,6 +29,7 @@ "dotenv": "^16.4.5", "express": "^4.18.2", "express-rate-limit": "^7.2.0", + "flatted": "^3.3.1", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", diff --git a/packages/backend/src/infrastructure/controllers/UserController.ts b/packages/backend/src/infrastructure/controllers/UserController.ts index 95f5cbc1..b050302d 100644 --- a/packages/backend/src/infrastructure/controllers/UserController.ts +++ b/packages/backend/src/infrastructure/controllers/UserController.ts @@ -1,7 +1,6 @@ import { ErrorResponse, type TODO } from "@org/shared"; -import type { MongoSort, RouteInput, RouteOutput } from "@org/backend/types"; +import type { RouteInput, RouteOutput } from "@org/backend/types"; import { Autowired, Contract, Injectable } from "@org/backend/decorators"; -import { withPaginableParams } from "@org/backend/infrastructure/middleware/locals/withPaginableParams"; import { type UserService } from "@org/backend/infrastructure/service/UserService"; @Injectable("userController") @@ -28,25 +27,11 @@ export class UserController { }; } - @Contract("User.pagination", withPaginableParams()) + @Contract("User.pagination") async pagination({ query }: RouteInput<"User.pagination">): RouteOutput<"User.pagination"> { - //throw new Error("Testing error"); - const paginationOptions = { - filters: {}, - sort: (query.sort ? query.sort.split(",").map(value => value.split("|")) : []) as MongoSort, - page: query.page, - limit: query.limit, - search: { - fields: ["username", "email"], - regex: query.search, - }, - }; - - const paginatedResult = (await this.userService.search(paginationOptions)) as TODO; - return { status: 200, - body: paginatedResult, + body: (await this.userService.search(query.paginationOptions)) as TODO, }; } @@ -58,4 +43,15 @@ export class UserController { body: user, }; } + + @Contract("User.deleteByUsername") + async deleteByUsername({ + body, + }: RouteInput<"User.deleteByUsername">): RouteOutput<"User.deleteByUsername"> { + await this.userService.deleteByUsername(body.username); + return { + status: 201, + body: "OK", + }; + } } diff --git a/packages/backend/src/infrastructure/index.ts b/packages/backend/src/infrastructure/index.ts index b6fe042c..1a54fb0b 100644 --- a/packages/backend/src/infrastructure/index.ts +++ b/packages/backend/src/infrastructure/index.ts @@ -16,11 +16,9 @@ export * from "./middleware/locals/withJwt"; export * from "./middleware/locals/withRateLimit"; export * from "./middleware/locals/withUserRoles"; export * from "./middleware/locals/withValidatedBody"; -export * from "./middleware/locals/withPaginableParams"; export * from "./middleware/globals/index"; /* @org/backend/infrastructure/repository */ -export * from "./repository/PaginableRepository"; export * from "./repository/UserRepository"; export * from "./repository/ErrorLogRepository"; export * from "./repository/impl/UserRepositoryImpl"; diff --git a/packages/backend/src/infrastructure/middleware/locals/withPaginableParams.ts b/packages/backend/src/infrastructure/middleware/locals/withPaginableParams.ts deleted file mode 100644 index 72a8140c..00000000 --- a/packages/backend/src/infrastructure/middleware/locals/withPaginableParams.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { RequestHandler, Request } from "express"; -import { type MongoPaginationOptions, type MongoSort } from "@org/backend/types"; - -function buildPaginationOptions(req: Request): MongoPaginationOptions { - const query = req.query; - const page = query.page ? parseInt(query.page as string) : 0; - const limit = query.limit ? parseInt(query.limit as string) : 10; - const sort = query.sort ? (query.sort as string).split(",").map(value => value.split("|")) : []; - const textSearch = (query.search as string) ?? ""; - return { - filters: {}, - sort: sort as MongoSort, - page, - limit, - search: { - fields: ["username", "email"], - regex: textSearch, - }, - }; -} - -export function withPaginableParams(): RequestHandler { - return function (req, res, next) { - res.locals.paginationOptions = buildPaginationOptions(req); - next(); - }; -} diff --git a/packages/backend/src/infrastructure/repository/AbstractRepository.ts b/packages/backend/src/infrastructure/repository/AbstractRepository.ts index 816c09b7..9944cc92 100644 --- a/packages/backend/src/infrastructure/repository/AbstractRepository.ts +++ b/packages/backend/src/infrastructure/repository/AbstractRepository.ts @@ -1,22 +1,25 @@ import { type ZodSchema } from "zod"; import { DatabaseManager } from "@org/backend/config"; -import { type PaginableRepository } from "@org/backend/infrastructure/repository/PaginableRepository"; -import { type MongoPaginationOptions } from "@org/backend/types"; +import { type PaginationOptions } from "@org/shared"; import { type PaginationResult } from "@org/shared"; import * as PaginationUtils from "@org/backend/infrastructure/utils/PaginationUtils"; -export abstract class AbstractRepository implements PaginableRepository { +export abstract class AbstractRepository { private readonly schema: ZodSchema; + private readonly searchFields: string[]; constructor(schema: ZodSchema) { this.schema = schema; + this.searchFields = this.buildSearch(); } + abstract buildSearch(): string[]; + protected get collection() { return DatabaseManager.getInstance().collection(this.schema); } - async search(options?: MongoPaginationOptions): Promise> { - return PaginationUtils.paginate(this.collection, options); + async search(options?: PaginationOptions): Promise> { + return PaginationUtils.paginate(this.collection, this.searchFields, options); } } diff --git a/packages/backend/src/infrastructure/repository/ErrorLogRepository.ts b/packages/backend/src/infrastructure/repository/ErrorLogRepository.ts index 6e0a4378..185fc078 100644 --- a/packages/backend/src/infrastructure/repository/ErrorLogRepository.ts +++ b/packages/backend/src/infrastructure/repository/ErrorLogRepository.ts @@ -1,6 +1,6 @@ -import { type ErrorLog } from "@org/shared"; -import { type PaginableRepository } from "@org/backend/infrastructure/repository/PaginableRepository"; +import type { PaginationOptions, PaginationResult, ErrorLog } from "@org/shared"; -export interface ErrorLogRepository extends PaginableRepository { +export interface ErrorLogRepository { + search: (options: PaginationOptions) => Promise>; insertOne: (user: Omit) => Promise; } diff --git a/packages/backend/src/infrastructure/repository/PaginableRepository.ts b/packages/backend/src/infrastructure/repository/PaginableRepository.ts deleted file mode 100644 index c3bf09b5..00000000 --- a/packages/backend/src/infrastructure/repository/PaginableRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type PaginationResult } from "@org/shared"; -import { type MongoPaginationOptions } from "@org/backend/types"; - -export interface PaginableRepository { - search: (options?: MongoPaginationOptions) => Promise>; -} diff --git a/packages/backend/src/infrastructure/repository/UserRepository.ts b/packages/backend/src/infrastructure/repository/UserRepository.ts index 37df8e13..02d129b0 100644 --- a/packages/backend/src/infrastructure/repository/UserRepository.ts +++ b/packages/backend/src/infrastructure/repository/UserRepository.ts @@ -1,7 +1,8 @@ -import { type User } from "@org/shared"; -import { type PaginableRepository } from "@org/backend/infrastructure/repository/PaginableRepository"; +import type { PaginationOptions, PaginationResult, User } from "@org/shared"; -export interface UserRepository extends PaginableRepository { +export interface UserRepository { + deleteByUsername(username: string): Promise; + search: (options: PaginationOptions) => Promise>; findOneByUsername: (username: string) => Promise; findOneByRefreshTokens: (refreshTokens: string[]) => Promise; findAll: () => Promise; diff --git a/packages/backend/src/infrastructure/repository/impl/ErrorLogRepositoryImpl.ts b/packages/backend/src/infrastructure/repository/impl/ErrorLogRepositoryImpl.ts index 22b2c2b4..a7cb8385 100644 --- a/packages/backend/src/infrastructure/repository/impl/ErrorLogRepositoryImpl.ts +++ b/packages/backend/src/infrastructure/repository/impl/ErrorLogRepositoryImpl.ts @@ -8,6 +8,10 @@ export class ErrorLogRepositoryImpl extends AbstractRepository implements ErrorLogRepository { + buildSearch(): string[] { + return []; + } + constructor() { super(ErrorLog); } diff --git a/packages/backend/src/infrastructure/repository/impl/UserRepositoryImpl.ts b/packages/backend/src/infrastructure/repository/impl/UserRepositoryImpl.ts index 0bc97bc7..b22b5dc8 100644 --- a/packages/backend/src/infrastructure/repository/impl/UserRepositoryImpl.ts +++ b/packages/backend/src/infrastructure/repository/impl/UserRepositoryImpl.ts @@ -1,14 +1,22 @@ import { Injectable } from "@org/backend/decorators"; -import { User, ObjectId } from "@org/shared"; +import { User } from "@org/shared"; import { type UserRepository } from "@org/backend/infrastructure/repository/UserRepository"; import { AbstractRepository } from "../AbstractRepository"; @Injectable("userRepository") export class UserRepositoryImpl extends AbstractRepository implements UserRepository { + buildSearch(): string[] { + return ["email", "username"]; + } + constructor() { super(User); } + async deleteByUsername(username: string): Promise { + await this.collection.deleteOne({ username }); + } + async findOneByUsername(username: string): Promise { return await this.collection.findOne({ username }); } @@ -23,9 +31,8 @@ export class UserRepositoryImpl extends AbstractRepository implements User //@Transactional() async insertOne(user: Omit): Promise { - const candidate = { ...user, _id: new ObjectId() }; - const { insertedId } = await this.collection.insertOne(candidate); - return { ...candidate, _id: insertedId }; + const { insertedId } = await this.collection.insertOne(user); + return { ...user, _id: insertedId }; } //@Transactional() diff --git a/packages/backend/src/infrastructure/service/UserService.ts b/packages/backend/src/infrastructure/service/UserService.ts index eb133aa6..cb4f8464 100644 --- a/packages/backend/src/infrastructure/service/UserService.ts +++ b/packages/backend/src/infrastructure/service/UserService.ts @@ -1,8 +1,8 @@ -import { type MongoPaginationOptions } from "@org/backend/types"; -import { type User, type PaginationResult } from "@org/shared"; +import { type User, type PaginationResult, type PaginationOptions } from "@org/shared"; export interface UserService { - search: (options?: MongoPaginationOptions) => Promise>; + deleteByUsername(username: string): Promise; + search: (params: Partial) => Promise>; findAll: () => Promise; create: (user: User) => Promise; } diff --git a/packages/backend/src/infrastructure/service/impl/UserServiceImpl.ts b/packages/backend/src/infrastructure/service/impl/UserServiceImpl.ts index bdbedce4..2d714d05 100644 --- a/packages/backend/src/infrastructure/service/impl/UserServiceImpl.ts +++ b/packages/backend/src/infrastructure/service/impl/UserServiceImpl.ts @@ -1,8 +1,6 @@ -import type { TODO } from "@org/shared"; - -import type { PaginationResult } from "@org/shared"; +import { PaginationOptions, type TODO } from "@org/shared"; +import { type PaginationResult } from "@org/shared"; import { Autowired, Injectable } from "@org/backend/decorators"; -import { type MongoPaginationOptions } from "@org/backend/types"; import { type UserRepository } from "@org/backend/infrastructure/repository/UserRepository"; import { type UserService } from "@org/backend/infrastructure/service/UserService"; import { type User } from "@org/shared"; @@ -11,8 +9,8 @@ import { type User } from "@org/shared"; export class UserServiceImpl implements UserService { @Autowired() userRepository: UserRepository; - async search(options?: MongoPaginationOptions): Promise> { - return this.userRepository.search(options); + async search(options: Partial): Promise> { + return this.userRepository.search(PaginationOptions.parse(options)); } async findAll(): Promise { @@ -22,4 +20,8 @@ export class UserServiceImpl implements UserService { async create(user: User): Promise { return this.userRepository.insertOne(user) as TODO; } + + async deleteByUsername(username: string): Promise { + return this.userRepository.deleteByUsername(username); + } } diff --git a/packages/backend/src/infrastructure/utils/PaginationUtils.ts b/packages/backend/src/infrastructure/utils/PaginationUtils.ts index 09af4d30..ec49e766 100644 --- a/packages/backend/src/infrastructure/utils/PaginationUtils.ts +++ b/packages/backend/src/infrastructure/utils/PaginationUtils.ts @@ -1,27 +1,30 @@ -import type { TODO, PaginationResult } from "@org/shared"; +import type { TODO, PaginationResult, PaginationOptions } from "@org/shared"; import type { Collection } from "mongodb"; -import type { - MongoFilters, - MongoSearch, - MongoSort, - MongoPaginationOptions, -} from "@org/backend/types"; +import type { MongoFilters, MongoSearch, MongoSort } from "@org/backend/types"; export async function paginate( collection: Collection, - options?: MongoPaginationOptions, + searchFields: string[], + options?: PaginationOptions, ): Promise> { - const limit = options?.limit ?? 10; + const limit = options?.rowsPerPage ?? 10; const page = options?.page ?? 0; - const search = options?.search ?? { fields: [], regex: "" }; - const sort = options?.sort ?? []; + const search = options?.search ?? ""; + const order = options?.order ?? []; const filters = options?.filters ?? {}; const skip = page * limit; const pipeline: TODO[] = []; - pipeline.push(...buildMatchPipeline(search, filters)); - pipeline.push(...buildSortPipeline(sort)); + pipeline.push(...buildMatchPipeline({ fields: searchFields, regex: search }, filters)); + pipeline.push( + ...buildSortPipeline( + order.map(s => { + const [field, sortOrder] = s.split(" "); + return [field, sortOrder as "asc" | "desc"]; + }) as TODO, + ), + ); pipeline.push( ...[ diff --git a/packages/frontend/package.json b/packages/frontend/package.json index c762222d..b793201e 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -33,7 +33,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.1.0", - "react-router-dom": "^6.22.3" + "react-router-dom": "^6.22.3", + "react-use": "^17.5.0" }, "devDependencies": { "@preact/signals-react-transform": "^0.3.1", diff --git a/packages/frontend/src/core/components/layout/variants/HorizontalNavVariant.tsx b/packages/frontend/src/core/components/layout/variants/HorizontalNavVariant.tsx index 25609f5e..4c5f7146 100644 --- a/packages/frontend/src/core/components/layout/variants/HorizontalNavVariant.tsx +++ b/packages/frontend/src/core/components/layout/variants/HorizontalNavVariant.tsx @@ -37,7 +37,7 @@ function HorizontalNavItem({ const hasChildren = "children" in item && item.children; const children: NavigationRoute[] = (hasChildren ? item.children : []) as TODO; const isMainNavButton = dropdownPosition.anchorY === "bottom"; - const borderRadius = isMainNavButton ? 8 : undefined; + const borderRadius = isMainNavButton ? 1 : undefined; if (hasChildren) { const isAnyRouteActiveInGroup = isAnyRouteActive(children); @@ -50,6 +50,7 @@ function HorizontalNavItem({ sx={{ flexGrow: 0, whiteSpace: "nowrap", + outline: isMainNavButton && isAnyRouteActiveInGroup ? "1px solid gray" : undefined, backgroundColor: popupState.isOpen ? "action.hover" : isAnyRouteActiveInGroup @@ -91,10 +92,17 @@ function HorizontalNavItem({ return <>; } + const isSelected = location.pathname === itemSingle.path; + return ( navigate(itemSingle.path)} > {item.icon && {item.icon}} diff --git a/packages/frontend/src/core/components/semantics/Datatable/Datatable.tsx b/packages/frontend/src/core/components/semantics/Datatable/Datatable.tsx new file mode 100644 index 00000000..22f90bc3 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/Datatable.tsx @@ -0,0 +1,52 @@ +import { + TableContainer, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, + Paper, +} from "@mui/material"; +import { DtColumnDef } from "../../../roberto/datatable/types/dt-column.types"; + +export type DatatablePropsV2 = { + data: T[]; + columnDefs: readonly DtColumnDef[]; +}; + +export function Datatable({ data, columnDefs }: DatatablePropsV2) { + return ( + <> + + + + + + {columnDefs.map(column => ( + + {column.label} + + ))} + + + + {data.map(item => ( + + {columnDefs.map(column => ( + + + {/* @ts-expect-error Fix later */} + {column.render ? column.render(item[column.id], item) : item[column.id]} + + + ))} + + ))} + +
+
+
+ + ); +} diff --git a/packages/frontend/src/core/components/semantics/Datatable/components/DatatableContainer/DatatableContainer.tsx b/packages/frontend/src/core/components/semantics/Datatable/components/DatatableContainer/DatatableContainer.tsx new file mode 100644 index 00000000..d482d7cb --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/components/DatatableContainer/DatatableContainer.tsx @@ -0,0 +1,6 @@ +import { Paper } from "@mui/material"; +import { PropsWithChildren } from "react"; + +export function DatatableContainer({ children }: PropsWithChildren) { + return {children}; +} diff --git a/packages/frontend/src/core/components/semantics/Datatable/components/DatatableContainer/index.ts b/packages/frontend/src/core/components/semantics/Datatable/components/DatatableContainer/index.ts new file mode 100644 index 00000000..9dd6a2c9 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/components/DatatableContainer/index.ts @@ -0,0 +1 @@ +export * from "./DatatableContainer"; diff --git a/packages/frontend/src/core/components/semantics/Datatable/components/DtSortableCell/DtSortableCell.tsx b/packages/frontend/src/core/components/semantics/Datatable/components/DtSortableCell/DtSortableCell.tsx new file mode 100644 index 00000000..bba746da --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/components/DtSortableCell/DtSortableCell.tsx @@ -0,0 +1,51 @@ +import { TableCell, Box, TableSortLabel } from "@mui/material"; +import { MouseEvent, useCallback, useState } from "react"; +import { DtBaseColumnAlign, DtBaseColumnRenderHeader } from "../../types"; + +export type DtSortableCellProps = { + id: string; + align?: DtBaseColumnAlign; + renderHeader: DtBaseColumnRenderHeader; + priority?: number; + direction: "asc" | "desc"; + active: boolean; + onClick: (id: string, event: MouseEvent) => void; +}; + +export function DtSortableCell({ + id, + align = "left", + renderHeader, + priority, + direction, + active, + onClick, +}: DtSortableCellProps) { + const [hovered, setHovered] = useState(false); + const onMouseEnter = useCallback(() => setHovered(true), []); + const onMouseLeave = useCallback(() => setHovered(false), []); + + return ( + <> + + + + {renderHeader()} + onClick(id, e)} + active={hovered || active} + direction={direction} + > + {priority} + + + + + + ); +} diff --git a/packages/frontend/src/core/components/semantics/Datatable/components/DtSortableCell/index.ts b/packages/frontend/src/core/components/semantics/Datatable/components/DtSortableCell/index.ts new file mode 100644 index 00000000..50f24f3c --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/components/DtSortableCell/index.ts @@ -0,0 +1 @@ +export * from "./DtSortableCell"; diff --git a/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/ClientDatatable.tsx b/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/ClientDatatable.tsx new file mode 100644 index 00000000..9af914a4 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/ClientDatatable.tsx @@ -0,0 +1,136 @@ +import { + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + TablePagination, +} from "@mui/material"; +import { TODO } from "@org/shared"; +import { Fragment, MouseEvent, useMemo, useState } from "react"; +import { DtSortableCell } from "../../components/DtSortableCell/DtSortableCell"; +import { ClientDatatableProps } from "./types"; +import { DEFAULT_PAGINATION_OPTIONS, DtBaseOrder } from "../../types"; + +export function ClientDatatable({ + data, + columns, + disablePagination = false, +}: ClientDatatableProps) { + const [sortData, setSortData] = useState([]); + const [paginationOptions, setPaginationOptions] = useState(DEFAULT_PAGINATION_OPTIONS); + + const onPageChange = (newPage: number) => { + setPaginationOptions({ ...paginationOptions, page: newPage }); + }; + + const onRowsPerPageChange = (newRowsPerPage: number) => { + setPaginationOptions({ ...paginationOptions, rowsPerPage: newRowsPerPage }); + }; + + const filteredData = useMemo(() => { + if (disablePagination) return data; + const { page, rowsPerPage } = paginationOptions; + let localData = data; + if (sortData.length > 0) { + localData = [...data].sort((a, b) => { + for (const sortProps of sortData) { + const { id, direction } = sortProps; + const column = columns.find(v => v.id === id); + if (!column || !column.sort) continue; + const sortValue = column.sort(a, b); + if (sortValue !== 0) return direction === "asc" ? sortValue : -sortValue; + } + return 0; + }); + } + return localData.slice(page * rowsPerPage, (page + 1) * rowsPerPage); + }, [data, paginationOptions, disablePagination, sortData]); + + const onSortColumnClick = (id: string, event: MouseEvent) => { + console.log(event); + const sortIndex = sortData.findIndex(v => v.id === id); + if (sortIndex < 0) { + setSortData([{ id, direction: "asc" }]); + return; + } + const sortProps = sortData[sortIndex]; + const oldDirection = sortProps.direction; + if (oldDirection === "desc") { + setSortData([]); + return; + } + setSortData([{ id, direction: "desc" }]); + }; + + return ( + <> + + + + + {columns.map(({ id, renderHeader, align, sort }) => { + const sortIndex = sortData.findIndex(v => v.id === id); + const sortCount = sortData.length; + const sortProps = sortData[sortIndex]; + const active = !!sortProps; + const direction = sortProps?.direction ?? "asc"; + const priority = sortIndex + 1; + return ( + + {sort ? ( + + ) : ( + {renderHeader()} + )} + + ); + })} + + + + {filteredData.map((item, i) => ( + + {columns.map(({ id, align, renderBody }) => ( + + {renderBody(item)} + + ))} + + ))} + +
+
+ {!disablePagination && ( + `${from}-${to} to ${count}`} + rowsPerPageOptions={[10, 25, 50, 100]} + count={data.length} + page={paginationOptions.page} + rowsPerPage={paginationOptions.rowsPerPage} + showFirstButton + showLastButton + onPageChange={(_, newPage) => onPageChange(newPage)} + onRowsPerPageChange={e => onRowsPerPageChange(+e.target.value)} + classes={{ toolbar: "toolbar-class" }} + /> + )} + + ); +} diff --git a/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/index.ts b/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/index.ts new file mode 100644 index 00000000..5181e16e --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./ClientDatatable"; diff --git a/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/types.ts b/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/types.ts new file mode 100644 index 00000000..423340e5 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/impl/ClientDatatable/types.ts @@ -0,0 +1,13 @@ +import { DtBaseColumn } from "../../types"; + +export type DtClientColumnSort = (o1: T, o2: T) => number; + +export type DtClientColumn = DtBaseColumn & { + sort?: DtClientColumnSort; +}; + +export type ClientDatatableProps = { + data: T[]; + columns: DtClientColumn[]; + disablePagination?: boolean; +}; diff --git a/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/ServerDatatable.tsx b/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/ServerDatatable.tsx new file mode 100644 index 00000000..874a0a50 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/ServerDatatable.tsx @@ -0,0 +1,120 @@ +import { + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + TablePagination, +} from "@mui/material"; +import { TODO } from "@org/shared"; +import { Fragment, MouseEvent, useCallback } from "react"; +import { DtSortableCell } from "../../components/DtSortableCell/DtSortableCell"; +import { ServerDatatableProps } from "./types"; +import { DtBaseSortItem } from "../../types"; + +export function ServerDatatable({ + data, + columns, + keyMapper, + paginationOptions, + onPaginationOptionsChange, + count, +}: ServerDatatableProps) { + const sortData = + paginationOptions?.order.map(order => { + const [id, direction] = order.split(" "); + return { id, direction } as DtBaseSortItem; + }) ?? []; + + const onPageChange = (newPage: number) => { + onPaginationOptionsChange({ ...paginationOptions, page: newPage }); + }; + + const onRowsPerPageChange = (newRowsPerPage: number) => { + onPaginationOptionsChange({ ...paginationOptions, rowsPerPage: newRowsPerPage }); + }; + + const onSortColumnClick = useCallback( + (id: string, event: MouseEvent) => { + console.log(event); + const sortIndex = sortData.findIndex(v => v.id === id); + if (sortIndex < 0) { + onPaginationOptionsChange({ ...paginationOptions, order: [`${id} asc`] }); + return; + } + const sortProps = sortData[sortIndex]; + const oldDirection = sortProps.direction; + if (oldDirection === "desc") { + onPaginationOptionsChange({ ...paginationOptions, order: [] }); + return; + } + onPaginationOptionsChange({ ...paginationOptions, order: [`${id} desc`] }); + }, + [paginationOptions, sortData], + ); + + return ( + <> + + + + + {columns.map(({ id, renderHeader, align, sort }) => { + const sortIndex = sortData.findIndex(v => v.id === id); + const sortCount = sortData.length; + const sortProps = sortData[sortIndex]; + const active = !!sortProps; + const direction = sortProps?.direction ?? "asc"; + const priority = sortIndex + 1; + return ( + + {sort ? ( + + ) : ( + {renderHeader()} + )} + + ); + })} + + + + {data.map(item => ( + + {columns.map(({ id, align, renderBody }) => ( + + {renderBody(item)} + + ))} + + ))} + +
+
+ + `${from}-${to} to ${count}`} + rowsPerPageOptions={[10, 25, 50, 100]} + count={count} + page={paginationOptions?.page ?? 0} + rowsPerPage={paginationOptions?.rowsPerPage ?? 0} + showFirstButton + showLastButton + onPageChange={(_, newPage) => onPageChange(newPage)} + onRowsPerPageChange={e => onRowsPerPageChange(+e.target.value)} + classes={{ toolbar: "toolbar-class" }} + /> + + ); +} diff --git a/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/index.ts b/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/index.ts new file mode 100644 index 00000000..0d64b93d --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/index.ts @@ -0,0 +1,2 @@ +export * from "./ServerDatatable"; +export * from "./types"; diff --git a/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/types.ts b/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/types.ts new file mode 100644 index 00000000..04d06493 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/impl/ServerDatatable/types.ts @@ -0,0 +1,15 @@ +import { PaginationOptions } from "@org/shared"; +import { DtBaseColumn } from "../../types"; + +export type DtServerColumn = DtBaseColumn & { + sort?: string; +}; + +export type ServerDatatableProps = { + data: T[]; + columns: DtServerColumn[]; + keyMapper: (value: T) => string; + count: number; + paginationOptions: PaginationOptions; + onPaginationOptionsChange: (paginationOptions: PaginationOptions) => void; +}; diff --git a/packages/frontend/src/core/components/semantics/Datatable/index.ts b/packages/frontend/src/core/components/semantics/Datatable/index.ts new file mode 100644 index 00000000..6d1b9305 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/index.ts @@ -0,0 +1 @@ +export * from "./Datatable"; diff --git a/packages/frontend/src/core/components/semantics/Datatable/types.ts b/packages/frontend/src/core/components/semantics/Datatable/types.ts new file mode 100644 index 00000000..110fa6c8 --- /dev/null +++ b/packages/frontend/src/core/components/semantics/Datatable/types.ts @@ -0,0 +1,22 @@ +import { PaginationOptions } from "@org/shared"; +import { ReactNode } from "react"; + +export type DtBaseColumnAlign = "left" | "center" | "right"; +export type DtBaseColumnRenderHeader = () => ReactNode; +export type DtBaseColumnRenderBody = (value: T) => ReactNode; +export type DtBaseOrder = DtBaseSortItem[]; +export type DtBaseSortItem = { id: string; direction: "asc" | "desc" }; +export type DtBaseColumn = { + id: string; + align?: DtBaseColumnAlign; + renderHeader: DtBaseColumnRenderHeader; + renderBody: DtBaseColumnRenderBody; +}; + +export const DEFAULT_PAGINATION_OPTIONS: PaginationOptions = { + order: [], + page: 0, + rowsPerPage: 10, + search: "", + filters: {}, +}; diff --git a/packages/frontend/src/core/components/semantics/Header/Header.tsx b/packages/frontend/src/core/components/semantics/Header/Header.tsx index c0af8d51..8833503e 100644 --- a/packages/frontend/src/core/components/semantics/Header/Header.tsx +++ b/packages/frontend/src/core/components/semantics/Header/Header.tsx @@ -10,11 +10,7 @@ import { } from "@mui/material"; import { useTranslation } from "react-i18next"; import { sigSidebarOpen } from "../../../signals"; -import { - InputLayoutToggle, - InputLocaleSelect, - InputThemeToggle, -} from "../../inputs"; +import { InputLayoutToggle, InputLocaleSelect, InputThemeToggle } from "../../inputs"; import { InputFuzzySearch } from "../../inputs/InputFuzzySearch"; export type MuiSxProps = SxProps; @@ -40,23 +36,19 @@ export function Header({ component="header" sx={{ backgroundColor, - borderBottom: borderBottom - ? "1px solid var(--mui-palette-divider)" - : undefined, + borderBottom: borderBottom ? "1px solid var(--mui-palette-divider)" : undefined, }} > {!matchesDesktop && ( - (sigSidebarOpen.value = !sigSidebarOpen.value)} - > + (sigSidebarOpen.value = !sigSidebarOpen.value)}> )} diff --git a/packages/frontend/src/core/hooks/useDatatable.ts b/packages/frontend/src/core/hooks/useDatatable.ts new file mode 100644 index 00000000..b489afa9 --- /dev/null +++ b/packages/frontend/src/core/hooks/useDatatable.ts @@ -0,0 +1,73 @@ +import { useCallback, useMemo, useState } from "react"; +import { DtColumnDef } from "../../core/roberto/datatable/types/dt-column.types"; +import { DtDataFilter, DtSearchFilter } from "../../core/roberto/datatable/types/dt-table.types"; + +type UseDatatableProps = { + data: T[]; + columnDefs: readonly DtColumnDef[]; + searchFilter?: DtSearchFilter; + dataFilter?: DtDataFilter; +}; + +export default function useDatatable({ + searchFilter, + columnDefs, + data, + dataFilter = () => true, +}: UseDatatableProps) { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [search, setSearch] = useState(""); + + const searchChangeHandler = useCallback((value: string) => { + setPage(0); + setSearch(value); + }, []); + + const rowsPerPageChangeHandler = useCallback((value: number) => { + setRowsPerPage(value); + setPage(0); + }, []); + + const pageChangeHandler = useCallback((value: number) => { + setPage(value); + }, []); + + const searchFilterOrDefaultFn = useCallback( + (item: T, search: string) => { + if (searchFilter) { + return searchFilter(item, search); + } + return columnDefs.some(({ render, textual, id }) => { + const renderedValue: unknown = render?.(item[id], item); + const value = textual + ? textual(item[id], item) + : render && typeof renderedValue === "string" + ? renderedValue + : String(item[id]); + const valueLowerCase = value.toLocaleLowerCase(); + return valueLowerCase.includes(search.toLocaleLowerCase()); + }); + }, + [searchFilter], + ); + + const results = useMemo(() => { + const res = data.filter(item => searchFilterOrDefaultFn(item, search) && dataFilter(item)); + return res; + }, [data, search]); + + return { + page, + setPage, + rowsPerPage, + setRowsPerPage, + searchFilterOrDefaultFn, + results, + changeHandler: { + search: searchChangeHandler, + rowsPerPage: rowsPerPageChangeHandler, + page: pageChangeHandler, + }, + } as const; +} diff --git a/packages/frontend/src/core/hooks/useTableSizePreference.ts b/packages/frontend/src/core/hooks/useTableSizePreference.ts new file mode 100644 index 00000000..a6186eb1 --- /dev/null +++ b/packages/frontend/src/core/hooks/useTableSizePreference.ts @@ -0,0 +1,13 @@ +import { useLocalStorage } from "react-use"; + +export type SizePreference = "small" | "medium"; + +const TABLE_SIZE_PREFERENCE_STORAGE_KEY = "tableSize"; +const DEFAULT_TABLE_SIZE_PREFERENCE_VALUE = "small"; + +export default function useSizePreference() { + return useLocalStorage( + TABLE_SIZE_PREFERENCE_STORAGE_KEY, + DEFAULT_TABLE_SIZE_PREFERENCE_VALUE, + ); +} diff --git a/packages/frontend/src/core/roberto/common/FormGroupDivider.tsx b/packages/frontend/src/core/roberto/common/FormGroupDivider.tsx new file mode 100644 index 00000000..12150851 --- /dev/null +++ b/packages/frontend/src/core/roberto/common/FormGroupDivider.tsx @@ -0,0 +1,8 @@ +import { Divider } from "@mui/material"; +import { memo } from "react"; + +function FormGroupDivider() { + return ; +} + +export default memo(FormGroupDivider); diff --git a/packages/frontend/src/core/roberto/common/RenderIf.tsx b/packages/frontend/src/core/roberto/common/RenderIf.tsx new file mode 100644 index 00000000..06e077c3 --- /dev/null +++ b/packages/frontend/src/core/roberto/common/RenderIf.tsx @@ -0,0 +1,15 @@ +import React, { memo } from "react"; + +export type RenderIfProps = { + test: boolean; + children: React.ReactNode; +}; + +function RenderIf({ test, children }: RenderIfProps) { + if (test) { + return <>{children}; + } + return <>; +} + +export default memo(RenderIf); diff --git a/packages/frontend/src/core/roberto/datatable/components/ActionContainer.tsx b/packages/frontend/src/core/roberto/datatable/components/ActionContainer.tsx new file mode 100644 index 00000000..e8dc9333 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/ActionContainer.tsx @@ -0,0 +1,31 @@ +import { Box } from "@mui/material"; +import { TODO } from "@org/shared"; +import { MouseEvent, ReactNode } from "react"; + +export type ActionContainerProps = { + data: T; + children: ReactNode | ReactNode[]; + actionDependency?: ReactNode; + actionRender?: ReactNode; + clickHandler: (item: T) => void; +}; + +export default function ActionContainer({ + actionDependency, + children, + clickHandler, + data, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + actionRender: _actionRender, +}: ActionContainerProps) { + const handleClick = (e: MouseEvent) => { + e?.stopPropagation(); + clickHandler(data); + }; + return ( + + {actionDependency} + {children} + + ); +} diff --git a/packages/frontend/src/core/roberto/datatable/components/DtActionButton.tsx b/packages/frontend/src/core/roberto/datatable/components/DtActionButton.tsx new file mode 100644 index 00000000..1fdd90fa --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtActionButton.tsx @@ -0,0 +1,36 @@ +import { IconButton, SvgIconTypeMap, Tooltip } from "@mui/material"; +import { OverridableComponent } from "@mui/material/OverridableComponent"; +import { memo } from "react"; + +type Color = + | "inherit" + | "action" + | "disabled" + | "primary" + | "secondary" + | "error" + | "info" + | "success" + | "warning"; + +export type DtActionButtonProps = { + color: Color; + translationKey: string; + Icon: IconType; +}; + +type IconType = OverridableComponent> & { + muiName: string; +}; + +function DtActionButton({ Icon, color, translationKey }: DtActionButtonProps) { + return ( + + + + + + ); +} + +export default memo(DtActionButton) as typeof DtActionButton; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtActionRender.tsx b/packages/frontend/src/core/roberto/datatable/components/DtActionRender.tsx new file mode 100644 index 00000000..a20ab8c2 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtActionRender.tsx @@ -0,0 +1,24 @@ +import { Box } from "@mui/material"; +import { MouseEvent } from "react"; +import { DtAction } from "../types/dt-action.types"; +import { TODO } from "@org/shared"; + +export type DtActionRenderProps = { + action: DtAction; + row: T; + index: number; +}; + +export default function DtActionRender({ action, row, index }: DtActionRenderProps) { + const handleClick = (e: MouseEvent) => { + e?.stopPropagation(); + action.clickHandler(row, index); + }; + + return ( + + {action.actionDependency} + {action.actionRender} + + ); +} diff --git a/packages/frontend/src/core/roberto/datatable/components/DtActionsCell.tsx b/packages/frontend/src/core/roberto/datatable/components/DtActionsCell.tsx new file mode 100644 index 00000000..f8745393 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtActionsCell.tsx @@ -0,0 +1,22 @@ +import { TableCell } from "@mui/material"; +import { memo } from "react"; +import { DtActionList } from "../types/dt-action.types"; +import DtActionRender from "./DtActionRender"; + +export type DtActionsCellProps = { + actions: DtActionList; + elem: T; + dataIndex: number; +}; + +function DtActionsCell({ actions, elem, dataIndex }: DtActionsCellProps) { + return ( + + {actions.map((action, index) => ( + + ))} + + ); +} + +export default memo(DtActionsCell) as typeof DtActionsCell; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtDataCell.tsx b/packages/frontend/src/core/roberto/datatable/components/DtDataCell.tsx new file mode 100644 index 00000000..754cfa9e --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtDataCell.tsx @@ -0,0 +1,25 @@ +import { memo } from "react"; +import { toFixed } from "../../utils/currency-utils"; +import { TableCell, Typography } from "@mui/material"; +import { DtColumnDef } from "../types/dt-column.types"; + +export type DtDataCellProps = { + row: T; + column: DtColumnDef; +}; + +function DtDataCell({ row, column }: DtDataCellProps) { + const value = row[column.id]; + const isValueNumber = typeof value === "number"; + const title = isValueNumber ? toFixed(value) : value; + return ( + + + {/* @ts-expect-error Fix later! */} + {column.render ? column.render(value, row) : value} + + + ); +} + +export default memo(DtDataCell) as typeof DtDataCell; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtDataRows.tsx b/packages/frontend/src/core/roberto/datatable/components/DtDataRows.tsx new file mode 100755 index 00000000..1faeb439 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtDataRows.tsx @@ -0,0 +1,71 @@ +import TableRow from "@mui/material/TableRow"; +import { Key, memo, useCallback } from "react"; +import { ctx } from "../../utils/contextMenu-utils"; +import { DtActionList } from "../types/dt-action.types"; +import { DtColumnDef } from "../types/dt-column.types"; +import { DtIdentifier } from "../types/dt-table.types"; +import DtActionsCell from "./DtActionsCell"; +import DtDataCell from "./DtDataCell"; +import { TODO } from "@org/shared"; + +export type DtDataRowsProps = { + actions: DtActionList; + identifier?: DtIdentifier; + rowsPerPage: number; + page: number; + data: T[]; + columnDefs: readonly DtColumnDef[]; + getRowSeparator?: GetRowSeparatorType; +}; + +const DEFAULT_GET_ROW_SEPARATOR = { + render: () => undefined, + predicate: () => false, +} as TODO; + +export type GetRowSeparatorType = { + render: (item: T) => string; + predicate: (current: T, next: T) => boolean; +}; + +function DtDataRows({ + data, + actions, + rowsPerPage, + page, + identifier, + columnDefs, + getRowSeparator = DEFAULT_GET_ROW_SEPARATOR, +}: DtDataRowsProps) { + const calculateRowSeparator = useCallback( + (current: T, data: T[], index: number) => + data.findIndex(item => getRowSeparator.predicate(current, item)) === index + ? getRowSeparator.render(current) + : undefined, + [], + ); + + return ( + <> + {data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, i, arr) => { + return ( + + {actions.length > 0 && } + {columnDefs.map(column => ( + + ))} + + ); + })} + + ); +} + +export default memo(DtDataRows) as typeof DtDataRows; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtHead.tsx b/packages/frontend/src/core/roberto/datatable/components/DtHead.tsx new file mode 100644 index 00000000..6c074685 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtHead.tsx @@ -0,0 +1,24 @@ +import { TableCell, TableHead, TableRow } from "@mui/material"; +import { memo } from "react"; +import { DtColumnDef } from "../types/dt-column.types"; +import DtHeaderCell from "./DtHeaderCell"; + +export type DtHeadProps = { + columnDefs: readonly DtColumnDef[]; + showActions: boolean; +}; + +function DtHead({ columnDefs, showActions }: DtHeadProps) { + return ( + + + {showActions && } + {columnDefs.map(c => ( + + ))} + + + ); +} + +export default memo(DtHead) as typeof DtHead; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtHeaderCell.tsx b/packages/frontend/src/core/roberto/datatable/components/DtHeaderCell.tsx new file mode 100644 index 00000000..58723241 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtHeaderCell.tsx @@ -0,0 +1,26 @@ +import { TableCell } from "@mui/material"; +import { memo, useMemo } from "react"; +import { DtColumnDef } from "../types/dt-column.types"; + +export type DtHeaderCellProps = { + column: DtColumnDef; +}; + +function DtHeaderCell({ column }: DtHeaderCellProps) { + const tableCellCommonClass = "whitespace-nowrap"; + const style = useMemo( + () => ({ + minWidth: column.minWidth, + width: column.width, + }), + [], + ); + + return ( + + {column.label} + + ); +} + +export default memo(DtHeaderCell) as typeof DtHeaderCell; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtNoDataRow.tsx b/packages/frontend/src/core/roberto/datatable/components/DtNoDataRow.tsx new file mode 100755 index 00000000..0fd9e87e --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtNoDataRow.tsx @@ -0,0 +1,22 @@ +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import { memo } from "react"; + +export type DtNoDataRowProps = { + columnsCount: number; + className?: string; + text?: string; +}; + +function DtNoDataRow({ className = "h-[53px]", columnsCount, text: text0 }: DtNoDataRowProps) { + const text = text0 ? text0 : /*TODO useLocalizedMessage("datatable.empty")*/ text0; + return ( + + + {text} + + + ); +} + +export default memo(DtNoDataRow) as typeof DtNoDataRow; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtPagination.tsx b/packages/frontend/src/core/roberto/datatable/components/DtPagination.tsx new file mode 100644 index 00000000..269a9718 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtPagination.tsx @@ -0,0 +1,55 @@ +import { LabelDisplayedRowsArgs, TablePagination } from "@mui/material"; +import React from "react"; + +const ROWS_PER_PAGE = [10, 25, 50, 100]; +const getLabelDisplayedRows = ( + loading: boolean, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { from: _from, to: _to, count: _count }: LabelDisplayedRowsArgs, +) => { + const localizedMessage = + /*TODO useLocalizedMessage("datatable.paginatedResults", { + from, + to, + count, + })*/ "TODO, CHANGE ME"; + + return loading ? "" : localizedMessage; +}; + +export type DtPaginationProps = { + loading: boolean; + page: number; + count: number; + rowsPerPage: number; + onPageChange: (value: number) => void; + onRowsPerPageChange: (value: number) => void; +}; + +function DtPagination({ + loading, + page, + count, + rowsPerPage, + onPageChange, + onRowsPerPageChange, +}: DtPaginationProps) { + return ( + getLabelDisplayedRows(loading, p)} + labelRowsPerPage={"resultsPerPage TODO CHANGE"} + rowsPerPageOptions={loading ? [] : ROWS_PER_PAGE} + count={count} + rowsPerPage={rowsPerPage} + showFirstButton + showLastButton + page={page} + onPageChange={(_, newPage) => onPageChange(newPage)} + onRowsPerPageChange={e => onRowsPerPageChange(+e.target.value)} + classes={{ toolbar: "flex-wrap justify-center sm:flex-nowrap sm:justify-end" }} + /> + ); +} + +export default React.memo(DtPagination) as typeof DtPagination; diff --git a/packages/frontend/src/core/roberto/datatable/components/DtSkeletonRows.tsx b/packages/frontend/src/core/roberto/datatable/components/DtSkeletonRows.tsx new file mode 100755 index 00000000..6d9f181b --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/components/DtSkeletonRows.tsx @@ -0,0 +1,28 @@ +import { memo } from "react"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import { Skeleton } from "@mui/material"; + +export type DtSkeletonRowsProps = { + rowsPerPage: number; + columnsCount: number; + height?: number; +}; + +function DtSkeletonRows({ rowsPerPage, columnsCount, height = 24 }: DtSkeletonRowsProps) { + return ( + <> + {Array.from({ length: rowsPerPage }).map((_, i) => ( + + {Array.from({ length: columnsCount }).map((_, j) => ( + + + + ))} + + ))} + + ); +} + +export default memo(DtSkeletonRows); diff --git a/packages/frontend/src/core/roberto/datatable/index.tsx b/packages/frontend/src/core/roberto/datatable/index.tsx new file mode 100755 index 00000000..ca850715 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/index.tsx @@ -0,0 +1,130 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import Paper from "@mui/material/Paper"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableContainer from "@mui/material/TableContainer"; +import { memo } from "react"; +import useTableSizePreference, { SizePreference } from "../../../core/hooks/useTableSizePreference"; +import RenderIf from "../common/RenderIf"; +import DtDataRows, { GetRowSeparatorType } from "./components/DtDataRows"; +import DtHead from "./components/DtHead"; +import DtNoDataRow from "./components/DtNoDataRow"; +import DtPagination from "./components/DtPagination"; +import DtSkeletonRows from "./components/DtSkeletonRows"; +import { DtActionList } from "./types/dt-action.types"; +import { DtColumnDef } from "./types/dt-column.types"; +import { DtDataFilter, DtIdentifier, DtSearchFilter, RowType } from "./types/dt-table.types"; +import useDatatable from "../../hooks/useDatatable"; + +export type DatatableProps = { + data: T[]; + columnDefs: readonly DtColumnDef[]; + identifier?: DtIdentifier; + actions?: DtActionList; + searchable?: boolean; + getRowSeparator?: GetRowSeparatorType; + height?: number; + autoHeight?: boolean; + loading?: boolean; + size?: SizePreference; + searchFilter?: DtSearchFilter; + dataFilter?: DtDataFilter; + disablePagination?: boolean; +}; + +function Datatable({ + columnDefs, + data = [], + actions = [], + identifier, + searchFilter, + dataFilter = () => true, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + searchable: _searchable, + getRowSeparator, + //autoHeight = false, + //height, + loading = false, + size: size0, + disablePagination = false, +}: DatatableProps) { + const [tableSizePreference] = useTableSizePreference(); + const size = size0 ? size0 : tableSizePreference; + + const { results, page, rowsPerPage, changeHandler } = useDatatable({ + data, + columnDefs, + searchFilter, + dataFilter, + }); + + const showActions = actions.length > 0; + const showNoDataRow = !loading && results.length === 0; + const showResultRows = !loading && results.length > 0; + const showSkeletonRows = loading; + const columnsCount = columnDefs.length + (showActions ? 1 : 0); + + //const showSearchInput = !!searchable; + //const paddingClass = disablePagination ? "!p-0" : "!p-4"; + //const paperClass = `overflow-hidden mb-2 flex flex-col gap-4 outline outline-1 outline-slate-300 ${paddingClass}`; + + /*const smallHeightClass = "max-h-[575px]"; + const mediumHeightClass = "max-h-[800px]"; + const containerStyle = useMemo(() => ({ height }), []); + const containerClass = autoHeight + ? "" + : height + ? "" + : size === "small" + ? smallHeightClass + : mediumHeightClass;*/ + + return ( + <> + + {/*showSearchInput && */} + {/**/} + + + + + + + + + + + + + + + actions={actions} + data={results} + columnDefs={columnDefs} + identifier={identifier} + getRowSeparator={getRowSeparator} + rowsPerPage={rowsPerPage} + page={page} + /> + + +
+
+ {/*
*/} + + {!disablePagination && ( + + )} +
+ + ); +} + +export default memo(Datatable) as typeof Datatable; diff --git a/packages/frontend/src/core/roberto/datatable/types/dt-action.types.ts b/packages/frontend/src/core/roberto/datatable/types/dt-action.types.ts new file mode 100644 index 00000000..61875e03 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/types/dt-action.types.ts @@ -0,0 +1,21 @@ +export type DtActionDef = () => DtAction; +export type DtActionList = DtActionDef[]; + +// TODO: Refactor DtAction to a plain Action. + +type ActionBaseProps = { + actionDependency?: React.ReactNode; + actionRender?: React.ReactNode; +}; + +type ActionDatatableProps = { + clickHandler: (item: T, index?: number) => void; +}; + +type ActionStandaloneProps = { + clickHandler: (item: T) => void; +}; + +export type Action = ActionBaseProps & ActionStandaloneProps; + +export type DtAction = ActionBaseProps & ActionDatatableProps; diff --git a/packages/frontend/src/core/roberto/datatable/types/dt-column.types.ts b/packages/frontend/src/core/roberto/datatable/types/dt-column.types.ts new file mode 100644 index 00000000..a8ee0c10 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/types/dt-column.types.ts @@ -0,0 +1,17 @@ +import { RowType } from "./dt-table.types"; + +export type Values = T[keyof T]; + +export type DtColumnDef = Values<{ + [Prop in keyof T]: DtColumnDefRaw; +}>; + +export type DtColumnDefRaw = { + id: K; + label: string; + align?: "inherit" | "left" | "center" | "right" | "justify"; + minWidth?: number | string; + width?: number | string; + textual?: (value: T[K], wrapper: T) => string; + render?: (value: T[K], wrapper: T) => React.ReactNode; +}; diff --git a/packages/frontend/src/core/roberto/datatable/types/dt-table.types.ts b/packages/frontend/src/core/roberto/datatable/types/dt-table.types.ts new file mode 100644 index 00000000..d35673d4 --- /dev/null +++ b/packages/frontend/src/core/roberto/datatable/types/dt-table.types.ts @@ -0,0 +1,10 @@ +import { TODO } from "@org/shared"; + +export type RowType = Record; + +type ExtractReactKeys = { + [K in keyof T]: T[K] extends string | number ? K : never; +}[keyof T]; +export type DtIdentifier = ExtractReactKeys; +export type DtDataFilter = (item: T) => boolean; +export type DtSearchFilter = (item: T, search: string) => boolean; diff --git a/packages/frontend/src/core/roberto/utils/contextMenu-utils.ts b/packages/frontend/src/core/roberto/utils/contextMenu-utils.ts new file mode 100644 index 00000000..d4c4256d --- /dev/null +++ b/packages/frontend/src/core/roberto/utils/contextMenu-utils.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { TODO } from "@org/shared"; + +type DataCtxAttributes = Record; + +function ctx(key: string, value: T) { + return { + [`data-ctx-${key}`]: JSON.stringify(value), + }; +} + +function ctxDataMapper>(attrs: DataCtxAttributes, key: string): T { + return JSON.parse(attrs[key]) as T; +} + +function findAllDataCtxAttributes(target: HTMLElement): DataCtxAttributes { + const result: Record = {}; + + function traverseDOM(element: HTMLElement | null) { + if (!element) return; + + const dataAttributes = Array.from(element.attributes).filter(attr => + attr.name.startsWith("data-ctx-"), + ); + + if (dataAttributes.length > 0) { + dataAttributes.forEach(attr => { + const key = attr.name.replace("data-ctx-", ""); + result[key] = attr.value; + }); + } + + traverseDOM(element.parentElement); + } + + traverseDOM(target); + return result; +} + +type ReadonlyValues = { + [K in keyof T]: T[K] extends Function ? never : K; +}[keyof T]; + +type Readonly = T extends Record ? Pick> : T; + +class ContextMenuAttrs { + #data: Record; + + constructor(target: HTMLElement) { + const attrs = findAllDataCtxAttributes(target); + this.#data = Object.keys(attrs).reduce( + (prev, key) => ({ + ...prev, + [key]: ctxDataMapper(attrs, key), + }), + {}, + ); + } + + get(key: string): Readonly { + return this.#data[key] as Readonly; + } +} + +export { ContextMenuAttrs, ctx }; diff --git a/packages/frontend/src/core/roberto/utils/currency-utils.ts b/packages/frontend/src/core/roberto/utils/currency-utils.ts new file mode 100644 index 00000000..a66661db --- /dev/null +++ b/packages/frontend/src/core/roberto/utils/currency-utils.ts @@ -0,0 +1,27 @@ +const EUR_TO_HRK = 7.5345; + +export function convertEurToHrk(eur: number): number { + return EUR_TO_HRK * eur; +} + +export function stringifyEuro(euro: number): string { + return toFixed(euro, "€"); +} + +export function stringifyHrk(hrk: number): string { + return toFixed(hrk, "HRK"); +} + +export function stringifyPercentage(percentage: number): string { + return toFixed(percentage, "%"); +} + +export function toFixed(value: number, suffix?: string) { + const formattedValue = value.toLocaleString("hr-HR", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: true, + }); + + return `${formattedValue}${suffix ? " " + suffix : ""}`; +} diff --git a/packages/frontend/src/core/roberto/utils/string-utils.ts b/packages/frontend/src/core/roberto/utils/string-utils.ts new file mode 100644 index 00000000..24c02f05 --- /dev/null +++ b/packages/frontend/src/core/roberto/utils/string-utils.ts @@ -0,0 +1,7 @@ +import { TODO } from "@org/shared"; + +export function sprintf(str: string, ...args: TODO[]) { + return str.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != "undefined" ? args[number] : match; + }); +} diff --git a/packages/frontend/src/core/roberto/utils/type-utils.ts b/packages/frontend/src/core/roberto/utils/type-utils.ts new file mode 100644 index 00000000..fb0eb72b --- /dev/null +++ b/packages/frontend/src/core/roberto/utils/type-utils.ts @@ -0,0 +1,5 @@ +type ChangeHandlerValue = T[K] | ((prev: T[K]) => T[K]); + +export type ChangeHandler = (key: K, value: ChangeHandlerValue) => void; + +export type Optional = T | null | undefined; diff --git a/packages/frontend/src/core/signals/sigTheme.ts b/packages/frontend/src/core/signals/sigTheme.ts index e6ed69fe..31ef9338 100644 --- a/packages/frontend/src/core/signals/sigTheme.ts +++ b/packages/frontend/src/core/signals/sigTheme.ts @@ -1,14 +1,10 @@ import { PaletteMode, PaletteOptions, ThemeOptions } from "@mui/material"; -import { - CssVarsTheme, - Theme, - experimental_extendTheme as extendTheme, -} from "@mui/material/styles"; +import { CssVarsTheme, Theme, experimental_extendTheme as extendTheme } from "@mui/material/styles"; import { signal } from "@preact/signals-react"; export function buildBaseThemeConfig( // eslint-disable-next-line @typescript-eslint/no-unused-vars - _schema: MuiThemeColors + _schema: MuiThemeColors, ): Omit { return { shape: { @@ -51,6 +47,13 @@ export function buildBaseThemeConfig( MenuListProps: { sx: { padding: "0 !important" } }, }, }, + MuiTableCell: { + styleOverrides: { + head: { + fontWeight: "bold", + }, + }, + }, MuiList: { styleOverrides: { root: { @@ -122,10 +125,7 @@ export function buildBaseThemeConfig( }; } -function buildBasePalette( - schema: MuiThemeColors, - mode: PaletteMode -): PaletteOptions { +function buildBasePalette(schema: MuiThemeColors, mode: PaletteMode): PaletteOptions { const lightColor = "58, 53, 65"; const darkColor = "231, 227, 252"; const mainColor = mode === "light" ? lightColor : darkColor; diff --git a/packages/frontend/src/pages/Home/HomePage.tsx b/packages/frontend/src/pages/Home/HomePage.tsx index d6e4580b..089ecfed 100644 --- a/packages/frontend/src/pages/Home/HomePage.tsx +++ b/packages/frontend/src/pages/Home/HomePage.tsx @@ -1,69 +1,186 @@ -import { LightMode } from "@mui/icons-material"; -import { Button, Card, CardContent, IconButton, Paper, Switch, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; +import { + Accordion, + AccordionActions, + AccordionDetails, + AccordionSummary, + Badge, + Box, + Button, + Menu, + Typography, +} from "@mui/material"; import { client } from "../../core/client"; -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { PaginationOptions, TODO, User } from "@org/shared"; +import { UserCreateFormButton } from "../UserCreateFormButton"; +import { ServerDatatable } from "../../core/components/semantics/Datatable/impl/ServerDatatable"; +import { DEFAULT_PAGINATION_OPTIONS } from "../../core/components/semantics/Datatable/types"; +import { type UserPageableResponseDto } from "@org/shared"; +import { ExpandMore, FilterAlt, FilterAltOutlined } from "@mui/icons-material"; +import { DatatableContainer } from "../../core/components/semantics/Datatable/components/DatatableContainer"; -const ThemeShowcaseComponent: React.FC = () => { - const { t } = useTranslation(); - return ( - - - - - {t("test")} - - - Check out how the theme changes are applied. - - - { - /* Handle theme toggle */ - }} - inputProps={{ "aria-label": "controlled" }} - /> - - - - - - +export function HomePage() { + const [userResponse, setUserResponse] = useState(); + const [paginationOptions, setPaginationOptions] = useState({ + ...DEFAULT_PAGINATION_OPTIONS, + order: ["username asc"], + }); + + const fetchUsers = useCallback(async () => { + const users = await client.User.pagination({ + query: { paginationOptions: JSON.stringify(paginationOptions) }, + }); + if (users.status !== 200) throw new Error("Failed to fetch users."); + setUserResponse(users.body); + }, [paginationOptions]); + + const deleteUser = useCallback( + async (username: string) => { + const response = await client.User.deleteByUsername({ body: { username } }); + if (response.status !== 201) throw new Error("Failed to delete user."); + fetchUsers(); + }, + [fetchUsers], ); -}; -export function HomePage() { useEffect(() => { - const fetchUsers = async () => { - const users = await client.User.pagination(); - console.log(users.body); - }; fetchUsers(); - }, []); + }, [fetchUsers]); + + const badgeContent: number = 2; + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: TODO) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; return ( <> - + + + + +
+ + + + + Filters + + + + + } + aria-controls="panel1-content" + id="panel1-header" + > + Accordion 1 + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada + lacus ex, sit amet blandit leo lobortis eget. + + + + } + aria-controls="panel2-content" + id="panel2-header" + > + Accordion 2 + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada + lacus ex, sit amet blandit leo lobortis eget. + + + + } + aria-controls="panel3-content" + id="panel3-header" + > + Accordion Actions + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada + lacus ex, sit amet blandit leo lobortis eget. + + + + + + +
+
+ +
+ + data={userResponse?.data ?? []} + count={userResponse?.totalElements ?? 0} + keyMapper={user => user.username} + paginationOptions={paginationOptions} + onPaginationOptionsChange={paginationOptions => setPaginationOptions(paginationOptions)} + columns={[ + { + id: "username", + renderHeader: () => "Username", + renderBody: user => user.username, + sort: "username", + }, + { + id: "email", + align: "left", + renderHeader: () => "Email", + renderBody: user => user.email, + sort: "email", + }, + { + id: "roles", + renderHeader: () => "Roles", + renderBody: user => user.roles.join(", "), + }, + { + id: "actions", + renderHeader: () => "Actions", + renderBody: user => ( + + ), + }, + ]} + /> +
); } diff --git a/packages/frontend/src/pages/UserCreateFormButton/UserCreateFormButton.tsx b/packages/frontend/src/pages/UserCreateFormButton/UserCreateFormButton.tsx new file mode 100644 index 00000000..ba931426 --- /dev/null +++ b/packages/frontend/src/pages/UserCreateFormButton/UserCreateFormButton.tsx @@ -0,0 +1,56 @@ +import { Button, Dialog, DialogContent } from "@mui/material"; +import { client } from "../../core/client"; +import { useState } from "react"; +import { TODO, User } from "@org/shared"; +import { UserForm } from "../UserForm/UserForm"; + +export type UserCreateFormButtonProps = { + afterUpdate?: () => void; +}; + +const DEFAULT_FORM_STATE: User = { + refreshToken: [], + username: "", + email: "", + password: "", + roles: ["USER"], +}; + +export function UserCreateFormButton({ afterUpdate }: UserCreateFormButtonProps) { + const [user, setUser] = useState(DEFAULT_FORM_STATE); + + const [open, setOpen] = useState(false); + + const onOpen = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; + + const handleSubmit = async (event: TODO) => { + event.preventDefault(); + // Handle form submission + console.log("Form submitted:", user); + await client.User.create({ + body: user, + }); + setUser(DEFAULT_FORM_STATE); + setOpen(false); + afterUpdate?.(); + }; + + return ( + <> + + + + + + + + ); +} diff --git a/packages/frontend/src/pages/UserCreateFormButton/index.ts b/packages/frontend/src/pages/UserCreateFormButton/index.ts new file mode 100644 index 00000000..fdb9616c --- /dev/null +++ b/packages/frontend/src/pages/UserCreateFormButton/index.ts @@ -0,0 +1 @@ +export * from "./UserCreateFormButton"; diff --git a/packages/frontend/src/pages/UserForm/UserForm.tsx b/packages/frontend/src/pages/UserForm/UserForm.tsx new file mode 100644 index 00000000..6f4c2ce0 --- /dev/null +++ b/packages/frontend/src/pages/UserForm/UserForm.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { TextField, Button, Box, Autocomplete, MenuItem, Chip } from "@mui/material"; +import { Role, User } from "@org/shared"; + +export type UserFormProps = { + value: User; + onChange: (newState: User) => void; + onSubmit: (event: React.FormEvent) => void; +}; + +export function UserForm({ value, onChange, onSubmit }: UserFormProps) { + const mutate = (diff: Partial) => { + onChange({ + ...value, + ...diff, + }); + }; + + return ( + + mutate({ username: e.target.value })} + required + /> + mutate({ email: e.target.value })} + required + /> + mutate({ password: e.target.value })} + required + /> + option} + onChange={(_, newValue) => mutate({ roles: newValue })} + value={value.roles} + disableCloseOnSelect + filterSelectedOptions + renderOption={(props, option) => ( + + {option} + + )} + renderInput={params => } + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + )) + } + /> + + + ); +} diff --git a/packages/frontend/src/pages/UserForm/index.ts b/packages/frontend/src/pages/UserForm/index.ts new file mode 100644 index 00000000..543de93a --- /dev/null +++ b/packages/frontend/src/pages/UserForm/index.ts @@ -0,0 +1 @@ +export * from "./UserForm"; diff --git a/packages/shared/src/models/domain/User.ts b/packages/shared/src/models/domain/User.ts index 962143d5..9079f3b5 100644 --- a/packages/shared/src/models/domain/User.ts +++ b/packages/shared/src/models/domain/User.ts @@ -3,7 +3,7 @@ import z from "zod"; export const User = z .object({ - _id: z.instanceof(ObjectId), + _id: z.instanceof(ObjectId).optional(), username: z.string().openapi({ example: "john_doe" }), password: z.string().openapi({ example: "password" }), email: z.string().email().openapi({ example: "john.doe@mail.com" }), diff --git a/packages/shared/src/web/contracts/UserContract.ts b/packages/shared/src/web/contracts/UserContract.ts index 10e49b5f..9ea9e3ab 100644 --- a/packages/shared/src/web/contracts/UserContract.ts +++ b/packages/shared/src/web/contracts/UserContract.ts @@ -35,6 +35,26 @@ export type PaginationResult = { export const UserPageableResponseDto = PageableResponseDto(User); +export type UserPageableResponseDto = z.infer; + +export function JsonQueryParam(schema: Schema) { + return z.string().transform(val => { + console.log(val); + const result = JSON.parse(val) as z.infer; + return result; + }); +} + +export const PaginationOptions = z.object({ + page: z.number().default(0), + rowsPerPage: z.number().default(10), + order: z.array(z.string()).default([]), + search: z.string().default(""), + filters: z.any().default({}), +}); + +export type PaginationOptions = z.infer; + export const UserContract = initContract().router({ findOne: { metadata, @@ -60,10 +80,7 @@ export const UserContract = initContract().router({ summary: "Get all users", description: "Get all users", query: z.object({ - page: z.number().default(0), - limit: z.number().default(10), - sort: z.string().default(""), - search: z.string().default(""), + paginationOptions: JsonQueryParam(PaginationOptions), }), responses: { 200: UserPageableResponseDto, @@ -83,4 +100,19 @@ export const UserContract = initContract().router({ ...defaultResponses, }, }, + deleteByUsername: { + metadata, + strictStatusCodes: true, + path: buildPath(), + method: "DELETE", + summary: "Delete User by username", + description: "Delete User by username", + body: z.object({ + username: z.string().openapi({ example: "brunotot" }), + }), + responses: { + 201: z.string(), + ...defaultResponses, + }, + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0f28163..a0c7554f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + react-use: + specifier: ^17.5.0 + version: 17.5.0(react-dom@18.3.1)(react@18.3.1) devDependencies: '@commitlint/cli': specifier: ^19.2.2 @@ -95,6 +99,9 @@ importers: express-rate-limit: specifier: ^7.2.0 version: 7.2.0(express@4.19.2) + flatted: + specifier: ^3.3.1 + version: 3.3.1 helmet: specifier: ^7.1.0 version: 7.1.0 @@ -276,6 +283,9 @@ importers: react-router-dom: specifier: ^6.22.3 version: 6.23.0(react-dom@18.3.1)(react@18.3.1) + react-use: + specifier: ^17.5.0 + version: 17.5.0(react-dom@18.3.1)(react@18.3.1) devDependencies: '@preact/signals-react-transform': specifier: ^0.3.1 @@ -2491,7 +2501,6 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true /@jridgewell/trace-mapping@0.3.25: resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -3235,6 +3244,10 @@ packages: pretty-format: 29.7.0 dev: true + /@types/js-cookie@2.2.7: + resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + dev: false + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3574,6 +3587,10 @@ packages: pretty-format: 29.7.0 dev: true + /@xobotyi/scrollbar-width@1.9.5: + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + dev: false + /@zeit/schemas@2.36.0: resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} dev: true @@ -4527,6 +4544,12 @@ packages: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true + /copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + dependencies: + toggle-selection: 1.0.6 + dev: false + /core-js-compat@3.37.0: resolution: {integrity: sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==} dependencies: @@ -4629,6 +4652,12 @@ packages: which: 2.0.2 dev: true + /css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + dependencies: + hyphenate-style-name: 1.0.4 + dev: false + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: @@ -4639,6 +4668,14 @@ packages: nth-check: 2.1.1 dev: true + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: false + /css-vendor@2.0.8: resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==} dependencies: @@ -4900,6 +4937,12 @@ packages: dependencies: is-arrayish: 0.2.1 + /error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + dependencies: + stackframe: 1.3.4 + dev: false + /es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} @@ -5203,7 +5246,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -5228,16 +5270,28 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-loops@1.1.3: + resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} + dev: false + /fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} dev: true + /fast-shallow-equal@1.0.0: + resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + dev: false + /fast-url-parser@1.1.3: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} dependencies: punycode: 1.4.1 dev: true + /fastest-stable-stringify@2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + dev: false + /fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: @@ -5354,7 +5408,6 @@ packages: /flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - dev: true /fluent_conv@3.3.0: resolution: {integrity: sha512-OsTQyVWo1WYmEnnH7m3MRlk5NQq/+jXOLzv0WOk8GGn99LdQV1kNp3IOR6HYb+fwDqYebLPLAThS2pFEaDbyHQ==} @@ -5872,6 +5925,13 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /inline-style-prefixer@7.0.0: + resolution: {integrity: sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==} + dependencies: + css-in-js-utils: 3.1.0 + fast-loops: 1.1.3 + dev: false + /ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -6529,6 +6589,10 @@ packages: hasBin: true dev: true + /js-cookie@2.2.1: + resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7099,6 +7163,10 @@ packages: resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==} dev: true + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -7398,6 +7466,24 @@ packages: engines: {node: '>=12.0.0'} dev: true + /nano-css@5.6.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + css-tree: 1.1.3 + csstype: 3.1.3 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.3.2 + dev: false + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -8029,6 +8115,40 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /react-universal-interface@0.6.2(react@18.3.1)(tslib@2.6.2): + resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} + peerDependencies: + react: '*' + tslib: '*' + dependencies: + react: 18.3.1 + tslib: 2.6.2 + dev: false + + /react-use@17.5.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} + peerDependencies: + react: '*' + react-dom: '*' + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.1(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-universal-interface: 0.6.2(react@18.3.1)(tslib@2.6.2) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.6.2 + dev: false + /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -8145,6 +8265,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -8241,6 +8365,12 @@ packages: fsevents: 2.3.3 dev: true + /rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + dependencies: + '@babel/runtime': 7.24.5 + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -8271,6 +8401,11 @@ packages: loose-envify: 1.4.0 dev: false + /screenfull@5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + dev: false + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -8370,6 +8505,11 @@ packages: to-object-path: 0.3.0 dev: true + /set-harmonic-interval@1.0.1: + resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} + engines: {node: '>=6.9'} + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -8479,6 +8619,11 @@ packages: source-map: 0.6.1 dev: true + /source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + dev: false + /source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -8487,7 +8632,6 @@ packages: /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} @@ -8507,6 +8651,12 @@ packages: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} dev: true + /stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + dependencies: + stackframe: 1.3.4 + dev: false + /stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: false @@ -8522,6 +8672,25 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + dev: false + + /stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + dev: false + + /stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -8655,6 +8824,10 @@ packages: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false + /stylis@4.3.2: + resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} + dev: false + /superagent@9.0.2: resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} engines: {node: '>=14.18.0'} @@ -8786,6 +8959,11 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + dev: false + /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -8843,6 +9021,10 @@ packages: is-number: 7.0.0 dev: true + /toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + dev: false + /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -8895,6 +9077,10 @@ packages: engines: {node: '>=14.13.1'} dev: false + /ts-easing@0.2.0: + resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + dev: false + /ts-jest@29.1.2(@babel/core@7.24.5)(babel-jest@29.7.0)(jest@29.7.0)(typescript@5.4.5): resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -9032,7 +9218,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true /tsx@4.9.3: resolution: {integrity: sha512-czVbetlILiyJZI5zGlj2kw9vFiSeyra9liPD4nG+Thh4pKTi0AmMEQ8zdV/L2xbIVKrIqif4sUNrsMAOksx9Zg==} diff --git a/scripts/data/dependencies.json b/scripts/data/dependencies.json index 46d6f6f2..2b06d7e5 100644 --- a/scripts/data/dependencies.json +++ b/scripts/data/dependencies.json @@ -25,6 +25,7 @@ "dotenv": "Loads environment variables from .env files", "express": "The web framework used for building the backend API", "express-rate-limit": "Provides rate limiting to protect against brute force attacks", + "flatted": "-", "helmet": "Collection of security middleware for Express.js", "hpp": "Protects against HTTP Parameter Pollution attacks", "http-status": "Utility for working with HTTP status codes", @@ -39,6 +40,7 @@ "react-dom": "Provides DOM-specific methods for React", "react-i18next": "Integrates i18next with React for internationalization", "react-router-dom": "Provides routing functionality for the React frontend application", + "react-use": "-", "swagger-jsdoc": "Generates OpenAPI documentation from JSDoc comments", "swagger-ui-express": "Renders the Swagger UI for the OpenAPI documentation", "winston": "Logging library used for application logging", diff --git a/scripts/js/writeDependenciesMarkdown.js b/scripts/js/writeDependenciesMarkdown.js index 303108ed..f6ab5a11 100644 --- a/scripts/js/writeDependenciesMarkdown.js +++ b/scripts/js/writeDependenciesMarkdown.js @@ -175,8 +175,8 @@ function main() { console.log( `Some dependencies are missing descriptions at ${pathFromDir(PATH_TO_DEPENDENCIES_JSON)}`, ); - console.log("Aborting..."); - process.exit(1); + //console.log("Aborting..."); + //process.exit(1); } const packageDependencies = getDependenciesFromPackages();