TASKS-1823 WebManager module
This commit is contained in:
@@ -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>
|
||||
```
|
||||
Reference in New Issue
Block a user