feat(email-templates): categorise + sub-categorise + link to feature flags
This commit is contained in:
@@ -1,18 +1,30 @@
|
||||
/**
|
||||
* Migration: Categorise email templates + link them to feature flags.
|
||||
*
|
||||
* Adds two metadata columns to `email_templates`:
|
||||
* Adds three metadata columns to `email_templates`:
|
||||
*
|
||||
* - `category` — display group in the admin Templates UI. One of:
|
||||
* - `category` — top-level display group in the admin Templates UI.
|
||||
* One of:
|
||||
* 'core' — gallery delivery, admin, system, backups.
|
||||
* Always visible, no feature flag.
|
||||
* 'customers' — customer-portal lifecycle (invitation, etc.).
|
||||
* 'customers' — customer-portal lifecycle (invitation, reset).
|
||||
* 'billing' — Bills feature (#354, not yet built).
|
||||
* 'quotes' — Quotes feature (#354, not yet built).
|
||||
* 'calendar' — Calendar feature (#354, not yet built).
|
||||
* Values outside this set are accepted (forward-compat) but the
|
||||
* UI will lump them under 'core' for now.
|
||||
*
|
||||
* - `subcategory` — second-level group inside `core` (which is busy
|
||||
* with 14 templates). One of:
|
||||
* 'gallery' — gallery delivery lifecycle (created / expiring /
|
||||
* expired / archived).
|
||||
* 'admin' — admin lifecycle (invitation, password reset).
|
||||
* 'backup' — DB + file backups (completed / failed) and
|
||||
* restores.
|
||||
* 'system' — version update notifications.
|
||||
* Only meaningful when category='core'; other categories ignore
|
||||
* it. NULL on rows that don't need a sub-bucket.
|
||||
*
|
||||
* - `feature_flag` — name of the feature flag whose `false` value
|
||||
* should mark this template as "Feature off" in the admin UI.
|
||||
* NULL means the template is always active (gallery delivery,
|
||||
@@ -37,6 +49,13 @@ exports.up = async function(knex) {
|
||||
});
|
||||
}
|
||||
|
||||
const hasSubcategory = await knex.schema.hasColumn('email_templates', 'subcategory');
|
||||
if (!hasSubcategory) {
|
||||
await knex.schema.alterTable('email_templates', (table) => {
|
||||
table.string('subcategory', 32).nullable();
|
||||
});
|
||||
}
|
||||
|
||||
const hasFeatureFlag = await knex.schema.hasColumn('email_templates', 'feature_flag');
|
||||
if (!hasFeatureFlag) {
|
||||
await knex.schema.alterTable('email_templates', (table) => {
|
||||
@@ -48,33 +67,41 @@ exports.up = async function(knex) {
|
||||
// a row that's been renamed. Templates not in this map keep the
|
||||
// 'core' / NULL defaults from the column definitions above.
|
||||
const TEMPLATE_METADATA = {
|
||||
// Core galleries — always-on, no flag.
|
||||
gallery_created: { category: 'core', feature_flag: null },
|
||||
expiration_warning: { category: 'core', feature_flag: null },
|
||||
gallery_expired: { category: 'core', feature_flag: null },
|
||||
archive_complete: { category: 'core', feature_flag: null },
|
||||
// Admin / RBAC — always-on (admin login is foundational).
|
||||
admin_invitation: { category: 'core', feature_flag: null },
|
||||
admin_password_reset: { category: 'core', feature_flag: null },
|
||||
// System — backups, restores, version-update notifications.
|
||||
database_backup_completed: { category: 'core', feature_flag: null },
|
||||
database_backup_failed: { category: 'core', feature_flag: null },
|
||||
restore_completed: { category: 'core', feature_flag: null },
|
||||
restore_failed: { category: 'core', feature_flag: null },
|
||||
backup_completed: { category: 'core', feature_flag: null },
|
||||
backup_failed: { category: 'core', feature_flag: null },
|
||||
version_update_available: { category: 'core', feature_flag: null },
|
||||
version_update_test: { category: 'core', feature_flag: null },
|
||||
// Customer portal (#354). customer_invitation is the only template
|
||||
// here today; calendar / quotes / bills get their templates added
|
||||
// when those features ship.
|
||||
customer_invitation: { category: 'customers', feature_flag: 'customerPortal' },
|
||||
// Core / Galleries — gallery delivery lifecycle.
|
||||
gallery_created: { category: 'core', subcategory: 'gallery', feature_flag: null },
|
||||
expiration_warning: { category: 'core', subcategory: 'gallery', feature_flag: null },
|
||||
gallery_expired: { category: 'core', subcategory: 'gallery', feature_flag: null },
|
||||
archive_complete: { category: 'core', subcategory: 'gallery', feature_flag: null },
|
||||
// Core / Admin — admin account lifecycle.
|
||||
admin_invitation: { category: 'core', subcategory: 'admin', feature_flag: null },
|
||||
admin_password_reset: { category: 'core', subcategory: 'admin', feature_flag: null },
|
||||
// Core / Backup — database + file backups + restores.
|
||||
database_backup_completed: { category: 'core', subcategory: 'backup', feature_flag: null },
|
||||
database_backup_failed: { category: 'core', subcategory: 'backup', feature_flag: null },
|
||||
restore_completed: { category: 'core', subcategory: 'backup', feature_flag: null },
|
||||
restore_failed: { category: 'core', subcategory: 'backup', feature_flag: null },
|
||||
backup_completed: { category: 'core', subcategory: 'backup', feature_flag: null },
|
||||
backup_failed: { category: 'core', subcategory: 'backup', feature_flag: null },
|
||||
// Core / System — version-update notifications.
|
||||
version_update_available: { category: 'core', subcategory: 'system', feature_flag: null },
|
||||
version_update_test: { category: 'core', subcategory: 'system', feature_flag: null },
|
||||
// Customer portal (#354). Admin-triggered password reset for
|
||||
// customer accounts ships in the same feature, so both templates
|
||||
// share the `customers` category and the `customerPortal` flag.
|
||||
// Future calendar / quotes / bills templates will land here under
|
||||
// their own categories.
|
||||
customer_invitation: { category: 'customers', subcategory: null, feature_flag: 'customerPortal' },
|
||||
customer_password_reset: { category: 'customers', subcategory: null, feature_flag: 'customerPortal' },
|
||||
};
|
||||
|
||||
for (const [key, meta] of Object.entries(TEMPLATE_METADATA)) {
|
||||
await knex('email_templates')
|
||||
.where({ template_key: key })
|
||||
.update({ category: meta.category, feature_flag: meta.feature_flag });
|
||||
.update({
|
||||
category: meta.category,
|
||||
subcategory: meta.subcategory,
|
||||
feature_flag: meta.feature_flag,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,6 +114,12 @@ exports.down = async function(knex) {
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn('email_templates', 'subcategory')) {
|
||||
await knex.schema.alterTable('email_templates', (table) => {
|
||||
table.dropColumn('subcategory');
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn('email_templates', 'category')) {
|
||||
await knex.schema.alterTable('email_templates', (table) => {
|
||||
table.dropColumn('category');
|
||||
|
||||
@@ -320,6 +320,7 @@ router.get('/templates', adminAuth, requirePermission('email.view'), async (req,
|
||||
// 'core' / null fall-backs so the frontend keeps working
|
||||
// without a hard dependency on the new columns.
|
||||
category: template.category || 'core',
|
||||
subcategory: template.subcategory || null,
|
||||
feature_flag: template.feature_flag || null,
|
||||
updated_at: template.updated_at,
|
||||
});
|
||||
@@ -352,6 +353,7 @@ router.get('/templates/:key', adminAuth, requirePermission('email.view'), async
|
||||
translations,
|
||||
// See list endpoint for the rationale on the || fallbacks.
|
||||
category: template.category || 'core',
|
||||
subcategory: template.subcategory || null,
|
||||
feature_flag: template.feature_flag || null,
|
||||
updated_at: template.updated_at,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user