Aller au contenu principal

Module System Documentation

Overview

The Module System is the backbone of the Baldr Dashboard's dynamic feature architecture. It allows administrators to enable, disable, configure, and organize application features without requiring code changes or deployments.


What is a Module?

A module represents a distinct feature area or section of the application. Each module:

  • Has its own data model and API endpoints
  • Can be independently enabled/disabled
  • Appears as a menu item when active
  • Has its own icon, label, and routing
  • Can support multiple languages
  • May display notification badges (via stats)

Common Modules

ModuleTypePurposeIcon
NewsnewsNews article managementnewspaper
EventseventsEvent calendar and managementevent
ProductsproductsProduct cataloginventory_2
GallerygalleryImage gallery managementphoto_library
FAQfaqFrequently asked questionshelp
StaffstaffTeam member profilesgroup
BookingsbookingsReservation systembook_online
ContactcontactContact form submissionsmail

Module Structure

Interface Definition

interface IModule {
_id: string; // Unique identifier
active: boolean; // Enabled/disabled state
title: string; // Display name (e.g., "News & Articles")
slug: string; // URL path (e.g., "news")
type: string; // Backend type identifier
icon: string; // Material Symbols icon name
config?: Config; // Optional configuration
createdAt?: Date; // Creation timestamp
updatedAt?: Date; // Last update timestamp
}

Configuration Object

interface Config {
languages?: string[]; // Supported language codes ["fr", "en", "es"]
order?: number; // Display order in menu (1 = first)
deletable?: boolean; // Can be deleted by users
stats?: string; // Stats key for notification badges
endpoint?: string; // API endpoint path
}

Module Lifecycle

┌─────────────────────────────────────────────────────────┐
│ Module Lifecycle │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. AVAILABLE │
│ ↓ │
│ Module template exists but not created │
│ (shown in "Available Modules" list) │
│ │
│ 2. CREATED │
│ ↓ │
│ Module instance created in database │
│ active: false by default │
│ │
│ 3. CONFIGURED │
│ ↓ │
│ Admin sets: title, icon, languages, order │
│ Still active: false │
│ │
│ 4. ACTIVATED │
│ ↓ │
│ Set active: true │
│ Appears in navigation menu │
│ Available to all users │
│ │
│ 5. DEACTIVATED (optional) │
│ ↓ │
│ Set active: false │
│ Hidden from menu but data preserved │
│ │
│ 6. DELETED (optional, if deletable) │
│ ↓ │
│ Module removed from database │
│ Associated data may be affected │
│ │
└─────────────────────────────────────────────────────────┘

Module API

Available Operations

import Module from "~/api/module.api";

// 1. Get all modules
const { items: modules } = await Module.getAll();

// 2. Get available module templates
const available = await Module.getAvailable();

// 3. Get single module
const module = await Module.getById(moduleId);

// 4. Create new module
const newModule = await Module.create({
name: "products",
title: "Products",
slug: "products",
type: "products",
icon: "inventory_2",
active: true,
config: {
languages: ["fr", "en"],
order: 3,
deletable: true,
stats: "products_count"
}
});

// 5. Update module
await Module.update(moduleId, {
title: "Updated Title",
order: 1,
active: false
});

// 6. Delete module
await Module.deleteMany({ ids: [moduleId] });

Integration Points

1. ModulesContext

The ModulesContext provides global access to the module list:

import { useModules } from "~/context/modules.context";

function MyComponent() {
const { modules, isLoading } = useModules();

// Filter active modules
const activeModules = modules.filter(m => m.active);

// Sort by order
const sortedModules = [...activeModules].sort((a, b) =>
(a.config?.order || 0) - (b.config?.order || 0)
);

return (
<nav>
{sortedModules.map(module => (
<NavLink key={module._id} to={`/${module.slug}`}>
<span className="material-symbols-outlined">{module.icon}</span>
{module.title}
</NavLink>
))}
</nav>
);
}

2. Dynamic Menu

The Menu organism automatically renders modules:

// app/components/organisms/menu.organism.tsx

const { modules } = useModules();

return (
<nav>
{modules
.filter(m => m.active)
.sort((a, b) => (a.config?.order || 0) - (b.config?.order || 0))
.map(module => (
<MenuItem
key={module._id}
icon={module.icon}
label={module.title}
to={`/${module.slug}`}
badge={getModuleStats(module.config?.stats)}
/>
))
}
</nav>
);

3. Routing

Modules automatically create routes:

// app/routes.ts

import { modules } from "~/context/modules.context";

// For each active module, routes are generated:
// /news → News listing page
// /news/:id → News detail page
// /news/new → Create news page
// /news/:id/edit → Edit news page

4. Stats Integration

Modules can display notification badges:

import { useStats } from "~/context/stats.context";

function ModuleMenuItem({ module }) {
const { stats } = useStats();

// Find stats for this module
const moduleStats = stats.find(s => s.key === module.config?.stats);

return (
<NavLink to={`/${module.slug}`}>
{module.title}
{moduleStats && moduleStats.value > 0 && (
<Badge value={moduleStats.value} />
)}
</NavLink>
);
}

Creating a New Module

Step 1: Define Module Template (Backend)

// Backend: module.templates.ts
{
name: "products",
title: "Products",
description: "Manage product catalog",
icon: "inventory_2",
type: "products",
endpoint: "/products",
stats: "products_count",
deletable: true
}

Step 2: Create Module Instance

// Admin clicks "Add Module" and selects "Products"
await Module.create({
name: "products",
title: "Products",
slug: "products",
type: "products",
icon: "inventory_2",
active: false, // Start disabled
config: {
languages: ["fr", "en"],
order: 5,
deletable: true
}
});

Step 3: Configure Module

