Skip to main content

Activity-Scoped Templates Feature Documentation

Overview

This document describes the implementation of activity-scoped templates, allowing organizations to define custom Handlebars templates for individual processing activities or the entire organization.

Date: 2026-01-02 Version: 1.0


Table of Contents

  1. Feature Summary
  2. Architecture Changes
  3. Database Schema Changes
  4. Component Documentation
  5. Service Layer Changes
  6. UI/UX Changes
  7. Usage Guide
  8. Migration Notes
  9. API Reference

Feature Summary

What Changed

Before:

  • Organizations could have only one template (stored as templateId field)
  • Template applied to all activities organization-wide

After:

  • Organizations can have multiple templates stored in a templates array
  • Each template is associated with an activityId:
    • activityId = 0: Full organization template (default)
    • activityId > 0: Specific activity template
  • Template Editor UI includes an ActivityPicker to select scope
  • Automatic fallback: activity template → organization template → default file template

Architecture Changes

High-Level Flow

User opens Template Editor

Selects activity via ActivityPicker (or defaults to "Full organization")

System loads template:
1. Check if template exists for specific activityId
2. If not, fallback to activityId=0 template (if available)
3. If not, load default .hbs file from filesystem

User edits and saves template

Template saved with associated activityId in organization.templates array

Template Loading Priority

Templates are NOT locale-specific in the organization's templates array. Each Template document has its own locale field.

Loading hierarchy (by activityId only):

  1. Specific Activity Template (activityId = N): Database template for the specific activity
  2. Organization Template (activityId = 0): Database template for full organization (if no activity-specific template exists)
  3. Hardcoded Template File: Always exists for all supported locales in templates/activity-declaration-{locale}.hbs

Database Schema Changes

Template Model

File: src/models/template.js

Updated Schema (2026-01-10)

A new description field has been added to the Template model to provide human-readable context for each template:

description: {
type: String,
required: true,
trim: true,
}

Description Values:

  • Organization-level template: "Organization generic activity template"
  • Activity-specific template: "Activity {activityId} template"

Note: The activityId field is retained in the Template model for backward compatibility and consistency with the organization's templates array lookup system.

Organization Model

File: src/models/organization.js

Old Schema

templateId: {
type: mongoose.Schema.Types.ObjectId,
required: false,
}

New Schema

templates: {
type: [
{
activityId: {
type: Number,
required: true,
},
templateId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
},
},
],
default: [],
}

Schema Version Update

Schema version incremented from 10 to 11 to track this migration.

Example Document Structure

{
"_id": "507f1f77bcf86cd799439011",
"shortName": "acme-corp",
"schemaVersion": 11,
"templates": [
{
"activityId": 0,
"templateId": "507f1f77bcf86cd799439012"
},
{
"activityId": 5,
"templateId": "507f1f77bcf86cd799439013"
},
{
"activityId": 12,
"templateId": "507f1f77bcf86cd799439014"
}
],
// ... other fields
}

Component Documentation

ActivityPicker Component

File: src/lib/ui/pickers/ActivityPicker.jsx

Purpose

Allows users to select between:

  • Full organization (activityId = 0)
  • Any specific processing activity from the ROPA

Props

PropTypeRequiredDefaultDescription
activitiesArrayNo[]List of available activities with OU information
currentActivityIdNumberNo0Currently selected activity ID
onActivityChangeFunctionNo-Callback when selection changes: (activityId, activity) => void
triggerFunctionNo-Custom trigger component: (triggerProps, displayText, icon) => ReactNode
textColorStringNo"inherit"Color for trigger text/icon

Activity Object Structure

Each activity in the activities array should have:

{
activityId: Number, // Unique activity ID
activityName: String, // Display name
activityIcon: String, // Icon name from RopaIcon set
ouId: Number, // Parent OU ID
ouName: String, // Parent OU name
ouColor: String, // OU color (hex code) - used as background
}

Features

  • Color-Coded Menu Items: Each activity displays with its OU's background color
  • Icon Support: Shows activity icon (or fallback to BusinessIcon)
  • Grouped Sorting: Activities sorted by OU name, then activity name
  • Full Organization Option: Special menu item at the top with activityId = 0
  • Localized Labels: Uses next-intl for internationalization

Usage Example

import { ActivityPicker } from "@/lib/ui/pickers";

function MyComponent({ activities }) {
const [currentActivityId, setCurrentActivityId] = useState(0);

return (
<ActivityPicker
activities={activities}
currentActivityId={currentActivityId}
onActivityChange={(activityId, activity) => {
console.log("Selected:", activityId, activity);
setCurrentActivityId(activityId);
}}
/>
);
}

Service Layer Changes

Template Service

File: src/services/templateService.js

createTemplate() - Updated (2026-01-10)

Now requires a description field:

