# 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([ ...(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 {page} // Conditional rendering {hasPermission(user, 'DELETE_ASSETS') && ( )} ``` --- ### 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 // 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