// Admin configures the module
await Module.update(moduleId, {
title: "Product Catalog",
icon: "shopping_cart",
config: {
languages: ["fr", "en", "es", "de"],
order: 3
}
});

Step 4: Activate Module

// Admin enables the module
await Module.update(moduleId, {
active: true
});

// Module now appears in navigation menu
// Users can access /products

Module Configuration Best Practices

1. Naming Conventions

// ✅ Good
{
name: "products", // Lowercase, singular or plural
slug: "products", // Same as name
type: "products", // Matches backend route
title: "Products" // Human-readable
}

// ❌ Bad
{
name: "Product", // Should be lowercase
slug: "product-list", // Doesn't match name
type: "items", // Doesn't match name
title: "products" // Should be capitalized
}

2. Icon Selection

Use Material Symbols icons that clearly represent the module:

// ✅ Good choices
{ name: "news", icon: "newspaper" }
{ name: "events", icon: "event" }
{ name: "products", icon: "inventory_2" }
{ name: "gallery", icon: "photo_library" }
{ name: "staff", icon: "group" }
{ name: "contact", icon: "mail" }

// ❌ Avoid generic icons
{ name: "news", icon: "star" } // Too generic
{ name: "events", icon: "check" } // Not descriptive

3. Module Ordering

// Typical order (lower = higher in menu)
1. News (most important/updated)
2. Events
3. Products
4. Gallery
5. Staff
6. FAQ
7. Contact (less frequently accessed)

4. Language Configuration

// ✅ Good - Specify all supported languages
config: {
languages: ["fr", "en", "es", "de"]
}

// ❌ Bad - Empty or missing
config: {
languages: [] // No languages enabled
}
// This would break translation system

Module Permissions

Checking Module Access

import { useModules } from "~/context/modules.context";

function ProtectedFeature() {
const { modules } = useModules();

// Check if module exists and is active
const hasProductsModule = modules.some(
m => m.type === "products" && m.active
);

if (!hasProductsModule) {
return <div>Products feature not available</div>;
}

return <ProductsPage />;
}

Role-Based Module Access

import { useUser } from "~/context/user.context";
import { useModules } from "~/context/modules.context";

function AdminModuleManager() {
const { credential } = useUser();
const { modules } = useModules();

// Only admins can manage modules
if (credential?.role !== "admin") {
return <AccessDenied />;
}

return (
<div>
<h1>Manage Modules</h1>
{modules.map(module => (
<ModuleCard key={module._id} module={module} />
))}
</div>
);
}

Module Stats System

How Stats Work

  1. Backend calculates counts/metrics for each module
  2. Stats API returns array of { key, value, label }
  3. Module's config.stats field links to a stats key
  4. Menu displays badge with the stats value

Example Stats Configuration

// Module configuration
{
type: "news",
config: {
stats: "news_draft" // Key to match in stats array
}
}

// Stats API returns
[
{ key: "news_draft", value: 5, label: "Draft articles" },
{ key: "news_published", value: 42, label: "Published articles" },
{ key: "events_upcoming", value: 3, label: "Upcoming events" }
]

// Menu displays badge with "5" for News module

Custom Stats Display

function ModuleStatsDisplay({ module }: { module: IModule }) {
const { stats } = useStats();

// Get all stats for this module type
const moduleStats = stats.filter(s =>
s.key.startsWith(module.type)
);

return (
<div>
<h3>{module.title} Statistics</h3>
{moduleStats.map(stat => (
<div key={stat.key}>
{stat.label}: {stat.value}
</div>
))}
</div>
);
}

Troubleshooting

Issue: Module not appearing in menu

Possible causes:

  1. active: false - Module is disabled
  2. Module not in database - Check Module.getAll()
  3. User doesn't have permission - Check role-based access
  4. Frontend cache stale - Refresh page or invalidate query

Solution:

// Check module state
const module = await Module.getById(moduleId);
console.log("Active:", module.active);

// Activate module
await Module.update(moduleId, { active: true });

// Invalidate cache
queryClient.invalidateQueries({ queryKey: ["modules"] });

Issue: Module appears in wrong order

Solution:

// Update module order
await Module.update(moduleId, {
config: {
...module.config,
order: 2 // Position in menu
}
});

Issue: Module stats not updating

Possible causes:

  1. Stats key mismatch - Module's config.stats doesn't match stats API key
  2. Stats not refreshing - Stats context refetches every 30 seconds
  3. Backend not calculating stats correctly

Solution:

// Check stats key
console.log("Module stats key:", module.config?.stats);

// Check available stats
const { stats } = useStats();
console.log("Available stats:", stats.map(s => s.key));

// Manually refetch stats
queryClient.invalidateQueries({ queryKey: ["stats"] });

Advanced: Dynamic Module Loading

Lazy Loading Module Components

// app/routes.ts
import { lazy } from "react";

const moduleRoutes = modules.map(module => ({
path: `/${module.slug}`,
component: lazy(() => import(`~/pages/${module.type}.page`))
}));

Module-Specific Layouts

function ModuleLayout({ module }: { module: IModule }) {
return (
<div>
<header>
<span className="material-symbols-outlined">{module.icon}</span>
<h1>{module.title}</h1>
</header>
<main>
{/* Module content */}
</main>
</div>
);
}

Summary

FeatureDescriptionKey Point
DynamicModules can be added/removed without code changesNo deployment required
ConfigurableEach module has its own settingsIcons, labels, languages
OrderedModules can be reordered in menuDrag-and-drop support
StatefulModules track active/inactive stateEnable/disable anytime
IntegratedWorks with routing, stats, permissionsSeamless UX
ScalableEasy to add new modulesTemplate-based creation

Last Updated: October 28, 2025
Version: 1.0.0