TASKS-1823 WebManager module

This commit is contained in:
Igor Martyniuk
2026-08-28 08:35:58 +00:00
parent fdb2cf3837
commit c93d8c025b
654 changed files with 26232 additions and 6075 deletions
@@ -0,0 +1,82 @@
# CRUX React Admin Panel Architecture
## Core Principles
1. **Blocks are composable and isolated** — each block (component, form, widget) has a single responsibility; higher-level blocks (widgets, pages) compose lower-level ones.
2. **Data vs. UI separation** — `data` handles API, state, and business data; `view` handles UI and interaction. No API logic in UI components — UI calls hooks from `data`.
3. **Entities are global types** — placed in `entities/`, usable by any block.
4. **Shared** — `shared/` = globally reusable components, hooks, utilities.
5. **Infrastructure** — `infrastructure/` = app-wide technical adapters: HTTP clients, React Query, storage adapters, formatting service wrappers.
6. **Reference data** — `refdata/` = app-wide API-backed lookup datasets shared by multiple modules.
## Recommended Layering
```plaintext
src/
├── app/ # composition root: providers, routing, config, i18n, layouts
├── infrastructure/ # technical adapters: HTTP, query, storage, error handling
├── refdata/ # API-backed common reference datasets
├── shared/ # reusable app-wide UI, hooks, utilities, and widgets
├── core/ # built-in product modules
├── modules/ # project-specific modules
└── devtools/ # development-only modules and tools
```
## Architecture Scope
The layer structure can be used at three levels:
| Scope | Example | Meaning |
| --------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| App-level architecture | `apps/admin/src/app`, `apps/admin/src/shared` | Global app shell, providers, routing, layout, infrastructure, reusable app-wide building blocks. |
| Core module architecture | `apps/admin/src/core/FileManager` | Built-in product modules shipped with the admin template. Each large core module can own its own `data`, `entities`, `pages`, `view`, and `shared`, plus feature-scoped context when needed. |
| Project module architecture | `apps/admin/src/modules/*` | Generated or project-specific modules. Each module should follow the same layer rules inside its own folder when it becomes more than a simple page. |
For small modules, do not create empty layers only to match the template. Add a layer when the module has real ownership for that layer.
## Full documentation
- [Entities Layer](./layers/entities.md)
- [Data Layer](./layers/data.md)
- [App Layer](./layers/app.md)
- [Infrastructure Layer](./layers/infrastructure.md)
- [Reference Data Layer](./layers/refdata.md)
- [View Layer](./layers/view.md)
- [Shared Layer](./layers/shared.md)
- [File & Folder Naming Convention](./naming.md)
## Disclaimer
This architecture guide is intended as a **recommendation**, not a strict rulebook. **Feel free to adapt and modify it based on your team’s needs and project context**.
Key points to keep in mind:
1. **Maintainability comes first**.
The main goal of this structure is to help someone new to the project quickly understand where to make changes.
It’s about finding the right place to touch the code now, rather than strictly following a pattern for its own sake.
**Ultimately, the main goal is to make development easier for everyone**
2. **Naming debates are inevitable**.
Any architecture guide may spark discussions about file, folder, or function naming. Use this guide as a reference, but remember that practicality and clarity are more important than strict adherence.
3. **Flexibility is allowed**.
- You can adjust layers, module structure, or naming conventions if it improves readability or reduces complexity.
- New patterns or exceptions can be added as the project evolves.
In short: this guide exists to make the project easier to understand and maintain, not to create unnecessary friction. Use judgment and prioritize clarity over rigid consistency.
---
Цей посібник з архітектури є **рекомендацією**, а не суворим зводом правил. **Не соромтеся адаптувати та змінювати його відповідно до потреб вашої команди та контексту проєкту**.
Ключові моменти, які варто мати на увазі:
1. **Насамперед — зручність підтримки**.
Головна мета цієї структури — допомогти новому учаснику проєкту швидко зрозуміти, де вносити зміни.
Йдеться про те, щоб знайти правильне місце в коді просто зараз, а не сліпо дотримуватися патерну заради самого патерну.
**Зрештою, головна мета — зробити розробку простішою для всіх.**
2. **Дискусії щодо назв неминучі**.
Будь-який посібник з архітектури може спровокувати суперечки щодо назв файлів, папок або функцій. Використовуйте цей посібник як орієнтир, але пам'ятайте, що практичність і зрозумілість важливіші за суворе дотримання правил.
3. **Гнучкість допускається**.
- Ви можете коригувати шари, структуру модулів або угоди щодо найменування, якщо це покращує читабельність або зменшує складність.
- У міру розвитку проєкту можна додавати нові патерни або винятки.
Коротко: цей посібник існує для того, щоб проєкт був зрозумілішим і простішим у підтримці, а не для того, щоб створювати зайві труднощі. Керуйтеся здоровим глуздом і надавайте перевагу ясності, а не жорсткій послідовності.
@@ -0,0 +1,33 @@
# App Layer
The composition root: owns concerns that exist once for the whole app and coordinates layers and modules without containing feature business logic. It composes global providers, configures app-wide settings and environment values, initializes i18n and global styles, defines top-level routing, provides layouts and menus, and registers core/project/development modules.
```plaintext
app/
├── config/ # global settings, environment values, and config types
├── css/ # global styles and app-level third-party overrides
├── i18n/ # languages, messages, and internationalization config
├── layouts/ # top-level Admin layout variants
├── providers/ # providers composed once for the entire application
│ ├── AppProviders.tsx
│ ├── I18nProvider.tsx
│ ├── QueryProvider.tsx
│ ├── SettingsProvider.tsx
│ └── ThemeProvider.tsx
├── routing/
│ ├── AppRouting.tsx
│ └── AppRoutingSetup.tsx
├── services/
│ └── modules/ # module registry, routes, menus, and messages
└── menu.tsx # app-level menu composition
```
Files may evolve, but ownership stays app-wide — a concern used by only one feature belongs to that feature module.
## Notes
- **Providers** — `app/providers/AppProviders.tsx` is the single composition point: TanStack Query, auth context (from the Auth core module), settings, tooltip/notification UI, theme, i18n and text direction, React Router.
- **Routing** — `app/routing/` composes top-level routes from routes contributed by registered modules; feature pages stay in their modules.
- **Config & i18n** — centralize environment access through `app/config/` instead of repeating it in feature code; `app/i18n/` owns languages and messages.
- **Layouts & CSS** — `app/layouts/` holds the Admin shell variants, `app/css/` global and third-party overrides; feature styles stay with the feature.
- **Module registration** — `app/services/modules/` wires modules in through their routes, menus, and messages; module implementation stays under `core/`, `modules/`, or `devtools/`.
@@ -0,0 +1,149 @@
# Data Layer
The `data` layer owns all data access and transformation: API calls, DTO types, DTO → entity mappers, React Query hooks, caching and errors. Higher layers (`forms`, `widgets`, `pages`) never call APIs directly — they consume data-layer hooks.
## Organization
Two ways to structure the layer:
| Approach | Layout | Best for | Trade-off |
| --------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------- |
| By Module | `data/api/`, `data/hooks/`, `data/dto/`, `data/mappers/` | Small apps, simple data layers | One feature's files scattered; hard to scale |
| By Entity | `data/user/`, `data/product/` — each with `api.ts`, `hooks.ts`, `dto.ts`, `mapper.ts`, `types.ts` | Medium/large apps | Harder to see all APIs/hooks globally |
```plaintext
data/
| --- user/
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
| | --- dto.ts # API DTO types for user
| | --- mapper.ts # map user DTO → entity
| | --- types.ts # API types/interfaces (optional)
```
## Data Flow
### 1. DTO Types
```ts
export type UserDTO = {
id: number;
full_name: string;
email_address: string;
};
export type CreateUserDTO = {
full_name: string;
email_address: string;
};
```
### 2. API Layer
API functions or class methods perform HTTP requests and always return DTOs.
```ts
export class UserApi {
private http: HttpClient;
async getDetailed(id: number): Promise<UserDTO> {
return this.http.get(`/users/${id}`);
}
async getList(): Promise<UserDTO[]> {
return this.http.get(`/users`);
}
async create(dto: CreateUserDTO): Promise<UserDTO> {
return this.http.post(`/users`, dto);
}
}
```
### 3. Mapper
Mappers convert `DTO` to `Entity`.
```ts
import { User } from "@/entities/user";
export class UserMapper {
toEntity(dto: UserDTO): User {
return {
id: dto.id,
name: dto.full_name,
email: dto.email_address,
};
}
toEntities(dtos: UserDTO[]): User[] {
return dtos.map((dto) => this.toEntity(dto));
}
toPaginated(dto: PaginatedDataDTO<UserDTO[]>): PaginatedResult<User[]> => {
return {
meta: this.paginationMetaMapper.toMeta(dto.meta),
data: this.toEntities(dto.data),
};
};
}
```
### Mapper Testing
Mappers are part of the data layer contract and need unit tests whenever they rename fields, normalize values (paths, dates, statuses, enums, defaults), map nested DTOs, arrays, paginated data, or optional fields. Cover: `toEntity` on a representative DTO, `toEntities` on arrays, defaults for missing fields, nested mapper shapes, enum/status casts. Test only transformation — no React, no rendering, no real API calls.
Keep tests at the app root, mirroring the source path:
```plaintext
apps/admin/
├── src/core/FileManager/data/mappers/entryMapper.ts
└── tests/unit/core/FileManager/data/mappers/entryMapper.test.ts
```
### 4. Query Hooks
Hooks combine API + mapper + React Query.
```ts
export const useUserDetailed = (id: number) => {
const api = new UserApi(http);
const mapper = new UserMapper();
return useQuery({
queryKey: ["user", id],
queryFn: async () => {
const dto = await api.getDetailed(id);
return mapper.toEntity(dto);
},
});
};
```
### Pagination
Pagination is not entity-specific — keep it in a shared data module.
```ts
export type PaginatedMetaDTO = {
total: number;
perPage: number;
currentPage: number;
lastPage: number;
};
export type PaginatedDTO<T> = {
data: T;
meta: PaginatedMetaDTO;
};
export class PaginationMapper {
toMeta(dto: PaginatedMetaDTO) {
return {
total: dto.total,
perPage: dto.perPage,
currentPage: dto.currentPage,
lastPage: dto.lastPage,
};
}
}
```
@@ -0,0 +1,25 @@
# Entities
Core domain models of the application — pure type definitions for business objects (`User`, `Product`, `Message`), their enums, and related constants. Stable, reusable, usable from any layer.
```plaintext
entities/
| --- user.ts # { id: string; name: string; email: string }
| --- product.ts # { id: string; name: string; price: number }
| --- enums.ts # { UserRole, ProductCategory }
```
Larger entities can use a folder (`user/` with `interfaces.ts`, `enums.ts`, `rules.ts`, `index.ts`).
```ts
export type User = {
id: number;
name: string;
email: string;
};
export enum UserRole {
ADMIN = "admin",
SIMLPE_JACK = "simple_jack",
}
```
@@ -0,0 +1,51 @@
# Infrastructure Layer
App-wide technical adapters connecting the app to technical systems and shared runtime services: HTTP clients and interceptors, shared React Query helpers (keys, response types, error normalization), storage/browser adapters, formatting service adapters, technical config wrappers. No feature business logic.
```plaintext
infrastructure/
├── axios/
│ ├── index.ts # configured axios client
│ ├── interceptors.ts # request/response interceptors
│ └── axios.d.ts # axios type augmentation
│
├── reactQuery/
│ ├── index.ts
│ ├── errors.ts # query error handling helpers
│ ├── helpers.ts # query key and pagination helpers
│ ├── queryParams.ts # table/query-param mapping helpers
│ └── types.ts # shared API response/query types
│
├── IntlDate.ts # date formatting service adapter
└── IntlName.ts # name formatting service adapter
```
## Rules
1. Feature-agnostic: no module business rules, no page/component composition, no entity mapping that belongs in a module `data/mapper`.
2. API modules use infrastructure clients (`import { axiosClient } from "#infrastructure/axios"`) instead of creating their own.
3. React Query helpers shared across modules (key builders, list response types, pagination, error extraction) live here.
4. UI never calls raw HTTP clients — view/page code uses data-layer hooks; direct `#infrastructure/axios` usage belongs in API/data modules.
5. Generic formatting adapters (`IntlDate`, `IntlName`) may be consumed by UI.
6. Keep app-state coupling narrow — prefer dependency injection; don't import feature modules into infrastructure.
## Examples
```ts
import { axiosClient } from "#infrastructure/axios";
export class UserAPI {
getDetailed(id: number) {
return axiosClient.get(`/users/${id}`).then((response) => response.data);
}
}
```
```ts
import {
createKeyBuilder,
getPaginationInfo,
} from "#infrastructure/reactQuery";
export const usersCacheKey = createKeyBuilder("USERS", "LIST");
```
@@ -0,0 +1,55 @@
# Reference Data Layer
App-wide API-backed lookup datasets reused across modules — countries, languages, currencies, time zones, common option dictionaries. The layer fetches them, exposes reusable query hooks, and owns their cache keys.
Small dataset → one file; larger → a folder:
```plaintext
refdata/
├── countries.ts
├── languages.ts
└── currencies/
├── api.ts
├── dto.ts
├── mapper.ts
├── queries.ts
└── keys.ts
```
## Rules
1. App-wide datasets only — a dataset owned by one feature belongs in that module's `data/` layer.
2. May use `#infrastructure/axios` and `#infrastructure/reactQuery`; must not import from `view/`, `pages/`, or feature modules.
3. Expose hooks or small service functions — consumers should not know endpoints or cache keys.
4. Add mappers when backend DTOs differ from app-facing data, tested like module data mappers.
## Example
```ts
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { axiosClient } from "#infrastructure/axios";
import { createKeyBuilder } from "#infrastructure/reactQuery";
export const countriesCacheKey = createKeyBuilder(
"REF_DATA",
"COUNTRIES",
"LIST",
);
interface CountryData {
code: string;
name: string;
}
export const useCountries = () => {
return useQuery({
queryKey: countriesCacheKey.store(),
placeholderData: keepPreviousData,
queryFn: async () => {
return axiosClient
.get<Array<CountryData>>("/datasets/countries")
.then((data) => data.data);
},
});
};
```
@@ -0,0 +1,56 @@
# Shared Layer
Global reusable building blocks used across the entire application — framework-level, never feature-specific.
```plaintext
shared/
| --- ui/ # primitive UI components (Button, Modal, Card)
| --- widgets/ # reusable composed UI blocks (DataGrid, screens)
| --- forms/ # reusable form composition (composed inputs)
| --- editors/ # reusable editor wrappers (tinymce, image_input)
| --- hooks/ # generic reusable hooks (useDebounce, useToggle, useLocalStorage)
| --- utils/ # pure helper functions (formatDate, generateId, sleep)
| --- lib/ # pure helpers and utility services (time/, phone_number/, resolveFileURL)
| --- assets/ # images, icons, fonts
| --- styles/ # global CSS, variables, Tailwind overrides
| --- constants/ # global constants
```
## UI Components
Simple, composable, customizable building blocks — Lego pieces that combine into larger UI:
- **Simple** — Button, Input, Card, Modal, Dropdown.
- **Composable** — complex UI is built by composition, not by modifying base components:
```tsx
<Card>
<CardHeader />
<CardContent />
<CardFooter />
</Card>
```
- **Customizable** — through `className`, variants, props, and composition:
```tsx
<Button variant="outline" size="lg">
Save
</Button>
```
- **No business logic** — presentation only (local state allowed): no API calls, React Query, form submission, or domain logic.
### Styling
Utility-first, design tokens, variants instead of style duplication, minimal centralized custom CSS.
Stack: **TailwindCSS** (primary styling), **class-variance-authority** (variants), **CSS variables** (design tokens), **Radix primitives** (accessible building blocks).
## Other Folders
- `hooks/` — generic hooks with no entity or business-logic dependencies.
- `utils/` — stateless pure helper functions.
- `lib/` — pure helpers and utility services (date, name, path, phone formatting).
- `assets/` — static resources (icons, fonts).
- `styles/` — CSS variables, Tailwind extensions, global resets.
@@ -0,0 +1,150 @@
# Views Layer
## Components
Reusable UI blocks: render UI, accept props, handle simple UI state (hover, toggle, open/close), compose other components. `UserAvatar`, `ProductBadge`.
No API calls, mutations, complex state management, or business workflows.
## Widgets
Self-contained UI blocks that implement a complete piece of functionality — panels, sections, complex interactive blocks used to compose pages. A widget combines UI, logic, and interactions: it composes multiple components, may include forms, dialogs, or actions, may fetch data via data hooks or take it from props, and exposes a simple API to pages while hiding its internals.
A widget can use another widget when the nested widget is a clear functional sub-block, the parent is grouping or orchestrating UI, and the dependency stays loose:
```ts
export const ProjectOverviewWidget = ({ projectId }: Props) => {
return (
<div>
<ProjectStatsWidget projectId={projectId} />
<ProjectActivityWidget projectId={projectId} />
<ProjectMembersWidget projectId={projectId} />
</div>
);
};
```
## Forms
### Form Architecture
```plaintext
view/forms/SomeForm/
| --- form.ts # validation schema, types, default values
| --- Component.tsx # UI + react-hook-form integration
| --- submit.ts # submission logic
| --- constants.ts # Optional: form IDs, UI constants
| --- schema.ts # Optional validation schema. Good for large or multistep forms
| --- index.ts # public exports
```
### Form Logic
`form.ts` contains pure form logic: validation schema, form types, default values, helper templates.
```ts
export const validationSchema = z.object({
first_name: z.string().min(3),
last_name: z.string().min(3),
});
```
With `i18n`, create a function that returns the schema:
```ts
function createValidationSchema(
translationFn: TFunction<"translation", undefined>, // Depend on library you use
) {
return z.object({
first_name: z.string({ message: translationFn("required") }),
last_name: z.string({ message: translationFn("required") }),
});
}
```
> [! WARNING]
> Dont include `i18n` singleton last_name: z.string().min(1, i18n.t('required'))
> It will not translate errors when language state change
Default values come from factory functions, not inline objects:
```ts
export function createFormValues(): FormValues {
return { first_name: "", last_name: "" };
}
export function toFormValues(data: SomeData): FormValues {
return {
first_name: data?.first_name ?? "",
last_name: data?.last_name ?? "",
};
}
```
Types for values and submit functions:
```ts
export type FormValues = z.infer<typeof validationSchema>;
export type SubmitFunc = (
values: FormValues,
handlers: UseFormReturn<FormValues>,
) => void;
```
### Submit Functions
A `useSubmit` hook centralizes submission: it picks create vs update by `values.id`, calls the `data/` layer mutations, invalidates list/detail queries on success, maps API validation errors onto form fields, and exposes `submit` + `isSubmitting`.
```ts
export const useSubmit = (params?: { onSuccess?: (id?: number) => void }) => {
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
const { mutate: updateMutation, isPending: isUpdatePending } = useUpdate();
const handleSubmit: SubmitFunc = (values, handlers) => {
const mutation = values.id ? updateMutation : createMutation;
mutation(values, {
onSuccess() {
invalidateList();
if (values.id) invalidateDetailed(values.id);
params?.onSuccess?.(values.id ?? undefined);
},
onError(error) {
extractValidationErrors(error).forEach((e) => {
handlers.setError(e.field as keyof typeof values, {
type: "apiError",
message: e.message,
});
});
},
});
};
return {
submit: handleSubmit,
isSubmitting: isCreatePending || isUpdatePending,
};
};
```
### Composed Form Elements
To avoid form boilerplate, wrap React Hook Form + shared UI into composed inputs. Each combines label, input, validation message, and layout, works with `FormProvider` via `useFormContext`, and can encapsulate any input type: text, switch, select, date, textarea, etc.
```tsx
<FormProvider {...form}>
<Component.FormElement
id={MAIN_FORM_ID}
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
>
<Component.FormGrid columns={2}>
<Component.GridElement colSpan={1}>
<Component.InputSwitch
name="notificationsEnabled"
required
label="Enable notifications"
/>
</Component.GridElement>
</Component.FormGrid>
</Component.FormElement>
</FormProvider>
```
@@ -0,0 +1,62 @@
# File & Folder Naming
Standard React community conventions:
| Type | Convention | Example |
| ---------------------- | -------------------------------------------- | ------------------------------------- |
| Folders | lowercase (PascalCase for component folders) | `user/`, `shared/`, `CreateUserForm/` |
| React component files | PascalCase | `UserProfile.tsx`, `Button.tsx` |
| Hook files | camelCase (`use` prefix) | `useDebounce.ts`, `useToggle.ts` |
| Utility / helper files | camelCase | `formatDate.ts`, `generateId.ts` |
| API / service files | camelCase (acronyms uppercase) | `userAPI.ts`, `productAPI.ts` |
| DTO / mapper files | camelCase (acronyms uppercase) | `userDTO.ts`, `userMapper.ts` |
| Type / interface files | camelCase | `types.ts`, `dto.ts`, `interfaces.ts` |
| Constants files | camelCase | `constants.ts`, `formConstants.ts` |
| Config / entry files | camelCase | `index.ts`, `queryClient.ts` |
Layer, entity, and infrastructure folders are lowercase; component, form, and widget folders are PascalCase, matching the component they contain:
```plaintext
src/
data/
user/
userAPI.ts
hooks.ts # or useUserQuery.ts, useCreateUserMutation.ts
userDTO.ts
userMapper.ts
view/
components/
UserAvatar.tsx
widgets/
UserProfileWidget/
index.ts
UserProfileWidget.tsx
forms/
CreateUserForm/
form.ts
Component.tsx
submit.ts
index.ts
shared/
ui/
Button.tsx
hooks/
useDebounce.ts
utils/
formatDate.ts
```
## Case-Only Rename Problem (macOS + Git)
macOS file systems are case-insensitive by default while Git is case-sensitive internally, so a rename like `api.ts` → `Api.ts` may not be detected by Git, break CI on case-sensitive Linux, or leave the repo inconsistent.
**Required: rename in two committed steps**, for files and folders alike — never by case only in one step, never relying on IDE rename alone; verify with `git status`:
```bash
git mv userApi.ts userApi-temp.ts
git commit -m "chore: temporary rename userApi.ts to userApi-temp.ts"
git mv userApi-temp.ts UserApi.ts
git commit -m "chore: rename userApi-temp.ts to UserApi.ts (case change)"
```
Optionally make Git case-sensitive for this repo with `git config core.ignorecase false` (verify with `git config core.ignorecase`). Even then, the file system stays case-insensitive and case-only renames can still misbehave — the two-step rename remains the safest approach.