391 lines
24 KiB
Markdown
391 lines
24 KiB
Markdown
# Page Layouts Architecture Diagram
|
|
|
|
## System Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ FRONTEND (Vue 3 + Nuxt) │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
│ │ Setup → Objects → [Object] → Layouts Tab │ │
|
|
│ ├───────────────────────────────────────────────────────────┤ │
|
|
│ │ │ │
|
|
│ │ ┌─────────────┐ ┌───────────────────────────────┐ │ │
|
|
│ │ │ Layouts │ │ PageLayoutEditor │ │ │
|
|
│ │ │ List │ --> │ ┌─────────────────────────┐ │ │ │
|
|
│ │ │ │ │ │ 6-Column Grid │ │ │ │
|
|
│ │ │ • Standard │ │ │ ┌───┬───┬───┬───┬───┐ │ │ │ │
|
|
│ │ │ • Compact │ │ │ │ F │ F │ F │ F │ F │ │ │ │ │
|
|
│ │ │ • Detailed │ │ │ ├───┴───┴───┴───┴───┤ │ │ │ │
|
|
│ │ │ │ │ │ │ Field 1 (w:5) │ │ │ │ │
|
|
│ │ │ [+ New] │ │ │ └─────────────────── │ │ │ │ │
|
|
│ │ └─────────────┘ │ └─────────────────────────┘ │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ Sidebar: │ │ │
|
|
│ │ │ ┌─────────────────────────┐ │ │ │
|
|
│ │ │ │ Available Fields │ │ │ │
|
|
│ │ │ │ □ Email │ │ │ │
|
|
│ │ │ │ □ Phone │ │ │ │
|
|
│ │ │ │ □ Status │ │ │ │
|
|
│ │ │ └─────────────────────────┘ │ │ │
|
|
│ │ └───────────────────────────────┘ │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ Record Detail/Edit Views │ │
|
|
│ ├───────────────────────────────────────────────────────┤ │
|
|
│ │ │ │
|
|
│ │ DetailViewEnhanced / EditViewEnhanced │ │
|
|
│ │ ↓ │ │
|
|
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
|
│ │ │ PageLayoutRenderer │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ Fetches default layout for object │ │ │
|
|
│ │ │ Renders fields in custom grid positions │ │ │
|
|
│ │ │ Fallback to 2-column if no layout │ │ │
|
|
│ │ └─────────────────────────────────────────────────┘ │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ Composables (usePageLayouts) │ │
|
|
│ ├───────────────────────────────────────────────────────┤ │
|
|
│ │ • getPageLayouts() • createPageLayout() │ │
|
|
│ │ • getPageLayout() • updatePageLayout() │ │
|
|
│ │ • getDefaultPageLayout()• deletePageLayout() │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↕ HTTP REST API
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ BACKEND (NestJS) │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ PageLayoutController (API Layer) │ │
|
|
│ ├───────────────────────────────────────────────────────┤ │
|
|
│ │ POST /page-layouts │ │
|
|
│ │ GET /page-layouts?objectId={id} │ │
|
|
│ │ GET /page-layouts/:id │ │
|
|
│ │ GET /page-layouts/default/:objectId │ │
|
|
│ │ PATCH /page-layouts/:id │ │
|
|
│ │ DELETE /page-layouts/:id │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ ↕ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ PageLayoutService (Business Logic) │ │
|
|
│ ├───────────────────────────────────────────────────────┤ │
|
|
│ │ • Tenant isolation │ │
|
|
│ │ • Default layout management │ │
|
|
│ │ • CRUD operations │ │
|
|
│ │ • Validation │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ ↕ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ PrismaService (Data Layer) │ │
|
|
│ ├───────────────────────────────────────────────────────┤ │
|
|
│ │ • Raw SQL queries │ │
|
|
│ │ • Tenant database routing │ │
|
|
│ │ • Transaction management │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
↕
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ DATABASE (PostgreSQL) │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|
│ │ Table: page_layouts │ │
|
|
│ ├───────────────────────────────────────────────────────┤ │
|
|
│ │ id UUID PRIMARY KEY │ │
|
|
│ │ name VARCHAR(255) │ │
|
|
│ │ object_id UUID → object_definitions(id) │ │
|
|
│ │ is_default BOOLEAN │ │
|
|
│ │ layout_config JSONB │ │
|
|
│ │ description TEXT │ │
|
|
│ │ created_at TIMESTAMP │ │
|
|
│ │ updated_at TIMESTAMP │ │
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Example layout_config JSONB: │
|
|
│ { │
|
|
│ "fields": [ │
|
|
│ { │
|
|
│ "fieldId": "uuid-123", │
|
|
│ "x": 0, // Column start (0-5) │
|
|
│ "y": 0, // Row start │
|
|
│ "w": 3, // Width (1-6 columns) │
|
|
│ "h": 1 // Height (fixed at 1) │
|
|
│ } │
|
|
│ ] │
|
|
│ } │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Data Flow Diagrams
|
|
|
|
### Creating a Layout
|
|
|
|
```
|
|
Admin User
|
|
│
|
|
├─→ Navigates to Setup → Objects → [Object] → Page Layouts
|
|
│
|
|
├─→ Clicks "New Layout"
|
|
│
|
|
├─→ Enters layout name
|
|
│
|
|
├─→ PageLayoutEditor mounts
|
|
│ │
|
|
│ ├─→ Loads object fields
|
|
│ ├─→ Initializes GridStack with 6 columns
|
|
│ └─→ Shows available fields in sidebar
|
|
│
|
|
├─→ Drags fields from sidebar to grid
|
|
│ │
|
|
│ ├─→ GridStack handles positioning
|
|
│ ├─→ User resizes field width (1-6 columns)
|
|
│ └─→ User arranges fields
|
|
│
|
|
├─→ Clicks "Save Layout"
|
|
│
|
|
├─→ usePageLayouts.createPageLayout()
|
|
│ │
|
|
│ └─→ POST /page-layouts
|
|
│ │
|
|
│ └─→ PageLayoutController.create()
|
|
│ │
|
|
│ └─→ PageLayoutService.create()
|
|
│ │
|
|
│ ├─→ If is_default, unset others
|
|
│ └─→ INSERT INTO page_layouts
|
|
│
|
|
└─→ Layout saved ✓
|
|
```
|
|
|
|
### Rendering a Layout in Detail View
|
|
|
|
```
|
|
User Opens Record
|
|
│
|
|
├─→ Navigates to /[object]/[id]/detail
|
|
│
|
|
├─→ DetailViewEnhanced mounts
|
|
│ │
|
|
│ └─→ onMounted() hook
|
|
│ │
|
|
│ └─→ usePageLayouts.getDefaultPageLayout(objectId)
|
|
│ │
|
|
│ └─→ GET /page-layouts/default/:objectId
|
|
│ │
|
|
│ └─→ PageLayoutService.findDefaultByObject()
|
|
│ │
|
|
│ └─→ SELECT * FROM page_layouts
|
|
│ WHERE object_id = $1
|
|
│ AND is_default = true
|
|
│
|
|
├─→ Layout received
|
|
│ │
|
|
│ ├─→ If layout exists:
|
|
│ │ │
|
|
│ │ └─→ PageLayoutRenderer renders with layout
|
|
│ │ │
|
|
│ │ ├─→ Creates CSS Grid (6 columns)
|
|
│ │ ├─→ Positions fields based on x, y, w, h
|
|
│ │ └─→ Renders FieldRenderer for each field
|
|
│ │
|
|
│ └─→ If no layout:
|
|
│ │
|
|
│ └─→ Falls back to 2-column layout
|
|
│
|
|
└─→ Record displayed with custom layout ✓
|
|
```
|
|
|
|
## Grid Layout System
|
|
|
|
### 6-Column Grid Structure
|
|
|
|
```
|
|
┌──────┬──────┬──────┬──────┬──────┬──────┐
|
|
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ ← Column indices
|
|
└──────┴──────┴──────┴──────┴──────┴──────┘
|
|
Each column = 16.67% of container width
|
|
```
|
|
|
|
### Example Layouts
|
|
|
|
#### Two-Column Layout (Default)
|
|
```
|
|
┌─────────────────────┬─────────────────────┐
|
|
│ Name (w:3) │ Email (w:3) │
|
|
├─────────────────────┼─────────────────────┤
|
|
│ Phone (w:3) │ Company (w:3) │
|
|
├─────────────────────┴─────────────────────┤
|
|
│ Description (w:6) │
|
|
└───────────────────────────────────────────┘
|
|
|
|
Field configs:
|
|
- Name: {x:0, y:0, w:3, h:1}
|
|
- Email: {x:3, y:0, w:3, h:1}
|
|
- Phone: {x:0, y:1, w:3, h:1}
|
|
- Company: {x:3, y:1, w:3, h:1}
|
|
- Description: {x:0, y:2, w:6, h:1}
|
|
```
|
|
|
|
#### Three-Column Layout
|
|
```
|
|
┌───────────┬───────────┬───────────┐
|
|
│ F1 (w:2) │ F2 (w:2) │ F3 (w:2) │
|
|
├───────────┴───────────┴───────────┤
|
|
│ F4 (w:6) │
|
|
└───────────────────────────────────┘
|
|
|
|
Field configs:
|
|
- F1: {x:0, y:0, w:2, h:1}
|
|
- F2: {x:2, y:0, w:2, h:1}
|
|
- F3: {x:4, y:0, w:2, h:1}
|
|
- F4: {x:0, y:1, w:6, h:1}
|
|
```
|
|
|
|
#### Mixed Width Layout
|
|
```
|
|
┌───────────────┬───────┬───────────┐
|
|
│ Title (w:3) │ ID(1) │ Type (w:2)│
|
|
├───────────────┴───────┴───────────┤
|
|
│ Address (w:6) │
|
|
├──────────┬────────────────────────┤
|
|
│ City(2) │ State/ZIP (w:4) │
|
|
└──────────┴────────────────────────┘
|
|
|
|
Field configs:
|
|
- Title: {x:0, y:0, w:3, h:1}
|
|
- ID: {x:3, y:0, w:1, h:1}
|
|
- Type: {x:4, y:0, w:2, h:1}
|
|
- Address: {x:0, y:1, w:6, h:1}
|
|
- City: {x:0, y:2, w:2, h:1}
|
|
- State: {x:2, y:2, w:4, h:1}
|
|
```
|
|
|
|
## Component Hierarchy
|
|
|
|
```
|
|
App.vue
|
|
│
|
|
└─→ NuxtLayout (default)
|
|
│
|
|
├─→ Setup Pages
|
|
│ │
|
|
│ └─→ pages/setup/objects/[apiName].vue
|
|
│ │
|
|
│ └─→ Tabs Component
|
|
│ │
|
|
│ ├─→ Tab: Fields (existing)
|
|
│ │
|
|
│ └─→ Tab: Page Layouts
|
|
│ │
|
|
│ ├─→ Layout List View
|
|
│ │ └─→ Card per layout
|
|
│ │
|
|
│ └─→ Layout Editor View
|
|
│ │
|
|
│ └─→ PageLayoutEditor
|
|
│ │
|
|
│ ├─→ GridStack (6 columns)
|
|
│ │ └─→ Field items
|
|
│ │
|
|
│ └─→ Sidebar
|
|
│ └─→ Available fields
|
|
│
|
|
└─→ Record Pages
|
|
│
|
|
└─→ pages/[objectName]/[[recordId]]/[[view]].vue
|
|
│
|
|
├─→ DetailViewEnhanced
|
|
│ │
|
|
│ └─→ PageLayoutRenderer
|
|
│ └─→ FieldRenderer (per field)
|
|
│
|
|
└─→ EditViewEnhanced
|
|
│
|
|
└─→ PageLayoutRenderer
|
|
└─→ FieldRenderer (per field)
|
|
```
|
|
|
|
## State Management
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ Component State (ref/reactive) │
|
|
├─────────────────────────────────────┤
|
|
│ • selectedLayout │
|
|
│ • layouts[] │
|
|
│ • loadingLayouts │
|
|
│ • pageLayout (current) │
|
|
│ • formData │
|
|
│ • gridItems │
|
|
│ • placedFieldIds │
|
|
└─────────────────────────────────────┘
|
|
↕
|
|
┌─────────────────────────────────────┐
|
|
│ Composables (Reactive) │
|
|
├─────────────────────────────────────┤
|
|
│ • usePageLayouts() - API calls │
|
|
│ • useApi() - HTTP client │
|
|
│ • useAuth() - Authentication │
|
|
│ • useToast() - Notifications │
|
|
└─────────────────────────────────────┘
|
|
↕
|
|
┌─────────────────────────────────────┐
|
|
│ Browser Storage │
|
|
├─────────────────────────────────────┤
|
|
│ • localStorage: token, tenantId │
|
|
│ • SessionStorage: (none yet) │
|
|
│ • Cookies: (managed by server) │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
## Security Flow
|
|
|
|
```
|
|
┌────────────────────────────────────────────────┐
|
|
│ 1. User Login │
|
|
│ → Receives JWT token │
|
|
│ → Token stored in localStorage │
|
|
└────────────────────────────────────────────────┘
|
|
↓
|
|
┌────────────────────────────────────────────────┐
|
|
│ 2. API Request │
|
|
│ → useApi() adds Authorization header │
|
|
│ → useApi() adds x-tenant-id header │
|
|
└────────────────────────────────────────────────┘
|
|
↓
|
|
┌────────────────────────────────────────────────┐
|
|
│ 3. Backend Validation │
|
|
│ → JwtAuthGuard validates token │
|
|
│ → Extracts user info (userId, tenantId) │
|
|
│ → Attaches to request object │
|
|
└────────────────────────────────────────────────┘
|
|
↓
|
|
┌────────────────────────────────────────────────┐
|
|
│ 4. Service Layer │
|
|
│ → Receives tenantId from request │
|
|
│ → All queries scoped to tenant │
|
|
│ → Tenant isolation enforced │
|
|
└────────────────────────────────────────────────┘
|
|
↓
|
|
┌────────────────────────────────────────────────┐
|
|
│ 5. Database │
|
|
│ → Tenant-specific database selected │
|
|
│ → Query executed in tenant context │
|
|
│ → Results returned │
|
|
└────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
**Legend:**
|
|
- `→` : Data flow direction
|
|
- `↕` : Bidirectional communication
|
|
- `├─→` : Hierarchical relationship
|
|
- `└─→` : Terminal branch
|
|
- `✓` : Successful operation
|