@actuate-media/cms-admin 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/__tests__/views/email-settings.render.test.d.ts +2 -0
  3. package/dist/__tests__/views/email-settings.render.test.d.ts.map +1 -0
  4. package/dist/__tests__/views/email-settings.render.test.js +184 -0
  5. package/dist/__tests__/views/email-settings.render.test.js.map +1 -0
  6. package/dist/__tests__/views/section-inspector-repeater.render.test.d.ts +2 -0
  7. package/dist/__tests__/views/section-inspector-repeater.render.test.d.ts.map +1 -0
  8. package/dist/__tests__/views/section-inspector-repeater.render.test.js +68 -0
  9. package/dist/__tests__/views/section-inspector-repeater.render.test.js.map +1 -0
  10. package/dist/actuate-admin.css +1 -1
  11. package/dist/views/Settings.d.ts.map +1 -1
  12. package/dist/views/Settings.js +5 -3
  13. package/dist/views/Settings.js.map +1 -1
  14. package/dist/views/page-editor/SectionInspector.d.ts +9 -0
  15. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
  16. package/dist/views/page-editor/SectionInspector.js +38 -1
  17. package/dist/views/page-editor/SectionInspector.js.map +1 -1
  18. package/dist/views/settings/EmailSettingsTab.d.ts +4 -0
  19. package/dist/views/settings/EmailSettingsTab.d.ts.map +1 -0
  20. package/dist/views/settings/EmailSettingsTab.js +75 -0
  21. package/dist/views/settings/EmailSettingsTab.js.map +1 -0
  22. package/dist/views/settings/useEmailSettings.d.ts +58 -0
  23. package/dist/views/settings/useEmailSettings.d.ts.map +1 -0
  24. package/dist/views/settings/useEmailSettings.js +209 -0
  25. package/dist/views/settings/useEmailSettings.js.map +1 -0
  26. package/package.json +2 -2
  27. package/src/__tests__/views/email-settings.render.test.tsx +225 -0
  28. package/src/__tests__/views/section-inspector-repeater.render.test.tsx +90 -0
  29. package/src/views/Settings.tsx +13 -0
  30. package/src/views/page-editor/SectionInspector.tsx +40 -2
  31. package/src/views/settings/EmailSettingsTab.tsx +436 -0
  32. package/src/views/settings/useEmailSettings.ts +296 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @actuate-media/cms-admin
2
2
 
