Skip to content

Commit

Permalink
chore: refactor more on frontend and add confirm dialog context
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed Nov 13, 2024
1 parent 986ffde commit 509288e
Show file tree
Hide file tree
Showing 39 changed files with 658 additions and 440 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import type { TODO } from "@org/lib-commons";

import { contract } from "@org/app-node-express/app/infrastructure/decorators";
import { withRouteSecured } from "@org/app-node-express/app/infrastructure/middleware/withRouteSecured";
import { withValidatedBody } from "@org/app-node-express/app/infrastructure/middleware/withValidatedBody";
import { autowired, inject } from "@org/app-node-express/lib/ioc";
import { contracts, Role } from "@org/lib-api-client";

import { VALIDATORS } from "../validators";

@inject("UserController")
export class UserController {
@autowired() private userService: UserService;
Expand Down Expand Up @@ -53,9 +56,9 @@ export class UserController {
@contract(
contracts.User.createUser,
withRouteSecured(Role.Enum["avr-admin"]),
// withValidator(UserValidator, { groups: ["create"] }),
// TODO
withValidatedBody(/*UserForm, */ VALIDATORS.User, { groups: ["create"] }),
)
// @validators("User", { groups: ["create"] })
async createUser(
payload: RouteInput<typeof contracts.User.createUser>,
): RouteOutput<typeof contracts.User.createUser> {
Expand All @@ -66,7 +69,11 @@ export class UserController {
};
}

@contract(contracts.User.updateUser, withRouteSecured(Role.Enum["avr-admin"]))
@contract(
contracts.User.updateUser,
withRouteSecured(Role.Enum["avr-admin"]),
withValidatedBody(VALIDATORS.User, { groups: ["update"] }),
)
async updateUser(
payload: RouteInput<typeof contracts.User.updateUser>,
): RouteOutput<typeof contracts.User.updateUser> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @packageDocumentation
*/

import type { VALIDATORS } from "../validators";
import type { RouteMiddlewareFactory } from "@org/app-node-express/lib/@ts-rest";
import type { RequestHandler } from "express";

import { IocRegistry, inject } from "@org/app-node-express/lib/ioc";
import { RestError, type ValidatorOptions, type Validators } from "@org/lib-api-client";

const IOC_KEY = "withValidatedBody";

export type ValidatorSchema = keyof typeof VALIDATORS;

export interface ValidatedBodyMiddleware {
middleware(validators: Validators, options: ValidatorOptions): RequestHandler[];
}

@inject(IOC_KEY)
export class WithValidatedBody implements ValidatedBodyMiddleware {
middleware(validators: Validators, options: ValidatorOptions): RequestHandler[] {
return [
async (req, _res, next) => {
const body = req.body || undefined;
if (!body) return next();
const validatorEntries = Object.entries(validators);
const validationResults = await Promise.all(
validatorEntries.map(([, validate]) => validate(body, options)),
);
const hasErrors = validationResults.some(res => !res);
if (hasErrors) return next(new RestError(400, "Body validation failed"));
next();
},
];
}
}

export function withValidatedBody(
validators: Validators,
options: ValidatorOptions = { groups: [] },
): RouteMiddlewareFactory {
return () => {
return IocRegistry.getInstance()
.inject<ValidatedBodyMiddleware>(IOC_KEY)
.middleware(validators, options);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import type { UserService } from "../service/UserService";
import type { UserForm, Validator, Validators } from "@org/lib-api-client";

import { IocRegistry } from "@org/app-node-express/lib";
import { RestError } from "@org/lib-api-client";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const usernameShouldBeUnique: Validator<UserForm> = async (model, _options) => {
// Returns false if validation success

export const usernameShouldBeUnique: Validator<UserForm> = async (model, options) => {
if (options.groups && !options.groups.includes("create")) return true;
const userService = IocRegistry.getInstance().inject<UserService>("UserService");
try {
return (await userService.findOneByUsername(model.username)) !== null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
await userService.findOneByUsername(model.username);
return false;
} catch (error: unknown) {
if (error instanceof RestError && error.content.status === 404) {
return true;
}
throw error;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type { AppRoute, ServerInferResponses } from "@org/lib-api-client";
import type { TODO } from "@org/lib-commons";
import type { AppRouteImplementation } from "@ts-rest/express";
import type { RequestHandler } from "express";

Expand All @@ -28,6 +29,11 @@ export type RouteInput<Route extends AppRoute> = Parameters<AppRouteImplementati
*/
export type RouteHandler<Route extends AppRoute> = (data: RouteInput<Route>) => RouteOutput<Route>;

/**
* Represents a handler function for any route, taking any data and returning any output.
*/
export type UntypedRouteHandler = (data: TODO) => TODO;

/**
* Represents a factory for generating an array of Express middleware request handlers.
* @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/express/index.d.ts#L112 RequestHandler}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"accountSettings": "إعدادات الحساب",
"appTheme": "الثيم",
"apps": "التطبيقات",
"borderRadius": "نصف قطر الحافة",
"calendar": "التقويم",
"cards": "البطاقات",
"dashboard": "لوحة التحكم",
"doSearch": "بحث",
"error": "خطأ",
"forms": "النماذج",
"icons": "الرموز",
"invoice": "فاتورة",
"list": "قائمة",
"login": "تسجيل الدخول",
"pages": "الصفحات",
"register": "التسجيل",
"rolesAndPermissions": "الأدوار والصلاحيات",
"setDarkMode": "وضع داكن",
"setLightMode": "وضع مضيء",
"systemLanguage": "اللغة",
"systemLayout": "التخطيط",
"systemLayoutHorizontal": "تصميم أفقي",
"systemLayoutSidebar": "تصميم بشريط جانبي",
"tables": "الجداول",
"test": "اختبار",
"typography": "الخطوط",
"user": "المستخدم",
"userInterface": "واجهة المستخدم",
"view": "عرض"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { TODO } from "@org/lib-commons";

import * as icons from "@mui/icons-material";
import * as mui from "@mui/material";
import { sigDirection } from "@org/app-vite-react/app/signals/sigDirection";
import { useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import { type UIMatch, useMatches } from "react-router-dom";
Expand Down Expand Up @@ -45,81 +46,35 @@ function convertToCrumbs(matches: UIMatch<unknown, unknown>[]): /*Crumb[]*/ TODO
return crumbs;
}

export function ComputedBreadcrumbs() {
const matchesDesktop = mui.useMediaQuery("(min-width:678px)");
const matches = useMatches();
const crumbs = convertToCrumbs(matches);
//.filter(match => "handle" in match && match.handle && typeof match.handle === "object" && "crumb" in match.handle && match.handle.crumb && typeof match.handle.crumb === "function")
//.map(match => match.handle.crumb(match.data));

const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);

const handleClose = () => {
setAnchorEl(null);
};

const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};

if (!matchesDesktop) {
return (
<>
<mui.Button
variant="outlined"
onClick={handleOpen}
data-driver="breadcrumbs"
sx={{ borderRadius: 0.25 }}
>
<mui.Box
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "65vw",
}}
>
{crumbs[crumbs.length - 1].label}
</mui.Box>
</mui.Button>
<mui.Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<mui.Box paddingInline={2}>
<mui.Breadcrumbs
separator={<icons.NavigateNext fontSize="small" />}
aria-label="breadcrumb"
sx={{
"& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:first-child": {
width: "100%",
},
"& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:nth-child(odd):not(:first-child)": {
flex: 1,
},
}}
>
{crumbs.map((crumb, index) => (
<mui.Link
key={index}
component={RouterLink}
to={crumb.match.handle?.disableLink === true ? "#" : crumb.match.pathname}
underline="hover"
color={index === crumbs.length - 1 ? "text.primary" : "inherit"}
>
{crumb.label}
</mui.Link>
))}
</mui.Breadcrumbs>
</mui.Box>
</mui.Menu>
</>
);
}
export function LocalBreadcrumbs({
crumbs,
mobileLayout,
}: {
crumbs: TODO[];
mobileLayout: boolean;
}) {
const sx: mui.SxProps<mui.Theme> | undefined = !mobileLayout
? undefined
: {
"& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:first-child": {
width: "100%",
},
"& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:nth-child(odd):not(:first-child)": {
flex: 1,
},
};

return (
<mui.Breadcrumbs
sx={sx}
aria-label="breadcrumb"
data-driver="breadcrumbs"
separator={<icons.NavigateNext fontSize="small" />}
separator={
sigDirection.value === "ltr" ? (
<icons.NavigateNext fontSize="small" />
) : (
<icons.NavigateBefore fontSize="small" />
)
}
>
{crumbs.map((crumb, index) => (
<mui.Link
Expand All @@ -135,3 +90,39 @@ export function ComputedBreadcrumbs() {
</mui.Breadcrumbs>
);
}

export function ComputedBreadcrumbs() {
const matchesDesktop = mui.useMediaQuery("(min-width:678px)");
const matches = useMatches();
const crumbs = convertToCrumbs(matches);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);

if (matchesDesktop) return <LocalBreadcrumbs crumbs={crumbs} mobileLayout={!matchesDesktop} />;

return (
<>
<mui.Button
variant="outlined"
onClick={e => setAnchorEl(e.currentTarget)}
sx={{ borderRadius: 0.25 }}
>
<mui.Box
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "65vw",
}}
>
{crumbs[crumbs.length - 1].label}
</mui.Box>
</mui.Button>
<mui.Menu anchorEl={anchorEl} open={open} onClose={() => setAnchorEl(null)}>
<mui.Box paddingInline={2}>
<LocalBreadcrumbs crumbs={crumbs} mobileLayout={!matchesDesktop} />
</mui.Box>
</mui.Menu>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { InputLayoutToggle } from "@org/app-vite-react/app/inputs/InputLayoutTog
import { InputLocaleSelect } from "@org/app-vite-react/app/inputs/InputLocaleSelect";
import { sigLayoutVariant } from "@org/app-vite-react/app/signals/sigLayoutVariant";
import { sigLayoutWidth } from "@org/app-vite-react/app/signals/sigLayoutWidth";
import { sigLocale } from "@org/app-vite-react/app/signals/sigLocale";
import { getFontFamily, sigLocale } from "@org/app-vite-react/app/signals/sigLocale";
import { sigSidebarOpen } from "@org/app-vite-react/app/signals/sigSidebarOpen";
import { sigThemeOpts } from "@org/app-vite-react/app/signals/sigTheme";

Expand Down Expand Up @@ -94,7 +94,10 @@ export function Header({
/>
<InputLocaleSelect
value={sigLocale.value}
onChange={locale => (sigLocale.value = locale)}
onChange={locale => {
sigLocale.value = locale;
sigThemeOpts.value = { ...sigThemeOpts.value, fontFamily: getFontFamily(locale) };
}}
/>
</mui.Box>
<UserMenuButton />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { ReactNode } from "react";

import { SwipeableDrawer, useMediaQuery } from "@mui/material";
import { sigLayoutVariant } from "@org/app-vite-react/app/signals/sigLayoutVariant";
import { sigSidebarPosition } from "@org/app-vite-react/app/signals/sigSidebarPosition";

export type SidebarProps = {
width?: number;
Expand All @@ -22,7 +21,7 @@ export function Sidebar({
children,
}: SidebarProps) {
const matchesDesktop = useMediaQuery("(min-width:678px)");
const computedAnchor = matchesDesktop ? sigSidebarPosition.value : "bottom";
const computedAnchor = matchesDesktop ? "left" : "bottom";

const paperWidth = matchesDesktop ? width : "100%";
const drawerWidth = hidden ? 0 : paperWidth;
Expand Down
Loading

0 comments on commit 509288e

Please sign in to comment.