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 (
<>
-
+
+
+
+
+
+
+
+ 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) => (
+
+ )}
+ 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();