18 KiB
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:
// 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
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.
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:
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:
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
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
- Returns
falseif user has no role - Returns
trueif no permission specified (permission is optional) - Combines permissions from:
user.custom_permissions(direct user permissions)user.app_role.permissions(role-based permissions)
- Administrator role has implicit access to all permissions
- For array of permissions, returns
trueif user has any of them
Implementation
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
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
interface NotificationState {
showNotification: boolean;
typeNotification: '' | 'success' | 'error' | 'warn';
textNotification: string;
}
Implementation
// 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
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
/**
* 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
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
export function humanize(str: string): string
Transformations
- Trim leading/trailing spaces and underscores
- Replace underscores and multiple spaces with single space
- Capitalize first letter
Implementation
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
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
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
import { saveAs } from 'file-saver';
export const saveFile = (e, url: string, name: string) => {
e.stopPropagation();
saveAs(url, name);
};
Usage Examples
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
// layouts/Authenticated.tsx
import { hasPermission } from '../helpers/userPermissions';
if (!hasPermission(currentUser, permission)) {
router.push('/403');
return null;
}
Data Formatting in Tables
// 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
// 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
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
// 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
// dataFormatter handles null/undefined
const name = dataFormatter.usersOneListFormatter(null);
// → '' (empty string, not error)
3. Use Appropriate Formatter Type
// For display (table cells, lists)
dataFormatter.rolesOneListFormatter(role); // → "Administrator"
// For edit forms (dropdowns)
dataFormatter.rolesOneListFormatterEdit(role); // → { id: "...", label: "Administrator" }
4. Consistent Notification Messages
// 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
// 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
// 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 - Uses hasPermission in createListPage
- stores-module.md - Uses notifyStateHandler in createEntitySlice
- types-module.md - Permission enum types
- components-module.md - Uses dataFormatter in tables