39948-vm/frontend/docs/helpers-module.md
2026-07-03 16:11:24 +02:00

739 lines
18 KiB
Markdown

# Frontend Helpers Module
## Overview
The Helpers module provides **utility functions** for common operations across the frontend application including data formatting, permission checking, notification handling, text transformation, and file saving.
**Location:** `frontend/src/helpers/`
**Total Files:** 6 files (~298 LOC)
---
## Architecture
```
frontend/src/helpers/
├── dataFormatter.js (176 LOC) # Entity data formatters for display/edit
├── userPermissions.ts (19 LOC) # RBAC permission checking
├── notifyStateHandler.ts (32 LOC) # Redux notification state handlers
├── textFormatters.ts (53 LOC) # Text transformation utilities
├── humanize.ts (12 LOC) # String humanization
└── fileSaver.ts (6 LOC) # File download trigger
```
---
## Helper Files
### 1. Data Formatter (`dataFormatter.js`)
**Purpose:** Format entity data for display in tables, lists, and edit forms.
**Lines:** ~210 LOC
**Dependencies:** `dayjs`, `dayjs/plugin/relativeTime`, `lodash`
#### Core Formatters
| Function | Input | Output | Description |
|----------|-------|--------|-------------|
| `filesFormatter(arr)` | File array | File array | Pass-through for file arrays |
| `imageFormatter(arr)` | Image array | `[{ publicUrl }]` | Extract public URLs from images |
| `oneImageFormatter(arr)` | Image array | `string` | Get first image's public URL |
| `dateFormatter(date)` | Date/string | `YYYY-MM-DD` | Format date for display |
| `dateTimeFormatter(date)` | Date/string | `YYYY-MM-DD HH:mm` | Format datetime for display |
| `relativeTimestamp(date)` | Date/string | Human-readable | Format as relative or absolute time |
| `booleanFormatter(val)` | boolean | `'Yes'` / `'No'` | Human-readable boolean |
| `dataGridEditFormatter(obj)` | Object | Object | Transform relations to IDs for DataGrid |
#### Entity Formatters Pattern
Each entity has 4 formatters following this naming convention:
```javascript
// Display formatters (for table cells/lists)
[entity]ManyListFormatter(val) // Array → display names array
[entity]OneListFormatter(val) // Object → display name string
// Edit formatters (for form selects)
[entity]ManyListFormatterEdit(val) // Array → [{ id, label }]
[entity]OneListFormatterEdit(val) // Object → { id, label }
```
#### Supported Entities
| Entity | Display Field | Formatters |
|--------|---------------|------------|
| `users` | `firstName` | 4 formatters |
| `roles` | `name` | 4 formatters |
| `permissions` | `name` | 4 formatters |
| `projects` | `name` | 4 formatters |
| `assets` | `name` | 4 formatters |
| `tour_pages` | `name` | 4 formatters |
| `transitions` | `name` | 4 formatters |
#### Usage Examples
```javascript
import dataFormatter from '@/helpers/dataFormatter';
// Display in table cell
const userName = dataFormatter.usersOneListFormatter(user);
// → "John"
// Display list of roles
const roleNames = dataFormatter.rolesManyListFormatter(roles);
// → ["Administrator", "Editor"]
// Format for edit dropdown
const roleOptions = dataFormatter.rolesManyListFormatterEdit(roles);
// → [{ id: "uuid-1", label: "Administrator" }, ...]
// Format date
const formattedDate = dataFormatter.dateFormatter(createdAt);
// → "2024-03-15"
// Transform object for DataGrid editing
const editData = dataFormatter.dataGridEditFormatter({
name: "Test",
project: { id: "uuid", name: "My Project" },
tags: [{ id: "1" }, { id: "2" }]
});
// → { name: "Test", project: "uuid", tags: ["1", "2"] }
```
#### relativeTimestamp Method
Formats dates as human-readable relative or absolute timestamps. Uses `dayjs` with the `relativeTime` plugin.
```javascript
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
relativeTimestamp(date) {
if (!date) return '';
const d = dayjs(date);
const now = dayjs();
const diffMinutes = now.diff(d, 'minute');
const diffHours = now.diff(d, 'hour');
// Within last hour
if (diffMinutes < 60) {
return diffMinutes <= 1 ? 'Just now' : `${diffMinutes} min ago`;
}
// Within last 24 hours
if (diffHours < 24) {
return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
}
// Format as date + time
const isToday = d.isSame(now, 'day');
const isYesterday = d.isSame(now.subtract(1, 'day'), 'day');
const timeStr = d.format('HH:mm');
if (isToday) return `Today at ${timeStr}`;
if (isYesterday) return `Yesterday at ${timeStr}`;
return `${d.format('MMM D')} at ${timeStr}`;
}
```
**Output Examples:**
| Time Difference | Output |
|-----------------|--------|
| < 2 minutes | `"Just now"` |
| 5 minutes ago | `"5 min ago"` |
| 2 hours ago | `"2 hours ago"` |
| Same day | `"Today at 14:30"` |
| Yesterday | `"Yesterday at 09:15"` |
| Older dates | `"Apr 28 at 16:45"` |
**Usage:**
```javascript
import dataFormatter from '@/helpers/dataFormatter';
// In publish status buttons
dataFormatter.relativeTimestamp(lastPublishedAt);
// → "Just now" or "5 min ago" or "Today at 14:30"
// In constructor menu
dataFormatter.relativeTimestamp(page.updatedAt);
// → "2 hours ago"
```
---
#### dataGridEditFormatter Logic
Transforms nested objects/arrays to their IDs for DataGrid inline editing:
```javascript
dataGridEditFormatter(obj) {
return _.transform(obj, (result, value, key) => {
if (_.isArray(value)) {
result[key] = _.map(value, 'id'); // Array → array of IDs
} else if (_.isObject(value)) {
result[key] = value.id; // Object → ID string
} else {
result[key] = value; // Primitive → unchanged
}
});
}
```
---
### 2. User Permissions (`userPermissions.ts`)
**Purpose:** Check if current user has required permission(s) for RBAC.
**Lines:** 19 LOC
**Used By:** 48 files (layouts, pages, components, factories)
#### Function Signature
```typescript
export function hasPermission(
user: User,
permission_name: string | string[]
): boolean
```
#### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `user` | Object | Current user with `app_role` and `custom_permissions` |
| `permission_name` | `string` or `string[]` | Permission(s) to check |
#### Logic
1. Returns `false` if user has no role
2. Returns `true` if no permission specified (permission is optional)
3. Combines permissions from:
- `user.custom_permissions` (direct user permissions)
- `user.app_role.permissions` (role-based permissions)
4. Administrator role has implicit access to all permissions
5. For array of permissions, returns `true` if user has **any** of them
#### Implementation
```typescript
export function hasPermission(user, permission_name: string | string[]) {
if (!user?.app_role?.name) return false;
if (!permission_name) return true;
// Combine custom + role permissions
const permissions = new Set<string>([
...(user?.custom_permissions ?? []).map((p) => p.name),
...(user?.app_role?.permissions ?? []).map((p) => p.name),
]);
if (typeof permission_name === 'string') {
return permissions.has(permission_name) || user.app_role.name === 'Administrator';
} else {
return permission_name.some((permission) => permissions.has(permission));
}
}
```
#### Usage Examples
```typescript
import { hasPermission } from '@/helpers/userPermissions';
// Check single permission
if (hasPermission(currentUser, 'READ_USERS')) {
// Show users list
}
// Check multiple permissions (OR logic)
if (hasPermission(currentUser, ['CREATE_PROJECTS', 'UPDATE_PROJECTS'])) {
// Show project form
}
// In layout guard
<LayoutAuthenticated permission="READ_ASSETS">
{page}
</LayoutAuthenticated>
// Conditional rendering
{hasPermission(user, 'DELETE_ASSETS') && (
<DeleteButton onClick={handleDelete} />
)}
```
---
### 3. Notify State Handler (`notifyStateHandler.ts`)
**Purpose:** Manage Redux notification state for async action feedback.
**Lines:** 32 LOC
**Used By:** `createEntitySlice.ts` (affects all 13 entity slices)
#### Functions
| Function | Purpose | Notification Type |
|----------|---------|-------------------|
| `resetNotify(state)` | Clear notification state | - |
| `fulfilledNotify(state, msg)` | Show success message | `'success'` |
| `rejectNotify(state, action)` | Show error message | `'error'` |
#### State Shape
```typescript
interface NotificationState {
showNotification: boolean;
typeNotification: '' | 'success' | 'error' | 'warn';
textNotification: string;
}
```
#### Implementation
```typescript
// Clear notification
export const resetNotify = (state) => {
state.notify.showNotification = false;
state.notify.typeNotification = '';
state.notify.textNotification = '';
};
// Show success notification
export const fulfilledNotify = (state, msg) => {
state.notify.textNotification = msg;
state.notify.typeNotification = 'success';
state.notify.showNotification = true;
};
// Show error notification (handles various error formats)
export const rejectNotify = (state, action) => {
if (typeof action.payload === 'string') {
state.notify.textNotification = action.payload;
} else if (typeof action === 'object') {
// Handle validation errors with field-level messages
const obj = { ...action.payload?.errors };
delete obj['_errors'];
let msg = '';
for (const key in obj) {
msg += `${key}: ${obj[key]['_errors']}; \n `;
}
state.notify.textNotification = msg;
} else {
state.notify.textNotification = 'Network error';
}
state.notify.textNotification = state.notify.textNotification || 'Network error';
state.notify.typeNotification = 'error';
state.notify.showNotification = true;
};
```
#### Usage in createEntitySlice
```typescript
import { resetNotify, fulfilledNotify, rejectNotify } from '../helpers/notifyStateHandler';
// In extraReducers
builder.addCase(create.pending, (state) => {
resetNotify(state);
});
builder.addCase(create.fulfilled, (state) => {
fulfilledNotify(state, 'User has been created');
});
builder.addCase(create.rejected, (state, action) => {
rejectNotify(state, action);
});
```
---
### 4. Text Formatters (`textFormatters.ts`)
**Purpose:** Common text transformation utilities for UI display.
**Lines:** 53 LOC
#### Functions
| Function | Input | Output | Example |
|----------|-------|--------|---------|
| `singularize(str)` | Plural string | Singular string | `'roles'` `'role'` |
| `capitalize(str)` | String | Capitalized string | `'hello'` `'Hello'` |
| `snakeToTitle(str)` | snake_case | Title Case | `'tour_pages'` `'Tour Pages'` |
| `camelToTitle(str)` | camelCase | Title Case | `'tourPages'` `'Tour Pages'` |
#### Implementation
```typescript
/**
* Convert a plural title to singular by removing the last character.
*/
export function singularize(pluralTitle: string): string {
return pluralTitle.slice(0, -1);
}
/**
* Capitalize the first letter of a string.
*/
export function capitalize(str: string): string {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Convert snake_case to Title Case.
*/
export function snakeToTitle(str: string): string {
return str.split('_').map(capitalize).join(' ');
}
/**
* Convert camelCase to Title Case.
*/
export function camelToTitle(str: string): string {
return str
.replace(/([A-Z])/g, ' $1')
.trim()
.split(' ')
.map(capitalize)
.join(' ');
}
```
#### Usage Examples
```typescript
import { singularize, capitalize, snakeToTitle, camelToTitle } from '@/helpers/textFormatters';
// Entity name transformations
singularize('View roles'); // 'View role'
singularize('users'); // 'user'
// String capitalization
capitalize('hello world'); // 'Hello world'
// Case conversions
snakeToTitle('tour_pages'); // 'Tour Pages'
snakeToTitle('project_audio_tracks'); // 'Project Audio Tracks'
camelToTitle('tourPages'); // 'Tour Pages'
camelToTitle('projectAudioTracks'); // 'Project Audio Tracks'
```
---
### 5. Humanize (`humanize.ts`)
**Purpose:** Convert programmatic strings to human-readable format.
**Lines:** 12 LOC
#### Function
```typescript
export function humanize(str: string): string
```
#### Transformations
1. Trim leading/trailing spaces and underscores
2. Replace underscores and multiple spaces with single space
3. Capitalize first letter
#### Implementation
```typescript
export function humanize(str: string) {
if (!str) return '';
return str
.toString()
.replace(/^[\s_]+|[\s_]+$/g, '') // Trim spaces/underscores
.replace(/[_\s]+/g, ' ') // Replace _/spaces with single space
.replace(/^[a-z]/, function (m) { // Capitalize first letter
return m.toUpperCase();
});
}
```
#### Usage Examples
```typescript
import { humanize } from '@/helpers/humanize';
humanize('user_name'); // 'User name'
humanize('_hello_world_'); // 'Hello world'
humanize('SOME_CONSTANT'); // 'SOME CONSTANT'
humanize(''); // ''
```
---
### 6. File Saver (`fileSaver.ts`)
**Purpose:** Trigger file download in browser.
**Lines:** 6 LOC
**Dependencies:** `file-saver` library
#### Function
```typescript
export const saveFile = (
e: Event,
url: string,
name: string
): void
```
#### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `e` | Event | Click event (stopped for propagation) |
| `url` | string | File URL to download |
| `name` | string | Downloaded filename |
#### Implementation
```typescript
import { saveAs } from 'file-saver';
export const saveFile = (e, url: string, name: string) => {
e.stopPropagation();
saveAs(url, name);
};
```
#### Usage Examples
```typescript
import { saveFile } from '@/helpers/fileSaver';
// In a component
<button onClick={(e) => saveFile(e, asset.cdn_url, asset.name)}>
Download
</button>
// In a table action
const handleDownload = (e, file) => {
saveFile(e, file.publicUrl, file.originalName);
};
```
---
## Usage Patterns
### Permission Guard in Layout
```typescript
// layouts/Authenticated.tsx
import { hasPermission } from '../helpers/userPermissions';
if (!hasPermission(currentUser, permission)) {
router.push('/403');
return null;
}
```
### Data Formatting in Tables
```typescript
// In column configuration
import dataFormatter from '@/helpers/dataFormatter';
const columns = [
{
field: 'createdAt',
valueFormatter: ({ value }) => dataFormatter.dateTimeFormatter(value),
},
{
field: 'project',
valueFormatter: ({ value }) => dataFormatter.projectsOneListFormatter(value),
},
{
field: 'isActive',
valueFormatter: ({ value }) => dataFormatter.booleanFormatter(value),
},
];
```
### Notification Handling in Redux
```typescript
// In entity slice
import { resetNotify, fulfilledNotify, rejectNotify } from '../helpers/notifyStateHandler';
builder.addCase(deleteItem.pending, (state) => {
state.loading = true;
resetNotify(state);
});
builder.addCase(deleteItem.fulfilled, (state) => {
state.loading = false;
fulfilledNotify(state, 'Item has been deleted');
});
builder.addCase(deleteItem.rejected, (state, action) => {
state.loading = false;
rejectNotify(state, action);
});
```
### Text Transformation in UI
```typescript
import { snakeToTitle, singularize } from '@/helpers/textFormatters';
// Generate page title from entity name
const pageTitle = snakeToTitle(entityName);
// 'tour_pages' → 'Tour Pages'
// Generate singular form for messages
const message = `${singularize(entityName)} created`;
// 'Users' → 'User created'
```
---
## Helper Dependencies
```
dataFormatter.js
├── dayjs (date formatting)
└── lodash (object transformation)
fileSaver.ts
└── file-saver (browser download)
userPermissions.ts
├── Used by: layouts/Authenticated.tsx
├── Used by: factories/createListPage.tsx
├── Used by: components/DataGrid/configBuilderFactory.tsx
└── Used by: 48+ component files
notifyStateHandler.ts
└── Used by: stores/createEntitySlice.ts
└── Affects: All 13 entity slices
```
---
## File Inventory
| File | LOC | Category | Exports | Used By |
|------|-----|----------|---------|---------|
| `dataFormatter.js` | ~210 | Data | 1 default object | 30+ files |
| `userPermissions.ts` | 19 | Auth | `hasPermission` | 48 files |
| `notifyStateHandler.ts` | 32 | Redux | 3 functions | createEntitySlice |
| `textFormatters.ts` | 53 | Text | 4 functions | UI components |
| `humanize.ts` | 12 | Text | `humanize` | UI components |
| `fileSaver.ts` | 6 | File | `saveFile` | Download buttons |
| **Total** | **~332** | | **11 exports** | |
---
## Best Practices
### 1. Use Type-Safe Permission Checks
```typescript
// Good - explicit permission constant
import { Permission } from '@/types/permissions';
hasPermission(user, Permission.READ_USERS);
// Avoid - magic strings
hasPermission(user, 'READ_USERS');
```
### 2. Handle Empty Values in Formatters
```typescript
// dataFormatter handles null/undefined
const name = dataFormatter.usersOneListFormatter(null);
// → '' (empty string, not error)
```
### 3. Use Appropriate Formatter Type
```typescript
// For display (table cells, lists)
dataFormatter.rolesOneListFormatter(role); // → "Administrator"
// For edit forms (dropdowns)
dataFormatter.rolesOneListFormatterEdit(role); // → { id: "...", label: "Administrator" }
```
### 4. Consistent Notification Messages
```typescript
// In createEntitySlice
fulfilledNotify(state, `${displayName} has been created`);
fulfilledNotify(state, `${displayName} has been updated`);
fulfilledNotify(state, `${displayName} has been deleted`);
```
---
## Adding New Helpers
### Adding Entity Formatter
```javascript
// In dataFormatter.js
widgetsManyListFormatter(val) {
if (!val || !val.length) return [];
return val.map((item) => item.name);
},
widgetsOneListFormatter(val) {
if (!val) return '';
return val.name;
},
widgetsManyListFormatterEdit(val) {
if (!val || !val.length) return [];
return val.map((item) => {
return { id: item.id, label: item.name };
});
},
widgetsOneListFormatterEdit(val) {
if (!val) return '';
return { label: val.name, id: val.id };
},
```
### Adding New Helper File
```typescript
// helpers/newHelper.ts
/**
* Helper description
*/
export function helperFunction(input: InputType): OutputType {
// Implementation
}
// No index.ts - import directly from file
import { helperFunction } from '@/helpers/newHelper';
```
---
## Related Documentation
- [factories-module.md](./factories-module.md) - Uses hasPermission in createListPage
- [stores-module.md](./stores-module.md) - Uses notifyStateHandler in createEntitySlice
- [types-module.md](./types-module.md) - Permission enum types
- [components-module.md](./components-module.md) - Uses dataFormatter in tables