createTemplate({
orgId,
ouId?,
activityId?,
locale,
handlebars,
description // REQUIRED: Human-readable template description
})

Behavior:

  • For organization-level templates (no ouId):
    1. Removes existing template with same activityId from templates array
    2. Pushes new template entry { activityId, templateId }
    3. Description is auto-generated: "Organization generic activity template" (activityId=0) or "Activity N template" (activityId>0)
    4. Defaults to activityId = 0 if not specified

updateTemplate() - Updated (2026-01-10)

Now accepts description as an updatable field:

updateTemplate(templateId, {
locale?,
handlebars?,
description? // NEW: Can update template description
})

deleteTemplate() - Updated

Behavior:

  • For organization-level templates:
    • Uses $pull to remove matching entry from templates array
    • Searches by templateId

Template Actions (Helper Functions)

File: src/lib/mongoose/templateActions.js

New Functions

loadTemplateByActivityId(activityId, locale)

Loads template with fallback logic:

loadTemplateByActivityId(5, 'en')

Returns:

{
isError: false,
data: {
template: "...", // Handlebars template content
source: "database", // "database" | "database-fallback" | "default" | "default-fallback"
templateId: ObjectId, // Template ID (or null if using fallback/default)
activityId: 5 // The requested activityId
}
}

Loading Priority:

  1. Template with exact activityId
  2. Template with activityId = 0 (if requested activityId !== 0)
  3. Default file template for locale
  4. Default English template
saveTemplateByActivityId(templateContent, locale, activityId)

Saves template for a specific activity scope:

saveTemplateByActivityId(templateContent, 'en', 5)

Behavior:

  • If template exists for activityId: Updates it
  • Otherwise: Creates new template with orgTemplateActivityId = activityId

Deprecated Functions

  • loadOrganizationTemplate(locale) → Use loadTemplateByActivityId(0, locale)
  • saveOrganizationTemplate(templateContent, locale) → Use saveTemplateByActivityId(templateContent, locale, 0)

UI/UX Changes

Template Editor Modal

File: src/lib/ui/modals/TemplateEditorModal.jsx

Changes

  1. Replaced EditNoteIcon with ActivityPicker in dialog title
  2. New Props:
    • activities: Array of available activities
    • currentActivityId: Selected activity scope
  3. Updated Handlers:
    • handleSave(): Now passes activityId to save action
    • handleRefresh(): Now passes activityId to refresh action
    • handleActivityChange(): Navigates to new URL with ?activityId=N parameter

Activity Change Flow

When user selects a different activity:

User clicks ActivityPicker

Selects new activity (e.g., activityId=5)

handleActivityChange(5) called

Router navigates to /e/org/templates?activityId=5

Page reloads with new template data

Template Page

File: src/app/[locale]/(app)/e/org/templates/page.jsx

Changes

  1. Accepts searchParams: Reads activityId from URL query parameter
  2. Loads All Activities: Calls getAllActivities() to fetch from ROPA
  3. Uses New Functions:
    • loadTemplateByActivityId(activityId, locale)
    • saveTemplateByActivityId(template, locale, activityId)
    • refreshTemplateAction(locale, activityId)

Server Actions Updated

All server actions now include activityId:

const saveTemplateAction = async (template, locale, activityId) => {
"use server";
const result = await saveTemplateByActivityId(template, locale, activityId);
return toPlain(result);
};

Usage Guide

For End Users

Editing Organization-Wide Template

  1. Navigate to Organization SettingsTemplates
  2. ActivityPicker defaults to "Full organization" (activityId = 0)
  3. Edit template and click Save
  4. Template applies to all activities without specific templates

Creating Activity-Specific Template

  1. Navigate to Organization SettingsTemplates
  2. Click ActivityPicker (shows current activity/organization name with icon)
  3. Select a specific activity from the dropdown
  4. Template Editor loads:
    • Existing template for that activity (if exists)
    • OR copy of organization template (if exists)
    • OR default template file
  5. Edit and save
  6. Template now applies only to that specific activity

Template Source Indicators

The template editor shows the source of the loaded template:

SourceMessage
database"Loaded from custom template"
database-fallback"Loaded from organization template (copied for this activity)"
default"Loaded from hardcoded template ({locale})"

For Developers

Adding Activity Support to New Features

  1. Import helper functions:
import {
loadTemplateByActivityId,
saveTemplateByActivityId
} from "@/lib/mongoose";
  1. Load template:
const activityId = 5; // or 0 for organization
const result = await loadTemplateByActivityId(activityId, locale);

if (!result.isError) {
const { template, source, templateId } = result.data;
// Use template
}
  1. Save template:
const result = await saveTemplateByActivityId(
templateContent,
locale,
activityId
);

Migration Notes

Migrating Existing Organizations