3
+ ## 0.45.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0f9c510: Editor & scaffold improvements from Upchat feedback.
8
+
9
+ **cms-admin** — Repeater rows now derive a scannable header from their values
10
+ ("Pro · $79/mo", "99.99% · Uptime") instead of only the positional label
11
+ ("Plan 2"), so long repeaters read like a list instead of a spreadsheet. Blank
12
+ rows still fall back to "Plan N".
13
+
14
+ **create-actuate-cms** — The `marketing-custom` template now ships the full
15
+ custom-section experience out of the box:
16
+ - `src/sections/admin-renderers.tsx` is scaffolded and pre-wired into
17
+ `<AdminRoot sectionRenderers={...}>`, so the editor canvas renders real
18
+ components for custom section types instead of structural placeholders.
19
+ - A pre-filled `actuate.seed.json` ("Marketing homepage" with hero, stats,
20
+ pricing, and CTA sections) loads via `npm run populate`, so the first visit
21
+ to the Page editor shows a fully-authored page instead of an empty canvas.
22
+ - `SECTIONS.md` and the project README document both workflows.
23
+
24
+ ## 0.44.0
25
+
26
+ ### Minor Changes
27
+
28
+ - d6b21e7: Add Settings > Email: configure the transactional email provider from the admin UI.
29
+ - **cms-admin**: new Email tab in Settings with a provider picker (Resend, SMTP.com, Amazon SES, generic SMTP), per-provider credential fields, masked stored secrets (last-4 only, write-only inputs), a "Send test email" button, and notices for environment/platform-managed configuration.
30
+ - **cms-core**: new `__email_config__` store with API keys and SMTP passwords encrypted at rest (`CMS_ENCRYPTION_KEY`), `GET/PUT /email/settings` + `POST /email/settings/test` admin routes (test sends only to the signed-in admin's own address, rate-limited), and a DB-aware `resolveEmailSenderAsync()` used by invites, password resets, and form notifications. Resolution order: platform adapter → admin-configured settings → environment variables.
31
+ - **plugin-email**: registers a provider factory with cms-core so admin-configured settings build real Resend/SMTP.com/SES/SMTP senders without env vars.
32
+
3
33
  ## 0.43.0
4
34
 
5
35
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email-settings.render.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-settings.render.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/views/email-settings.render.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,184 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment happy-dom
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
5
+ const EMPTY_RESPONSE = {
6
+ settings: { provider: null, hasApiKey: false, hasSmtpPass: false },
7
+ envProvider: null,
8
+ factoryAvailable: true,
9
+ envSenderActive: false,
10
+ platformAdapter: false,
11
+ };
12
+ let getResponse = EMPTY_RESPONSE;
13
+ let putResponse = null;
14
+ let testResponse = {
15
+ data: { success: true, sentTo: 'admin@example.com' },
16
+ status: 200,
17
+ };
18
+ const cmsApi = vi.fn(async (endpoint, options) => {
19
+ const method = options?.method ?? 'GET';
20
+ if (endpoint === '/email/settings' && method === 'GET') {
21
+ return { data: getResponse, status: 200 };
22
+ }
23
+ if (endpoint === '/email/settings' && method === 'PUT') {
24
+ return putResponse
25
+ ? { data: putResponse, status: 200 }
26
+ : { error: 'unexpected PUT', status: 500 };
27
+ }
28
+ if (endpoint === '/email/settings/test' && method === 'POST') {
29
+ return testResponse;
30
+ }
31
+ return { data: {}, status: 200 };
32
+ });
33
+ vi.mock('../../lib/api.js', () => ({
34
+ cmsApi: (e, o) => cmsApi(e, o),
35
+ ensureCsrfToken: vi.fn(async () => { }),
36
+ }));
37
+ const { EmailSettingsTab } = await import('../../views/settings/EmailSettingsTab.js');
38
+ function deepClone(v) {
39
+ return JSON.parse(JSON.stringify(v));
40
+ }
41
+ beforeEach(() => {
42
+ cmsApi.mockClear();
43
+ getResponse = deepClone(EMPTY_RESPONSE);
44
+ putResponse = null;
45
+ testResponse = { data: { success: true, sentTo: 'admin@example.com' }, status: 200 };
46
+ });
47
+ describe('EmailSettingsTab', () => {
48
+ it('renders the provider picker with the unconfigured state', async () => {
49
+ render(_jsx(EmailSettingsTab, { role: "ADMIN" }));
50
+ expect(await screen.findByRole('radio', { name: 'None' })).toBeTruthy();
51
+ expect(screen.getByRole('radio', { name: 'Resend' })).toBeTruthy();
52
+ expect(screen.getByRole('radio', { name: 'SMTP.com' })).toBeTruthy();
53
+ expect(screen.getByRole('radio', { name: 'Amazon SES' })).toBeTruthy();
54
+ expect(screen.getByRole('radio', { name: 'SMTP server' })).toBeTruthy();
55
+ expect(screen.getByText(/No email provider configured/)).toBeTruthy();
56
+ });
57
+ it('saves an SMTP.com configuration with the entered key (PUT payload contract)', async () => {
58
+ putResponse = {
59
+ settings: {
60
+ provider: 'smtpcom',
61
+ from: 'noreply@example.com',
62
+ channel: 'transactional',
63
+ hasApiKey: true,
64
+ apiKeyLast4: '-key',
65
+ hasSmtpPass: false,
66
+ },
67
+ };
68
+ render(_jsx(EmailSettingsTab, { role: "ADMIN" }));
69
+ fireEvent.click(await screen.findByRole('radio', { name: 'SMTP.com' }));
70
+ fireEvent.change(screen.getByLabelText('Sender address'), {
71
+ target: { value: 'noreply@example.com' },
72
+ });
73
+ fireEvent.change(screen.getByLabelText('API key'), {
74
+ target: { value: 'smtpcom-secret-key' },
75
+ });
76
+ fireEvent.change(screen.getByLabelText('Sender channel'), {
77
+ target: { value: 'transactional' },
78
+ });
79
+ fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
80
+ await waitFor(() => {
81
+ const put = cmsApi.mock.calls.find(([e, o]) => e === '/email/settings' && o?.method === 'PUT');
82
+ expect(put).toBeTruthy();
83
+ expect(JSON.parse(put[1].body)).toEqual({
84
+ provider: 'smtpcom',
85
+ from: 'noreply@example.com',
86
+ apiKey: 'smtpcom-secret-key',
87
+ channel: 'transactional',
88
+ });
89
+ });
90
+ // After save the key input is cleared and the masked hint appears.
91
+ expect(screen.getByLabelText('API key').value).toBe('');
92
+ expect(await screen.findByText(/ends in -key/)).toBeTruthy();
93
+ });
94
+ it('omits the apiKey from the payload when a stored key is kept', async () => {
95
+ getResponse = {
96
+ ...deepClone(EMPTY_RESPONSE),
97
+ settings: {
98
+ provider: 'resend',
99
+ from: 'old@example.com',
100
+ hasApiKey: true,
101
+ apiKeyLast4: 'k3y9',
102
+ hasSmtpPass: false,
103
+ },
104
+ };
105
+ putResponse = {
106
+ settings: {
107
+ provider: 'resend',
108
+ from: 'new@example.com',
109
+ hasApiKey: true,
110
+ apiKeyLast4: 'k3y9',
111
+ hasSmtpPass: false,
112
+ },
113
+ };
114
+ render(_jsx(EmailSettingsTab, { role: "ADMIN" }));
115
+ const from = await screen.findByLabelText('Sender address');
116
+ expect(await screen.findByText(/ends in k3y9/)).toBeTruthy();
117
+ fireEvent.change(from, { target: { value: 'new@example.com' } });
118
+ fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
119
+ await waitFor(() => {
120
+ const put = cmsApi.mock.calls.find(([e, o]) => e === '/email/settings' && o?.method === 'PUT');
121
+ expect(put).toBeTruthy();
122
+ const body = JSON.parse(put[1].body);
123
+ expect(body).toEqual({ provider: 'resend', from: 'new@example.com' });
124
+ expect(body).not.toHaveProperty('apiKey');
125
+ });
126
+ });
127
+ it('sends a test email and shows the result', async () => {
128
+ getResponse = {
129
+ ...deepClone(EMPTY_RESPONSE),
130
+ settings: {
131
+ provider: 'resend',
132
+ from: 'a@b.co',
133
+ hasApiKey: true,
134
+ apiKeyLast4: 'k3y9',
135
+ hasSmtpPass: false,
136
+ },
137
+ };
138
+ render(_jsx(EmailSettingsTab, { role: "ADMIN" }));
139
+ fireEvent.click(await screen.findByRole('button', { name: /Send test email/ }));
140
+ await waitFor(() => expect(cmsApi).toHaveBeenCalledWith('/email/settings/test', expect.objectContaining({ method: 'POST' })));
141
+ expect(await screen.findByText(/Test email sent to admin@example.com/)).toBeTruthy();
142
+ });
143
+ it('surfaces a failed test send', async () => {
144
+ getResponse = {
145
+ ...deepClone(EMPTY_RESPONSE),
146
+ settings: {
147
+ provider: 'resend',
148
+ from: 'a@b.co',
149
+ hasApiKey: true,
150
+ hasSmtpPass: false,
151
+ },
152
+ };
153
+ testResponse = { error: 'SMTP.com API error (401): bad key', status: 502 };
154
+ render(_jsx(EmailSettingsTab, { role: "ADMIN" }));
155
+ fireEvent.click(await screen.findByRole('button', { name: /Send test email/ }));
156
+ expect(await screen.findByText(/SMTP\.com API error \(401\)/)).toBeTruthy();
157
+ });
158
+ it('shows the env-managed notice and disables editing for non-admins', async () => {
159
+ getResponse = {
160
+ ...deepClone(EMPTY_RESPONSE),
161
+ envProvider: 'resend',
162
+ envSenderActive: true,
163
+ };
164
+ render(_jsx(EmailSettingsTab, { role: "EDITOR" }));
165
+ expect(await screen.findByText(/read-only access/)).toBeTruthy();
166
+ expect(screen.getByText(/Environment variables currently configure/)).toBeTruthy();
167
+ expect(screen.getByRole('radio', { name: 'Resend' }).disabled).toBe(true);
168
+ });
169
+ it('warns when the email plugin is missing for a configured provider', async () => {
170
+ getResponse = {
171
+ ...deepClone(EMPTY_RESPONSE),
172
+ factoryAvailable: false,
173
+ settings: {
174
+ provider: 'resend',
175
+ from: 'a@b.co',
176
+ hasApiKey: true,
177
+ hasSmtpPass: false,
178
+ },
179
+ };
180
+ render(_jsx(EmailSettingsTab, { role: "ADMIN" }));
181
+ expect(await screen.findByText(/requires the email plugin/)).toBeTruthy();
182
+ });
183
+ });
184
+ //# sourceMappingURL=email-settings.render.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-settings.render.test.js","sourceRoot":"","sources":["../../../src/__tests__/views/email-settings.render.test.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=section-inspector-repeater.render.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-inspector-repeater.render.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/views/section-inspector-repeater.render.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { render, screen } from '@testing-library/react';
5
+ import { createSection } from '../../lib/page-editor-service.js';
6
+ import { SectionInspector, repeaterRowSummary } from '../../views/page-editor/SectionInspector.js';
7
+ /**
8
+ * Repeater rows derive a scannable header from their values ("Pro · $79/mo")
9
+ * instead of only the positional "Plan 2" label, so editors can find a row
10
+ * without expanding every form (Upchat feedback: "repeaters feel like
11
+ * spreadsheets").
12
+ */
13
+ const PLAN_FIELDS = [
14
+ { key: 'name', label: 'Name', type: 'text' },
15
+ { key: 'price', label: 'Price', type: 'text' },
16
+ { key: 'description', label: 'Description', type: 'textarea' },
17
+ {
18
+ key: 'tier',
19
+ label: 'Tier',
20
+ type: 'select',
21
+ options: [{ label: 'Most popular', value: 'popular' }],
22
+ },
23
+ ];
24
+ describe('repeaterRowSummary', () => {
25
+ it('joins the first two short text values', () => {
26
+ expect(repeaterRowSummary(PLAN_FIELDS, { name: 'Pro', price: '$79/mo' })).toBe('Pro · $79/mo');
27
+ });
28
+ it('skips empty values and long-form fields', () => {
29
+ expect(repeaterRowSummary(PLAN_FIELDS, {
30
+ name: ' ',
31
+ price: '$79/mo',
32
+ description: 'long prose that must never appear',
33
+ })).toBe('$79/mo');
34
+ });
35
+ it('maps select values to their human label', () => {
36
+ expect(repeaterRowSummary(PLAN_FIELDS, { tier: 'popular' })).toBe('Most popular');
37
+ });
38
+ it('returns null for a blank row (caller falls back to "Plan N")', () => {
39
+ expect(repeaterRowSummary(PLAN_FIELDS, {})).toBeNull();
40
+ expect(repeaterRowSummary(PLAN_FIELDS, { name: '', price: '' })).toBeNull();
41
+ });
42
+ it('truncates very long summaries with an ellipsis', () => {
43
+ const summary = repeaterRowSummary(PLAN_FIELDS, { name: 'x'.repeat(100) });
44
+ expect(summary.length).toBeLessThanOrEqual(60);
45
+ expect(summary.endsWith('…')).toBe(true);
46
+ });
47
+ });
48
+ describe('SectionInspector repeater rows', () => {
49
+ function renderStatsInspector(stats) {
50
+ const section = createSection('stats');
51
+ section.content = { ...section.content, stats };
52
+ render(_jsx(SectionInspector, { section: section, canEdit: true, onClose: vi.fn(), onContentChange: vi.fn(), onSettingsChange: vi.fn(), onMetaChange: vi.fn(), onToggleVisibility: vi.fn() }));
53
+ }
54
+ it('shows value-derived row headers instead of "Stat N"', () => {
55
+ renderStatsInspector([
56
+ { value: '99.99%', label: 'Uptime', description: '' },
57
+ { value: '3M+', label: 'API requests', description: '' },
58
+ ]);
59
+ expect(screen.getByText('99.99% · Uptime')).toBeTruthy();
60
+ expect(screen.getByText('3M+ · API requests')).toBeTruthy();
61
+ expect(screen.queryByText('Stat 1')).toBeNull();
62
+ });
63
+ it('falls back to the positional label for blank rows', () => {
64
+ renderStatsInspector([{ value: '', label: '', description: '' }]);
65
+ expect(screen.getByText('Stat 1')).toBeTruthy();
66
+ });
67
+ });
68
+ //# sourceMappingURL=section-inspector-repeater.render.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-inspector-repeater.render.test.js","sourceRoot":"","sources":["../../../src/__tests__/views/section-inspector-repeater.render.test.tsx"],"names":[],"mappings":"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"}