16 KiB
SettingsPage
Overview
The SettingsPage provides a centralized interface for configuring all system-wide settings including organization branding, theme colors, email (SMTP), and feature toggles. It uses a tabbed interface with separate sections for each settings category.
Route: /app/settings
Component: admin/src/pages/SettingsPage.tsx (420 lines)
Auth Required: Yes (SUPER_ADMIN role recommended for production)
Layout: AppLayout
Screenshot
[Screenshot: Settings page with 4 tabs (Organization, Theme Colors, Email, Feature Toggles). Currently showing Email tab with sections for Sender configuration, Active SMTP Provider toggle (MailHog vs Production), connection details, and test buttons. At bottom is a large "Save Settings" button.]
Features
- Tabbed interface — 4 organized sections:
- Organization (branding, logo, footer)
- Theme Colors (admin + public themes with live preview)
- Email (SMTP configuration with dual providers)
- Feature Toggles (enable/disable modules)
- SMTP provider switching — Toggle between MailHog (dev) and Production
- Live theme preview — Color swatches + gradient preview
- SMTP testing — Test connection + send test email
- Form persistence — Settings loaded from Zustand store
- Optimistic updates — Immediate UI feedback on save
- ColorPicker integration — Visual color selection with hex output
- Segmented control — Large toggle for SMTP provider switching
User Workflow
Updating Organization Settings
- Navigate to
/app/settings - Verify "Organization" tab is selected (default)
- Modify fields:
- Organization Name
- Short Name (max 10 chars, shown in collapsed sidebar)
- Logo URL
- Favicon URL
- Footer Text
- Login Subtitle
- Click "Save Settings" button at bottom
- Success message: "Settings saved successfully"
- Changes apply immediately (refresh not required)
Customizing Theme Colors
- Click "Theme Colors" tab
- Modify Admin Theme colors:
- Primary Color (ColorPicker)
- Background Color (ColorPicker)
- Modify Public Theme colors:
- Primary Color
- Background Color
- Container Color
- Header Gradient (CSS gradient string)
- View live preview swatches below form
- Click "Save Settings"
- Theme updates apply on next page load
Configuring SMTP Email
- Click "Email" tab
- Set Sender info:
- From Name (e.g., "Changemaker Lite")
- From Address (e.g., "noreply@cmlite.org")
- Switch SMTP Provider:
- Click MailHog or Production segment
- Confirmation: "Switched to [provider] SMTP"
- Configure Production SMTP:
- SMTP Host (e.g., smtp.protonmail.ch)
- SMTP Port (587 for STARTTLS, 465 for SSL)
- SMTP User
- SMTP Password
- Enable Test Mode (optional):
- Toggle "Enable Test Mode" switch
- Set Test Recipient email
- All emails redirect to test recipient
- Click "Save Settings"
- Test configuration:
- Click "Test Connection" → Verify "Connection successful"
- Click "Send Test Email" → Check inbox for test message
Testing SMTP Configuration
- Navigate to Email tab
- Ensure production credentials are saved
- Switch to "Production" provider
- Click "Test Connection" button
- Wait for result (success/error alert)
- If successful, click "Send Test Email"
- Check email inbox for test message
- If failed, review error message and fix credentials
Enabling/Disabling Features
- Click "Feature Toggles" tab
- Toggle switches:
- Enable Influence (campaigns, responses, reps)
- Enable Map (locations, cuts, shifts, canvassing)
- Enable Newsletter (Listmonk integration)
- Enable Landing Pages (page builder)
- Info alert: "Disabling a module hides it from navigation but does not delete data"
- Click "Save Settings"
- Navigation menu updates to hide/show disabled modules
Component Breakdown
Ant Design Components Used
- Typography.Text — Labels, descriptions
- Tabs — Main navigation (4 tabs)
- Form — All settings wrapped in single form instance
- Form.Item — Individual fields with labels + extra descriptions
- Input — Text fields (org name, logo URL, SMTP host, etc.)
- Input.Password — SMTP password field (masked)
- InputNumber — SMTP port (numeric, min 0, max 65535)
- Switch — Boolean toggles (test mode, feature flags)
- ColorPicker — Color selection with hex preview
- Segmented — SMTP provider toggle (large button style)
- Tag — Active provider indicator (green)
- Alert — Info messages, connection/send test results
- Divider — Section separators
- Space — Button grouping
- Button — Test actions + save button
- Spin — Loading indicator during initial settings fetch
Tab Structure
const items = [
{
key: 'organization',
label: 'Organization',
icon: <SettingOutlined />,
children: (/* Organization form fields */)
},
{
key: 'theme',
label: 'Theme Colors',
children: (/* Theme form fields */)
},
{
key: 'email',
label: 'Email',
children: (/* Email form fields */)
},
{
key: 'features',
label: 'Feature Toggles',
children: (/* Feature toggle switches */)
},
];
return (
<Form form={form} layout="vertical">
<Tabs items={items} />
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
Save Settings
</Button>
</Form>
);
Color Swatch Preview
function Swatch({ label, color }: { label: string; color: string }) {
return (
<div style={{ textAlign: 'center' }}>
<div
style={{
width: 48,
height: 48,
borderRadius: 8,
background: color,
border: '2px solid rgba(255,255,255,0.2)',
marginBottom: 4,
}}
/>
<Text style={{ fontSize: 11 }}>{label}</Text>
</div>
);
}
State Management
Zustand Store Used
- settings.store — Centralized settings state
settings— Current settings objectloading— Loading statefetchAdminSettings()— Load settings from APIupdateSettings(partial)— Update and persist settings
import { useSettingsStore } from '@/stores/settings.store';
const { settings, loading, fetchAdminSettings, updateSettings } = useSettingsStore();
useEffect(() => {
fetchAdminSettings();
}, [fetchAdminSettings]);
Local State
const [form] = Form.useForm();
const [testingConnection, setTestingConnection] = useState(false);
const [connectionResult, setConnectionResult] = useState<SmtpTestResult | null>(null);
const [sendingTest, setSendingTest] = useState(false);
const [sendResult, setSendResult] = useState<SmtpSendTestResult | null>(null);
Form Initialization
useEffect(() => {
if (settings) {
form.setFieldsValue(settings);
}
}, [settings, form]);
When settings load from store, form automatically populates with current values.
API Integration
Endpoints Used
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/settings |
Load settings (via store) |
| PUT | /api/settings |
Update settings |
| POST | /api/settings/email/test-connection |
Test SMTP connection |
| POST | /api/settings/email/test-send |
Send test email |
Save Settings
const handleSave = async () => {
try {
const values = form.getFieldsValue();
// Convert ColorPicker values to hex strings
const colorFields = [
'adminColorPrimary',
'adminColorBgBase',
'publicColorPrimary',
'publicColorBgBase',
'publicColorBgContainer',
] as const;
for (const field of colorFields) {
const val = values[field];
if (val && typeof val === 'object' && 'toHexString' in val) {
values[field] = val.toHexString();
}
}
await updateSettings(values);
setConnectionResult(null);
setSendResult(null);
message.success('Settings saved successfully');
} catch {
message.error('Failed to save settings');
}
};
Request Payload:
{
"organizationName": "Changemaker Lite",
"organizationShortName": "CML",
"organizationLogoUrl": "https://example.com/logo.png",
"smtpHost": "smtp.protonmail.ch",
"smtpPort": 587,
"smtpUser": "user@example.com",
"smtpPass": "***",
"smtpActiveProvider": "production",
"adminColorPrimary": "#1890ff",
"publicColorPrimary": "#3498db",
"enableInfluence": true,
"enableMap": true
}
Test SMTP Connection
const handleTestConnection = async () => {
setTestingConnection(true);
setConnectionResult(null);
try {
const { data } = await api.post<SmtpTestResult>('/settings/email/test-connection');
setConnectionResult(data);
} catch {
setConnectionResult({ success: false, message: 'Request failed' });
} finally {
setTestingConnection(false);
}
};
Response (Success):
{
"success": true,
"message": "Connection successful"
}
Response (Failure):
{
"success": false,
"message": "Connection failed: Authentication failed"
}
Send Test Email
const handleSendTest = async () => {
setSendingTest(true);
setSendResult(null);
try {
const to = form.getFieldValue('testEmailRecipient');
const { data } = await api.post<SmtpSendTestResult>('/settings/email/test-send', { to });
setSendResult(data);
} catch {
setSendResult({ success: false, testMode: false, recipient: '' });
} finally {
setSendingTest(false);
}
};
Request:
{
"to": "admin@example.com"
}
Response (Success):
{
"success": true,
"testMode": false,
"recipient": "admin@example.com",
"messageId": "<abc123@smtp.protonmail.ch>"
}
Toggle SMTP Provider
const handleProviderToggle = async (value: string | number) => {
const provider = value as 'mailhog' | 'production';
try {
await updateSettings({ smtpActiveProvider: provider });
form.setFieldsValue({ smtpActiveProvider: provider });
message.success(`Switched to ${provider === 'mailhog' ? 'MailHog' : 'Production'} SMTP`);
setConnectionResult(null);
setSendResult(null);
} catch {
message.error('Failed to switch SMTP provider');
}
};
Why clear test results?
Test results are provider-specific. Switching providers invalidates previous test results.
ColorPicker Integration
Converting Color Values
Ant Design ColorPicker returns an object with toHexString() method:
// ColorPicker value
const colorValue = {
toHexString: () => '#1890ff',
// ... other methods
};
// Convert before saving
for (const field of colorFields) {
const val = values[field];
if (val && typeof val === 'object' && 'toHexString' in val) {
values[field] = val.toHexString();
}
}
Theme Preview
{settings && (
<div style={{ marginTop: 24 }}>
<Text strong style={{ fontSize: 15 }}>Preview</Text>
<Divider style={{ margin: '12px 0' }} />
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Swatch label="Admin Primary" color={settings.adminColorPrimary} />
<Swatch label="Admin BG" color={settings.adminColorBgBase} />
<Swatch label="Public Primary" color={settings.publicColorPrimary} />
<Swatch label="Public BG" color={settings.publicColorBgBase} />
<Swatch label="Public Container" color={settings.publicColorBgContainer} />
</div>
<div
style={{
marginTop: 12,
padding: '12px 24px',
background: settings.publicHeaderGradient,
borderRadius: 8,
color: '#fff',
fontWeight: 600,
}}
>
Header Gradient Preview
</div>
</div>
)}
Performance Considerations
Single Form Instance
All settings use one form instance:
const [form] = Form.useForm();
<Form form={form} layout="vertical">
<Tabs items={items} />
<Button onClick={handleSave}>Save Settings</Button>
</Form>
Benefits:
- Single save operation — One API call saves all modified fields
- Consistent validation — All fields validated together
- Simplified state — No need to track which tab has changes
Optimistic Provider Switching
Provider toggle updates immediately without waiting for API:
await updateSettings({ smtpActiveProvider: provider });
form.setFieldsValue({ smtpActiveProvider: provider }); // Update form immediately
message.success(`Switched to ${provider}`);
Why optimistic?
- Instant feedback — User sees immediate response
- Better UX — No loading delay for simple toggle
- Safe operation — Provider toggle is low-risk (can always switch back)
Troubleshooting
SMTP Test Connection Failing
Problem: Click "Test Connection" → Error: "Connection failed: Authentication failed"
Diagnosis:
Check SMTP credentials:
{
smtpHost: "smtp.protonmail.ch",
smtpPort: 587,
smtpUser: "user@protonmail.com",
smtpPass: "***"
}
Common Issues:
-
Wrong port:
- Use 587 for STARTTLS
- Use 465 for SSL/TLS
- Port 25 often blocked by ISPs
-
App-specific password required:
- Gmail requires app-specific passwords (not account password)
- ProtonMail requires ProtonMail Bridge for SMTP
-
Wrong provider selected:
- Ensure "Production" is selected before testing production credentials
Solution:
- Verify credentials with email provider documentation
- Switch to "Production" provider
- Save settings before testing
- Check firewall rules (port 587/465 outbound)
Theme Colors Not Applying
Problem: Change colors, save settings, but theme doesn't update.
Diagnosis:
Check if page reload is required:
// Theme updates apply on NEXT page load, not immediately
await updateSettings({ adminColorPrimary: '#ff0000' });
// Current page still shows old color
Solution:
Refresh page after saving theme changes:
const handleSave = async () => {
await updateSettings(values);
message.success('Settings saved. Refreshing page...');
setTimeout(() => window.location.reload(), 1000);
};
Feature Toggle Not Hiding Module
Problem: Disable "Enable Influence" toggle, save, but Influence menu items still visible.
Diagnosis:
Check AppLayout navigation logic:
// AppLayout should check settings.enableInfluence
{settings.enableInfluence && (
<SubMenu key="influence" title="Influence">
{/* Influence menu items */}
</SubMenu>
)}
Solution:
Ensure AppLayout reads settings from store and conditionally renders menu items.
Test Email Not Sending
Problem: Click "Send Test Email" → Success message, but no email in inbox.
Diagnosis:
-
Check active provider:
settings.smtpActiveProvider === 'mailhog' // MailHog (dev) settings.smtpActiveProvider === 'production' // Real SMTP -
Check test mode:
settings.emailTestMode === true // All emails redirect to testEmailRecipient -
Check spam folder
-
Check MailHog web UI (http://localhost:8025) if MailHog is active
Solution:
- Switch to "Production" provider
- Disable test mode if you want emails to go to actual recipients
- Save settings before sending test email
Related Documentation
- Settings Module — Backend API reference
- Email Service — Email sending service
- Settings Store — Settings state management
- AppLayout Component — Feature toggle integration
- User Guide: Site Configuration — Configuration guide
- Troubleshooting: Email Issues — Email debugging