Organizations with the old templateId field will continue to work but won't appear in the new ActivityPicker until migrated.

async function migrateOrganizationTemplates() {
const orgs = await OrganizationModel.find({
templateId: { $exists: true, $ne: null }
});

for (const org of orgs) {
await OrganizationModel.updateOne(
{ _id: org._id },
{
$set: {
templates: [
{
activityId: 0,
templateId: org.templateId
}
],
schemaVersion: 11
},
$unset: { templateId: "" }
}
);
}
}

Backward Compatibility

  • Old loadOrganizationTemplate() still works (marked as deprecated)
  • Old saveOrganizationTemplate() still works (marked as deprecated)
  • Template Editor gracefully handles missing templates array

API Reference

Localization Keys

File: messages/en.json

{
"activityPicker": {
"changeActivity": "Change activity or organization scope",
"fullOrganization": "Full organization",
"noActivities": "No activities available",
"selectActivity": "Select activity",
"title": "Select Template Scope",
"unknownOu": "Unknown OU",
"unnamedActivity": "Unnamed activity"
}
}

File Changes Summary

FileChangesLines Changed
src/models/organization.jsSchema update: templateIdtemplates array~20
src/services/templateService.jsUpdated createTemplate() and deleteTemplate()~40
src/lib/mongoose/templateActions.jsAdded loadTemplateByActivityId(), saveTemplateByActivityId()~180
src/app/[locale]/(app)/e/org/templates/page.jsxAdded activities loading, activityId support~90
src/lib/ui/modals/TemplateEditorModal.jsxIntegrated ActivityPicker, updated handlers~30
src/lib/ui/pickers/ActivityPicker.jsxNew component~250
src/lib/ui/pickers/index.jsAdded export1
messages/en.jsonAdded activityPicker section7

Total: ~618 lines changed/added


Testing Checklist

Functional Testing

  • Load template for organization (activityId=0)
  • Load template for specific activity
  • Save template for organization
  • Save template for specific activity
  • Switch between activities using ActivityPicker
  • Verify fallback to organization template when activity template doesn't exist
  • Verify fallback to default file when no database templates exist
  • Delete activity-specific template
  • Delete organization template
  • Verify template preview works for all scopes

UI/UX Testing

  • ActivityPicker displays "Full organization" by default
  • ActivityPicker shows all activities with correct names and icons
  • Activity items display with correct OU colors
  • Clicking activity navigates to correct URL with ?activityId parameter
  • Template source label displays correctly
  • Modal title shows selected activity name

Edge Cases

  • Organization with no templates (uses default file)
  • Organization with only activity-specific templates (no activityId=0)
  • Activity with no activities in ROPA
  • Invalid activityId in URL parameter
  • Missing locale template file (fallback to English)

Bug Fixes and Enhancements (2026-01-03)

Critical Bug Fix: Template Save from Validation Modal

Issue: When saving a template through the validation modal (after sanitization), the currentActivityId was not being passed to the save function, causing templates to be saved with activityId=0 regardless of the selected activity.

Root Cause: The handleSaveFromModal function in TemplateEditorModal.jsx was only passing two parameters (template, locale) instead of three (template, locale, currentActivityId).

Fix: Updated handleSaveFromModal to pass currentActivityId as the third parameter:

// Before
const result = await onSave(template, locale);

// After
const result = await onSave(template, locale, currentActivityId);

Files Changed:

  • src/lib/ui/modals/TemplateEditorModal.jsx (line 408)

ActivityPicker Enhancements

1. Organization Icon Position

Change: BusinessIcon now appears on the left side of "Full organization" text in the trigger button, while activity icons remain on the right.

Implementation:

  • Added iconPosition property to currentActivityData state
  • Updated defaultTrigger to conditionally render icon based on position
  • Full organization: iconPosition: "left", mr: 1 (margin-right)
  • Activities: iconPosition: "right", ml: 1 (margin-left)

2. Alphabetical Sorting

Change: Activities are now sorted alphabetically by activity name only (previously sorted by OU name first, then activity name).

Implementation:

// Before
.sort((a, b) => {
const ouCompare = (a.ouName || "").localeCompare(b.ouName || "");
if (ouCompare !== 0) return ouCompare;
return (a.activityName || "").localeCompare(b.activityName || "");
})

// After
.sort((a, b) => {
return (a.activityName || "").localeCompare(b.activityName || "");
})

3. Organization Name Display

Change: "Full organization" menu item now displays the organization name as a caption (where OU name appears for regular activities).

Implementation:

  • Optimized data fetching: Combined getAllActivities and organization fetch into single getAllActivitiesAndOrg function
  • Returns both activities array and organization name
  • Passes organizationName prop through TemplateEditorModal to ActivityPicker
  • Full organization menu item shows organization name in Typography caption

