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
- Feature Summary
- Architecture Changes
- Database Schema Changes
- Component Documentation
- Service Layer Changes
- UI/UX Changes
- Usage Guide
- Migration Notes
- API Reference
Feature Summary
What Changed
Before:
- Organizations could have only one template (stored as
templateIdfield) - Template applied to all activities organization-wide
After:
- Organizations can have multiple templates stored in a
templatesarray - 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):
- Specific Activity Template (
activityId = N): Database template for the specific activity - Organization Template (
activityId = 0): Database template for full organization (if no activity-specific template exists) - 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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
activities | Array | No | [] | List of available activities with OU information |
currentActivityId | Number | No | 0 | Currently selected activity ID |
onActivityChange | Function | No | - | Callback when selection changes: (activityId, activity) => void |
trigger | Function | No | - | Custom trigger component: (triggerProps, displayText, icon) => ReactNode |
textColor | String | No | "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-intlfor 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):- Removes existing template with same
activityIdfromtemplatesarray - Pushes new template entry
{ activityId, templateId } - Description is auto-generated:
"Organization generic activity template"(activityId=0) or"Activity N template"(activityId>0) - Defaults to
activityId = 0if not specified
- Removes existing template with same
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
$pullto remove matching entry fromtemplatesarray - Searches by
templateId
- Uses
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:
- Template with exact
activityId - Template with
activityId = 0(if requestedactivityId !== 0) - Default file template for locale
- 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)→ UseloadTemplateByActivityId(0, locale)saveOrganizationTemplate(templateContent, locale)→ UsesaveTemplateByActivityId(templateContent, locale, 0)
UI/UX Changes
Template Editor Modal
File: src/lib/ui/modals/TemplateEditorModal.jsx
Changes
- Replaced EditNoteIcon with ActivityPicker in dialog title
- New Props:
activities: Array of available activitiescurrentActivityId: Selected activity scope
- Updated Handlers:
handleSave(): Now passesactivityIdto save actionhandleRefresh(): Now passesactivityIdto refresh actionhandleActivityChange(): Navigates to new URL with?activityId=Nparameter
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
- Accepts
searchParams: ReadsactivityIdfrom URL query parameter - Loads All Activities: Calls
getAllActivities()to fetch from ROPA - 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
- Navigate to Organization Settings → Templates
- ActivityPicker defaults to "Full organization" (
activityId = 0) - Edit template and click Save
- Template applies to all activities without specific templates
Creating Activity-Specific Template
- Navigate to Organization Settings → Templates
- Click ActivityPicker (shows current activity/organization name with icon)
- Select a specific activity from the dropdown
- Template Editor loads:
- Existing template for that activity (if exists)
- OR copy of organization template (if exists)
- OR default template file
- Edit and save
- Template now applies only to that specific activity
Template Source Indicators
The template editor shows the source of the loaded template:
| Source | Message |
|---|---|
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
- Import helper functions:
import {
loadTemplateByActivityId,
saveTemplateByActivityId
} from "@/lib/mongoose";
- 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
}
- 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.
Migration Script (Recommended)
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
templatesarray
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
| File | Changes | Lines Changed |
|---|---|---|
src/models/organization.js | Schema update: templateId → templates array | ~20 |
src/services/templateService.js | Updated createTemplate() and deleteTemplate() | ~40 |
src/lib/mongoose/templateActions.js | Added loadTemplateByActivityId(), saveTemplateByActivityId() | ~180 |
src/app/[locale]/(app)/e/org/templates/page.jsx | Added activities loading, activityId support | ~90 |
src/lib/ui/modals/TemplateEditorModal.jsx | Integrated ActivityPicker, updated handlers | ~30 |
src/lib/ui/pickers/ActivityPicker.jsx | New component | ~250 |
src/lib/ui/pickers/index.js | Added export | 1 |
messages/en.json | Added activityPicker section | 7 |
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
iconPositionproperty tocurrentActivityDatastate - Updated
defaultTriggerto 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
getAllActivitiesand organization fetch into singlegetAllActivitiesAndOrgfunction - Returns both activities array and organization name
- Passes
organizationNameprop 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
-
Template Model Schema (src/models/template.js):
- Added
descriptionfield (required, trimmed string) - Kept
activityIdfield for backward compatibility - Description values:
"Organization generic activity template"or"Activity {N} template"
- Added
-
Service Layer Updates (src/services/templateService.js):
createTemplate(): Now requiresdescriptionparameterupdateTemplate(): Addeddescriptionto allowed update fieldssaveTemplate(): Auto-generates description based on activityId parameter
-
Template Actions Updates (src/lib/mongoose/templateActions.js):
saveTemplateByActivityId(): Auto-generates description before creating/updating templatessaveOrganizationTemplate(): Auto-generates organization template description
Backward Compatibility
- The
activityIdfield is retained in the Template model - The organization model's
templatesarray still usesactivityIdas 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
-
OU-Level Templates: Extend to support templates at OU level
- Already supported in Template model schema
- Would require similar picker for OU selection
-
Template Inheritance Visualization: Show template hierarchy
- Visual indicator of which template is being used (inherited vs. custom)
- Tree view showing template relationships
-
Bulk Template Operations:
- Apply organization template to multiple activities
- Reset activity templates to organization default
- Export/import templates
-
Template Versioning:
- Keep history of template changes
- Rollback to previous versions
- Compare versions
-
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.jscomments - Component Examples: See
src/lib/ui/pickers/OuPicker.jsxfor 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