TASKS-1823 WebManager module
This commit is contained in:
+15
-65
@@ -1,74 +1,24 @@
|
||||
# CRUX Documentation
|
||||
|
||||
This is documentation home for the CRUX monorepo. Use it for system-level architecture, application guides, shared package references, and core module documentation.
|
||||
|
||||
## Monorepo Map
|
||||
|
||||
| Area | Source | Documentation |
|
||||
| --------------- | -------------------------------------------------------- | -------------------------------------------- |
|
||||
| Backend API | `apps/backend/` | Core module docs and backend-specific guides |
|
||||
| Admin frontend | `apps/admin/` | [Admin app docs](./apps/admin/Readme.md) |
|
||||
| Shared packages | `packages/` | [Packages index](./packages/Readme.md) |
|
||||
| Devtools | `apps/admin/src/devtools/` | [Devtools docs](./devtools/Readme.md) |
|
||||
| Core modules | `apps/backend/app/base_modules/`, `apps/admin/src/core/` | [Core modules](#core-modules) |
|
||||
| Future apps | `apps/*` | Add an app page under `docs/apps/` |
|
||||
|
||||
## App Architecture
|
||||
|
||||
| Architecture | Scope | Documentation |
|
||||
| -------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| React frontend | Frontend apps and frontend modules under `apps/*` | [React frontend architecture](./apps/admin/architecture/Readme.md) |
|
||||
| Backend | AdonisJS backend apps and backend modules | [Backend architecture](./apps/backend/architecture/Readme.md) |
|
||||
Documentation home for the CRUX monorepo. See also [Architecture](./architecture/Readme.md), [Packages](./packages/Readme.md). App docs live with each app: [admin UI style guide](../apps/admin/ui-style-guide.md), [web](../apps/web/Readme.md).
|
||||
|
||||
## Core Modules
|
||||
|
||||
Core modules are reusable admin panel areas that may have both backend and frontend implementations.
|
||||
| Module | Backend | Frontend | Documentation |
|
||||
| ---------------- | -------------------------------------------- | -------------------------------------- | --------------------------------------------- |
|
||||
| Access Control | Admin, Role, Permission base modules | `apps/admin/src/core/AccessControl/` | [Overview](./core/access-control/Readme.md) |
|
||||
| Account Settings | Admin profile endpoints | `apps/admin/src/core/AccountSettings/` | [Overview](./core/account-settings/Readme.md) |
|
||||
| Auth | `apps/backend/app/base_modules/Auth/` | `apps/admin/src/core/Auth/` | [Overview](./core/auth/Readme.md) |
|
||||
| Devtools | Dev-only | `apps/admin/src/devtools/` | [Overview](./core/devtools/Readme.md) |
|
||||
| Fallback Screens | Frontend-only | `apps/admin/src/core/FallbackScreens/` | [Overview](./core/fallback-screens/Readme.md) |
|
||||
| File Manager | `apps/backend/app/base_modules/FileManager/` | `apps/admin/src/core/FileManager/` | [Overview](./core/file-manager/Readme.md) |
|
||||
| Permission | Permission service and policies | `apps/admin/src/core/Permission/` | [Overview](./core/permission/Readme.md) |
|
||||
| Web Manager | `apps/backend/app/base_modules/WebManager/` | `apps/admin/src/core/WebStudio/` | [Overview](./core/web-manager/Readme.md) |
|
||||
|
||||
| Module | Backend | Frontend | Documentation |
|
||||
| ---------------- | -------------------------------------------- | -------------------------------------- | ----------------------------------------------------- |
|
||||
| Access Control | Admin, Role, Permission base modules | `apps/admin/src/core/AccessControl/` | [Overview](./core-modules/access-control/Readme.md) |
|
||||
| Account Settings | Admin profile endpoints | `apps/admin/src/core/AccountSettings/` | [Overview](./core-modules/account-settings/Readme.md) |
|
||||
| Auth | `apps/backend/app/base_modules/Auth/` | `apps/admin/src/core/Auth/` | [Overview](./core-modules/auth/Readme.md) |
|
||||
| Fallback Screens | Frontend-only | `apps/admin/src/core/FallbackScreens/` | [Overview](./core-modules/fallback-screens/Readme.md) |
|
||||
| File Manager | `apps/backend/app/base_modules/FileManager/` | `apps/admin/src/core/FileManager/` | [Overview](./core-modules/file-manager/Readme.md) |
|
||||
| Permission | Permission service and policies | `apps/admin/src/core/Permission/` | [Overview](./core-modules/permission/Readme.md) |
|
||||
## Project Modules
|
||||
|
||||
Add a new page when the information has a stable owner. Keep temporary notes in issue trackers or task files until they are worth maintaining.
|
||||
Project-specific modules: [docs/modules/](./modules/Readme.md).
|
||||
|
||||
## How To Write Module Docs
|
||||
## Shared Packages
|
||||
|
||||
Each module should answer these questions:
|
||||
|
||||
1. What user or system problem does the module solve?
|
||||
2. Where is the backend code?
|
||||
3. Where is the frontend code?
|
||||
4. What are the main flows from route to service to UI?
|
||||
5. What data contracts cross app boundaries?
|
||||
6. Which files should a developer edit for common changes?
|
||||
7. What extension points are supported?
|
||||
8. What behavior should not be changed without a migration plan?
|
||||
|
||||
Prefer short sections, source paths, and concrete examples over long prose.
|
||||
|
||||
## Documentation Rules
|
||||
|
||||
- Use `Readme.md` as the index for each folder.
|
||||
- Use relative links between documentation pages.
|
||||
- Reference source files with repository-relative paths.
|
||||
- Keep backend and frontend details separate when their ownership differs.
|
||||
- Document current behavior first, then list planned work under a `Planned Work` section.
|
||||
- Update docs in the same change when modifying routes, DTOs, permissions, storage contracts, package APIs, or module state ownership.
|
||||
|
||||
## Local Development
|
||||
|
||||
Common root scripts:
|
||||
|
||||
| Command | Purpose |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| `npm run dev:backend` | Start the AdonisJS backend. |
|
||||
| `npm run dev:admin-app` | Start the React admin app. |
|
||||
| `npm run dev:all` | Start backend and admin app together. |
|
||||
| `npm run build:backend` | Build the backend. |
|
||||
| `npm run build:admin` | Build the admin app. |
|
||||
|
||||
For product overview, setup details, and environment variables, see the project root `Readme.md`.
|
||||
Package docs live with the package source; see the [Packages index](./packages/Readme.md).
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Admin App Documentation
|
||||
|
||||
The admin app is a reusable React admin panel template.
|
||||
It includes the app shell, layouts, shared infrastructure,
|
||||
built-in core modules, development tools, and generated project modules.
|
||||
|
||||
## Architecture Direction
|
||||
|
||||
`apps/admin` follows the general React architecture described in [React Frontend Architecture](./architecture/Readme.md).
|
||||
@@ -1,83 +0,0 @@
|
||||
# App Layer
|
||||
|
||||
## Purpose
|
||||
|
||||
The `app` layer is the composition root of the Admin application. It owns the concerns
|
||||
that exist once for the whole app and coordinates lower-level layers and feature
|
||||
modules without containing feature business logic.
|
||||
|
||||
Global React providers belong inside `app/providers/`. They are an implementation
|
||||
detail of the app layer, not a separate architectural layer.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Compose global React providers in one root component.
|
||||
- Configure application-wide settings and environment-derived values.
|
||||
- Initialize internationalization and global styles.
|
||||
- Define top-level routing and connect routes registered by modules.
|
||||
- Provide application layouts, navigation, and menu composition.
|
||||
- Register core, project, and development modules with the app shell.
|
||||
|
||||
## Current Structure
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
The exact files may evolve, but their ownership should remain app-wide. A concern
|
||||
used by only one feature belongs to that feature module instead.
|
||||
|
||||
## Global Providers
|
||||
|
||||
`app/providers/AppProviders.tsx` is the single provider composition point. The
|
||||
current provider tree supplies:
|
||||
|
||||
- TanStack Query configuration and error handling
|
||||
- authentication context from the Auth core module
|
||||
- application settings
|
||||
- shared tooltip and notification UI
|
||||
- theme state
|
||||
- internationalization and text direction
|
||||
- React Router with the configured base URL
|
||||
|
||||
## Routing
|
||||
|
||||
`app/routing/` owns top-level route composition. It connects the application shell
|
||||
to routes contributed by registered modules. Feature pages and route-specific logic
|
||||
remain inside their owning modules.
|
||||
|
||||
## Configuration and Internationalization
|
||||
|
||||
- `app/config/` owns global settings, application metadata, and configuration types.
|
||||
- `app/i18n/` owns supported languages, translated messages, and i18n configuration.
|
||||
- Environment access should be centralized through app configuration instead of
|
||||
being repeated throughout feature code.
|
||||
|
||||
## Layouts and Global CSS
|
||||
|
||||
`app/layouts/` contains the Admin shell variants. `app/css/` contains global styles,
|
||||
layout styles, and app-wide overrides for third-party components. Feature-specific
|
||||
styles should stay with the feature that owns them.
|
||||
|
||||
## Module Registration
|
||||
|
||||
`app/services/modules/` integrates modules into the application through their
|
||||
routes, menus, and translated messages. It is integration code owned by the app
|
||||
shell; module implementation details remain under `core/`, `modules/`, or
|
||||
`devtools/`.
|
||||
@@ -1,260 +0,0 @@
|
||||
# Data Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Data Layer](#data-layer)
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Data Layer Organization Approaches](#data-layer-organization-approaches)
|
||||
- [By Module](#by-module)
|
||||
- [By Entity](#by-entity)
|
||||
- [Data Flow](#data-flow)
|
||||
- [DTO Types](#1-dto-types)
|
||||
- [API Layer](#2-api-layer)
|
||||
- [Mapper](#3-mapper)
|
||||
- [Mapper Testing](#mapper-testing)
|
||||
- [Query Hooks](#4-query-hooks)
|
||||
- [Pagination](#pagination)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `data` layer is responsible for **all data access and data transformation** in the application.
|
||||
|
||||
It is the only layer that knows:
|
||||
|
||||
- how to call APIs
|
||||
- how to map API responses (DTOs) to application entities
|
||||
- how to handle caching, queries, and mutations
|
||||
|
||||
Higher layers (`forms`, `widgets`, `pages`) **do not interact with APIs directly**.
|
||||
Instead, they consume **hooks provided by the data layer**.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `data/` layer is responsible for:
|
||||
|
||||
- performing **API calls**
|
||||
- defining **DTO types (Data Transfer Objects)**
|
||||
- converting DTOs → **entities** using **mappers**
|
||||
- providing **React Query hooks**
|
||||
- handling **side effects** (requests, caching, errors)
|
||||
- defining **API-related types**
|
||||
|
||||
## Data Layer Organization Approaches
|
||||
|
||||
```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
|
||||
|
|
||||
| --- pagination/
|
||||
| | --- dto.ts
|
||||
| | --- mapper.ts
|
||||
| | --- types.ts
|
||||
```
|
||||
|
||||
There are two main ways to structure the `data` layer:
|
||||
|
||||
- By Module — grouped by type of file (API, hooks, DTOs, mappers, types)
|
||||
- By Entity — grouped by business feature/entity (user, product, message)
|
||||
|
||||
| Approach | Best For | Pros | Cons |
|
||||
| --------- | ------------------------------- | -------------------------------------------- | ---------------------------------------------- |
|
||||
| By Module | Small apps, simple data layers | Easy to find all API/hooks/DTOs in one place | Files for one feature scattered; hard to scale |
|
||||
| By Entity | Medium/large apps, scalable CRM | Encapsulated, modular, easier to maintain | Harder to see all APIs/hooks globally |
|
||||
|
||||
### By Module
|
||||
|
||||
```plaintext
|
||||
data/
|
||||
| --- api/ # API request functions
|
||||
| | --- userAPI.ts
|
||||
| | --- productAPI.ts
|
||||
| | --- messageAPI.ts
|
||||
|
|
||||
| --- hooks/ # React Query hooks or other data hooks
|
||||
| | --- useUserQuery.ts
|
||||
| | --- useCreateProductMutation.ts
|
||||
| | --- useMessages.ts
|
||||
|
|
||||
| --- dto/ # API DTO types
|
||||
| | --- userDTO.ts
|
||||
| | --- productDTO.ts
|
||||
| | --- messageDTO.ts
|
||||
|
|
||||
├ --- mappers/ # DTO → entity mappers
|
||||
| | --- userMapper.ts
|
||||
| | --- productMapper.ts
|
||||
|
|
||||
| --- types/ # API types/interfaces
|
||||
```
|
||||
|
||||
### By Entity
|
||||
|
||||
```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)
|
||||
|
|
||||
| --- product/
|
||||
| | --- api.ts
|
||||
| | --- hooks.ts
|
||||
| | --- dto.ts
|
||||
| | --- mapper.ts
|
||||
| | --- types.ts
|
||||
```
|
||||
|
||||
## 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. API always returns DTO objects.
|
||||
|
||||
```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 should have unit tests.
|
||||
|
||||
Mapper tests are required when a mapper:
|
||||
|
||||
- renames fields from API DTOs to app entities
|
||||
- normalizes values, paths, dates, sizes, statuses, enums, or defaults
|
||||
- maps nested DTOs through other mappers
|
||||
- maps arrays, paginated data, or optional API fields
|
||||
- protects the app from backend response shape changes
|
||||
|
||||
These tests should focus only on transformation behavior. Do not mock React, render components, or call real APIs in mapper tests.
|
||||
|
||||
Recommended test coverage:
|
||||
|
||||
- `toEntity` maps a representative DTO to the expected entity
|
||||
- `toEntities` maps arrays consistently
|
||||
- optional or missing DTO fields use the correct defaults
|
||||
- nested mappers preserve the expected entity shape
|
||||
- enum/status casts are covered with expected values
|
||||
|
||||
For apps that keep tests outside production source, use a test folder at the app root:
|
||||
|
||||
```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**, so it should live 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
# Entities
|
||||
|
||||
## Purpose
|
||||
|
||||
The `entities` layer defines the **core domain models of the application**.
|
||||
|
||||
Entities represent **business objects** used throughout the system, such as `User`, `Product`, `Project`, or `Message`.
|
||||
|
||||
This layer contains **pure type definitions** that describe how data is structured inside the application.
|
||||
Entities are **stable, reusable models** that can be used across any layer.
|
||||
|
||||
```plaintext
|
||||
entities/
|
||||
| --- user.ts # { id: string; name: string; email: string }
|
||||
| --- product.ts # { id: string; name: string; price: number }
|
||||
| --- message.ts # { id: string; content: string; createdAt: string }
|
||||
| --- enums.ts # { UserRole, ProductCategory }
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```plaintext
|
||||
entities/
|
||||
| --- user/
|
||||
| | --- interfaces.ts # { id: string; name: string; email: string }
|
||||
| | --- types.ts # { id: string; name: string; email: string }
|
||||
| | --- enums.ts # { UserRole }
|
||||
| | --- rules.ts # MIN_ALLOWED_AGE = 18
|
||||
| | --- index.ts # export all entity types
|
||||
| --- product.ts # { id: string; name: string; price: number }
|
||||
| --- message.ts # { id: string; content: string; createdAt: string }
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The entities layer is responsible for:
|
||||
|
||||
- defining **domain models**
|
||||
- defining **entity-related enums or constants**
|
||||
- providing **shared types used across the application**
|
||||
|
||||
```ts
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export enum UserRole {
|
||||
ADMIN = "admin",
|
||||
SIMLPE_JACK = "simple_jack",
|
||||
}
|
||||
```
|
||||
@@ -1,106 +0,0 @@
|
||||
# Infrastructure Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [Rules](#rules)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `infrastructure/` layer contains app-wide technical adapters and integration helpers.
|
||||
|
||||
Use this layer for code that connects the frontend application to technical systems or shared runtime services, not for feature-specific business logic.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `infrastructure/` layer is responsible for:
|
||||
|
||||
- HTTP clients and request/response interceptors
|
||||
- React Query shared helpers, response types, query key helpers, and error normalization
|
||||
- storage, browser, or runtime adapters
|
||||
- shared formatting/service adapters such as date or name formatting services
|
||||
- app-wide technical configuration wrappers
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
```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. Keep infrastructure feature-agnostic.
|
||||
|
||||
- No module-specific business rules
|
||||
- No page or component composition
|
||||
- No entity mapping that belongs in a module `data/mapper`
|
||||
|
||||
2. API modules should use infrastructure clients instead of creating their own clients.
|
||||
|
||||
```ts
|
||||
import { axiosClient } from "#infrastructure/axios";
|
||||
```
|
||||
|
||||
3. React Query helpers that are shared across modules belong here.
|
||||
|
||||
Examples: query key builders, list response types, pagination helpers, error extraction helpers.
|
||||
|
||||
4. UI layers must not call raw HTTP clients directly.
|
||||
|
||||
View and page code should call data-layer hooks. Direct `#infrastructure/axios` usage belongs in API/data modules, not in components.
|
||||
|
||||
5. Generic formatting adapters may be consumed by UI when they are not tied to a feature.
|
||||
|
||||
Examples: `IntlDate`, `IntlName`.
|
||||
|
||||
6. Prefer dependency injection for app-state coupling.
|
||||
|
||||
If an interceptor needs auth or query-client behavior, keep that coupling narrow and documented. Avoid importing random feature modules into infrastructure.
|
||||
|
||||
## Examples
|
||||
|
||||
API modules use the configured HTTP client:
|
||||
|
||||
```ts
|
||||
import { axiosClient } from "#infrastructure/axios";
|
||||
|
||||
export class UserAPI {
|
||||
getDetailed(id: number) {
|
||||
return axiosClient.get(`/users/${id}`).then((response) => response.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Data/query modules use shared query helpers:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createKeyBuilder,
|
||||
getPaginationInfo,
|
||||
} from "#infrastructure/reactQuery";
|
||||
|
||||
export const usersCacheKey = createKeyBuilder("USERS", "LIST");
|
||||
```
|
||||
|
||||
UI can consume generic app-wide formatters:
|
||||
|
||||
```ts
|
||||
import { IntlDate } from "#infrastructure/IntlDate";
|
||||
```
|
||||
@@ -1,109 +0,0 @@
|
||||
# Reference Data Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [Rules](#rules)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `refdata/` layer contains app-wide reference datasets that are reused across modules.
|
||||
|
||||
Reference data is data that usually comes from the backend, but is not owned by one feature module.
|
||||
|
||||
Examples:
|
||||
|
||||
- countries
|
||||
- languages
|
||||
- currencies
|
||||
- time zones
|
||||
- common option dictionaries
|
||||
- shared lookup datasets
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `refdata/` layer is responsible for:
|
||||
|
||||
- fetching common reference datasets
|
||||
- exposing reusable query hooks for those datasets
|
||||
- defining cache keys for reference data
|
||||
- keeping app-wide lookup data out of feature modules
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
For small datasets, a single file per dataset is enough:
|
||||
|
||||
```plaintext
|
||||
refdata/
|
||||
├── countries.ts
|
||||
├── languages.ts
|
||||
└── currencies.ts
|
||||
```
|
||||
|
||||
For larger datasets, use a folder:
|
||||
|
||||
```plaintext
|
||||
refdata/
|
||||
└── countries/
|
||||
├── api.ts
|
||||
├── dto.ts
|
||||
├── mapper.ts
|
||||
├── queries.ts
|
||||
└── keys.ts
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. Use `refdata/` only for app-wide datasets.
|
||||
|
||||
If a dataset belongs to one feature module, keep it in that module's `data/` layer.
|
||||
|
||||
2. `refdata/` may use infrastructure.
|
||||
|
||||
Reference data hooks can use `#infrastructure/axios` and `#infrastructure/reactQuery`.
|
||||
|
||||
3. `refdata/` should not import from `view/`, `pages/`, or feature modules.
|
||||
|
||||
It should remain reusable across the app.
|
||||
|
||||
4. Expose hooks or small service functions.
|
||||
|
||||
Consumers should not need to know backend endpoints or cache key details.
|
||||
|
||||
5. Add mappers when backend DTOs differ from app-facing reference data.
|
||||
|
||||
If a reference dataset performs DTO transformation, test the mapper the same way module data mappers are tested.
|
||||
|
||||
## Examples
|
||||
|
||||
```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);
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
@@ -1,252 +0,0 @@
|
||||
# Shared Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [UI Components](#ui-components)
|
||||
- [Purpose](#ui-components-purpose)
|
||||
- [Core Principles](#ui-components-core-principles)
|
||||
1. [Simple Components](#ui-components-core-principles-1)
|
||||
2. [Composable Design](#ui-components-core-principles-2)
|
||||
3. [Easily Customizable](#ui-components-core-principles-3)
|
||||
4. [No Business Logic](#ui-components-core-principles-4)
|
||||
- [Styling Strategy](#ui-components-styling-strategy)
|
||||
- [Styling Philosophy](#ui-components-styling-strategy-philosophy)
|
||||
- [Technology Stack](#ui-components-styling-strategy-tech-stack)
|
||||
- [Hooks](#hooks)
|
||||
- [Utilities](#utilities)
|
||||
- [Libraries](#libraries)
|
||||
- [Assets](#assets)
|
||||
- [Styles](#styles)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `shared` layer contains **global reusable building blocks used across the entire application**.
|
||||
The goal is to keep these utilities **framework-level and reusable across the entire application**.
|
||||
|
||||
```plaintext
|
||||
shared/
|
||||
├── ui/ # dumb or basic UI components
|
||||
│ ├── Button.tsx
|
||||
│ ├── Modal.tsx
|
||||
│ └── Card.tsx
|
||||
│
|
||||
├── widgets/ # reusable composed UI blocks
|
||||
│ ├── DataGrid/
|
||||
│ └── screens/
|
||||
│
|
||||
├── forms/ # reusable form composition helpers
|
||||
│ └── composed/
|
||||
│
|
||||
├── editors/ # reusable editor integrations
|
||||
│ ├── tinymce/
|
||||
│ └── image_input/
|
||||
│
|
||||
├── hooks/ # reusable hooks
|
||||
│ ├── useDebounce.ts
|
||||
│ ├── useToggle.ts
|
||||
│ └── useLocalStorage.ts
|
||||
│
|
||||
├── lib/ # pure helpers & utilities
|
||||
│ └── formatDate.ts
|
||||
│
|
||||
├── assets/ # images, icons, fonts
|
||||
│ ├── icons/
|
||||
│ └── fonts/
|
||||
│
|
||||
└── styles/ # global CSS, variables, Tailwind overrides
|
||||
├── globals.css
|
||||
└── variables.css
|
||||
```
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
```plaintext
|
||||
shared/
|
||||
|
|
||||
| --- ui/ # primitive UI components
|
||||
|
|
||||
| --- widgets/ # reusable composed UI blocks
|
||||
|
|
||||
| --- forms/ # reusable form composition
|
||||
|
|
||||
| --- editors/ # reusable editor wrappers
|
||||
|
|
||||
| --- hooks/ # generic reusable hooks
|
||||
|
|
||||
| --- utils/ # pure helper functions
|
||||
|
|
||||
| --- lib/ # pure helpers and utility services
|
||||
|
|
||||
| --- assets/ # images, icons, fonts
|
||||
|
|
||||
| --- styles/ # global styling
|
||||
|
|
||||
| --- constants/ # global constants
|
||||
```
|
||||
|
||||
### UI Components
|
||||
|
||||
#### Purpose {#ui-components-purpose}
|
||||
|
||||
The goal of the UI layer is to provide simple, composable, and customizable components that can be reused across the application.
|
||||
|
||||
- components are simple building blocks
|
||||
- components are composable
|
||||
- components are fully customizable
|
||||
- components are not tied to business logic
|
||||
|
||||
UI components should behave like Lego blocks that can be combined into larger UI structures.
|
||||
|
||||
#### Core Principles {#ui-components-core-principles}
|
||||
|
||||
##### 1. Simple Components {#ui-components-core-principles-1}
|
||||
|
||||
Examples:
|
||||
|
||||
- Button
|
||||
- Input
|
||||
- Card
|
||||
- Modal
|
||||
- Dropdown
|
||||
|
||||
##### 2. Composable Design {#ui-components-core-principles-2}
|
||||
|
||||
Components must support composition.
|
||||
Composition allows developers to build complex UI without modifying base components.
|
||||
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
<Card>
|
||||
<CardHeader />
|
||||
<CardContent />
|
||||
<CardFooter />
|
||||
</Card>
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<Icon />
|
||||
Save
|
||||
</Button>
|
||||
```
|
||||
|
||||
##### 3. Easily Customizable {#ui-components-core-principles-3}
|
||||
|
||||
UI components should allow customization through:
|
||||
|
||||
- className
|
||||
- variants
|
||||
- props
|
||||
- composition
|
||||
|
||||
```tsx
|
||||
<Button variant="outline" size="lg">
|
||||
Save
|
||||
</Button>
|
||||
```
|
||||
|
||||
##### 4. No Business Logic {#ui-components-core-principles-4}
|
||||
|
||||
UI components must remain presentation-only. May include local state.
|
||||
|
||||
They should NOT:
|
||||
|
||||
- call API
|
||||
- use React Query
|
||||
- contain form submission logic
|
||||
- contain domain-specific logic
|
||||
|
||||
#### Styling Strategy {#ui-components-styling-strategy}
|
||||
|
||||
##### Styling Philosophy {#ui-components-styling-strategy-philosophy}
|
||||
|
||||
Styling follows these principles:
|
||||
|
||||
- Utility-first styling
|
||||
- Design tokens for consistency
|
||||
- Component-level styling
|
||||
- Variants instead of style duplication
|
||||
- Minimal custom CSS
|
||||
|
||||
##### Technology Stack {#ui-components-styling-strategy-tech-stack}
|
||||
|
||||
- **TailwindCSS** - primary styling system
|
||||
- **class-variance-authority (CVA)** - component variants
|
||||
- **CSS variables** - design tokens
|
||||
- **Radix primitives** - accessible UI building blocks
|
||||
|
||||
Custom CSS should be minimal and centralized.
|
||||
|
||||
### Hooks
|
||||
|
||||
The `shared/hooks/` folder contains generic reusable hooks.
|
||||
These hooks must not depend on entities or business logic.
|
||||
|
||||
```plaintext
|
||||
shared/hooks/
|
||||
| --- useDebounce.ts
|
||||
| --- useToggle.ts
|
||||
| --- usePrevious.ts
|
||||
| --- useLocalStorage.ts
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
The `shared/utils/` folder contains pure helper functions.
|
||||
These functions should be stateless and reusable
|
||||
|
||||
```plaintext
|
||||
shared/utils/
|
||||
| --- formatDate.ts
|
||||
| --- formatCurrency.ts
|
||||
| --- generateId.ts
|
||||
| --- sleep.ts
|
||||
```
|
||||
|
||||
### Libraries
|
||||
|
||||
The `shared/lib/` folder contains pure helpers and utility services.
|
||||
|
||||
Examples include:
|
||||
|
||||
- date formatting helpers
|
||||
- name formatting helpers
|
||||
- URL/file path helpers
|
||||
- phone-number formatting helpers
|
||||
|
||||
```plaintext
|
||||
shared/lib/
|
||||
| --- time/
|
||||
| --- phone_number/
|
||||
| --- resolveFileURL.ts
|
||||
```
|
||||
|
||||
### Assets
|
||||
|
||||
The `shared/assets/` folder stores static resources used across the application.
|
||||
|
||||
```plaintext
|
||||
shared/assets/
|
||||
| --- icons/
|
||||
| --- fonts/
|
||||
```
|
||||
|
||||
### Styles
|
||||
|
||||
The `shared/styles/` folder contains global styling resources.
|
||||
|
||||
Typical usage:
|
||||
|
||||
- CSS variables
|
||||
- Tailwind configuration extensions
|
||||
- global resets
|
||||
|
||||
```plaintext
|
||||
shared/styles/
|
||||
| --- globals.css
|
||||
| --- variables.css
|
||||
| --- tailwind.css
|
||||
```
|
||||
@@ -1,345 +0,0 @@
|
||||
# Views Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Views Layer](#views-layer)
|
||||
- [Components](#components)
|
||||
- [Components Purpose](#components-purpose)
|
||||
- [Components Responsibilities](#components-responsibilities)
|
||||
- [What Components should NOT do](#what-components-should-not-do)
|
||||
- [Widgets](#widgets)
|
||||
- [Widget Purpose](#widget-purpose)
|
||||
- [Widget Characteristics](#widget-characteristics)
|
||||
- [Widget Responsibilities](#widget-responsibilities)
|
||||
- [Forms](#forms)
|
||||
- [Form Architecture](#form-architecture)
|
||||
- [Form Logic](#form-logic)
|
||||
- [Form Validation](#form-validation)
|
||||
- [Form Values](#form-values)
|
||||
- [Form Types](#form-types)
|
||||
- [Submit Functions](#submit-functions)
|
||||
- [Purpose](#submit-functions-purpose)
|
||||
- [API Example](#api-example)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Composed Form Elements](#composed-form-elements)
|
||||
- [Example: InputSwitch](#example-inputswitch)
|
||||
- [Usage in a Form](#usage-in-a-form)
|
||||
|
||||
## Components
|
||||
|
||||
### Components Purpose
|
||||
|
||||
Components are reusable UI blocks used to build user interfaces.
|
||||
|
||||
They encapsulate presentation and simple UI behavior, but do not contain business logic or data fetching. `UserAvatar`, `ProductBadge`
|
||||
|
||||
Components are designed to be small, composable, and reusable across multiple parts of the application.
|
||||
|
||||
### Components Responsibilities:
|
||||
|
||||
- render UI
|
||||
- accept props
|
||||
- handle simple UI state (hover, toggle, open/close)
|
||||
- compose other components
|
||||
|
||||
### What **Components** should NOT do
|
||||
|
||||
Components should not contain logic such as:
|
||||
|
||||
- API calls
|
||||
- mutations
|
||||
- complex state management
|
||||
- business workflows
|
||||
|
||||
## Widgets
|
||||
|
||||
### Widget Purpose
|
||||
|
||||
Widgets are **self-contained UI blocks that implement a specific piece of functionality**.
|
||||
|
||||
Unlike simple components, widgets combine **UI, logic, and interactions** to deliver a complete user-facing feature.
|
||||
|
||||
Widgets are typically used to **compose pages** and represent meaningful parts of the interface such as panels, sections, or complex interactive blocks. Widgets should remain modular building blocks.
|
||||
|
||||
### Widget Characteristics
|
||||
|
||||
A widget usually:
|
||||
|
||||
- encapsulates **UI and feature logic**
|
||||
- composes **multiple components**
|
||||
- may include **forms, dialogs, or actions**
|
||||
- can interact with **data**
|
||||
- exposes a **simple API for pages**
|
||||
|
||||
Widgets should hide their internal implementation and provide a **clean interface** to the outside.
|
||||
|
||||
### Widget Responsibilities
|
||||
|
||||
Widgets may:
|
||||
|
||||
- orchestrate UI interactions
|
||||
- manage local UI state
|
||||
- connect to features or data hooks
|
||||
- combine multiple components/forms into a functional block
|
||||
- may fetch data or rely on data passed in props
|
||||
- A **widget** can use another **widget** if:
|
||||
- the nested widget represents a clear functional sub-block
|
||||
- the parent widget is grouping or orchestrating UI
|
||||
- the dependency does not create tight coupling
|
||||
|
||||
```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
|
||||
| --- Component.tsx
|
||||
| --- submit.ts
|
||||
| --- constants.ts # Optional
|
||||
| --- schema.ts # Optional validation schema. Good for large forms or multistep forms
|
||||
| --- index.ts
|
||||
```
|
||||
|
||||
| File | Responsibility |
|
||||
| --------------- | ---------------------------------- |
|
||||
| `form.ts` | validation schema, types, defaults |
|
||||
| `Component.tsx` | UI + react-hook-form integration |
|
||||
| `submit.ts` | Submission logic |
|
||||
| `constants.ts` | form IDs, UI constants |
|
||||
| `index.ts` | public exports |
|
||||
|
||||
### Form Logic
|
||||
|
||||
`form.ts` contains pure form logic.
|
||||
|
||||
It should include:
|
||||
|
||||
- validation schema
|
||||
- form types
|
||||
- default values
|
||||
- helper templates
|
||||
|
||||
#### Form Validation
|
||||
|
||||
```ts
|
||||
export const validationSchema = z.object({
|
||||
first_name: z.string().min(3),
|
||||
last_name: z.string().min(3),
|
||||
});
|
||||
```
|
||||
|
||||
If you use `i18n` you can create function that returns validation schema.
|
||||
|
||||
```ts
|
||||
function createValidationSchema(
|
||||
translationFn: TFunction<"translation", undefined>, // Depend on libraty 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
|
||||
|
||||
#### Form Values
|
||||
|
||||
Default values should be defined using factory functions.
|
||||
Avoid inline default objects.
|
||||
|
||||
```ts
|
||||
export function createFormValues(): FormValues {
|
||||
return {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export function toFormValues(data: SomeData): FormValues {
|
||||
return {
|
||||
first_name: data?.first_name ?? "",
|
||||
last_name: data?.last_name ?? "",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Form Types
|
||||
|
||||
Define types for form values and submit functiuons
|
||||
|
||||
```ts
|
||||
export type FormValues = z.infer<typeof validationSchema>;
|
||||
export type SubmitFunc = (
|
||||
values: FormValues,
|
||||
handlers: UseFormReturn<FormValues>,
|
||||
) => void;
|
||||
```
|
||||
|
||||
#### Submit Functions
|
||||
|
||||
##### Purpose {#submit-functions-purpose}
|
||||
|
||||
The `useSubmit` hook standardizes form submission logic:
|
||||
|
||||
- Handles create and update operations
|
||||
- Integrates with `data/` layer hooks (`useCreate`, `useUpdate`)
|
||||
- Automatically handles validation errors from the API
|
||||
- Invalidates lists or detailed queries to refresh UI
|
||||
- Exposes a unified API for forms
|
||||
|
||||
This pattern allows keep submission logic centralized.
|
||||
|
||||
##### API Example
|
||||
|
||||
```ts
|
||||
interface Params {
|
||||
onSuccess?: (id?: number) => void;
|
||||
}
|
||||
export const useSubmit = (params?: Params) => {
|
||||
const onSuccess = params?.onSuccess;
|
||||
|
||||
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
|
||||
const { mutate: updateMutation, isPending: isUpdateending } = useUpdate();
|
||||
|
||||
const handleSubmit: SubmitFunc = (values, handlers) => {
|
||||
if (values.id) {
|
||||
handleUpdate(values, handlers);
|
||||
} else {
|
||||
handleCreate(values, handlers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate: SubmitFunc = (values, handlers) => {
|
||||
createMutation(values, {
|
||||
onSuccess() {
|
||||
invalidateList();
|
||||
if (typeof onSuccess === "function") onSuccess();
|
||||
},
|
||||
onError(error) {
|
||||
extractValidationErrors(error).forEach((e) => {
|
||||
handlers.setError(e.field as keyof typeof values, {
|
||||
type: "apiError",
|
||||
message: e.message,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate: SubmitFunc = (values, handlers) => {
|
||||
if (!values.id) return;
|
||||
|
||||
updateMutation(
|
||||
{ ...values, id: values.id },
|
||||
{
|
||||
onSuccess() {
|
||||
invalidateList();
|
||||
if (values.id) invalidateDetailed(values.id);
|
||||
if (typeof onSuccess === "function") {
|
||||
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 || isUpdateending,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
##### Responsibilities
|
||||
|
||||
1. Determine operation type
|
||||
- If values.id exists -> update
|
||||
- Otherwise -> create
|
||||
2. Call the appropriate mutation
|
||||
- `createMutation` or `updateMutation` from `data` layer
|
||||
- Hooks handle API calls internally
|
||||
3. Handle success
|
||||
- Invalidate queries (list/detail)
|
||||
- Trigger optional `onSuccess` callback
|
||||
4. Handle errors
|
||||
- Extract validation errors from API
|
||||
- Set errors on form fields using `handlers.setError`
|
||||
5. Expose state
|
||||
- `submit` -> function to call from form
|
||||
- `isSubmitting` -> combines pending state from both mutations
|
||||
|
||||
#### Composed Form Elements
|
||||
|
||||
To avoid repetitive boilerplate code when building forms, we encourages creating composed form elements that integrate React Hook Form with shared UI components.
|
||||
|
||||
- Combines label, input, validation message, and layout in one reusable component.
|
||||
- Can encapsulate any input type: text, switch, select, date, textarea, etc.
|
||||
- Works seamlessly with FormProvider for context propagation.
|
||||
|
||||
##### Example: InputSwitch
|
||||
|
||||
InputSwitch wraps a Switch component into a reusable form field:
|
||||
|
||||
```tsx
|
||||
<InputSwitch
|
||||
name="notificationsEnabled"
|
||||
label="Enable notifications"
|
||||
description="Toggle notifications on/off"
|
||||
required
|
||||
/>
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- Uses `useFormContext` to access form control.
|
||||
- Wraps `FormField`, `FormItem`, `FormLabel`, `FormMessage`, and `FormControl`.
|
||||
- Handles required and disabled states.
|
||||
- This allows you to reuse the same input type across forms without writing repetitive boilerplate.
|
||||
|
||||
##### Usage in a Form
|
||||
|
||||
```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>
|
||||
```
|
||||
@@ -1,251 +0,0 @@
|
||||
# File & Folder Naming
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Naming Convention](#naming-convention)
|
||||
- [Folders](#folders)
|
||||
- [Component Files](#component-files)
|
||||
- [Hook Files](#hook-files)
|
||||
- [Utility & Helper Files](#utility--helper-files)
|
||||
- [Other TypeScript Files](#other-typescript-files)
|
||||
- [Examples by Layer](#examples-by-layer)
|
||||
- [Case-Only Rename Problem (macOS + Git)](#case-only-rename-problem-macos--git)
|
||||
- [Required: Correct Rename Procedure](#required-correct-rename-procedure)
|
||||
- [Making Git Case-Sensitive on macOS](#making-git-case-sensitive-on-macos)
|
||||
- [Important Reality Check](#important-reality-check)
|
||||
|
||||
## Naming Convention
|
||||
|
||||
We follow **standard React community naming 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` |
|
||||
|
||||
### Folders
|
||||
|
||||
Layer and infrastructure folders use **lowercase**.
|
||||
Component, form, and widget folders use **PascalCase** — matching the component they contain.
|
||||
|
||||
```
|
||||
src/
|
||||
data/ # lowercase — layer folder
|
||||
user/ # lowercase — entity folder
|
||||
product/
|
||||
view/ # lowercase — layer folder
|
||||
components/
|
||||
forms/
|
||||
CreateUserForm/ # PascalCase — component folder
|
||||
widgets/
|
||||
UserProfileWidget/
|
||||
shared/
|
||||
ui/
|
||||
hooks/
|
||||
lib/
|
||||
```
|
||||
|
||||
### Component Files
|
||||
|
||||
React component files use **PascalCase** — matching the name of the exported component.
|
||||
|
||||
```
|
||||
UserProfile.tsx
|
||||
ProductCard.tsx
|
||||
LoginButton.tsx
|
||||
UserProfileWidget.tsx
|
||||
CreateProductForm.tsx
|
||||
```
|
||||
|
||||
### Hook Files
|
||||
|
||||
Hook files use **camelCase** and always start with `use`.
|
||||
|
||||
```
|
||||
useDebounce.ts
|
||||
useToggle.ts
|
||||
useLocalStorage.ts
|
||||
useUserQuery.ts
|
||||
useCreateProductMutation.ts
|
||||
```
|
||||
|
||||
### Utility & Helper Files
|
||||
|
||||
Utility and helper files use **camelCase**.
|
||||
|
||||
```
|
||||
formatDate.ts
|
||||
formatCurrency.ts
|
||||
generateId.ts
|
||||
sleep.ts
|
||||
```
|
||||
|
||||
### Other TypeScript Files
|
||||
|
||||
All other `.ts` files (APIs, DTOs, mappers, types, config) use **camelCase**, with acronyms kept uppercase.
|
||||
|
||||
```
|
||||
userAPI.ts
|
||||
userDTO.ts
|
||||
userMapper.ts
|
||||
types.ts
|
||||
constants.ts
|
||||
queryClient.ts
|
||||
axiosInstance.ts
|
||||
```
|
||||
|
||||
### Examples by Layer
|
||||
|
||||
#### `shared/`
|
||||
|
||||
```plaintext
|
||||
shared/
|
||||
├── ui/
|
||||
│ ├── Button.tsx
|
||||
│ ├── Modal.tsx
|
||||
│ └── Card.tsx
|
||||
│
|
||||
├── hooks/
|
||||
│ ├── useDebounce.ts
|
||||
│ ├── useToggle.ts
|
||||
│ └── useLocalStorage.ts
|
||||
│
|
||||
├── lib/
|
||||
│ ├── axios/
|
||||
│ │ └── axiosInstance.ts
|
||||
│ └── query/
|
||||
│ └── queryClient.ts
|
||||
│
|
||||
└── utils/
|
||||
├── formatDate.ts
|
||||
└── formatCurrency.ts
|
||||
```
|
||||
|
||||
#### `data/`
|
||||
|
||||
```plaintext
|
||||
data/
|
||||
├── user/
|
||||
│ ├── userAPI.ts
|
||||
│ ├── hooks.ts # or useUserQuery.ts, useCreateUserMutation.ts
|
||||
│ ├── userDTO.ts
|
||||
│ └── userMapper.ts
|
||||
│
|
||||
└── product/
|
||||
├── productAPI.ts
|
||||
├── hooks.ts
|
||||
├── productDTO.ts
|
||||
└── productMapper.ts
|
||||
```
|
||||
|
||||
#### `app/`
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
├── config/
|
||||
├── i18n/
|
||||
├── layouts/
|
||||
├── providers/
|
||||
│ ├── AppProviders.tsx
|
||||
│ ├── I18nProvider.tsx
|
||||
│ ├── QueryProvider.tsx
|
||||
│ ├── SettingsProvider.tsx
|
||||
│ └── ThemeProvider.tsx
|
||||
├── routing/
|
||||
└── services/
|
||||
```
|
||||
|
||||
#### `view/`
|
||||
|
||||
```plaintext
|
||||
view/
|
||||
├── components/
|
||||
│ ├── UserAvatar.tsx
|
||||
│ └── ProductCard.tsx
|
||||
│
|
||||
├── widgets/
|
||||
│ └── UserProfileWidget/
|
||||
│ ├── index.ts
|
||||
│ └── UserProfileWidget.tsx
|
||||
│
|
||||
└── forms/
|
||||
└── CreateUserForm/
|
||||
├── form.ts
|
||||
├── Component.tsx
|
||||
├── submit.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Case-Only Rename Problem (macOS + Git)
|
||||
|
||||
macOS file systems are **case-insensitive by default**, while Git is **case-sensitive internally**.
|
||||
|
||||
Renaming a file like `api.ts` → `Api.ts` may:
|
||||
|
||||
- Not be detected by Git
|
||||
- Break CI (Linux environments are case-sensitive)
|
||||
- Cause inconsistent repository state
|
||||
|
||||
### REQUIRED: Correct Rename Procedure
|
||||
|
||||
If you **must rename a file or folder by case only**, you are required to perform a two-step rename.
|
||||
|
||||
#### Step 1 — Rename to Temporary Name
|
||||
|
||||
```bash
|
||||
git mv userApi.ts userApi-temp.ts
|
||||
git commit -m "chore: temporary rename userApi.ts to userApi-temp.ts"
|
||||
```
|
||||
|
||||
#### Step 2 — Rename to Final Name
|
||||
|
||||
```bash
|
||||
git mv userApi-temp.ts UserApi.ts
|
||||
git commit -m "chore: rename userApi-temp.ts to UserApi.ts (case change)"
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- Never rename by case only in one step
|
||||
- Always use an intermediate name
|
||||
- Always commit both steps
|
||||
- Applies to files and folders
|
||||
- Do not rely only on IDE rename
|
||||
- Always verify with `git status`
|
||||
|
||||
### Making Git Case-Sensitive on macOS
|
||||
|
||||
By default, Git on macOS usually has:
|
||||
|
||||
`core.ignorecase=true`
|
||||
|
||||
This makes Git ignore case-only changes.
|
||||
|
||||
#### Set Case Sensitivity for This Repository
|
||||
|
||||
```bash
|
||||
git config core.ignorecase false
|
||||
```
|
||||
|
||||
#### Verify:
|
||||
|
||||
```bash
|
||||
git config core.ignorecase
|
||||
```
|
||||
|
||||
It should return: `false`
|
||||
|
||||
### Important Reality Check
|
||||
|
||||
Even if `core.ignorecase=false`:
|
||||
|
||||
- macOS file system is still case-insensitive
|
||||
- Case-only renames can still behave unexpectedly
|
||||
- The two-step rename process is still the safest approach
|
||||
@@ -0,0 +1,45 @@
|
||||
# CRUX Architecture
|
||||
|
||||
System-level docs and stack-scoped convention guides. Built-in feature docs live in `docs/core/`, project-module docs in `docs/modules/`, single-app docs with the app (`apps/<app>/Readme.md`).
|
||||
|
||||
## Stack Guides
|
||||
|
||||
| Guide | Applies to |
|
||||
| --------------------------------------------------------- | ------------------------------------------------- |
|
||||
| [React frontend architecture](./react-frontend/Readme.md) | Frontend apps and frontend modules under `apps/*` |
|
||||
| [Backend architecture](./backend/Readme.md) | AdonisJS backend apps and backend modules |
|
||||
| [Next.js web architecture](./web/Readme.md) | The public website (`apps/web/`) |
|
||||
|
||||
## System Overview
|
||||
|
||||
| App | Source | Stack | Role |
|
||||
| -------------- | --------------- | ------------ | ---------------------------------------------------- |
|
||||
| Backend API | `apps/backend/` | AdonisJS v6 | REST API, business logic, database, authorization |
|
||||
| Admin frontend | `apps/admin/` | React (Vite) | Admin panel: core modules, devtools, project modules |
|
||||
| Public website | `apps/web/` | Next.js | Server-rendered public site fed by the public API |
|
||||
|
||||
```txt
|
||||
┌──────────────────┐ authenticated REST ┌──────────────────────┐
|
||||
│ Admin (React) │────────────────────────►│ Backend (AdonisJS) │
|
||||
│ apps/admin │◄────────────────────────│ apps/backend │
|
||||
└──────────────────┘ DTOs └──────────┬───────────┘
|
||||
│
|
||||
public REST + │ cache purge
|
||||
revalidation hooks │ webhooks
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ Public site (Next.js) │
|
||||
│ apps/web │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
Apps share contracts, never runtime code:
|
||||
|
||||
- **Admin ↔ Backend** — authenticated REST, authorized per action via policies and permission slugs. See [Auth](../core/auth/Readme.md), [Access Control](../core/access-control/Readme.md), [Permission](../core/permission/Readme.md).
|
||||
- **Web ↔ Backend** — public REST plus cache revalidation webhooks. See [Web Manager integration](../core/web-manager/integration.md).
|
||||
|
||||
Feature-scoped contracts (DTOs, cache tags, env vars) live next to the feature in `docs/core/<feature>/integration.md`.
|
||||
|
||||
## Shared Packages
|
||||
|
||||
`packages/` holds `@crux-kit/*` packages shared across apps. Each documents itself in its own `Readme.md`; see the [Packages index](../packages/Readme.md).
|
||||
@@ -1,15 +1,9 @@
|
||||
# Backend Architecture
|
||||
|
||||
This guide is a short backend architecture reference for CRUX backend apps.
|
||||
|
||||
The current backend is based on AdonisJS v6. Use the official AdonisJS folder structure as the default baseline:
|
||||
|
||||
https://v6-docs.adonisjs.com/guides/getting-started/folder-structure
|
||||
|
||||
## Main Idea
|
||||
|
||||
Keep the backend close to the standard AdonisJS structure. Add project-specific folders only when they make ownership clearer.
|
||||
|
||||
## Default Folders
|
||||
|
||||
| Folder | Purpose |
|
||||
@@ -34,23 +28,6 @@ start/routes.ts
|
||||
|
||||
Controllers should handle HTTP concerns. Services should hold business logic. Validators should own request validation. Policies and abilities should own authorization.
|
||||
|
||||
## Module Folders
|
||||
|
||||
If a backend app has reusable or generated modules, keep each module's files together when that improves maintainability.
|
||||
|
||||
Example:
|
||||
|
||||
```txt
|
||||
app/base_modules/ModuleName/
|
||||
controller/
|
||||
services/
|
||||
validator/
|
||||
policy/
|
||||
routes.ts
|
||||
```
|
||||
|
||||
This is an app convention, not an AdonisJS requirement. Prefer the default AdonisJS structure unless a module boundary is useful.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow AdonisJS conventions first.
|
||||
@@ -58,4 +35,3 @@ This is an app convention, not an AdonisJS requirement. Prefer the default Adoni
|
||||
- Keep business logic out of controllers.
|
||||
- Validate request input before calling services.
|
||||
- Use policies or abilities for protected operations.
|
||||
- Document module-specific architecture in module docs, not in this general guide.
|
||||
+42
-86
@@ -1,93 +1,13 @@
|
||||
# CRUX React Admin Panel Architecture
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Disclaimer](#disclaimer)
|
||||
- [Core Principles](#core-principles)
|
||||
- [Recommended Layering](#recommended-layering)
|
||||
- [Architecture Scope](#architecture-scope)
|
||||
- [Entities Layer](#1-entities-layer)
|
||||
- [Data Layer (Data Access Layer)](#2-data-layer-data-access-layer)
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities-2)
|
||||
- [Rules and Guidelines](#rules-and-guidelines)
|
||||
- [Infrastructure Layer](#infrastructure-layer)
|
||||
- [Reference Data Layer](#reference-data-layer)
|
||||
- [App Layer](#3-app-layer)
|
||||
- [Pages Layer](#4-pages-layer)
|
||||
- [View Layer](#5-view-layer)
|
||||
- [view/components/](#viewcomponents)
|
||||
- [view/forms/](#viewforms)
|
||||
- [view/widgets/](#viewwidgets)
|
||||
- [Shared Layer](#6-shared-layer)
|
||||
- [Responsibilities](#responsibilities-4)
|
||||
- [Rules](#rules-2)
|
||||
- [Full documentation](#full-documentation)
|
||||
|
||||
## 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. **Гнучкість допускається**.
|
||||
- Ви можете коригувати шари, структуру модулів або угоди щодо найменування, якщо це покращує читабельність або зменшує складність.
|
||||
- У міру розвитку проєкту можна додавати нові патерни або винятки.
|
||||
|
||||
Коротко: цей посібник існує для того, щоб проєкт був зрозумілішим і простішим у підтримці, а не для того, щоб створювати зайві труднощі. Керуйтеся здоровим глуздом і надавайте перевагу ясності, а не жорсткій послідовності.
|
||||
|
||||
## 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 blocks.
|
||||
|
||||
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/interfaces
|
||||
|
||||
- Placing them in `entities`.
|
||||
- Any block (data hook, form, widget, page) can use these.
|
||||
|
||||
4. Shared
|
||||
|
||||
- shared/ = globally reusable components, hooks, utilities.
|
||||
|
||||
5. Infrastructure
|
||||
|
||||
- infrastructure/ = app-wide technical adapters such as HTTP clients, React Query, storage adapters, and formatting service wrappers.
|
||||
|
||||
6. Reference data
|
||||
- refdata/ = app-wide API-backed lookup datasets shared by multiple modules.
|
||||
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
|
||||
|
||||
@@ -124,3 +44,39 @@ For small modules, do not create empty layers only to match the template. Add a
|
||||
- [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.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Next.js Web Architecture
|
||||
|
||||
Architecture of the public website (`apps/web/`): a Next.js App Router app that server-renders the pages editors publish in [Web Manager](../../core/web-manager/Readme.md). App-specific setup (env vars, dev commands, tests) lives in [apps/web/Readme.md](../../../apps/web/Readme.md).
|
||||
|
||||
## How a Request Works
|
||||
|
||||
```txt
|
||||
GET /en/about-us
|
||||
proxy.ts (next-intl middleware) validates the locale segment
|
||||
app/[locale]/[[...path]]/page.tsx
|
||||
resolvePageRenderData('en', 'about-us') web-runtime/api/render-data.ts
|
||||
GET {BACKEND_API_URL}/web-manager/public/pages/resolve?locale=en&path=about-us
|
||||
normalizePageResolution(payload) web-runtime/data/normalize.ts
|
||||
PageRenderer web-runtime/renderer.tsx
|
||||
SiteHeader → RegisteredPageLayout → SiteFooter
|
||||
├─ PageHero
|
||||
├─ Breadcrumbs
|
||||
└─ PageBlocks → block registry → ContentBlock, FaqBlock, …
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```txt
|
||||
apps/web/
|
||||
├── app/ Next.js routes — thin, they only resolve data and delegate
|
||||
│ ├── [locale]/
|
||||
│ │ ├── layout.tsx <html>, NextIntlClientProvider, metadataBase, globals.css
|
||||
│ │ ├── [[...path]]/page.tsx catch-all: every CMS page in this locale
|
||||
│ │ ├── preview/page.tsx token-gated draft rendering (force-dynamic, noindex)
|
||||
│ │ └── not-found.tsx 404 screen
|
||||
│ ├── api/revalidate/route.ts secret-guarded cache purge, called by the backend
|
||||
│ ├── sitemap.ts, robots.ts generated from the public API
|
||||
│ └── globals.css one line: @import 'tailwindcss'
|
||||
├── web-runtime/ the rendering engine — everything that turns CMS data into a page
|
||||
│ ├── api/ HTTP client, render-data resolution, cache tags
|
||||
│ ├── data/normalize.ts untrusted payload → typed contract
|
||||
│ ├── types.ts that contract
|
||||
│ ├── blocks/ 6 block renderers + registry + unsupported fallback
|
||||
│ ├── layouts/ 3 page layouts + registry, hero, breadcrumbs
|
||||
│ ├── chrome/ site header, nav, footer, language switch
|
||||
│ ├── components/ link, image and rich-text primitives
|
||||
│ ├── lib/ metadata, site URL, sitemap, structured data
|
||||
│ └── renderer.tsx header + layout + footer
|
||||
├── i18n/ next-intl wiring, navigation helpers, translation JSON
|
||||
├── shared/ui/ design-system primitives (Button)
|
||||
├── site.config.ts locales and default locale
|
||||
├── proxy.ts the next-intl middleware
|
||||
└── tests/ Vitest specs for the pure logic
|
||||
```
|
||||
|
||||
### Layer responsibilities
|
||||
|
||||
| Layer | Owns | Never does |
|
||||
| ------------------- | --------------------------------------------------------------- | ----------------------------- |
|
||||
| `app/` | Route params, data resolution, `notFound()`, `generateMetadata` | Markup beyond composition |
|
||||
| `web-runtime/api/` | Every backend call, cache tags, `server-only` | Rendering |
|
||||
| `web-runtime/data/` | Turning untrusted JSON into `types.ts` shapes | Fetching or throwing |
|
||||
| `web-runtime/` | All page markup, registries, primitives | Direct `fetch` calls |
|
||||
| `i18n/` | Locale resolution, `Link`/`getPathname`, message loading | Content |
|
||||
| `shared/ui/` | Reusable, content-agnostic primitives | Knowing about pages or blocks |
|
||||
|
||||
## Backend Boundary
|
||||
|
||||
`web-runtime/api/web-runtime-api.ts` is the single door to the backend — a `server-only` module, a lazily constructed singleton, and the only place `fetch` appears.
|
||||
|
||||
| Call | Endpoint | Cache |
|
||||
| ------------------ | ----------------------------------- | ------------------------- |
|
||||
| `resolvePage` | `/web-manager/public/pages/resolve` | `revalidate: 60` + tags |
|
||||
| `listBlogPosts` | `/web-manager/public/blog/pages` | `revalidate: 60` + tags |
|
||||
| `listSitemapPages` | `/web-manager/public/pages/sitemap` | `revalidate: 3600` + tags |
|
||||
| `getSettings` | `/web-manager/public/settings` | `revalidate: 60` + tags |
|
||||
| `getPreview` | `/web-manager/public/pages/preview` | `cache: 'no-store'` |
|
||||
|
||||
A `404` becomes `null` (the caller renders the 404 page); any other non-OK status throws. The sitemap walk is capped at 50 requests.
|
||||
|
||||
Every response goes through `data/normalize.ts` first. Its one rule is **degrade, never throw**: a missing field becomes a default, an unknown block type becomes `UnknownBlock`, a malformed media object disappears. `types.ts` is what the rest of the app may assume after that.
|
||||
|
||||
Publishing purges this app's cache: the backend POSTs tag names to `app/api/revalidate/route.ts`, guarded by `REVALIDATE_SECRET`. The tags in `web-runtime/api/cache-tags.ts` mirror the backend's — see [Web Manager caching](../../core/web-manager/integration.md#caching-and-revalidation).
|
||||
|
||||
## Rendering
|
||||
|
||||
Two registries decide what gets drawn, and both fall back rather than fail:
|
||||
|
||||
```txt
|
||||
layouts/registry.tsx landing → StandardPageLayout unknown → StandardPageLayout
|
||||
blog → BlogPageLayout
|
||||
blog-item → EditorialPageLayout
|
||||
|
||||
blocks/registry.tsx content, faq, media-content, articles, banner, media
|
||||
unknown → UnsupportedBlock (dev-only banner, nothing in production)
|
||||
```
|
||||
|
||||
The CMS can publish a page type or block the site has not learned yet — the worst case is a missing section, never a broken deploy.
|
||||
|
||||
`StandardPageLayout` is hero + blocks. `EditorialPageLayout` adds breadcrumbs, `Article` JSON-LD, and a narrower reading measure. `BlogPageLayout` ignores the page's blocks and fetches its own post list (12 per page, `?page=` pager). Each sets `data-page-layout` on its `<main>` — the hook for tests and debugging. `Breadcrumbs` renders nothing for a single-item trail and emits `BreadcrumbList` JSON-LD.
|
||||
|
||||
Three shared primitives:
|
||||
|
||||
- **`components/links.tsx`** — `PageLink` validates the href (`/`, `#`, or `http/https/mailto/tel`) and renders an unusable link as disabled text. Link variants map onto `shared/ui/Button` variants.
|
||||
- **`components/media-image.tsx`** — a native `<img>`, not `next/image`: media origins are CMS-managed and unrestricted, so the optimizer's allowlist would be a permanent tax.
|
||||
- **`components/rich-text.tsx`** — `dangerouslySetInnerHTML`. Safe **only** because the backend sanitizes rich text in `PublicContentService`; never render HTML from any other source through it.
|
||||
|
||||
## Internationalization
|
||||
|
||||
Locales live in `site.config.ts` (`en`, `uk`, `de`). `i18n/routing.ts` sets `localePrefix` to `always` with multiple locales and `never` with one, so a single-language site serves clean URLs.
|
||||
|
||||
- Navigate with `Link`, `redirect`, `usePathname`, `useRouter`, `getPathname` from `i18n/navigation.ts` — never `next/link` or `next/navigation` directly, or the locale prefix is lost.
|
||||
- CMS hrefs arrive locale-prefixed; pass them through `toPublicHref` (`i18n/content-href.ts`).
|
||||
- Messages are five namespaces (`messages`, `pages`, `components`, `errors`, `form`) under `i18n/translations/`, merged at request time; a missing locale file falls back per namespace.
|
||||
- `i18n/types.ts` derives the `Messages` type from the English files — a missing key is a type error.
|
||||
|
||||
## SEO
|
||||
|
||||
- `app/[locale]/layout.tsx` sets `metadataBase` from `siteUrl()` plus the default title template.
|
||||
- `web-runtime/lib/page-metadata.ts` builds per-page metadata — title, description, canonical, hreflang with `x-default`, and Open Graph / Twitter cards from the hero image. A pure function of the render data, which is why it has direct tests.
|
||||
- `app/sitemap.ts` walks the paginated backend listing and `buildSitemap` groups translations into `alternates.languages`.
|
||||
- `app/robots.ts` allows everything except `/*/preview`.
|
||||
- `web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` and, on editorial pages, `Article`.
|
||||
@@ -1,368 +0,0 @@
|
||||
# File Manager Backend
|
||||
|
||||
The backend module exposes authenticated file operations under `/file-manager`. It validates requests, authorizes actions, normalizes paths, orchestrates file-manager behavior, and delegates raw storage work to a storage adapter.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/FileManager/
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
```txt
|
||||
routes.ts
|
||||
-> controller/file_manager_controller.ts
|
||||
-> validator/*
|
||||
-> policy/file_manager_policy.ts
|
||||
-> services/file_manager_service.ts
|
||||
-> services/storage_service/storage_service_interface.ts
|
||||
-> services/storage_service/create_storage_service.ts
|
||||
-> services/storage_service/fs_storage_service.ts | services/storage_service/s3_storage_service.ts
|
||||
-> Adonis Drive
|
||||
```
|
||||
|
||||
The controller should stay thin. It handles HTTP concerns: auth policy checks, request validation, path extraction, status codes, and response payload shape.
|
||||
|
||||
`FileManagerService` owns business orchestration. Storage adapters own raw persistence operations.
|
||||
|
||||
## Routes
|
||||
|
||||
All routes are prefixed with `/file-manager` and use `middleware.auth()`.
|
||||
|
||||
| Method | Route | Controller Method | Purpose |
|
||||
| -------- | ------------------ | ----------------- | ------------------------------------------------------- |
|
||||
| `GET` | `/` | `index` | List root directory entries. |
|
||||
| `GET` | `/*` | `index` | List entries for a nested directory. |
|
||||
| `GET` | `/files/details/*` | `show` | Return file metadata and optional text preview content. |
|
||||
| `GET` | `/files/raw/*` | `raw` | Stream raw file content. |
|
||||
| `GET` | `/search?q=value` | `search` | Search entries recursively. |
|
||||
| `POST` | `/folders` | `createFolder` | Create a folder. |
|
||||
| `POST` | `/files` | `upload` | Upload a file. |
|
||||
| `POST` | `/files/duplicate` | `duplicate` | Duplicate one or more **file** entries. |
|
||||
| `PUT` | `/entries/rename` | `rename` | Rename or move an entry within its parent path. |
|
||||
| `DELETE` | `/*` | `destroy` | Delete an entry. |
|
||||
|
||||
## Authorization
|
||||
|
||||
Policy file:
|
||||
|
||||
```txt
|
||||
policy/file_manager_policy.ts
|
||||
```
|
||||
|
||||
Controller methods call policy abilities before performing work:
|
||||
|
||||
| Ability | Used By |
|
||||
| ---------- | ---------------------------------------------- |
|
||||
| `viewList` | List entries, search entries. |
|
||||
| `view` | File details, raw file stream. |
|
||||
| `create` | Create folder, upload file, duplicate entries. |
|
||||
| `update` | Rename entry. |
|
||||
| `delete` | Delete entry. |
|
||||
|
||||
When adding a new operation, add or reuse an explicit policy ability before adding service logic.
|
||||
|
||||
## Validation
|
||||
|
||||
Validators live in:
|
||||
|
||||
```txt
|
||||
validator/
|
||||
```
|
||||
|
||||
Current validators:
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------------------- | ----------------------------------------------------------------- |
|
||||
| `list_validator.ts` | Validates filter, sort, order, max results, and pagination token. |
|
||||
| `create_directory_validator.ts` | Validates parent path and folder name. |
|
||||
| `rename_entry_validator.ts` | Validates source path and new name. |
|
||||
| `upload_file_validator.ts` | Validates upload path and file payload. |
|
||||
| `duplicate_entries_validator.ts` | Validates duplicate source paths. |
|
||||
| `schemas.ts` | Shared validation schema helpers. |
|
||||
|
||||
Path extraction from wildcard routes goes through:
|
||||
|
||||
```txt
|
||||
helpers/get_validated_path_from_request.ts
|
||||
```
|
||||
|
||||
Keep path validation centralized. Avoid parsing wildcard paths directly in controllers or storage adapters.
|
||||
|
||||
## Services
|
||||
|
||||
### `file_manager_service.ts`
|
||||
|
||||
Main orchestration service.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- List directory entries with filtering, sorting, and pagination.
|
||||
- Compose file detail responses with preview content.
|
||||
- Create directory response entries.
|
||||
- Validate and move entries.
|
||||
- Generate unique upload names.
|
||||
- Generate duplicate copy names.
|
||||
- Search entries.
|
||||
- Coordinate storage adapter calls.
|
||||
|
||||
### `path_service.ts`
|
||||
|
||||
Shared path and filename utility service.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Convert client paths to storage paths.
|
||||
- Convert storage paths to client paths.
|
||||
- Join paths safely.
|
||||
- Get dirname and basename.
|
||||
- Parse file extension metadata.
|
||||
- Sanitize filenames.
|
||||
- Generate unique names.
|
||||
|
||||
Use this service for path behavior instead of string manipulation in feature code.
|
||||
|
||||
### `entry_list_service.ts`
|
||||
|
||||
Applies list behavior to raw storage entries.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Filter entries by category.
|
||||
- Sort entries.
|
||||
- Keep folders before files.
|
||||
- Paginate entries.
|
||||
- Encode and decode pagination tokens.
|
||||
|
||||
Frontend list controls must stay compatible with this service's accepted filter and sort values.
|
||||
|
||||
### `file_preview_service.ts`
|
||||
|
||||
Adds text preview content to previewable files.
|
||||
|
||||
Previewable text extensions are currently defined in:
|
||||
|
||||
```txt
|
||||
services/file_preview_service.ts
|
||||
```
|
||||
|
||||
If a file type needs rich frontend preview, keep the backend response small and stream raw content through `/files/raw/*` when possible.
|
||||
|
||||
### `entry_search_service.ts`
|
||||
|
||||
Searches entries by path and includes matching parent folders so the frontend can display useful context.
|
||||
|
||||
Search receives an abort signal from the controller. Long-running search implementations should respect that signal.
|
||||
|
||||
## Storage
|
||||
|
||||
Storage abstractions live in:
|
||||
|
||||
```txt
|
||||
services/storage_service/
|
||||
```
|
||||
|
||||
### `storage_service_interface.ts`
|
||||
|
||||
Defines the storage adapter contract. Adapters expose raw operations:
|
||||
|
||||
- List directory entries.
|
||||
- List recursive entries.
|
||||
- Check entry existence.
|
||||
- Get file metadata.
|
||||
- Read file content.
|
||||
- Stream file content.
|
||||
- Write file.
|
||||
- Create directory.
|
||||
- Move entry.
|
||||
- Delete entry.
|
||||
- Copy entry.
|
||||
|
||||
Services should call the interface, not a concrete adapter.
|
||||
|
||||
### `fs_storage_service.ts`
|
||||
|
||||
Filesystem storage adapter.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Call the Adonis Drive filesystem disk.
|
||||
- Map filesystem objects to File Manager entries.
|
||||
- Handle file streams and metadata.
|
||||
- Create, move, copy, and delete filesystem entries.
|
||||
- Preserve filesystem directory marker behavior.
|
||||
|
||||
### `s3_storage_service.ts`
|
||||
|
||||
S3-compatible storage adapter.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Call the Adonis Drive `s3` disk.
|
||||
- Map S3 objects and common prefixes to File Manager entries.
|
||||
- Upload multipart files by streaming the temporary upload file into S3 with `putStream`.
|
||||
- Read file content and streams from S3.
|
||||
- Create folders with directory marker objects.
|
||||
- Move folders by moving every object under the folder prefix.
|
||||
- Delete files with exact object deletion and folders with folder-prefix deletion.
|
||||
- Copy file entries.
|
||||
|
||||
S3 has no native directories. The adapter uses the same `.keep` marker convention as the filesystem adapter so empty folders can exist.
|
||||
|
||||
Folder operations must use slash-terminated prefixes. For example, deleting `test/` must not delete objects such as `test123.txt`.
|
||||
|
||||
### `fs_directory_marker_service.ts`
|
||||
|
||||
Encapsulates the filesystem directory marker convention.
|
||||
|
||||
Current marker:
|
||||
|
||||
```txt
|
||||
.keep
|
||||
```
|
||||
|
||||
Do not duplicate marker logic outside this service. Storage adapters that need virtual folders should use this marker service behind the storage interface.
|
||||
|
||||
### `create_storage_service.ts`
|
||||
|
||||
Storage service factory.
|
||||
|
||||
Current supported storage:
|
||||
|
||||
```txt
|
||||
fs
|
||||
s3
|
||||
```
|
||||
|
||||
The active adapter is selected by `DRIVE_DISK`.
|
||||
|
||||
```env
|
||||
DRIVE_DISK=fs
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```env
|
||||
DRIVE_DISK=s3
|
||||
```
|
||||
|
||||
## Drive Configuration
|
||||
|
||||
Adonis Drive config lives in:
|
||||
|
||||
```txt
|
||||
apps/backend/config/drive.ts
|
||||
```
|
||||
|
||||
Configured disks:
|
||||
|
||||
| Disk | Driver | Purpose |
|
||||
| ---- | ------------- | ---------------------------------------------------- |
|
||||
| `fs` | `services.fs` | Local development and filesystem-backed storage. |
|
||||
| `s3` | `services.s3` | S3-compatible object storage, including local MinIO. |
|
||||
|
||||
The S3 disk expects these environment variables:
|
||||
|
||||
| Variable | Local Docker Value | Purpose |
|
||||
| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `AWS_ACCESS_KEY_ID` | `minioadmin` | S3 access key. |
|
||||
| `AWS_SECRET_ACCESS_KEY` | `minioadmin` | S3 secret key. |
|
||||
| `AWS_REGION` | `us-east-1` | S3 region. |
|
||||
| `S3_BUCKET` | `crux-drive` | Bucket used by the file manager. |
|
||||
| `S3_ENDPOINT` | `http://minio:9000` in Docker, `http://localhost:9000` outside Docker | S3 API endpoint. |
|
||||
| `S3_CDN_URL` | `http://localhost:9000/crux-drive/` | Browser-facing public URL base for file URLs. |
|
||||
|
||||
For MinIO, the S3 disk uses path-style addressing:
|
||||
|
||||
```ts
|
||||
forcePathStyle: true;
|
||||
```
|
||||
|
||||
The adapter also disables ACL API calls:
|
||||
|
||||
```ts
|
||||
supportsACL: false;
|
||||
```
|
||||
|
||||
Bucket public-read behavior is handled by the Docker bucket setup container.
|
||||
|
||||
## Local S3 Simulation
|
||||
|
||||
Local S3 is simulated with MinIO in:
|
||||
|
||||
```txt
|
||||
docker-compose.yml
|
||||
```
|
||||
|
||||
Services:
|
||||
|
||||
| Service | Purpose |
|
||||
| --------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `minio` | Runs the S3-compatible object storage server. |
|
||||
| `minio_create_bucket` | One-shot setup container that creates the bucket and enables anonymous download access. |
|
||||
|
||||
MinIO console:
|
||||
|
||||
```txt
|
||||
http://localhost:9001
|
||||
```
|
||||
|
||||
Default local credentials:
|
||||
|
||||
```txt
|
||||
user: minioadmin
|
||||
pass: minioadmin
|
||||
```
|
||||
|
||||
The `minio_create_bucket` container is expected to exit after setup. A stopped container with successful bucket logs is normal.
|
||||
|
||||
To rebuild after dependency or Dockerfile changes:
|
||||
|
||||
```sh
|
||||
docker compose build --no-cache
|
||||
docker compose up
|
||||
```
|
||||
|
||||
## Response Shape
|
||||
|
||||
Directory and file operations return `Entry` objects:
|
||||
|
||||
```ts
|
||||
interface EntryBase {
|
||||
name: string;
|
||||
path: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
interface FileEntry extends EntryBase {
|
||||
type: "file";
|
||||
size?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface FolderEntry extends EntryBase {
|
||||
type: "folder";
|
||||
}
|
||||
```
|
||||
|
||||
List responses include pagination metadata:
|
||||
|
||||
```ts
|
||||
{
|
||||
data: Entry[];
|
||||
meta: {
|
||||
nextPaginationToken: string | null;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Backend Change Checklist
|
||||
|
||||
Before changing backend behavior:
|
||||
|
||||
1. Confirm the policy ability for the operation.
|
||||
2. Add or update a validator for all request input.
|
||||
3. Keep client paths and storage paths separated through `path_service.ts`.
|
||||
4. Update the frontend DTO or mapper if response shape changes.
|
||||
5. Update this documentation when routes, permissions, storage contracts, or response shapes change.
|
||||
@@ -1,484 +0,0 @@
|
||||
# File Manager Frontend
|
||||
|
||||
The frontend module implements the admin file manager UI. It provides file browsing, previews,
|
||||
uploads, deletion, duplication, renaming, image editing tools, image map tools, settings, and
|
||||
reusable file picker widgets.
|
||||
|
||||
This module follows the general
|
||||
[React frontend architecture](../../apps/admin/architecture/Readme.md) inside its own
|
||||
module boundary.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/FileManager/
|
||||
```
|
||||
|
||||
## Main Principle
|
||||
|
||||
React Query owns server state. MobX owns local UI and operation state.
|
||||
|
||||
Server state examples:
|
||||
|
||||
- Directory entries.
|
||||
- File details.
|
||||
- Raw file blobs.
|
||||
- Search results.
|
||||
- Create, rename, delete, duplicate, and upload mutations.
|
||||
|
||||
Local state examples:
|
||||
|
||||
- Active path.
|
||||
- Selected paths.
|
||||
- Expanded tree paths.
|
||||
- Filter and sort controls.
|
||||
- Dialog open state.
|
||||
- Upload and delete operation progress.
|
||||
- Cached lookup of already loaded entries for UI behavior.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
FileManager/
|
||||
config/ Routes, menu entries, messages, translations.
|
||||
data/
|
||||
api/ Raw HTTP methods and DTOs.
|
||||
hooks/ Feature hooks that combine queries with stores.
|
||||
mappers/ DTO-to-entity mapping.
|
||||
queries/ React Query hooks, keys, mutations, and cache helpers.
|
||||
stores/ MobX local state stores.
|
||||
entities/ Domain entities and settings types.
|
||||
providers/ Feature providers and context hooks.
|
||||
pages/ Route-level screens.
|
||||
shared/ FileManager utilities that do not depend on view code.
|
||||
view/ UI components, widgets, forms, and layouts.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Frontend route config:
|
||||
|
||||
```txt
|
||||
config/routing.tsx
|
||||
```
|
||||
|
||||
Mounted module path:
|
||||
|
||||
```txt
|
||||
/file-manager/*
|
||||
```
|
||||
|
||||
Current child routes:
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| -------------------------------------- | ------------------- | ------------------------------------------- |
|
||||
| `/file-manager/drive/explorer/*` | `DriveExplorerPage` | Browse folders and files. |
|
||||
| `/file-manager/drive/viewer/-/*` | `DriveViewerPage` | View file details and preview. |
|
||||
| `/file-manager/drive/settings` | `DriveSettingsPage` | Configure file-manager UI settings. |
|
||||
| `/file-manager/tools/image-editor/-/*` | `ImageEditorPage` | Edit image files. |
|
||||
| `/file-manager/tools/image-map/-/*` | `ImageMapPage` | Create and edit image map areas for images. |
|
||||
|
||||
Path helpers live in:
|
||||
|
||||
```txt
|
||||
shared/routing/driveLinks.ts
|
||||
shared/routing/openEntryNewTab.ts
|
||||
shared/path/leadingSlash.ts
|
||||
```
|
||||
|
||||
Use these helpers instead of manually building file-manager URLs.
|
||||
|
||||
## Menu Structure
|
||||
|
||||
File Manager navigation is grouped into three top-level menu entries:
|
||||
|
||||
- `Drive` for file browsing, preview, and viewer-related flows.
|
||||
- `Tools` for standalone file tools such as image editing and image map editing.
|
||||
- `Settings` for persisted file-manager UI settings.
|
||||
|
||||
The `Tools` group currently contains:
|
||||
|
||||
- `ImageEditorPage`
|
||||
- `ImageMapPage`
|
||||
|
||||
## API Boundary
|
||||
|
||||
Raw backend calls are centralized in:
|
||||
|
||||
```txt
|
||||
data/api/api.ts
|
||||
data/api/dto.ts
|
||||
```
|
||||
|
||||
The frontend calls the backend routes documented in [backend.md](./backend.md):
|
||||
|
||||
| API Method | Backend Route |
|
||||
| ----------------- | ------------------------------------ |
|
||||
| `getEntries` | `GET /file-manager/*` |
|
||||
| `getFile` | `GET /file-manager/files/details/*` |
|
||||
| `getRawFile` | `GET /file-manager/files/raw/*` |
|
||||
| `createDirectory` | `POST /file-manager/folders` |
|
||||
| `renameEntry` | `PUT /file-manager/entries/rename` |
|
||||
| `removeEntry` | `DELETE /file-manager/*` |
|
||||
| `uploadFile` | `POST /file-manager/files` |
|
||||
| `duplicateFiles` | `POST /file-manager/files/duplicate` |
|
||||
| `searchEntries` | `GET /file-manager/search` |
|
||||
|
||||
DTOs should be mapped before they reach UI code. Entity types live in:
|
||||
|
||||
```txt
|
||||
entities/entry.ts
|
||||
data/mappers/entryMapper.ts
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Reading Entries
|
||||
|
||||
Directory entries are fetched through:
|
||||
|
||||
```ts
|
||||
useGetEntries();
|
||||
```
|
||||
|
||||
The higher-level hook:
|
||||
|
||||
```ts
|
||||
useEntries();
|
||||
```
|
||||
|
||||
does three things:
|
||||
|
||||
1. Builds the query payload from `ExplorerStore`.
|
||||
2. Fetches entries with React Query.
|
||||
3. Syncs loaded entries into `EntriesStore`.
|
||||
|
||||
`EntriesStore` is not the fetch owner. It is an index of loaded entries used by the sidebar tree,
|
||||
selection lookups, active entry lookup, and operation state.
|
||||
|
||||
### Deep Links
|
||||
|
||||
When opening a nested folder directly, `useEntries()` preloads ancestor folders through React Query.
|
||||
This keeps the sidebar tree and entry index usable for paths like:
|
||||
|
||||
```txt
|
||||
/folder/subfolder/current
|
||||
```
|
||||
|
||||
### Mutations
|
||||
|
||||
Mutations use React Query hooks:
|
||||
|
||||
- `useCreateDirectory`
|
||||
- `useRenameEntry`
|
||||
- `useDeleteEntry`
|
||||
- `useDuplicateFiles`
|
||||
- `useUploadFile`
|
||||
|
||||
After a successful mutation, the UI updates local cache and stores directly. It does not invalidate
|
||||
entries queries after every operation.
|
||||
|
||||
This avoids unnecessary refetches when many items are created, deleted, or uploaded in sequence.
|
||||
|
||||
## Query Cache Updates
|
||||
|
||||
Entries cache helpers live in:
|
||||
|
||||
```txt
|
||||
data/queries/cache.ts
|
||||
```
|
||||
|
||||
Current helpers:
|
||||
|
||||
```ts
|
||||
addEntriesToCache(parentPath, entries);
|
||||
removeEntriesFromCache(paths);
|
||||
renameEntryInCache(prevPath, renamedEntry);
|
||||
```
|
||||
|
||||
These helpers update existing React Query cache data in memory. They do not trigger a network
|
||||
request.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- Added entries are inserted into cached lists for the matching parent folder.
|
||||
- Added entries are filter-aware.
|
||||
- Deleted entries are removed from all cached entry pages.
|
||||
- Renamed entries are updated across cached entry pages.
|
||||
|
||||
Tradeoff: because the module avoids refetching, exact backend sort order may not be perfect after
|
||||
local cache insertion until the folder is naturally refetched or reloaded.
|
||||
|
||||
## Stores
|
||||
|
||||
### `EntriesStore`
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
data/stores/entriesStore.ts
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Keep an index of loaded entries by path.
|
||||
- Provide child lookup by parent path.
|
||||
- Support selected-entry and active-entry lookup.
|
||||
- Support local remove and rename updates for already loaded subtrees.
|
||||
|
||||
It should not fetch data.
|
||||
|
||||
### `ExplorerStore`
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
data/stores/explorerStore.ts
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Active path.
|
||||
- Selected paths.
|
||||
- Expanded tree paths.
|
||||
- Breadcrumbs.
|
||||
- Entries filter.
|
||||
- Sort field and direction.
|
||||
|
||||
### `DriveDialogStore`
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
data/stores/driveDialogStore.ts
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Create-directory dialog state.
|
||||
- Rename-entry dialog state.
|
||||
- Current entry selected for rename.
|
||||
|
||||
### `UploadOperationStore`
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
data/stores/uploadOperationStore.ts
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Upload queue.
|
||||
- Upload progress.
|
||||
- Upload retry, remove, and abort.
|
||||
- Upload operation card visibility.
|
||||
|
||||
It receives the React Query mutation function from the view layer and manages only operation state.
|
||||
|
||||
### `DeleteOperationStore`
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
data/stores/deleteOperationStore.ts
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Delete queue.
|
||||
- Delete progress.
|
||||
- Delete retry and remove.
|
||||
- Delete operation card visibility.
|
||||
|
||||
It receives the React Query mutation function from the view layer and manages only operation state.
|
||||
|
||||
### `SettingsStore`
|
||||
|
||||
File:
|
||||
|
||||
```txt
|
||||
data/stores/settingsStore.ts
|
||||
```
|
||||
|
||||
Purpose:
|
||||
|
||||
- Persisted view mode.
|
||||
- Compact mode.
|
||||
- Sidebar visibility.
|
||||
- Grid size and related display preferences.
|
||||
|
||||
## Providers
|
||||
|
||||
`DriveProvider` creates and exposes feature stores:
|
||||
|
||||
```ts
|
||||
entriesStore;
|
||||
explorerStore;
|
||||
driveDialogStore;
|
||||
uploadOperationStore;
|
||||
deleteOperationStore;
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```ts
|
||||
const { entriesStore, explorerStore } = useDrive();
|
||||
```
|
||||
|
||||
`SettingsProvider` owns persisted settings such as view mode, compact mode, sidebar visibility, and
|
||||
grid size.
|
||||
|
||||
## Pages And Widgets
|
||||
|
||||
### `DriveExplorerPage`
|
||||
|
||||
Main file browser page. Composes:
|
||||
|
||||
- `DriveProvider`
|
||||
- Explorer toolbar.
|
||||
- Explorer content.
|
||||
- Explorer sidebar.
|
||||
- Footer.
|
||||
- Operation watcher.
|
||||
|
||||
### `DriveViewerPage`
|
||||
|
||||
File details and preview page. Uses:
|
||||
|
||||
```ts
|
||||
useGetFile();
|
||||
```
|
||||
|
||||
### `DriveSettingsPage`
|
||||
|
||||
Settings page for persisted file-manager UI preferences.
|
||||
|
||||
### `ImageEditorPage`
|
||||
|
||||
Image editor page. Uses:
|
||||
|
||||
```ts
|
||||
useGetFile();
|
||||
useGetRawFile();
|
||||
useUploadFile();
|
||||
```
|
||||
|
||||
Image editor config and helpers live in the widget layer:
|
||||
|
||||
```txt
|
||||
view/widgets/ImageEditor/config.ts
|
||||
view/widgets/ImageEditor/helpers.ts
|
||||
```
|
||||
|
||||
This prevents `view` code from importing from `pages`.
|
||||
|
||||
### `ImageMapPage`
|
||||
|
||||
Image map page. Uses:
|
||||
|
||||
```ts
|
||||
useGetFile();
|
||||
useGetRawFile();
|
||||
```
|
||||
|
||||
Image map config and helpers live in the widget layer:
|
||||
|
||||
```txt
|
||||
view/widgets/ImageEditor/config.ts
|
||||
view/widgets/ImageEditor/helpers.ts
|
||||
```
|
||||
|
||||
This prevents `view` code from importing from `pages`.
|
||||
|
||||
### `FilePickerDialog`
|
||||
|
||||
Reusable picker widgets live in:
|
||||
|
||||
```txt
|
||||
view/widgets/FilePickerDialog/
|
||||
```
|
||||
|
||||
Keep picker behavior feature-scoped, but pass reusable UI state through props and context instead of
|
||||
coupling low-level UI components to page-level stores.
|
||||
|
||||
## View Layer Rules
|
||||
|
||||
Feature widgets may use File Manager providers such as `useDrive()` and `useSettings()`.
|
||||
|
||||
Lower-level reusable UI components should stay prop-driven. Examples:
|
||||
|
||||
- `ExplorerFileEntryGrid`
|
||||
- `ExplorerFolderEntryList`
|
||||
- `ExplorerToolbarFilterDropdown`
|
||||
- `FileManagerFileIcon`
|
||||
- `FilePreview`
|
||||
|
||||
Do not import backend DTOs into view components. View components should consume frontend entities
|
||||
and props.
|
||||
|
||||
## File Types And Icons
|
||||
|
||||
File helpers live in:
|
||||
|
||||
```txt
|
||||
shared/file/
|
||||
```
|
||||
|
||||
File icons live in:
|
||||
|
||||
```txt
|
||||
apps/admin/public/media/files/
|
||||
```
|
||||
|
||||
To add or change an icon:
|
||||
|
||||
1. Add the asset under `apps/admin/public/media/files/`.
|
||||
2. Update `shared/file/fileTypeImportMap.ts`.
|
||||
3. Confirm `FileManagerFileIcon` renders the expected fallback for unknown extensions.
|
||||
|
||||
## Adding A New Query
|
||||
|
||||
1. Add or reuse a method in:
|
||||
|
||||
```txt
|
||||
data/api/api.ts
|
||||
```
|
||||
|
||||
2. Add a key in:
|
||||
|
||||
```txt
|
||||
data/queries/keys.ts
|
||||
```
|
||||
|
||||
3. Create a query file:
|
||||
|
||||
```txt
|
||||
data/queries/example.ts
|
||||
```
|
||||
|
||||
4. Map DTOs to entities before returning app-facing data.
|
||||
|
||||
Do not import MobX stores or providers inside `data/queries`.
|
||||
|
||||
## Adding A New Mutation
|
||||
|
||||
1. Add a raw API method in `data/api/api.ts`.
|
||||
2. Add a mutation hook in `data/queries`.
|
||||
3. In the consuming widget or operation store flow, call `mutateAsync`.
|
||||
4. Update `EntriesStore` if lookup state must change.
|
||||
5. Update `data/queries/cache.ts` helpers if visible query cache must change.
|
||||
|
||||
Do not invalidate entries queries after each operation unless a full refetch is intentionally
|
||||
required.
|
||||
|
||||
## Frontend Change Checklist
|
||||
|
||||
Before changing frontend behavior:
|
||||
|
||||
1. Identify whether the data is server state or local UI state.
|
||||
2. Keep backend calls in `data/api`.
|
||||
3. Keep React Query hooks free of MobX provider imports.
|
||||
4. Map DTOs before passing data to view components.
|
||||
5. Update cache helpers when mutation results should appear immediately.
|
||||
6. Update backend docs if the frontend change requires a backend contract change.
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
The frontend module provides the admin-facing UI for managing admins, roles, and permissions.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/AccessControl/
|
||||
```
|
||||
Source: `apps/admin/src/core/AccessControl/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
+1
-5
@@ -2,11 +2,7 @@
|
||||
|
||||
The frontend module implements the current admin profile settings page.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/AccountSettings/
|
||||
```
|
||||
Source: `apps/admin/src/core/AccountSettings/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
The backend Auth module exposes token-based admin authentication routes.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Auth/
|
||||
```
|
||||
Source: `apps/backend/app/base_modules/Auth/`
|
||||
|
||||
## Routes
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
The frontend module implements authentication screens, session provider, current-user loading, and route guards.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/Auth/
|
||||
```
|
||||
Source: `apps/admin/src/core/Auth/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
The admin devtools module contains development-only interfaces for generating business modules, reviewing UI and form components in the sandbox, and managing the local application version.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/devtools/
|
||||
```
|
||||
Source: `apps/admin/src/devtools/`
|
||||
|
||||
The frontend module is registered through `apps/admin/src/devtools/config/module.ts`. Its routes, menu, and translations are exposed only when `IS_DEV_MODE` is enabled. All devtools frontend routes are protected routes.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Module Generator
|
||||
|
||||
Creates a backend module and a matching admin frontend module from an existing PostgreSQL table, combining database metadata with frontend code builders and Handlebars templates. The output is a maintainable starting point — generated authorization, validation, relationships, API behavior, labels, and translations must still be reviewed for the product domain.
|
||||
|
||||
| Part | Source |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| Generator page and form | `apps/admin/src/devtools/` |
|
||||
| Copied frontend module template | `apps/admin/src/modules/_template/` |
|
||||
| Generator HTTP endpoints | `apps/backend/start/routes.ts` |
|
||||
| Generator controller | `apps/backend/app/controllers/module_controller.ts` |
|
||||
| Database inspection | `apps/backend/app/services/database_inspector.ts` |
|
||||
| Backend Handlebars templates | `apps/backend/app/templates/backend/` |
|
||||
| Frontend Handlebars templates | `apps/backend/app/templates/frontend/` |
|
||||
|
||||
Details: [frontend.md](./frontend.md) (UI, payload builders, generated frontend), [backend.md](./backend.md) (endpoints, inspection, templates, generated backend), [troubleshooting.md](./troubleshooting.md) (module not loaded / missing from menu).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Backend running from `apps/backend/` (destinations resolve relative to `process.cwd()`); admin app in dev mode; authenticated session (all endpoints use auth middleware).
|
||||
2. The source table created and migrated — the form reads PostgreSQL `information_schema`.
|
||||
3. A module name valid as a TypeScript identifier, route segment, permission slug, and folder name.
|
||||
|
||||
## Generation Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Select table and assets"] --> B["Configure form and table"]
|
||||
B --> C["Frontend builders create source strings"]
|
||||
C --> D["POST /makeModule/generate"]
|
||||
D --> E["Inspect database metadata"]
|
||||
E --> F["Copy frontend _template"]
|
||||
E --> G["Render backend templates"]
|
||||
F --> H["Write configured frontend files"]
|
||||
G --> I["Write backend module and tests"]
|
||||
```
|
||||
|
||||
Output goes to `apps/backend/modules/<ModuleName>/`, `apps/backend/tests/modules/<ModuleName>/`, and `apps/admin/src/modules/<name>/`. Frontend modules are discovered through their `config/module.ts` manifest; backend routes load from `apps/backend/modules/` at startup.
|
||||
|
||||
## Operational Behavior
|
||||
|
||||
- Generation writes directly into both workspaces — no preview, transaction, or rollback; existing files may be overwritten. Generate into a clean name and inspect the Git diff.
|
||||
- Select `model` whenever you select `api` — API generation imports the model for its Swagger schema.
|
||||
- The frontend template is always copied; form and datatable files are populated only when selected.
|
||||
- Generated code is not formatted or committed automatically.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
1. Inspect the Git diff; confirm route paths, API prefixes, permission slugs, and naming.
|
||||
2. Review nullable fields, dates, passwords, foreign keys, validation, relationship names; replace generic labels and translations.
|
||||
3. Verify list/create/update/delete, authorization, and validation-error behavior.
|
||||
4. Run `npm run build:backend` and `npm run build:admin`; run the generated backend tests and add domain coverage.
|
||||
|
||||
Generated frontend follows the [React frontend architecture](../../../architecture/react-frontend/Readme.md); review backend modules against the [backend architecture](../../../architecture/backend/Readme.md).
|
||||
@@ -0,0 +1,61 @@
|
||||
# Module Generator Backend
|
||||
|
||||
Authenticated schema-inspection and generation endpoints; backend files rendered from Handlebars templates, frontend files written from a copied template plus source strings supplied by the admin app.
|
||||
|
||||
## Endpoints
|
||||
|
||||
Registered in `apps/backend/start/routes.ts`, all behind `middleware.auth()`:
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| ------ | ---------------------------- | -------------------------------------------------- |
|
||||
| `GET` | `/makeModule/tables` | List eligible public PostgreSQL tables |
|
||||
| `GET` | `/makeModule/columns/:table` | Return columns used to configure form/table fields |
|
||||
| `POST` | `/makeModule/generate` | Generate selected frontend and backend assets |
|
||||
|
||||
## Database Inspection
|
||||
|
||||
`database_inspector.ts` reads `information_schema`: only the `public` schema, excluding framework/auth/access-control/migration/system tables. Columns ending in `_id` become belongs-to candidates; tables containing a `<table>_id` column become has-many candidates; searchable string/numeric columns are derived for resource controllers. **Relationship inference is heuristic** — review generated model imports, class and property names.
|
||||
|
||||
## Type Mapping
|
||||
|
||||
| Database category | Lucid TypeScript type | Vine type | Frontend type |
|
||||
| --------------------- | --------------------- | --------- | --------------- |
|
||||
| integer/numeric types | `number` | `number` | `number` |
|
||||
| boolean | `boolean` | `boolean` | `boolean` |
|
||||
| date/timestamp | `DateTime` | `date` | `IsoDateString` |
|
||||
| other values | `string` | `string` | `string` |
|
||||
|
||||
Nullable columns get `| null` and optional validation; `created_at`, `updated_at`, `deleted_at` receive special handling.
|
||||
|
||||
## Backend Output
|
||||
|
||||
```txt
|
||||
apps/backend/modules/<ModuleName>/
|
||||
├── api/ # controller, create/update validators, routes.ts
|
||||
├── config/permissions.ts
|
||||
├── controller/<entity>_controller.ts
|
||||
├── model/<entity>.ts
|
||||
├── policy/<entity>_policy.ts
|
||||
├── validators/ # <entity>_create/_update_validator.ts
|
||||
└── routes.ts
|
||||
|
||||
apps/backend/tests/modules/<ModuleName>/ # <entity>.spec.ts, <entity>.api.spec.ts
|
||||
```
|
||||
|
||||
`<entity>` is the table name minus its final character — assumes a plural table name; review irregular plurals.
|
||||
|
||||
## Asset Behavior
|
||||
|
||||
- **Model** — `model.hbs` with columns, inferred relationships, searchable fields, optional password hashing.
|
||||
- **Controller and routes** — authenticated resource controller with list/search/sort/pagination and CRUD; `routes.hbs` registers an `apiOnly` resource route with authentication.
|
||||
- **Validators** — separate create/update Vine validators; primary keys and timestamp/deletion fields excluded; email, password, nullable, and trimming behavior inferred from names and metadata.
|
||||
- **Policy and permissions** — Bouncer policy with view/create/update/delete, `config/permissions.ts`, and a lazy policy import added to `app/policies/main.ts` when missing. Slugs derive from the module name (`<name>s.view`, `.create`, `.update`, `.delete`) — review already-plural names, this is not a general pluralization system.
|
||||
- **API subtree** — a second CRUD controller and routes under `/api/<entity>` with validators and a test; imports the model for `modelToSchema` Swagger metadata. **The generated API template adds no authentication middleware — add auth and authorization before exposing the route.**
|
||||
|
||||
## Frontend Writes
|
||||
|
||||
The backend copies `apps/admin/src/modules/_template` to `apps/admin/src/modules/<name>`, then writes the configured form (component + definition), datatable and list page, manage trigger, menu, module manifest, generated `config/meta.ts`, and database-derived `entities/Entity.ts`. See [frontend.md](./frontend.md).
|
||||
|
||||
## Extending
|
||||
|
||||
Handlebars helpers available: `eq`, `not`, `or`, `and`. To add a generated artifact: add the template under `apps/backend/app/templates/`, an explicit destination in the controller `paths` object (parent dir via existing path initialization), render only when its asset is selected, and pass normalized metadata — never query the database inside templates. Add generation tests for nullable values, dates, numerics, passwords, and irregular names; verify both builds. No hidden writes — every destination, including shared registries like `app/policies/main.ts`, stays visible in the controller flow and documented here.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Module Generator Frontend
|
||||
|
||||
The generator UI collects module metadata, selected assets, form layout, and datatable configuration, converts it into TypeScript/TSX source strings, and sends them to the backend generator.
|
||||
|
||||
| Responsibility | Source (under `apps/admin/src/devtools/`) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| Page submission, notifications, and mutation callbacks | `pages/ModuleGeneratorPage/index.tsx` |
|
||||
| Payload assembly | `pages/ModuleGeneratorPage/lib/buildGenerateModulePayload.ts` |
|
||||
| Form state, initial values, and sections | `view/forms/ModuleGeneratorForm/` |
|
||||
| Payload DTOs and HTTP client | `data/api/` |
|
||||
| React Query hooks | `data/queries/` |
|
||||
| Source-code builders | `data/builders/` |
|
||||
|
||||
The page submits through `useGenerateModule`; success resets the form and shows an alert, errors go through the shared React Query error helpers.
|
||||
|
||||
## Metadata and Assets
|
||||
|
||||
Required metadata: `name` (frontend folder, backend module identity, route segment, permission-slug base), `table` (existing PostgreSQL table), `generate` (selected assets). Backend assets are all selected by default; form and datatable are opt-in.
|
||||
|
||||
| Asset | Workspace | Output |
|
||||
| ------------ | --------- | --------------------------------------------------- |
|
||||
| `model` | Backend | Lucid model |
|
||||
| `controller` | Backend | Authenticated resource controller and test |
|
||||
| `routes` | Backend | Resource route registration |
|
||||
| `validator` | Backend | Create and update Vine validators |
|
||||
| `policy` | Backend | Bouncer policy and permission configuration |
|
||||
| `api` | Backend | API controller, validators, routes, and test |
|
||||
| `form` | Frontend | Form definition, component, and modal/sheet trigger |
|
||||
| `datatable` | Frontend | List page table and menu entry |
|
||||
|
||||
Before generation the UI reads `GET /makeModule/tables` (table selector) and `GET /makeModule/columns/:table` (field choices) — cached via React Query, columns disabled until a table is selected.
|
||||
|
||||
## Payload Builders
|
||||
|
||||
`buildGenerateModulePayload` delegates to focused builders:
|
||||
|
||||
| Builder | Payload field | Generated destination |
|
||||
| ----------------------------------- | ---------------- | ----------------------------------- |
|
||||
| `FormSourceBuilder.buildComponent` | `form` | `view/forms/MainForm/Component.tsx` |
|
||||
| `FormSourceBuilder.buildDefinition` | `formDefinition` | `view/forms/MainForm/form.ts` |
|
||||
| `ManageTriggerSourceBuilder` | `formDialog` | `view/widgets/ManageTrigger.tsx` |
|
||||
| `DataTableSourceBuilder` | `dataTable` | `view/widgets/DataTable.tsx` |
|
||||
| `ListPageSourceBuilder` | `list` | `pages/ListPage/index.tsx` |
|
||||
| `MenuSourceBuilder` | `menu` | `config/menu.ts` |
|
||||
| `ModuleConfigSourceBuilder` | `module` | `config/module.ts` |
|
||||
|
||||
Form disabled → `form`, `formDefinition`, `formDialog` are `null`. Datatable disabled → `dataTable` and `menu` are `null`, and the list-page builder emits a page without a table.
|
||||
|
||||
## Generated Frontend Architecture
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/
|
||||
├── api/ # create.ts, delete.ts, detailed.ts, keys.ts, list.ts, update.ts
|
||||
├── config/ # translation/, menu.ts, messages.ts, meta.ts, module.ts, routing.tsx
|
||||
├── entities/ # Entity.ts (from database columns)
|
||||
├── pages/ListPage/
|
||||
└── view/
|
||||
├── forms/MainForm/ # Component.tsx, constants.ts, form.ts, index.ts, submit.ts
|
||||
├── layouts/Layout.tsx
|
||||
└── widgets/ # DataTable.tsx, DeleteTrigger.tsx, ManageTrigger.tsx
|
||||
```
|
||||
|
||||
`api/` deliberately has no barrel file — each CRUD operation is its own file, imported directly (`import { useCreate } from "../../../api/create"`); `keys.ts` owns query key builders, `list.ts`/`detailed.ts` their invalidation helpers.
|
||||
|
||||
Layer ownership: `entities/Entity.ts` is shared by API hooks, forms, and widgets; `api/` owns Axios calls, hooks, keys, and notifications; `pages/` owns route composition and permission gates; `view/forms/` owns schema, values, UI, and submission; `view/widgets/` owns stateful interactions; `config/` integrates routes, menu, translations, and permissions with the module registry.
|
||||
|
||||
## Extending
|
||||
|
||||
To add a generated frontend artifact: add its configuration type under `entities/`, UI controls to `ModuleGeneratorForm`, a focused builder under `data/builders/` (for dynamic source), the payload field to `GenerateModulePayload` + `buildGenerateModulePayload`, accept and write it in `module_controller.ts`, and add static structure to `_template/` when every module needs it. Verify generation with and without the asset selected.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Module Generator Troubleshooting
|
||||
|
||||
## Generated Files Exist but the Module Is Not Loaded
|
||||
|
||||
Docker file watching does not reliably pick up entirely new directories: the backend discovers `apps/backend/modules/` routes at startup, and the admin app discovers `apps/admin/src/modules/**/config/module.ts` through Vite's module registry. Restart both services and reload the admin app:
|
||||
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
Still not loaded? Check logs for TypeScript errors, invalid generated imports, route-loading or manifest errors:
|
||||
|
||||
```bash
|
||||
docker compose logs --tail=200 backend admin
|
||||
```
|
||||
|
||||
(Outside Compose, restart the backend and admin dev processes manually.)
|
||||
|
||||
## Module Loads but Is Missing from the Menu
|
||||
|
||||
Usually a permission mismatch or invalid menu/module configuration. Check in order:
|
||||
|
||||
1. **Permission slugs match exactly** — backend `apps/backend/modules/<ModuleName>/config/permissions.ts` (e.g. `{ slug: 'products.view' }`) vs frontend `apps/admin/src/modules/<name>/config/meta.ts` (`MODULE_PERMISSION_VIEW = 'products.view'`), for view/create/update/delete. Pluralization is heuristic — already-plural or irregular names need manual correction.
|
||||
2. **The current role owns the view permission** — the menu filters on `MODULE_PERMISSION_VIEW`: the permission exists in access control, the role has it with the exact slug, and the session was refreshed after role changes.
|
||||
3. **`config/menu.ts`** — valid exported menu factory, `path` uses `MODULE_ROOT_PATH`, `permissions` contains `MODULE_PERMISSION_VIEW`, the route exists. The menu is generated with the Datatable asset — without it, add a menu manually or keep the module hidden.
|
||||
4. **`config/module.ts`** — exports a valid `AppModuleConfig`, unique module ID, not `enabled: false`, exposes `menu`, `routes`, `messages`.
|
||||
5. **`config/routing.tsx`** — `MODULE_ROOT_PATH`, the menu path, and the generated route all point to the same URL (a broken route and an invisible menu item are separate problems).
|
||||
|
||||
## Final Checks
|
||||
|
||||
Run `npm run build:backend` and `npm run build:admin` to expose generated TypeScript or import errors, then verify: both module directories exist with the selected assets, services were restarted, slugs match, the role has the view permission, and the menu path matches the route. Module discovery errors are reported in backend and admin startup logs.
|
||||
+1
-5
@@ -2,11 +2,7 @@
|
||||
|
||||
The frontend module provides fallback UI for app-level error and recovery states.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/FallbackScreens/
|
||||
```
|
||||
Source: `apps/admin/src/core/FallbackScreens/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# File Manager Module
|
||||
|
||||
The File Manager module provides authenticated file browsing and file operations for the admin app.
|
||||
It has a backend API for storage operations and a frontend UI for browsing, previewing, uploading,
|
||||
renaming, deleting, duplicating, and editing supported files.
|
||||
Authenticated file browsing and file operations for the admin app: a backend API for storage operations and a frontend UI for browsing, previewing, uploading, renaming, deleting, duplicating, and editing supported files.
|
||||
|
||||
## Source Map
|
||||
|
||||
@@ -24,19 +22,12 @@ renaming, deleting, duplicating, and editing supported files.
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- Browse folders and files.
|
||||
- Filter, sort, and paginate directory entries.
|
||||
- Preview file details and text content for supported extensions.
|
||||
- Stream raw files for preview or download.
|
||||
- Create folders.
|
||||
- Upload files with progress and cancellation support.
|
||||
- Rename, delete, and duplicate entries.
|
||||
- Search entries.
|
||||
- Open an image editor for image files.
|
||||
- Create and edit image map zones for supported image files.
|
||||
- Pick files through reusable frontend picker components.
|
||||
- Insert files into RichText editor through reusable frontend picker components.
|
||||
- Use local filesystem storage or S3-compatible storage through Adonis Drive.
|
||||
- Browse, filter, sort, paginate, and search folders and files.
|
||||
- Preview details and text content; stream raw files.
|
||||
- Create folders; upload with progress and cancellation; rename, delete, duplicate entries.
|
||||
- Image editor and image map zones for supported image files.
|
||||
- Reusable picker components for choosing files and inserting them into the RichText editor.
|
||||
- Local filesystem or S3-compatible storage through Adonis Drive.
|
||||
|
||||
## High-Level Flow
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# File Manager Backend
|
||||
|
||||
Authenticated file operations under `/file-manager`: validation, authorization, path normalization, orchestration, with raw storage delegated to a storage adapter. Source: `apps/backend/app/base_modules/FileManager/`.
|
||||
|
||||
## Request Flow
|
||||
|
||||
```txt
|
||||
routes.ts
|
||||
-> controller/file_manager_controller.ts
|
||||
-> validator/*
|
||||
-> policy/file_manager_policy.ts
|
||||
-> services/file_manager_service.ts
|
||||
-> services/storage_service/ (interface -> factory -> fs | s3 adapter)
|
||||
-> Adonis Drive
|
||||
```
|
||||
|
||||
The controller stays thin (auth checks, validation, path extraction, status codes, response shape). `FileManagerService` owns orchestration; storage adapters own raw persistence.
|
||||
|
||||
## Routes
|
||||
|
||||
All prefixed with `/file-manager`, behind `middleware.auth()`; controller methods check policy abilities (`policy/file_manager_policy.ts`) before doing work. When adding an operation, add or reuse an explicit ability first.
|
||||
|
||||
| Method | Route | Controller Method | Ability | Purpose |
|
||||
| -------- | ------------------ | ----------------- | ---------- | ------------------------------------------------------- |
|
||||
| `GET` | `/` and `/*` | `index` | `viewList` | List entries for root or a nested directory. |
|
||||
| `GET` | `/files/details/*` | `show` | `view` | Return file metadata and optional text preview content. |
|
||||
| `GET` | `/files/raw/*` | `raw` | `view` | Stream raw file content. |
|
||||
| `GET` | `/search?q=value` | `search` | `viewList` | Search entries recursively. |
|
||||
| `POST` | `/folders` | `createFolder` | `create` | Create a folder. |
|
||||
| `POST` | `/files` | `upload` | `create` | Upload a file. |
|
||||
| `POST` | `/files/duplicate` | `duplicate` | `create` | Duplicate one or more **file** entries. |
|
||||
| `PUT` | `/entries/rename` | `rename` | `update` | Rename or move an entry within its parent path. |
|
||||
| `DELETE` | `/*` | `destroy` | `delete` | Delete an entry. |
|
||||
|
||||
## Validation
|
||||
|
||||
Validators in `validator/`: `list_validator.ts` (filter, sort, order, max results, pagination token), `create_directory_validator.ts`, `rename_entry_validator.ts`, `upload_file_validator.ts`, `duplicate_entries_validator.ts`, plus shared helpers in `schemas.ts`.
|
||||
|
||||
Path extraction from wildcard routes goes through `helpers/get_validated_path_from_request.ts` — never parse wildcard paths in controllers or storage adapters.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Owns |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `file_manager_service.ts` | Orchestration: listing with filter/sort/pagination, file detail + preview composition, moves, unique upload and copy names, search, storage calls. |
|
||||
| `path_service.ts` | Client↔storage path conversion, safe joins, dirname/basename, extension metadata, filename sanitizing, unique names. Use it instead of string manipulation. |
|
||||
| `entry_list_service.ts` | List behavior on raw storage entries: category filter, sort, folders-first, pagination + token encoding. Frontend list controls must stay compatible with its accepted values. |
|
||||
| `file_preview_service.ts` | Text preview content for previewable extensions (defined in the same file). For rich previews, keep the response small and stream via `/files/raw/*`. |
|
||||
| `entry_search_service.ts` | Path search including matching parent folders for context. Receives an abort signal from the controller — long searches should respect it. |
|
||||
|
||||
## Storage
|
||||
|
||||
Abstractions in `services/storage_service/`:
|
||||
|
||||
- `storage_service_interface.ts` — the adapter contract: list (flat and recursive), existence, metadata, read/stream/write, create directory, move, delete, copy. Services call the interface, never a concrete adapter.
|
||||
- `fs_storage_service.ts` — filesystem adapter over the Adonis Drive `fs` disk: maps filesystem objects to entries, handles streams and metadata, preserves directory marker behavior.
|
||||
- `s3_storage_service.ts` — S3-compatible adapter over the `s3` disk: maps objects and common prefixes to entries, uploads multipart files by streaming the temp file via `putStream`, creates folders with directory marker objects, moves folders by moving every object under the prefix, deletes files exactly and folders by prefix, copies file entries. S3 has no native directories, so it shares the `.keep` marker convention. **Folder operations must use slash-terminated prefixes** — deleting `test/` must not delete `test123.txt`.
|
||||
- `fs_directory_marker_service.ts` — encapsulates the `.keep` marker convention; never duplicate marker logic outside it.
|
||||
- `create_storage_service.ts` — factory; the active adapter (`fs` or `s3`) is selected by `DRIVE_DISK`.
|
||||
|
||||
## Drive Configuration
|
||||
|
||||
`apps/backend/config/drive.ts` configures two disks: `fs` (local development, filesystem storage) and `s3` (S3-compatible object storage, including local MinIO).
|
||||
|
||||
S3 environment variables:
|
||||
|
||||
| Variable | Local Docker Value | Purpose |
|
||||
| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `AWS_ACCESS_KEY_ID` | `minioadmin` | S3 access key. |
|
||||
| `AWS_SECRET_ACCESS_KEY` | `minioadmin` | S3 secret key. |
|
||||
| `AWS_REGION` | `us-east-1` | S3 region. |
|
||||
| `S3_BUCKET` | `crux-drive` | Bucket used by the file manager. |
|
||||
| `S3_ENDPOINT` | `http://minio:9000` in Docker, `http://localhost:9000` outside Docker | S3 API endpoint. |
|
||||
| `S3_CDN_URL` | `http://localhost:9000/crux-drive/` | Browser-facing public URL base for file URLs. |
|
||||
|
||||
For MinIO the disk uses `forcePathStyle: true` and `supportsACL: false`; bucket public-read is handled by the Docker bucket setup container.
|
||||
|
||||
## Local S3 Simulation
|
||||
|
||||
`docker-compose.yml` runs `minio` (the S3 server) and `minio_create_bucket` (one-shot setup that creates the bucket and enables anonymous download — it exits after setup, which is normal). Console at `http://localhost:9001`, credentials `minioadmin` / `minioadmin`. After dependency or Dockerfile changes: `docker compose build --no-cache && docker compose up`.
|
||||
|
||||
## Response Shape
|
||||
|
||||
```ts
|
||||
interface EntryBase {
|
||||
name: string;
|
||||
path: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
interface FileEntry extends EntryBase {
|
||||
type: "file";
|
||||
size?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface FolderEntry extends EntryBase {
|
||||
type: "folder";
|
||||
}
|
||||
```
|
||||
|
||||
List responses: `{ data: Entry[]; meta: { nextPaginationToken: string | null } }`.
|
||||
|
||||
## Backend Change Checklist
|
||||
|
||||
1. Confirm the policy ability for the operation.
|
||||
2. Add or update a validator for all request input.
|
||||
3. Keep client paths and storage paths separated through `path_service.ts`.
|
||||
4. Update the frontend DTO or mapper if the response shape changes.
|
||||
5. Update this documentation when routes, permissions, storage contracts, or response shapes change.
|
||||
@@ -0,0 +1,123 @@
|
||||
# File Manager Frontend
|
||||
|
||||
Admin file manager UI: browsing, previews, uploads, delete/duplicate/rename, image editing and image map tools, settings, and reusable file pickers. Source: `apps/admin/src/core/FileManager/`.
|
||||
|
||||
**Main principle: React Query owns server state** (entries, file details, raw blobs, search, mutations); **MobX owns local UI and operation state** (active/selected/expanded paths, filters and sort, dialog state, upload/delete progress, loaded-entry lookup).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
FileManager/
|
||||
config/ Routes, menu entries, messages, translations.
|
||||
data/
|
||||
api/ Raw HTTP methods and DTOs.
|
||||
hooks/ Feature hooks that combine queries with stores.
|
||||
mappers/ DTO-to-entity mapping.
|
||||
queries/ React Query hooks, keys, mutations, and cache helpers.
|
||||
stores/ MobX local state stores.
|
||||
entities/ Domain entities and settings types.
|
||||
providers/ Feature providers and context hooks.
|
||||
pages/ Route-level screens.
|
||||
shared/ FileManager utilities that do not depend on view code.
|
||||
view/ UI components, widgets, forms, and layouts.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Module mounted at `/file-manager/*`, configured in `config/routing.tsx`. Menu groups: `Drive` (browsing, viewer), `Tools` (image editor, image map), `Settings`.
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| -------------------------------------- | ------------------- | ------------------------------------------- |
|
||||
| `/file-manager/drive/explorer/*` | `DriveExplorerPage` | Browse folders and files. |
|
||||
| `/file-manager/drive/viewer/-/*` | `DriveViewerPage` | View file details and preview. |
|
||||
| `/file-manager/drive/settings` | `DriveSettingsPage` | Configure file-manager UI settings. |
|
||||
| `/file-manager/tools/image-editor/-/*` | `ImageEditorPage` | Edit image files. |
|
||||
| `/file-manager/tools/image-map/-/*` | `ImageMapPage` | Create and edit image map areas for images. |
|
||||
|
||||
Build file-manager URLs with the helpers in `shared/routing/driveLinks.ts`, `shared/routing/openEntryNewTab.ts`, `shared/path/leadingSlash.ts` — never manually.
|
||||
|
||||
## API Boundary
|
||||
|
||||
Raw backend calls are centralized in `data/api/api.ts` + `data/api/dto.ts`, calling the routes documented in [backend.md](./backend.md):
|
||||
|
||||
| API Method | Backend Route |
|
||||
| ----------------- | ------------------------------------ |
|
||||
| `getEntries` | `GET /file-manager/*` |
|
||||
| `getFile` | `GET /file-manager/files/details/*` |
|
||||
| `getRawFile` | `GET /file-manager/files/raw/*` |
|
||||
| `createDirectory` | `POST /file-manager/folders` |
|
||||
| `renameEntry` | `PUT /file-manager/entries/rename` |
|
||||
| `removeEntry` | `DELETE /file-manager/*` |
|
||||
| `uploadFile` | `POST /file-manager/files` |
|
||||
| `duplicateFiles` | `POST /file-manager/files/duplicate` |
|
||||
| `searchEntries` | `GET /file-manager/search` |
|
||||
|
||||
DTOs are mapped before reaching UI code (`entities/entry.ts`, `data/mappers/entryMapper.ts`).
|
||||
|
||||
## Data Flow
|
||||
|
||||
- **Reading entries** — `useEntries()` builds the query payload from `ExplorerStore`, fetches via React Query (`useGetEntries`), and syncs loaded entries into `EntriesStore`. `EntriesStore` is not the fetch owner — it is an index of loaded entries for the sidebar tree, selection, active-entry lookup, and operation state.
|
||||
- **Deep links** — for a nested folder opened directly, `useEntries()` preloads ancestor folders so the sidebar tree and entry index work for paths like `/folder/subfolder/current`.
|
||||
- **Mutations** — `useCreateDirectory`, `useRenameEntry`, `useDeleteEntry`, `useDuplicateFiles`, `useUploadFile`. After success, the UI updates the local cache and stores directly instead of invalidating entries queries — no refetch storm when many items are created, deleted, or uploaded in sequence.
|
||||
|
||||
## Query Cache Updates
|
||||
|
||||
`data/queries/cache.ts` updates existing React Query cache data in memory (no network request):
|
||||
|
||||
```ts
|
||||
addEntriesToCache(parentPath, entries); // filter-aware insertion into the parent folder's cached lists
|
||||
removeEntriesFromCache(paths); // removal from all cached entry pages
|
||||
renameEntryInCache(prevPath, renamedEntry);
|
||||
```
|
||||
|
||||
Tradeoff: since the module avoids refetching, exact backend sort order may drift after local insertion until the folder is naturally refetched.
|
||||
|
||||
## Stores
|
||||
|
||||
All in `data/stores/`:
|
||||
|
||||
| Store | Owns |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `EntriesStore` | Index of loaded entries by path, child/selected/active lookup, local remove and rename. Never fetches. |
|
||||
| `ExplorerStore` | Active path, selected paths, expanded tree paths, breadcrumbs, filter, sort. |
|
||||
| `DriveDialogStore` | Create-directory and rename dialogs, entry selected for rename. |
|
||||
| `UploadOperationStore` | Upload queue, progress, retry/remove/abort, operation card visibility. |
|
||||
| `DeleteOperationStore` | Delete queue, progress, retry/remove, operation card visibility. |
|
||||
| `SettingsStore` | Persisted view mode, compact mode, sidebar visibility, grid size. |
|
||||
|
||||
Operation stores receive the React Query mutation function from the view layer and manage only operation state.
|
||||
|
||||
## Providers
|
||||
|
||||
`DriveProvider` creates and exposes the feature stores — `const { entriesStore, explorerStore } = useDrive();`. `SettingsProvider` owns persisted settings.
|
||||
|
||||
## Pages and Widgets
|
||||
|
||||
- `DriveExplorerPage` — main browser: `DriveProvider`, toolbar, content, sidebar, footer, operation watcher.
|
||||
- `DriveViewerPage` — details and preview, via `useGetFile()`.
|
||||
- `DriveSettingsPage` — persisted UI preferences.
|
||||
- `ImageEditorPage` / `ImageMapPage` — use `useGetFile()` + `useGetRawFile()` (editor also `useUploadFile()`). Config and helpers live in `view/widgets/ImageEditor/` so `view` code never imports from `pages`.
|
||||
- `FilePickerDialog` — reusable picker widgets in `view/widgets/FilePickerDialog/`; keep picker behavior feature-scoped and pass reusable UI state through props/context instead of coupling low-level components to page-level stores.
|
||||
|
||||
## View Layer Rules
|
||||
|
||||
Feature widgets may use `useDrive()` and `useSettings()`. Lower-level reusable components (`ExplorerFileEntryGrid`, `FileManagerFileIcon`, `FilePreview`, …) stay prop-driven. Never import backend DTOs into view components — views consume frontend entities and props.
|
||||
|
||||
## File Types and Icons
|
||||
|
||||
Helpers in `shared/file/`; icons in `apps/admin/public/media/files/`. To add an icon: add the asset there, update `shared/file/fileTypeImportMap.ts`, and confirm `FileManagerFileIcon` falls back correctly for unknown extensions.
|
||||
|
||||
## Adding a Query or Mutation
|
||||
|
||||
1. Add/reuse a method in `data/api/api.ts`.
|
||||
2. Query: add a key in `data/queries/keys.ts` and a query file in `data/queries/`, mapping DTOs to entities before returning. Mutation: add a mutation hook in `data/queries`, call `mutateAsync` from the consuming widget or operation store.
|
||||
3. Update `EntriesStore` if lookup state changes; update `data/queries/cache.ts` if the visible cache must change.
|
||||
4. Never import MobX stores or providers inside `data/queries`. Don't invalidate entries queries per operation unless a full refetch is intentional.
|
||||
|
||||
## Frontend Change Checklist
|
||||
|
||||
1. Is the data server state or local UI state?
|
||||
2. Backend calls stay in `data/api`; React Query hooks stay free of MobX provider imports.
|
||||
3. Map DTOs before view components.
|
||||
4. Update cache helpers when mutation results should appear immediately.
|
||||
5. Update backend docs if the change requires a backend contract change.
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
The backend Permission module exposes available permission definitions. It is used by AccessControl when assigning permissions to roles.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Permission/
|
||||
```
|
||||
Source: `apps/backend/app/base_modules/Permission/`
|
||||
|
||||
## Route
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
The frontend module provides reusable permission-checking primitives for the admin app.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/Permission/
|
||||
```
|
||||
Source: `apps/admin/src/core/Permission/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Web Manager
|
||||
|
||||
The CMS behind the public website. Editors assemble localized pages from ready-made blocks in the
|
||||
admin panel; the public Next.js site renders what they publish.
|
||||
|
||||
| Part | Source | Owns |
|
||||
| ---------------------- | ------------------------------------------- | -------------------------------------------------------- |
|
||||
| Backend (`WebManager`) | `apps/backend/app/base_modules/WebManager/` | Data, validation, draft/publish transactions, APIs |
|
||||
| Admin (`Web Studio`) | `apps/admin/src/core/WebStudio/` | Editor UI, drafts, publishing actions, preview, settings |
|
||||
| Public site | `apps/web/web-runtime/`, `apps/web/app/` | Server rendering, layouts, blocks, caching, SEO |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------------- | ------------------------------------------------------------------ |
|
||||
| [Backend](./backend.md) | Data model, HTTP API, query budget, tuning constants. |
|
||||
| [Admin](./admin.md) | Web Studio: routes, editor, config files, UI conventions. |
|
||||
| [Web](./web.md) | Public site rendering, layouts, hero, SEO, degradation. |
|
||||
| [Integration](./integration.md) | Contracts, cross-app flows, caching, env wiring, extending, tests. |
|
||||
|
||||
## For Editors
|
||||
|
||||
A page is a title, an optional hero, an ordered list of blocks, and SEO fields. Blocks are the
|
||||
prebuilt sections — text columns, FAQ, media, banner, article list — so building a page is picking
|
||||
blocks and filling them in, not writing markup.
|
||||
|
||||
| Term | What it means |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Page** | One address in one language, e.g. `/en/about-us`. |
|
||||
| **Group key** | Ties the same page across languages together (`about-us` links `/en/about-us` and `/uk/pro-nas`) so the site can offer a language switch. |
|
||||
| **Page type** | Landing, Blog, or Blog item. Decides the layout and which blocks are offered. |
|
||||
| **Hero** | The opening section. Four intensities: none, low, middle, high. |
|
||||
| **Draft** | Your working copy. Never visible to visitors. |
|
||||
| **Published** | Live on the site. |
|
||||
| **Unpublished** | Taken down, but kept — content, blocks and address stay reserved, so you can put it back. |
|
||||
| **Preview** | A private, time-limited link that renders the draft exactly as the site would. |
|
||||
| **Site settings** | Header menus, footer, and legal links — shared by every page in one language. |
|
||||
|
||||
Everything you edit is a draft. A live page keeps serving the old version until you press Publish,
|
||||
so an unfinished edit can never reach visitors. Nothing is deleted unless you press Delete.
|
||||
|
||||
## Page Lifecycle
|
||||
|
||||
```txt
|
||||
create publish
|
||||
(none) ────────► draft ───────────► published ◄──────┐
|
||||
│ │ │
|
||||
discard│ │unpublish │publish
|
||||
▼ ▼ │
|
||||
deleted unpublished ───────┘
|
||||
|
||||
published + open draft clone: visitors see the published row, the editor edits the clone
|
||||
```
|
||||
|
||||
Every write happens in one transaction with rows locked `FOR UPDATE`; routes and permissions are in [backend.md](./backend.md), purge behavior in [integration.md](./integration.md#caching-and-revalidation).
|
||||
|
||||
- **Create** — `createDraft` validates the payload, asserts the `locale` + `path` is free across every root row (any status), and inserts the page with `status = draft` plus its components. Nothing public yet.
|
||||
- **Save a draft** — `ensureDraft` edits a `draft` or `unpublished` page in place; for a `published` page it reuses or creates the draft clone. Components are replaced wholesale in the submitted order; a clone inherits its original's address.
|
||||
- **Edit a published page** — the clone is a second row with `draft_of_id` pointing at the original (one clone per page, enforced by a partial unique index). Edit always opens the clone; the original keeps serving until the clone is published.
|
||||
- **Preview** — a signed token (HMAC over `APP_KEY`, TTL 900s clamped to 60–3600s) lets the web app render the draft in an iframe sheet: `force-dynamic`, `noindex`, disallowed in robots. Absent, tampered, or expired tokens render "preview unavailable".
|
||||
- **Publish** — publishes the **last saved** draft (the confirmation dialog says so). A pending clone is always preferred, so publishing can never drop the editor's work: a root draft goes live directly; a clone is folded into its original and deleted. A _structural_ change (first publish, republish, or changed locale/path/title/group key/page type) purges the whole cache tag; anything else purges only the affected page tags.
|
||||
- **Unpublish** — same permission as publish; only valid on a published page (else 409). Sets `status = unpublished`, keeping content, components, and any open clone; the address stays reserved. Full purge — the URL now 404s.
|
||||
- **Republish** — publish again; an unpublished page is edited in place, and republishing counts as structural.
|
||||
- **Discard a draft** — deletes the draft row and its components; a clone's original is untouched. No purge.
|
||||
- **Delete** — the only destructive action, behind a confirmation dialog. Components and any open clone cascade with the row; full purge if it was published.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Web Manager — Admin (Web Studio)
|
||||
|
||||
Admin side of [Web Manager](./Readme.md). Source: `apps/admin/src/core/WebStudio/`.
|
||||
|
||||
## Routes
|
||||
|
||||
```txt
|
||||
/web-studio/pages/listing list, search, filters, row actions
|
||||
/web-studio/pages/-/create new draft
|
||||
/web-studio/pages/-/:id editor (page or draft clone)
|
||||
/web-studio/settings/:section header | footer | legal
|
||||
```
|
||||
|
||||
The list shows title with full path, locale, slug, group key, page type, status, draft marker and
|
||||
updated time; search is debounced and filters, pagination and locale live in the query string
|
||||
(`useWebStudioQueryParams`), so links and the back arrow keep the editor's context. Row actions are
|
||||
Edit, Preview, Open in web app, Publish, Unpublish, Discard and Delete — Edit opens the draft clone
|
||||
when `draftId` is present, so an editor always lands on the working copy.
|
||||
|
||||
## Editor
|
||||
|
||||
The editor is a flat two-column layout: a sticky ~320px Page details column, and Hero / Blocks / SEO
|
||||
as line tabs beside it. One React Hook Form instance (`PAGE_EDITOR_FORM_ID`) owns everything;
|
||||
the header's Save button submits it by form id, and submitting disables the whole fieldset.
|
||||
Actions sit in a split button: Save draft, then Preview / Publish / Unpublish / Discard / Delete in
|
||||
the dropdown, each behind its own control component in `view/controls/` and its own permission.
|
||||
Publish is offered whenever the page is not already live, so it doubles as republish.
|
||||
|
||||
Blocks are added from a sheet filtered to the page type's `allowedBlockTypes`, reordered by drag
|
||||
(`view/components/SortableList.tsx`), and duplicated or removed from their own header. Nested
|
||||
collections — content columns, CTAs, FAQ items, navigation links — behave the same way one level
|
||||
down.
|
||||
|
||||
## Key Files
|
||||
|
||||
| Concern | File |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| Module registration | `config/module.ts`, `config/routing.tsx`, `config/menu.ts` |
|
||||
| Languages | `config/languages.ts` — code, labels, direction, public prefix |
|
||||
| Page types | `config/pages.ts` — allowed blocks and default path per type |
|
||||
| Block catalogue | `config/blocks.ts` — label, description, preview image, factory |
|
||||
| Settings shape | `config/siteSettings.ts` |
|
||||
| HTTP + multipart | `data/api/WebStudioAPI.ts`, `data/api/PagePayloadSerializer.ts` |
|
||||
| Domain types | `entities/` — `PageType`, `PageStatus`, `BlockType`, `HeroType`, block and field shapes |
|
||||
| Form ↔ DTO | `pages/PageEditPage/helpers/toEditorValues.ts`, `view/forms/PageEditorForm/helpers/toCreatePayload.ts` |
|
||||
|
||||
Page types decide what an editor can build:
|
||||
|
||||
| Type | Layout | Blocks offered | Default path |
|
||||
| ----------- | ---------- | --------------------------------------------------- | ------------ |
|
||||
| `landing` | Standard | all six | `/` |
|
||||
| `blog` | Blog index | none — the listing is rendered from published posts | `/blog` |
|
||||
| `blog-item` | Editorial | all except `articles` | `/` |
|
||||
|
||||
## UI Conventions
|
||||
|
||||
The module is a content tool, not a dashboard of cards. Flat surfaces, few separators, one primary
|
||||
action per screen.
|
||||
|
||||
| Element | Treatment |
|
||||
| ------------------- | -------------------------------------------------------- |
|
||||
| Screen heading | `text-mono text-xl font-semibold` |
|
||||
| Supporting copy | `text-muted-foreground text-sm` |
|
||||
| Editor settings | `bg-muted/40 space-y-5 p-5` |
|
||||
| Top-level block | `bg-muted/35`, compact header, one `border-b` while open |
|
||||
| Nested field group | `border-l-2 pl-4` (`view/components/FieldCard.tsx`) |
|
||||
| Inline actions | small ghost icon buttons |
|
||||
| Destructive trigger | ghost button, destructive text, confirmation dialog |
|
||||
| Empty collection | `bg-muted/35`, generous padding, short copy |
|
||||
|
||||
`view/components/BlockCard.tsx` is named for compatibility but is a flat section, not a card — no
|
||||
shadow, no hover lift, no bordered container nested inside every block. The block picker stays a
|
||||
sheet with a divided list, because picking is a temporary task. Shared visual principles live in the
|
||||
[Admin UI Style Guide](../../../apps/admin/ui-style-guide.md); style Web Studio from Web Studio and do
|
||||
not edit `apps/admin/src/shared` for a module-only visual change.
|
||||
|
||||
A visual change must not alter routes, `locale` / `group_key` query behavior, list filtering or DTO
|
||||
mapping, draft-clone semantics, form values or validation, block factory output, settings payload
|
||||
shape, or the toast / error / loading states. Anything that does is a feature change.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Web Manager — Backend
|
||||
|
||||
Backend of [Web Manager](./Readme.md). Source: `apps/backend/app/base_modules/WebManager/`.
|
||||
Lifecycle mechanics: [Page Lifecycle](./Readme.md#page-lifecycle). Cross-app flows: [Integration](./integration.md).
|
||||
|
||||
## Data Model
|
||||
|
||||
### `web_pages`
|
||||
|
||||
```txt
|
||||
id
|
||||
status -- draft | published | unpublished
|
||||
draft_of_id -- nullable FK to web_pages, ON DELETE CASCADE
|
||||
locale -- en, uk, de
|
||||
path -- '' for home, else 'about-us' or 'blog/post/about-me' (no leading slash)
|
||||
group_key -- links localized versions of the same page
|
||||
page_type -- landing | blog | blog-item
|
||||
title
|
||||
hero -- jsonb, nullable
|
||||
seo_title -- defaults to ''
|
||||
seo_description
|
||||
published_at -- nullable
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
Indexes: `(group_key, status)`, `(locale, path, status)`, a partial unique index on
|
||||
`(locale, path) WHERE status = 'published'`, and a partial unique index on
|
||||
`(draft_of_id) WHERE draft_of_id IS NOT NULL` — one draft clone per page, enforced by the database.
|
||||
|
||||
Rules:
|
||||
|
||||
- `path` is the full route after the locale, without leading or trailing slashes: `/en/blog/post/x`
|
||||
is `locale = en`, `path = blog/post/x`. The editor form shows it with a leading slash.
|
||||
- Public reads only ever match `status = published`.
|
||||
- A root row is `draft_of_id = null`; a draft clone points at its published original.
|
||||
- Route uniqueness is enforced twice: the partial index for live collisions, and
|
||||
`assertNewRouteAvailable` for root rows of any status, so an unpublished page keeps its address.
|
||||
|
||||
### `web_components`
|
||||
|
||||
```txt
|
||||
id
|
||||
page_id -- FK to web_pages, ON DELETE CASCADE
|
||||
type -- content | faq | media-content | articles | banner | media
|
||||
data -- jsonb, block-specific shape validated by validator/web_content_validator.ts
|
||||
sort_order -- position in the page, index of the submitted array
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
Indexed on `(page_id, sort_order)`. Saving a draft deletes and reinserts the whole set rather than
|
||||
diffing it.
|
||||
|
||||
### `web_settings`
|
||||
|
||||
```txt
|
||||
id
|
||||
locale -- unique
|
||||
header -- jsonb { menus: [{ title, items }], ctaItems }
|
||||
footer -- jsonb { copyright, menus }
|
||||
legal -- jsonb { privacyPath, termsPath }
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
One row per locale, upserted by locale. Shape lives in `WebStudio/config/siteSettings.ts`.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Admin routes are under `/web-manager` behind `middleware.auth()`; every action authorizes through
|
||||
`WebManagerPolicy`.
|
||||
|
||||
| Method | Route | Permission | Purpose |
|
||||
| ------ | -------------------------------------- | ---------------------- | ------------------------------------------- |
|
||||
| GET | `/web-manager/pages` | `web_manager.view` | Paginated root rows with a `draft` relation |
|
||||
| GET | `/web-manager/pages/group-keys` | `web_manager.view` | Distinct group keys, alphabetized |
|
||||
| POST | `/web-manager/pages` | `web_manager.create` | Create a draft (multipart) |
|
||||
| GET | `/web-manager/pages/:id` | `web_manager.view` | Page + components |
|
||||
| PUT | `/web-manager/pages/:id/draft` | `web_manager.update` | Save a draft (multipart) |
|
||||
| POST | `/web-manager/pages/:id/publish` | `web_manager.publish` | Publish, returns `meta.revalidation` |
|
||||
| POST | `/web-manager/pages/:id/unpublish` | `web_manager.publish` | Take down, returns `meta.revalidation` |
|
||||
| POST | `/web-manager/pages/:id/preview-token` | `web_manager.view` | Signed preview token |
|
||||
| DELETE | `/web-manager/pages/:id` | `web_manager.delete` | Delete page or discard draft |
|
||||
| GET | `/web-manager/settings` | `web_manager.view` | Settings, all locales or one |
|
||||
| PUT | `/web-manager/settings` | `web_manager.settings` | Upsert settings for a locale |
|
||||
|
||||
Public routes are under `/web-manager/public`, unauthenticated, and return published content only —
|
||||
except `preview`, which is gated by the signed token.
|
||||
|
||||
| Route | Returns |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `/pages/resolve?locale=&path=` | Page, components, settings, breadcrumbs, `languageAlternatives` |
|
||||
| `/pages/preview?token=` | The same shape for any status, for a valid token |
|
||||
| `/pages/sitemap?page=&perPage=` | `locale`, `path`, `groupKey`, `pageType`, `publishedAt`, `updatedAt` + `pagination` |
|
||||
| `/blog/pages?locale=&page=&perPage=` | Published `blog-item` pages, newest first, with hero media |
|
||||
| `/settings?locale=` | Header, footer, legal for one locale |
|
||||
|
||||
The sitemap is the one public read proportional to the site, so it is paginated
|
||||
(`SITEMAP_PAGE_SIZE` 1000, capped at `SITEMAP_MAX_PAGE_SIZE` 5000) and the caller walks it;
|
||||
`groupKey` is included so hreflang alternates need no second lookup. The resolve payload carries
|
||||
`publishedAt` / `updatedAt`, which the web app emits as `datePublished` / `dateModified`.
|
||||
|
||||
Permissions are seeded from `WebManager/config/permissions.ts`: `web_manager.view`, `.create`,
|
||||
`.update`, `.publish`, `.delete`, `.settings`.
|
||||
|
||||
## Public Resolve Query Budget
|
||||
|
||||
| Query | Bounded by |
|
||||
| --------------------- | ----------------------------------------------------------- |
|
||||
| the page row | route lookup |
|
||||
| its components | blocks on the page |
|
||||
| site settings | one row per locale |
|
||||
| language alternatives | locales sharing the `group_key` |
|
||||
| breadcrumbs | path depth (~4–6 rows), plus one query per group-key jump |
|
||||
| internal page links | internal links in the page's own content, skipped when none |
|
||||
|
||||
Two details keep that true. **Link map by id**: `collectInternalPageIds` walks hero, components and
|
||||
settings first, then `createPageLinkMap` fetches only those ids — unknown or unpublished ids are
|
||||
absent and render as an empty href. **Breadcrumbs by candidate**: the walk can only climb to a
|
||||
shorter prefix of the current path, the home page, or the group key in
|
||||
`BREADCRUMB_PAGE_TYPE_PARENTS`, so `buildBreadcrumbs` asks for precisely those rows; a second query
|
||||
happens only when a group-key jump lands on a branch whose ancestors were not already loaded
|
||||
(`ancestorsAlreadyLoaded`). Both are served by the existing indexes.
|
||||
|
||||
## Tuning Constants
|
||||
|
||||
Backend tuning constants live in `WebManager/config/general.ts`: media upload path and 10 MB size
|
||||
limit, preview token TTLs, sitemap page sizes, and `BREADCRUMB_PAGE_TYPE_PARENTS` (which makes
|
||||
`blog` the breadcrumb parent of every `blog-item`).
|
||||
@@ -0,0 +1,185 @@
|
||||
# Web Manager — Integration
|
||||
|
||||
How the three apps of [Web Manager](./Readme.md) connect: contracts, flows, caching, env wiring, and cross-app extension checklists.
|
||||
|
||||
## How the Three Apps Connect
|
||||
|
||||
```txt
|
||||
┌───────────────────────┐ ┌──────────────────────────┐
|
||||
│ Admin (Web Studio) │───────►│ Backend (AdonisJS) │
|
||||
│ editor, drafts, │◄───────│ pages, components, │
|
||||
│ publish, preview │ DTOs │ settings, transactions │
|
||||
└───────────┬───────────┘ └────────────┬─────────────┘
|
||||
│ │
|
||||
│ iframe /preview?token=… │ POST /api/revalidate (tag purge)
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Public site (Next.js) │
|
||||
│ server-renders GET /web-manager/public/*, tagged 60s cache │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The three never share code, only contracts:
|
||||
|
||||
| Contract | Defined by | Consumed by |
|
||||
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
|
||||
| Page / block payloads | `types/web_content.ts`, `validator/web_content_validator.ts` | Admin DTOs, `web-runtime/types.ts` |
|
||||
| Public read API | `WebManager/routes.ts` (`/web-manager/public/*`) | `web-runtime/api/web-runtime-api.ts` |
|
||||
| Cache tag names | `services/web_revalidation_service.ts` | `web-runtime/api/cache-tags.ts` — **must stay in sync** |
|
||||
| Preview token | `services/preview_token_service.ts` | Admin preview sheet, `app/[locale]/preview/` |
|
||||
| Block / page types | `types/web_content.ts` | `WebStudio/config/blocks.ts`, `web-runtime/*/registry` |
|
||||
|
||||
## Save Flow (admin → backend)
|
||||
|
||||
```txt
|
||||
editor submits React Hook Form values
|
||||
PagePayloadSerializer → FormData: `payload` JSON string + one binary part per new file
|
||||
POST /web-manager/pages | PUT /web-manager/pages/:id/draft (multipart/form-data)
|
||||
PagePayloadService parses `payload`, validates, stores files through FileManager,
|
||||
rewrites { name, fileKey } media values into { name, ref }
|
||||
WebManagerService writes page + components in one transaction
|
||||
```
|
||||
|
||||
Media inside `hero` and block `data` is either `{ name, ref }` (already stored) or
|
||||
`{ name, fileKey }` (new upload); only `ref` values are ever persisted. Part names are the
|
||||
`fileKey`s (`file-0`, `file-1`, …). A request without a `payload` part is read as a plain JSON body,
|
||||
so the endpoints stay usable from tests and scripts. The payload travels as one JSON string so
|
||||
booleans, numbers, nulls and empty arrays survive the multipart transport.
|
||||
|
||||
## Publish Flow (backend → public site)
|
||||
|
||||
```txt
|
||||
publish/unpublish/settings commit
|
||||
WebRevalidationService POSTs { tags } to WEB_APP_URL/api/revalidate with X-Revalidate-Secret
|
||||
Next.js revalidateTag(tag, 'max') per tag
|
||||
response meta.revalidation = purged | retrying | disabled → admin toast
|
||||
```
|
||||
|
||||
## Public Request Flow (visitor → web → backend)
|
||||
|
||||
```txt
|
||||
GET /en/about-us
|
||||
next-intl middleware resolves the locale segment
|
||||
app/[locale]/[[...path]]/page.tsx → resolvePageRenderData(locale, path)
|
||||
GET /web-manager/public/pages/resolve?locale=en&path=about-us (next: revalidate 60 + tags)
|
||||
serializePublicPage returns page, components, settings, breadcrumbs, languageAlternatives
|
||||
renderer.tsx → header + RegisteredPageLayout + footer; blocks via the block registry
|
||||
```
|
||||
|
||||
Before it leaves the backend, `PublicContentService.prepare` resolves internal links to hrefs,
|
||||
turns stored file refs into URLs, and sanitizes rich text (`content_sanitizer.ts`: allowlisted tags,
|
||||
`http/https/mailto/tel` only, `rel` limited to `nofollow`/`noopener`/`noreferrer`). The web app
|
||||
renders rich text as HTML and trusts that sanitization.
|
||||
|
||||
## Caching and Revalidation
|
||||
|
||||
Every public fetch is tagged and time-boxed, so a purge that never lands costs about a minute of
|
||||
stale content rather than correctness.
|
||||
|
||||
| Fetch | `revalidate` | Tags |
|
||||
| --------- | ------------ | --------------------------------------------------------------- |
|
||||
| page | 60s | `web-manager`, `web-manager:page:{locale}:{path}`, settings tag |
|
||||
| settings | 60s | `web-manager`, `web-manager:settings:{locale}` |
|
||||
| blog list | 60s | `web-manager`, `web-manager:blog:{locale}` |
|
||||
| sitemap | 3600s | `web-manager` only — it spans every locale and page |
|
||||
|
||||
Tag builders are duplicated in `web_revalidation_service.ts` (backend) and `cache-tags.ts` (web) and
|
||||
must stay in sync; both sides have tests over the names, because a drift stops purges from landing
|
||||
without anything failing.
|
||||
|
||||
Delivery is one attempt inline with the request (so the outcome can be reported to the editor) and
|
||||
attempts 2–3 in the background with exponential backoff and jitter. Publish, unpublish and settings
|
||||
save therefore return `meta.revalidation`:
|
||||
|
||||
| `status` | Meaning |
|
||||
| ---------- | ----------------------------------------------------------------------- |
|
||||
| `purged` | The web app confirmed the purge. |
|
||||
| `retrying` | The first attempt failed; background retries are running. |
|
||||
| `disabled` | `WEB_APP_URL` or `WEB_REVALIDATE_SECRET` is unset, so nothing was sent. |
|
||||
|
||||
The admin shows a warning toast on anything but `purged`, so an editor is never told the site is up
|
||||
to date when it is not. Retries live in memory only — a restart drops them onto the same 60s floor a
|
||||
failed purge already has. Exhausting all attempts logs at `error`; publishing again sends a fresh
|
||||
purge. A deployment missing either variable logs each skipped purge at `warn` in production.
|
||||
|
||||
## Configuration
|
||||
|
||||
| App | Variable | Purpose |
|
||||
| ------- | ----------------------- | ----------------------------------------------------------------------------- |
|
||||
| backend | `APP_KEY` | Signs preview tokens. |
|
||||
| backend | `WEB_APP_URL` | Where to POST cache purges. Server-side URL (compose service name in Docker). |
|
||||
| backend | `WEB_REVALIDATE_SECRET` | Must equal the web app's `REVALIDATE_SECRET`. |
|
||||
| web | `BACKEND_API_URL` | Backend base URL, without the `/web-manager/public/...` part. Required. |
|
||||
| web | `REVALIDATE_SECRET` | Guards `/api/revalidate`. |
|
||||
| web | `SITE_URL` | Canonical origin for metadata. Required in production. |
|
||||
| admin | `VITE_WEB_URL` | Public site origin used to build preview and "view page" links. |
|
||||
|
||||
Backend tuning constants live in `WebManager/config/general.ts` — see
|
||||
[Backend → Tuning Constants](./backend.md#tuning-constants).
|
||||
|
||||
## Extending
|
||||
|
||||
### Add a block type
|
||||
|
||||
1. Backend: add the value to `BLOCK_TYPES` and its data type in `WebManager/types/web_content.ts`,
|
||||
and its shape to the component union in `validator/web_content_validator.ts`, so the payload is
|
||||
validated server-side.
|
||||
2. Admin: add the enum value to `entities/BlockType.ts`, the block shape under `entities/block/`,
|
||||
an editor under `view/forms/blocks/<Name>Block/` with its `schema.ts`, and an entry in
|
||||
`config/blocks.ts` (label, description, preview image, `createBlock` factory). Add it to the
|
||||
`allowedBlockTypes` of every page type that should offer it in `config/pages.ts`.
|
||||
3. Web: add the renderer under `web-runtime/blocks/components/` and register it in
|
||||
`web-runtime/blocks/registry.tsx`, plus the type in `web-runtime/types.ts`.
|
||||
4. Translations: add the label and description keys to both `config/translation/en.json` and
|
||||
`uk.json` — the two files are kept at exact key parity.
|
||||
|
||||
`apps/web/tests/registry.test.ts` keeps a hand-mirrored copy of `BLOCK_TYPES` and `PAGE_TYPES` and
|
||||
asserts the registries match it exactly — add the type there too, and it becomes the tripwire for a
|
||||
block the backend can publish but the site cannot draw.
|
||||
|
||||
### Block preview images
|
||||
|
||||
Wireframe SVGs in `apps/admin/public/media/web-studio/blocks/<type>.svg`, referenced by
|
||||
`previewImage` in `config/blocks.ts` and rendered by `BlockPickerSheet` as a 4:3 thumbnail. They are
|
||||
schematic, not screenshots: `viewBox="0 0 400 300"`, no `width`/`height`, content inside
|
||||
`x: 40–360` / `y: 32–268`, and no text — everything reads as bars and shapes at 112px wide.
|
||||
|
||||
| Token | Hex | Used for |
|
||||
| -------------- | --------- | --------------------------------------------- |
|
||||
| background | `#f7f4ec` | full-canvas rect, always the first element |
|
||||
| surface | `#fffdf8` | cards, accordion rows, banner strip |
|
||||
| surface border | `#d9dacf` | 1px stroke on surfaces and image placeholders |
|
||||
| image fill | `#e7e9dc` | image placeholder rects |
|
||||
| ink | `#1f291f` | titles, button inner bars, icons |
|
||||
| text line | `#ccd1c0` | body copy bars |
|
||||
| muted line | `#b9bfab` | subtitles, captions, image glyphs |
|
||||
| accent | `#c7ff3f` | primary CTA pills, sun circle, active state |
|
||||
| accent border | `#a8c930` | stroke marking an active or open element |
|
||||
|
||||
Reuse the shared vocabulary so previews read as one family: title bar (dark rect, `height 14–18`,
|
||||
`rx = height/2`), body line (`height 9–10`, last line ~70% width), card (surface + border,
|
||||
`rx 10–14`), CTA pill (accent, `height 24–28`, dark label bar inside), image placeholder (mountain
|
||||
polygon plus accent sun). Draw the distinguishing structure only — three cards means "a list",
|
||||
2–4 lines mean "a paragraph" — and keep at most one accent element per preview.
|
||||
|
||||
### Add a page type
|
||||
|
||||
Add the value to `PAGE_TYPES` in `WebManager/types/web_content.ts` and `entities/PageType.ts`, a
|
||||
`WebStudioPageConfig` in `config/pages.ts` (allowed blocks, default path, label keys), a layout
|
||||
component in `web-runtime/layouts/components/`, its entry in `PAGE_LAYOUT_REGISTRY`, and the
|
||||
mirrored list in `registry.test.ts`. An unregistered type still renders through the standard
|
||||
layout, so the site cannot break mid-rollout.
|
||||
|
||||
## Tests
|
||||
|
||||
| Suite | Command | Location | Covers |
|
||||
| ------- | ---------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Backend | `npm run test:backend` | `apps/backend/tests/functional/web_manager/` | Preview token, revalidation, public content, sanitizer, breadcrumbs; page lifecycle (`web_manager_service.spec.ts`: draft/clone/publish/unpublish, route reservation, purge tags); HTTP surface (`pages_api.spec.ts`: 401/403 per permission, multipart, public endpoints). |
|
||||
| Admin | `npm run test:admin` | `apps/admin/tests/unit/core/WebStudio/` | Menu, SEO helpers, list mapper, payload serializer, editor-value mapping and validation, `toCreatePayload`, block factory↔schema round-trips, field schemas, page paths, permission helper. |
|
||||
| Web | `npm run test:web` | `apps/web/tests/` | Normalization, cache tags, block/layout registries, page metadata, sitemap, site URL, structured data, `safeHref`, published-only gate, `/api/revalidate`, API client, blog pager. |
|
||||
|
||||
Notes:
|
||||
|
||||
- DB-backed backend specs run against a dedicated `adonis_test` database (`.env.test`): the runner migrates up/down and each test runs in a global transaction (fixtures in `tests/utils/web_manager.ts`). `.env.test` leaves `WEB_REVALIDATE_SECRET` empty, so revalidation reports `disabled` while still exposing computed tags for assertions.
|
||||
- `apps/web/vitest.config.ts` maps `#*` to the app root (Vitest ignores `tsconfig` `paths`) and stubs `server-only`, which only resolves inside a Next build.
|
||||
- Not covered: rendering the block and layout components — needs jsdom and `@testing-library/react`, neither a dependency yet.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Web Manager — Public Site (Next.js)
|
||||
|
||||
How the public site renders what [Web Manager](./Readme.md) publishes. Source: `apps/web/web-runtime/`, `apps/web/app/`.
|
||||
|
||||
## Structure
|
||||
|
||||
```txt
|
||||
app/[locale]/[[...path]]/page.tsx localized catch-all
|
||||
app/[locale]/preview/page.tsx token-gated draft rendering, noindex, force-dynamic
|
||||
app/api/revalidate/route.ts secret-guarded tag purge
|
||||
app/sitemap.ts, app/robots.ts generated from the public API
|
||||
web-runtime/api/ HTTP client, render-data resolution, cache tags
|
||||
web-runtime/data/ transport normalization
|
||||
web-runtime/blocks/ block renderers + registry
|
||||
web-runtime/layouts/ page layouts and hero
|
||||
web-runtime/chrome/ header and footer
|
||||
web-runtime/components/ link, rich text and media primitives
|
||||
web-runtime/lib/ metadata, site URL, sitemap, structured data
|
||||
web-runtime/types.ts normalized public contract
|
||||
web-runtime/renderer.tsx header + layout + footer
|
||||
```
|
||||
|
||||
Locales come from `site.config.ts` (`en`, `uk`, `de`, default `en`); with more than one locale
|
||||
next-intl always prefixes the URL.
|
||||
|
||||
Everything degrades rather than fails. An unknown `page_type` falls back to the standard layout; an
|
||||
unknown block type renders a dev-only "renderer missing" banner and nothing at all in production;
|
||||
the normalizer degrades malformed payloads instead of throwing. Publishing a block type the web app
|
||||
does not know yet can therefore never take the site down.
|
||||
|
||||
## Hero
|
||||
|
||||
Hero impact is an intensity choice, not a block type:
|
||||
|
||||
| Impact | Use for | Rendering |
|
||||
| -------- | ------------------------------- | ------------------------------------------------------- |
|
||||
| `none` | pages that start with content | hero omitted |
|
||||
| `low` | legal, utility, index pages | compact padding, smallest title scale |
|
||||
| `middle` | default marketing and content | medium padding, balanced scale |
|
||||
| `high` | campaigns, launches, conversion | near-viewport height, largest scale, radial accent wash |
|
||||
|
||||
Media beside the hero switches it to a two-column grid at every impact. A page with no hero object
|
||||
falls back to a page-type eyebrow plus the title.
|
||||
|
||||
## SEO
|
||||
|
||||
`generateMetadata` delegates to `pageMetadata` (`web-runtime/lib/page-metadata.ts`), a pure function
|
||||
of the resolved render data:
|
||||
|
||||
- **title / description** — the SEO fields, falling back to the page title; an empty description is
|
||||
omitted rather than sent blank.
|
||||
- **canonical** — the page's own locale-prefixed path.
|
||||
- **hreflang** — one alternate per locale in `languageAlternatives`, plus `x-default` pointing at
|
||||
the default locale, or at this page when the default locale has no version. Omitted for a page
|
||||
with no translations.
|
||||
- **Open Graph / Twitter** — the hero image becomes the share card with width, height and alt when
|
||||
supplied; `summary_large_image` with an image, `summary` without; `og:type` is `article` for
|
||||
`blog-item`, `website` otherwise.
|
||||
|
||||
`app/sitemap.ts` walks the paginated sitemap endpoint (capped at 50 requests — the protocol's
|
||||
50,000-URL limit) and hands entries to `buildSitemap`; pages sharing a `groupKey` link to each other
|
||||
through `alternates.languages`, and `lastModified` uses `updatedAt` falling back to `publishedAt`.
|
||||
`app/robots.ts` allows everything except `/*/preview` and advertises the sitemap.
|
||||
`web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` on every page with a trail and
|
||||
`Article` on editorial pages — skipped entirely without a publication date rather than shipping
|
||||
markup a crawler discards.
|
||||
|
||||
**`SITE_URL` must be set.** `metadataBase` comes from `siteUrl()`, and Next resolves every relative
|
||||
metadata URL against it. Without it Next falls back to `http://localhost:3000`: the build succeeds
|
||||
and pages look fine, but canonicals and share cards point at localhost. A production build without
|
||||
it logs an error; a malformed value throws.
|
||||
@@ -1,78 +0,0 @@
|
||||
# Module Generator
|
||||
|
||||
The module generator creates a backend module and a matching admin frontend module from an existing PostgreSQL table. It combines database metadata with frontend code builders and Handlebars templates.
|
||||
|
||||
The generator is intended to produce a maintainable starting point. Generated authorization rules, validation, relationships, API behavior, labels, translations, and field controls must still be reviewed for the product domain.
|
||||
|
||||
## Source Ownership
|
||||
|
||||
| Part | Source |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| Generator page and form | `apps/admin/src/devtools/` |
|
||||
| Copied frontend module template | `apps/admin/src/modules/_template/` |
|
||||
| Generator HTTP endpoints | `apps/backend/start/routes.ts` |
|
||||
| Generator controller | `apps/backend/app/controllers/module_controller.ts` |
|
||||
| Database inspection | `apps/backend/app/services/database_inspector.ts` |
|
||||
| Backend Handlebars templates | `apps/backend/app/templates/backend/` |
|
||||
| Frontend Handlebars templates | `apps/backend/app/templates/frontend/` |
|
||||
|
||||
See [frontend.md](./frontend.md) for the UI, payload builders, and generated frontend architecture. See [backend.md](./backend.md) for endpoints, database inspection, templates, and generated backend files. If generated files are not loaded or the module is missing from the menu, use the [troubleshooting guide](./troubleshooting.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Run the backend from the `apps/backend/` workspace. The controller resolves backend and frontend destinations relative to `process.cwd()`.
|
||||
2. Run the admin app with dev mode enabled so the devtools module is registered.
|
||||
3. Authenticate in the admin app. All generator endpoints use authentication middleware.
|
||||
4. Create and migrate the source database table before opening the generator. The form reads tables and columns from PostgreSQL `information_schema`.
|
||||
5. Use a module name compatible with TypeScript identifiers, route segments, permission slugs, and folder names.
|
||||
|
||||
## Generation Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Select table and assets"] --> B["Configure form and table"]
|
||||
B --> C["Frontend builders create source strings"]
|
||||
C --> D["POST /makeModule/generate"]
|
||||
D --> E["Inspect database metadata"]
|
||||
E --> F["Copy frontend _template"]
|
||||
E --> G["Render backend templates"]
|
||||
F --> H["Write configured frontend files"]
|
||||
G --> I["Write backend module and tests"]
|
||||
```
|
||||
|
||||
The generator first copies the complete frontend template, then overwrites generated frontend files with the payload content and database-derived entity metadata. Backend files are rendered according to the selected assets.
|
||||
|
||||
## Generated Locations
|
||||
|
||||
```txt
|
||||
apps/backend/modules/<ModuleName>/
|
||||
apps/backend/tests/modules/<ModuleName>/
|
||||
apps/admin/src/modules/<name>/
|
||||
```
|
||||
|
||||
Frontend modules are discovered automatically through their `config/module.ts` manifest. Backend module routes and API routes are loaded automatically from `apps/backend/modules/` during application startup.
|
||||
|
||||
## Important Operational Behavior
|
||||
|
||||
- Generation writes directly into both workspaces; it does not provide a preview, transaction, or rollback.
|
||||
- Existing destination files may be overwritten. Generate into a clean name and inspect the Git diff before keeping the result.
|
||||
- File writes are synchronous, but schema inspection and Swagger model generation are asynchronous.
|
||||
- API generation imports the generated model to build its Swagger schema. Select `model` when selecting `api`.
|
||||
- The frontend module template is always copied. Frontend form and datatable files are populated only when those assets are selected.
|
||||
- Generated code is not automatically formatted or committed.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After generating a module:
|
||||
|
||||
1. Inspect all generated files and the Git diff.
|
||||
2. Confirm route paths, API prefixes, permission slugs, and module naming.
|
||||
3. Review nullable fields, dates, passwords, foreign keys, validation, and relationship names.
|
||||
4. Replace generic labels and translation values.
|
||||
5. Verify list, create, update, delete, authorization, and validation-error behavior.
|
||||
6. Run `npm run build:backend` and `npm run build:admin` from the repository root.
|
||||
7. Run the generated backend tests and add domain-specific coverage.
|
||||
|
||||
For restart, module-discovery, permission, and menu problems, see [Troubleshooting](./troubleshooting.md).
|
||||
|
||||
The generated frontend structure follows the [React frontend architecture](../../apps/admin/architecture/Readme.md). Backend modules should be reviewed against the [backend architecture](../../apps/backend/architecture/Readme.md).
|
||||
@@ -1,134 +0,0 @@
|
||||
# Module Generator Backend
|
||||
|
||||
The backend generator exposes authenticated schema-inspection and generation endpoints. It renders backend files from Handlebars templates and writes frontend files using a copied template plus source strings supplied by the admin app.
|
||||
|
||||
## Endpoints
|
||||
|
||||
Routes are registered in `apps/backend/start/routes.ts`.
|
||||
|
||||
| Method | Path | Controller method | Purpose |
|
||||
| ------ | ---------------------------- | ----------------- | -------------------------------------------------- |
|
||||
| `GET` | `/makeModule/tables` | `tables` | List eligible public PostgreSQL tables |
|
||||
| `GET` | `/makeModule/columns/:table` | `columns` | Return columns used to configure form/table fields |
|
||||
| `POST` | `/makeModule/generate` | `generate` | Generate selected frontend and backend assets |
|
||||
|
||||
All three endpoints use `middleware.auth()`.
|
||||
|
||||
## Database Inspection
|
||||
|
||||
`apps/backend/app/services/database_inspector.ts` reads PostgreSQL `information_schema`.
|
||||
|
||||
- Table discovery is limited to the `public` schema.
|
||||
- framework, authentication, access-control, migration, and PostgreSQL system tables are excluded.
|
||||
- columns provide `column_name`, `data_type`, and `is_nullable`.
|
||||
- columns ending in `_id` are treated as belongs-to relationship candidates.
|
||||
- tables containing a `<table>_id` column are treated as has-many relationship candidates.
|
||||
- searchable string and numeric columns are derived for generated resource controllers.
|
||||
|
||||
Relationship inference is heuristic. Review generated model imports, related class names, property names, and module availability.
|
||||
|
||||
## Type Mapping
|
||||
|
||||
The controller maps database types separately for Lucid models, Vine validators, and frontend entities.
|
||||
|
||||
| Database category | Lucid TypeScript type | Vine type | Frontend type |
|
||||
| --------------------- | --------------------- | --------- | --------------- |
|
||||
| integer/numeric types | `number` | `number` | `number` |
|
||||
| boolean | `boolean` | `boolean` | `boolean` |
|
||||
| date/timestamp | `DateTime` | `date` | `IsoDateString` |
|
||||
| other values | `string` | `string` | `string` |
|
||||
|
||||
Nullable database columns receive `| null` in generated model/entity types and optional validation where applicable. `created_at`, `updated_at`, and `deleted_at` receive special handling.
|
||||
|
||||
## Backend Output
|
||||
|
||||
For module name `<name>`, table `<table>`, and derived module class `<ModuleName>`, selected assets are written under:
|
||||
|
||||
```txt
|
||||
apps/backend/modules/<ModuleName>/
|
||||
├── api/
|
||||
│ ├── controllers/<entity>_api_controller.ts
|
||||
│ ├── validators/<entity>_create_api_validator.ts
|
||||
│ ├── validators/<entity>_update_api_validator.ts
|
||||
│ └── routes.ts
|
||||
├── config/permissions.ts
|
||||
├── controller/<entity>_controller.ts
|
||||
├── model/<entity>.ts
|
||||
├── policy/<entity>_policy.ts
|
||||
├── validators/<entity>_create_validator.ts
|
||||
├── validators/<entity>_update_validator.ts
|
||||
└── routes.ts
|
||||
```
|
||||
|
||||
Tests are written under:
|
||||
|
||||
```txt
|
||||
apps/backend/tests/modules/<ModuleName>/
|
||||
├── <entity>.spec.ts
|
||||
└── <entity>.api.spec.ts
|
||||
```
|
||||
|
||||
`<entity>` is derived by removing the final character from the selected table name. This assumes a plural table name and must be reviewed for irregular plurals.
|
||||
|
||||
## Asset Behavior
|
||||
|
||||
### Model
|
||||
|
||||
Renders `apps/backend/app/templates/backend/model.hbs` with columns, inferred relationships, searchable fields, and optional password hashing.
|
||||
|
||||
### Controller and routes
|
||||
|
||||
Renders an authenticated resource controller with list/search/sort/pagination and CRUD actions. `routes.hbs` registers an `apiOnly` resource route and applies authentication.
|
||||
|
||||
### Validators
|
||||
|
||||
Creates separate create and update Vine validators. Primary keys and timestamp/deletion fields are excluded according to controller rules. Email, password, nullable, and string trimming behavior is inferred from column names and database metadata.
|
||||
|
||||
### Policy and permissions
|
||||
|
||||
Creates a Bouncer policy with view/create/update/delete checks, creates `config/permissions.ts`, and adds a lazy policy import to `apps/backend/app/policies/main.ts` when missing.
|
||||
|
||||
Permission slugs are derived from the module name, for example:
|
||||
|
||||
```txt
|
||||
<name>s.view
|
||||
<name>s.create
|
||||
<name>s.update
|
||||
<name>s.delete
|
||||
```
|
||||
|
||||
Review names that are already plural because policy and frontend metadata derivation are not a general-purpose pluralization system.
|
||||
|
||||
### API subtree
|
||||
|
||||
Creates a second CRUD controller and route group under `/api/<entity>`, plus API-specific validators and a test. The controller imports the generated model and passes it to `modelToSchema` for Swagger response metadata.
|
||||
|
||||
API routes currently do not add authentication middleware in the generated API template. Add the required authentication and authorization explicitly before exposing the route.
|
||||
|
||||
## Frontend Writes Performed by the Backend
|
||||
|
||||
The backend copies `apps/admin/src/modules/_template` to `apps/admin/src/modules/<name>` and then writes:
|
||||
|
||||
- configured form component and form definition;
|
||||
- configured datatable and list page;
|
||||
- configured manage trigger, menu, and module manifest;
|
||||
- generated `config/meta.ts`;
|
||||
- database-derived `entities/Entity.ts`.
|
||||
|
||||
See [frontend.md](./frontend.md) for the complete frontend structure and payload ownership.
|
||||
|
||||
## Template Helpers
|
||||
|
||||
The controller registers Handlebars helpers for `eq`, `not`, `or`, and `and`. New templates should use the existing render function and receive an explicit context object from the controller.
|
||||
|
||||
## Extending the Backend Generator
|
||||
|
||||
1. Add or update a Handlebars template under `apps/backend/app/templates/`.
|
||||
2. Add an explicit destination path in the controller `paths` object.
|
||||
3. Create its parent directory through the existing path initialization.
|
||||
4. Render only when the corresponding asset is selected.
|
||||
5. Pass normalized metadata rather than querying the database inside templates.
|
||||
6. Add generation tests for nullable values, dates, numeric types, passwords, and irregular names.
|
||||
7. Verify both backend and frontend builds after generation.
|
||||
|
||||
Avoid adding hidden writes. Every generated or modified destination, including shared registries such as `app/policies/main.ts`, should remain visible in the controller flow and documented here.
|
||||
@@ -1,137 +0,0 @@
|
||||
# Module Generator Frontend
|
||||
|
||||
The frontend generator UI collects module metadata, selected assets, form layout, and datatable configuration. It converts the configuration into TypeScript/TSX source strings and sends them to the backend generator.
|
||||
|
||||
## Main Flow
|
||||
|
||||
| Responsibility | Source |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
||||
| Page submission, notifications, and mutation callbacks | `apps/admin/src/devtools/pages/ModuleGeneratorPage/index.tsx` |
|
||||
| Payload assembly | `apps/admin/src/devtools/pages/ModuleGeneratorPage/lib/buildGenerateModulePayload.ts` |
|
||||
| Form state and initial values | `apps/admin/src/devtools/view/forms/ModuleGeneratorForm/form.ts` |
|
||||
| Metadata, form, and datatable sections | `apps/admin/src/devtools/view/forms/ModuleGeneratorForm/sections/` |
|
||||
| Payload DTOs | `apps/admin/src/devtools/data/api/dto.ts` |
|
||||
| HTTP client | `apps/admin/src/devtools/data/api/index.ts` |
|
||||
| React Query hooks | `apps/admin/src/devtools/data/queries/` |
|
||||
| Source-code builders | `apps/admin/src/devtools/data/builders/` |
|
||||
|
||||
The page submits through `useGenerateModule`. On success it resets the form and shows a success alert; backend or network errors are normalized through the shared React Query error helpers.
|
||||
|
||||
## Metadata and Assets
|
||||
|
||||
The required metadata is:
|
||||
|
||||
- `name`: frontend folder name, backend module identity, route segment, and permission-slug base.
|
||||
- `table`: existing PostgreSQL source table.
|
||||
- `generate`: selected backend and frontend assets.
|
||||
|
||||
Supported asset values are defined by `ModuleAsset`:
|
||||
|
||||
| Asset | Workspace | Output |
|
||||
| ------------ | --------- | --------------------------------------------------- |
|
||||
| `model` | Backend | Lucid model |
|
||||
| `controller` | Backend | Authenticated resource controller and test |
|
||||
| `routes` | Backend | Resource route registration |
|
||||
| `validator` | Backend | Create and update Vine validators |
|
||||
| `policy` | Backend | Bouncer policy and permission configuration |
|
||||
| `api` | Backend | API controller, validators, routes, and test |
|
||||
| `form` | Frontend | Form definition, component, and modal/sheet trigger |
|
||||
| `datatable` | Frontend | List page table and menu entry |
|
||||
|
||||
The default selection includes all backend assets. Form and datatable generation are opt-in.
|
||||
|
||||
## Schema Queries
|
||||
|
||||
The frontend uses two authenticated read endpoints before generation:
|
||||
|
||||
- `GET /makeModule/tables` populates the table selector.
|
||||
- `GET /makeModule/columns/:table` populates form and datatable field choices.
|
||||
|
||||
Table and column requests are cached through React Query. Column queries remain disabled until a table is selected.
|
||||
|
||||
## Payload Builders
|
||||
|
||||
`buildGenerateModulePayload` delegates source generation to focused builders:
|
||||
|
||||
| Builder | Payload field | Generated destination |
|
||||
| ----------------------------------- | ---------------- | ----------------------------------- |
|
||||
| `FormSourceBuilder.buildComponent` | `form` | `view/forms/MainForm/Component.tsx` |
|
||||
| `FormSourceBuilder.buildDefinition` | `formDefinition` | `view/forms/MainForm/form.ts` |
|
||||
| `ManageTriggerSourceBuilder` | `formDialog` | `view/widgets/ManageTrigger.tsx` |
|
||||
| `DataTableSourceBuilder` | `dataTable` | `view/widgets/DataTable.tsx` |
|
||||
| `ListPageSourceBuilder` | `list` | `pages/ListPage/index.tsx` |
|
||||
| `MenuSourceBuilder` | `menu` | `config/menu.ts` |
|
||||
| `ModuleConfigSourceBuilder` | `module` | `config/module.ts` |
|
||||
|
||||
If form generation is disabled, `form`, `formDefinition`, and `formDialog` are `null`. If datatable generation is disabled, `dataTable` and `menu` are `null`, while the list-page builder emits a page without a table.
|
||||
|
||||
## Generated Frontend Architecture
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/
|
||||
├── api/
|
||||
│ ├── create.ts
|
||||
│ ├── delete.ts
|
||||
│ ├── detailed.ts
|
||||
│ ├── keys.ts
|
||||
│ ├── list.ts
|
||||
│ └── update.ts
|
||||
├── config/
|
||||
│ ├── translation/
|
||||
│ ├── menu.ts
|
||||
│ ├── messages.ts
|
||||
│ ├── meta.ts
|
||||
│ ├── module.ts
|
||||
│ └── routing.tsx
|
||||
├── entities/
|
||||
│ └── Entity.ts
|
||||
├── pages/
|
||||
│ └── ListPage/
|
||||
│ └── index.tsx
|
||||
└── view/
|
||||
├── forms/
|
||||
│ └── MainForm/
|
||||
│ ├── Component.tsx
|
||||
│ ├── constants.ts
|
||||
│ ├── form.ts
|
||||
│ ├── index.ts
|
||||
│ └── submit.ts
|
||||
├── layouts/
|
||||
│ └── Layout.tsx
|
||||
└── widgets/
|
||||
├── DataTable.tsx
|
||||
├── DeleteTrigger.tsx
|
||||
└── ManageTrigger.tsx
|
||||
```
|
||||
|
||||
The `api` directory intentionally has no barrel file. Consumers import each operation directly, for example:
|
||||
|
||||
```ts
|
||||
import { useCreate } from "../../../api/create";
|
||||
import { invalidateDetailed } from "../../../api/detailed";
|
||||
import { invalidateList } from "../../../api/list";
|
||||
import { useUpdate } from "../../../api/update";
|
||||
```
|
||||
|
||||
Each CRUD operation remains isolated in its own file. `keys.ts` owns React Query key builders, while `list.ts` and `detailed.ts` own their cache invalidation helpers.
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
- `entities/Entity.ts` is generated from database columns and is shared by API hooks, forms, and widgets.
|
||||
- `api/` owns Axios calls, React Query hooks, cache keys, and mutation notifications.
|
||||
- `pages/` owns route-level composition and permission page gates.
|
||||
- `view/forms/` owns schema, form values, UI integration, and submission orchestration.
|
||||
- `view/widgets/` owns stateful table, delete, and manage interactions.
|
||||
- `config/` integrates routes, menu, translations, metadata, and permissions with the app module registry.
|
||||
|
||||
## Extending the Frontend Generator
|
||||
|
||||
When adding a new generated frontend artifact:
|
||||
|
||||
1. Add its configuration type under `apps/admin/src/devtools/entities/`.
|
||||
2. Add UI controls to `ModuleGeneratorForm`.
|
||||
3. Add a focused builder under `data/builders/` if the artifact contains dynamic source.
|
||||
4. Add the payload field to `GenerateModulePayload` and `buildGenerateModulePayload`.
|
||||
5. Accept and write the field in `apps/backend/app/controllers/module_controller.ts`.
|
||||
6. Add the static structure to `apps/admin/src/modules/_template/` when every module needs it.
|
||||
7. Verify generation with and without the new asset selected.
|
||||
@@ -1,122 +0,0 @@
|
||||
# Module Generator Troubleshooting
|
||||
|
||||
## Generated Files Exist but the Module Is Not Loaded
|
||||
|
||||
Docker file watching does not always restart the backend or rebuild the admin module registry after the generator creates new directories. Existing file changes may be detected while newly generated module files are not.
|
||||
|
||||
Restart both application services manually from the repository root:
|
||||
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
Then reload the admin app. If the module still is not loaded, inspect service logs:
|
||||
|
||||
```bash
|
||||
docker compose logs --tail=200 backend admin
|
||||
```
|
||||
|
||||
Check for TypeScript errors, invalid generated imports, route-loading failures, or module-manifest errors. If the containers are not running through Compose, restart the corresponding backend and admin development processes manually.
|
||||
|
||||
The restart is necessary because:
|
||||
|
||||
- the backend discovers generated route files under `apps/backend/modules/` during startup;
|
||||
- the admin app discovers `apps/admin/src/modules/**/config/module.ts` through Vite's module registry;
|
||||
- adding an entirely new directory may not trigger the same reload behavior as editing an existing file.
|
||||
|
||||
## Module Loads but Is Missing from the Menu
|
||||
|
||||
If generation succeeded and both services restarted, a missing menu item usually indicates a permission mismatch or an invalid menu/module configuration.
|
||||
|
||||
### 1. Compare permission slugs
|
||||
|
||||
Check the generated backend permissions:
|
||||
|
||||
```txt
|
||||
apps/backend/modules/<ModuleName>/config/permissions.ts
|
||||
```
|
||||
|
||||
Check the frontend permission constants:
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/config/meta.ts
|
||||
```
|
||||
|
||||
The view slug must match exactly. For example:
|
||||
|
||||
```ts
|
||||
// backend permissions.ts
|
||||
{ slug: 'products.view', name: 'Product view' }
|
||||
|
||||
// frontend meta.ts
|
||||
export const MODULE_PERMISSION_VIEW = 'products.view';
|
||||
```
|
||||
|
||||
Also compare the create, update, and delete slugs. Module-name pluralization is generated heuristically, so names that are already plural or have irregular plurals require manual correction.
|
||||
|
||||
### 2. Verify the current role owns the permission
|
||||
|
||||
The menu filters items using `MODULE_PERMISSION_VIEW`. Confirm that:
|
||||
|
||||
- the generated permission is available to the access-control system;
|
||||
- the current admin role has the generated view permission;
|
||||
- the role-to-permission assignment uses the same exact slug;
|
||||
- the session or permission data has been refreshed after changing the role.
|
||||
|
||||
### 3. Check menu configuration
|
||||
|
||||
Inspect:
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/config/menu.ts
|
||||
```
|
||||
|
||||
Confirm that:
|
||||
|
||||
- the exported menu factory is valid;
|
||||
- `path` uses `MODULE_ROOT_PATH`;
|
||||
- `permissions` contains `MODULE_PERMISSION_VIEW`;
|
||||
- the file is not empty;
|
||||
- the configured route exists.
|
||||
|
||||
The generator creates the menu content with the Datatable frontend asset. If Datatable was not selected, add a menu implementation manually or keep the module intentionally hidden from navigation.
|
||||
|
||||
### 4. Check the module manifest
|
||||
|
||||
Inspect:
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/config/module.ts
|
||||
```
|
||||
|
||||
The manifest must export a valid `AppModuleConfig`, must not set `enabled: false`, and must expose the intended `menu`, `routes`, and `messages` values. Its module ID should be unique.
|
||||
|
||||
### 5. Check route configuration
|
||||
|
||||
Inspect:
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/config/routing.tsx
|
||||
```
|
||||
|
||||
Confirm that `MODULE_ROOT_PATH`, the menu path, and the generated route all point to the same URL. A visible menu item with a broken route and an invisible menu item are separate problems; verify both configuration files.
|
||||
|
||||
## Final Checks
|
||||
|
||||
Run both builds to expose generated TypeScript or import errors:
|
||||
|
||||
```bash
|
||||
npm run build:backend
|
||||
npm run build:admin
|
||||
```
|
||||
|
||||
Then verify:
|
||||
|
||||
1. `apps/backend/modules/<ModuleName>/` exists and contains the selected backend assets.
|
||||
2. `apps/admin/src/modules/<name>/` exists and contains a valid module manifest.
|
||||
3. Docker services or local development processes were restarted.
|
||||
4. Backend and frontend permission slugs match.
|
||||
5. The current role owns the view permission.
|
||||
6. The menu path matches the frontend route.
|
||||
|
||||
If all checks pass, review the backend and admin logs during startup. Module discovery errors are normally reported there.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Project Modules
|
||||
|
||||
Documentation for project-specific modules
|
||||
|
||||
| Module | Backend | Frontend | Documentation |
|
||||
| ------ | ------- | -------- | ------------- |
|
||||
| - | - | - | - |
|
||||
+10
-50
@@ -1,54 +1,14 @@
|
||||
# Crux Kit Packages
|
||||
|
||||
Reusable packages shared across CRUX applications.
|
||||
Reusable packages shared across CRUX applications. Each package documents itself in its own `Readme.md`.
|
||||
|
||||
## React Hooks
|
||||
| Package | Description |
|
||||
| -------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| [@crux-kit/hooks](../../packages/hooks/Readme.md) | Reusable React hooks |
|
||||
| [@crux-kit/utils](../../packages/utils/Readme.md) | Common utilities for frontend and backend |
|
||||
| [@crux-kit/intl-date](../../packages/services/intl-date/Readme.md) | Date formatting and localization |
|
||||
| [@crux-kit/intl-name](../../packages/services/intl-name/Readme.md) | Person and organization name formatting |
|
||||
| [@crux-kit/local-storage](../../packages/services/local-storage/Readme.md) | Local storage abstraction |
|
||||
| [@crux-kit/keenicon](../../packages/design-system/keenicon/Readme.md) | KeenIcons integration and icon components |
|
||||
|
||||
Packages that provide reusable React hooks and frontend utilities.
|
||||
|
||||
| Package | Description |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| [@crux-kit/hooks](../../packages/hooks/Readme.md) | Collection of reusable React hooks used across applications. |
|
||||
|
||||
---
|
||||
|
||||
## Utility Functions
|
||||
|
||||
General-purpose utility functions and helpers.
|
||||
|
||||
| Package | Description |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| [@crux-kit/utils](../../packages/utils/Readme.md) | Common utility functions shared across frontend and backend projects |
|
||||
|
||||
---
|
||||
|
||||
## Services
|
||||
|
||||
Shared service abstractions and implementations.
|
||||
|
||||
| Package | Description |
|
||||
| -------------------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| [@crux-kit/intl-date](../../packages/services/intl-date/Readme.md) | Date formatting and localization utilities |
|
||||
| [@crux-kit/intl-name](../../packages/services/intl-name/Readme.md) | Person and organization name formatting utilities |
|
||||
| [@crux-kit/local-storage](../../packages/services/local-storage/Readme.md) | Local storage abstraction and helpers |
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
Packages related to UI and visual consistency.
|
||||
|
||||
| Package | Description |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| [@crux-kit/keenicon](../../packages/design-system/keenicon/Readme.md) | KeenIcons integration and icon components. |
|
||||
|
||||
---
|
||||
|
||||
## Adding New Packages
|
||||
|
||||
When creating a new package:
|
||||
|
||||
1. Add a `Readme.md` file inside the package root.
|
||||
2. Document installation, usage, API, and examples.
|
||||
3. Add the package to this index.
|
||||
4. Link related packages where appropriate.
|
||||
New package: add a `Readme.md` in the package root (installation, usage, API) and list it here.
|
||||
|
||||
Reference in New Issue
Block a user