Structure:

{/* Full Organization */}
<MenuItem>
<BusinessIcon />
<Box>
<Typography variant="body2">Full organization</Typography>
<Typography variant="caption">{organizationName}</Typography>
</Box>
</MenuItem>

{/* Activities */}
<MenuItem>
<ActivityIcon />
<Box>
<Typography variant="body2">{activityName}</Typography>
<Typography variant="caption">{ouName}</Typography>
</Box>
</MenuItem>

4. Icon Display Rules

Change: BusinessIcon is now shown ONLY for the "Full organization" option. Activities without an icon show no icon (previously showed BusinessIcon as fallback).

Implementation:

  • Full organization: Always shows BusinessIcon
  • Activities with icon: Shows their specific icon via ShowIcon component
  • Activities without icon: Shows null (no icon)

Files Changed:

  • src/lib/ui/pickers/ActivityPicker.jsx (lines 63-80, 175-187, 214-216)
  • src/app/[locale]/(app)/e/org/templates/page.jsx (lines 70-118, 133)
  • src/lib/ui/modals/TemplateEditorModal.jsx (lines 70-71, 635)

Performance Optimization

Change: Eliminated duplicate organization data fetching in page.jsx.

Before:

  • getAllActivities() fetched organization internally
  • Page component fetched organization again for organization name
  • Result: 2 database queries for same data

After:

  • getAllActivitiesAndOrg() returns both activities and organization name
  • Single database query
  • Improved performance and reduced database load

Schema Enhancement (2026-01-10)

Template Model: Adding description field

Motivation: Improve template management by adding human-readable descriptions alongside the existing activityId field for better clarity and logging.

Changes Made

  1. Template Model Schema (src/models/template.js):

    • Added description field (required, trimmed string)
    • Kept activityId field for backward compatibility
    • Description values: "Organization generic activity template" or "Activity {N} template"
  2. Service Layer Updates (src/services/templateService.js):

    • createTemplate(): Now requires description parameter
    • updateTemplate(): Added description to allowed update fields
    • saveTemplate(): Auto-generates description based on activityId parameter
  3. Template Actions Updates (src/lib/mongoose/templateActions.js):

    • saveTemplateByActivityId(): Auto-generates description before creating/updating templates
    • saveOrganizationTemplate(): Auto-generates organization template description

Backward Compatibility

  • The activityId field is retained in the Template model
  • The organization model's templates array still uses activityId as the reference key
  • No changes required to UI components or API routes
  • Description is automatically generated from activityId during save operations

Migration Notes

Existing templates in the database will need to be migrated to add the description field. A migration script should:

// Example migration for existing templates
const templates = await TemplateModel.find({});

for (const template of templates) {
const description = template.activityId === 0
? "Organization generic activity template"
: `Activity ${template.activityId} template`;

await TemplateModel.updateOne(
{ _id: template._id },
{ $set: { description } }
);
}

Future Enhancements

Potential Features

  1. OU-Level Templates: Extend to support templates at OU level

    • Already supported in Template model schema
    • Would require similar picker for OU selection
  2. Template Inheritance Visualization: Show template hierarchy

    • Visual indicator of which template is being used (inherited vs. custom)
    • Tree view showing template relationships
  3. Bulk Template Operations:

    • Apply organization template to multiple activities
    • Reset activity templates to organization default
    • Export/import templates
  4. Template Versioning:

    • Keep history of template changes
    • Rollback to previous versions
    • Compare versions
  5. Template Preview Per Activity:

    • When editing organization template, preview how it renders for different activities
    • Side-by-side comparison

Troubleshooting

Common Issues

ActivityPicker shows no activities

Cause: No activities in ROPA for current locale Solution: Ensure ROPA has activities created; check locale matches

Template doesn't save

Cause: Missing activityId in save action Solution: Verify onSave callback receives all three parameters: (template, locale, activityId)

URL parameter not working

Cause: searchParams not passed to Page component Solution: Ensure Next.js page component receives searchParams prop:

const Page = async ({ params, searchParams }) => {
// ...
}

Template loads default instead of custom

Cause: Template not properly saved to database Solution: Check MongoDB templates array in organization document; verify templateId references exist


Support

For questions or issues, please refer to:

  • Code Repository: Check recent commits for implementation details
  • Schema Documentation: Review src/models/organization.js comments
  • Component Examples: See src/lib/ui/pickers/OuPicker.jsx for similar patterns

Document Version: 1.2 Last Updated: 2026-01-10 Author: Claude Code Implementation Changelog:

  • v1.2 (2026-01-10): Added description field to Template model, updated all services to generate and pass descriptions
  • v1.1 (2026-01-03): Added bug fixes and enhancements section, documented ActivityPicker improvements
  • v1.0 (2026-01-02): Initial documentation