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

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

  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

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

  1. Trim leading/trailing spaces and underscores
  2. Replace underscores and multiple spaces with single space
  3. 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';