@actuate-media/cms-admin 0.25.0 → 0.27.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.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +3 -3
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/views/appearance-settings.render.test.d.ts +2 -0
- package/dist/__tests__/views/appearance-settings.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/appearance-settings.render.test.js +95 -0
- package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -0
- package/dist/__tests__/views/general-settings.render.test.d.ts +2 -0
- package/dist/__tests__/views/general-settings.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/general-settings.render.test.js +105 -0
- package/dist/__tests__/views/general-settings.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/ThemeProvider.d.ts +47 -4
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js +173 -27
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/components/ui/Card.d.ts +2 -2
- package/dist/components/ui/Input.d.ts +1 -1
- package/dist/components/ui/Select.d.ts +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +47 -8
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/Settings.d.ts +3 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +10 -33
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/settings/AdminThemeCard.d.ts +6 -0
- package/dist/views/settings/AdminThemeCard.d.ts.map +1 -0
- package/dist/views/settings/AdminThemeCard.js +31 -0
- package/dist/views/settings/AdminThemeCard.js.map +1 -0
- package/dist/views/settings/AppearanceTab.d.ts +7 -0
- package/dist/views/settings/AppearanceTab.d.ts.map +1 -0
- package/dist/views/settings/AppearanceTab.js +24 -0
- package/dist/views/settings/AppearanceTab.js.map +1 -0
- package/dist/views/settings/BrandPreviewCard.d.ts +5 -0
- package/dist/views/settings/BrandPreviewCard.d.ts.map +1 -0
- package/dist/views/settings/BrandPreviewCard.js +18 -0
- package/dist/views/settings/BrandPreviewCard.js.map +1 -0
- package/dist/views/settings/BrandingCard.d.ts +6 -0
- package/dist/views/settings/BrandingCard.d.ts.map +1 -0
- package/dist/views/settings/BrandingCard.js +38 -0
- package/dist/views/settings/BrandingCard.js.map +1 -0
- package/dist/views/settings/GeneralSettingsTab.d.ts +7 -0
- package/dist/views/settings/GeneralSettingsTab.d.ts.map +1 -0
- package/dist/views/settings/GeneralSettingsTab.js +19 -0
- package/dist/views/settings/GeneralSettingsTab.js.map +1 -0
- package/dist/views/settings/LanguageRegionCard.d.ts +10 -0
- package/dist/views/settings/LanguageRegionCard.d.ts.map +1 -0
- package/dist/views/settings/LanguageRegionCard.js +48 -0
- package/dist/views/settings/LanguageRegionCard.js.map +1 -0
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts +12 -0
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -0
- package/dist/views/settings/SeoRobotsDefaultsCard.js +45 -0
- package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -0
- package/dist/views/settings/SiteIndexStatusCard.d.ts +12 -0
- package/dist/views/settings/SiteIndexStatusCard.d.ts.map +1 -0
- package/dist/views/settings/SiteIndexStatusCard.js +51 -0
- package/dist/views/settings/SiteIndexStatusCard.js.map +1 -0
- package/dist/views/settings/SiteInformationCard.d.ts +11 -0
- package/dist/views/settings/SiteInformationCard.d.ts.map +1 -0
- package/dist/views/settings/SiteInformationCard.js +21 -0
- package/dist/views/settings/SiteInformationCard.js.map +1 -0
- package/dist/views/settings/TypographyMotionCard.d.ts +6 -0
- package/dist/views/settings/TypographyMotionCard.d.ts.map +1 -0
- package/dist/views/settings/TypographyMotionCard.js +8 -0
- package/dist/views/settings/TypographyMotionCard.js.map +1 -0
- package/dist/views/settings/components.d.ts +38 -0
- package/dist/views/settings/components.d.ts.map +1 -0
- package/dist/views/settings/components.js +43 -0
- package/dist/views/settings/components.js.map +1 -0
- package/dist/views/settings/useAppearanceSettings.d.ts +69 -0
- package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -0
- package/dist/views/settings/useAppearanceSettings.js +344 -0
- package/dist/views/settings/useAppearanceSettings.js.map +1 -0
- package/dist/views/settings/useGeneralSettings.d.ts +82 -0
- package/dist/views/settings/useGeneralSettings.d.ts.map +1 -0
- package/dist/views/settings/useGeneralSettings.js +286 -0
- package/dist/views/settings/useGeneralSettings.js.map +1 -0
- package/package.json +3 -3
- package/src/AdminRoot.tsx +5 -3
- package/src/__tests__/views/appearance-settings.render.test.tsx +132 -0
- package/src/__tests__/views/general-settings.render.test.tsx +135 -0
- package/src/components/ThemeProvider.tsx +249 -36
- package/src/layout/Sidebar.tsx +81 -13
- package/src/styles/theme.css +32 -2
- package/src/views/Settings.tsx +19 -155
- package/src/views/settings/AdminThemeCard.tsx +145 -0
- package/src/views/settings/AppearanceTab.tsx +81 -0
- package/src/views/settings/BrandPreviewCard.tsx +125 -0
- package/src/views/settings/BrandingCard.tsx +332 -0
- package/src/views/settings/GeneralSettingsTab.tsx +126 -0
- package/src/views/settings/LanguageRegionCard.tsx +119 -0
- package/src/views/settings/SeoRobotsDefaultsCard.tsx +149 -0
- package/src/views/settings/SiteIndexStatusCard.tsx +184 -0
- package/src/views/settings/SiteInformationCard.tsx +122 -0
- package/src/views/settings/TypographyMotionCard.tsx +65 -0
- package/src/views/settings/components.tsx +198 -0
- package/src/views/settings/useAppearanceSettings.ts +513 -0
- package/src/views/settings/useGeneralSettings.ts +379 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { cmsApi } from '../../lib/api.js';
|
|
4
|
+
const EMPTY_FORM = {
|
|
5
|
+
siteTitle: '',
|
|
6
|
+
tagline: '',
|
|
7
|
+
siteUrl: '',
|
|
8
|
+
language: 'en',
|
|
9
|
+
timezone: 'UTC',
|
|
10
|
+
defaultNoIndex: false,
|
|
11
|
+
defaultNoFollow: false,
|
|
12
|
+
noIndexNonProduction: false,
|
|
13
|
+
};
|
|
14
|
+
/** Validate a site URL: absolute http(s), no trailing slash, parseable. */
|
|
15
|
+
export function validateSiteUrl(value) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return { valid: true };
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(trimmed);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { valid: false, error: 'Enter a valid absolute URL, e.g. https://example.com' };
|
|
25
|
+
}
|
|
26
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
27
|
+
return { valid: false, error: 'Site URL must start with http:// or https://' };
|
|
28
|
+
}
|
|
29
|
+
if (trimmed.endsWith('/')) {
|
|
30
|
+
return { valid: false, error: 'Remove the trailing slash (e.g. https://example.com)' };
|
|
31
|
+
}
|
|
32
|
+
return { valid: true };
|
|
33
|
+
}
|
|
34
|
+
/** Best-effort IANA timezone check using the Intl API. */
|
|
35
|
+
export function isValidTimeZone(tz) {
|
|
36
|
+
if (!tz)
|
|
37
|
+
return false;
|
|
38
|
+
try {
|
|
39
|
+
new Intl.DateTimeFormat('en-US', { timeZone: tz });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Best-effort BCP 47 locale check. */
|
|
47
|
+
export function isValidLocale(locale) {
|
|
48
|
+
if (!locale)
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
// Throws RangeError on structurally invalid tags.
|
|
52
|
+
new Intl.Locale(locale);
|
|
53
|
+
return /^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]{2,8})*$/.test(locale);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function validate(form, isProduction) {
|
|
60
|
+
const issues = [];
|
|
61
|
+
if (!form.siteTitle.trim()) {
|
|
62
|
+
issues.push({ field: 'siteTitle', message: 'Site Title is required.', severity: 'error' });
|
|
63
|
+
}
|
|
64
|
+
const urlCheck = validateSiteUrl(form.siteUrl);
|
|
65
|
+
if (!urlCheck.valid) {
|
|
66
|
+
issues.push({ field: 'siteUrl', message: urlCheck.error, severity: 'error' });
|
|
67
|
+
}
|
|
68
|
+
else if (!form.siteUrl.trim() && isProduction) {
|
|
69
|
+
issues.push({
|
|
70
|
+
field: 'siteUrl',
|
|
71
|
+
message: 'Site URL is not set — canonical, sitemap, and Open Graph URLs may be invalid.',
|
|
72
|
+
severity: 'warning',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!isValidLocale(form.language)) {
|
|
76
|
+
issues.push({
|
|
77
|
+
field: 'language',
|
|
78
|
+
message: 'Language must be a valid locale code, e.g. en-US.',
|
|
79
|
+
severity: 'error',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!isValidTimeZone(form.timezone)) {
|
|
83
|
+
issues.push({
|
|
84
|
+
field: 'timezone',
|
|
85
|
+
message: 'Timezone must be a valid IANA name, e.g. America/Los_Angeles.',
|
|
86
|
+
severity: 'error',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return issues;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Loads the General tab from its three real backends — the `settings` global
|
|
93
|
+
* (site info / locale), the shared `/seo/config` (site URL + robots defaults),
|
|
94
|
+
* and the computed `/seo/index-status` — tracks a dirty diff, and on save
|
|
95
|
+
* writes each field back to its OWNING store (never a duplicate), preserving
|
|
96
|
+
* unrelated keys.
|
|
97
|
+
*/
|
|
98
|
+
export function useGeneralSettings(canEdit) {
|
|
99
|
+
const [loading, setLoading] = useState(true);
|
|
100
|
+
const [loadError, setLoadError] = useState(null);
|
|
101
|
+
const [baseline, setBaseline] = useState(EMPTY_FORM);
|
|
102
|
+
const [form, setForm] = useState(EMPTY_FORM);
|
|
103
|
+
const [indexStatus, setIndexStatus] = useState(null);
|
|
104
|
+
const [sources, setSources] = useState({});
|
|
105
|
+
// Full settings-global blob so a partial save never wipes AI/security keys.
|
|
106
|
+
const [settingsBlob, setSettingsBlob] = useState({});
|
|
107
|
+
const [overrideSite, setOverrideSite] = useState({});
|
|
108
|
+
const [saveState, setSaveState] = useState('idle');
|
|
109
|
+
const [saveError, setSaveError] = useState(null);
|
|
110
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
let cancelled = false;
|
|
113
|
+
setLoading(true);
|
|
114
|
+
setLoadError(null);
|
|
115
|
+
Promise.all([
|
|
116
|
+
cmsApi('/globals/settings'),
|
|
117
|
+
cmsApi('/seo/config'),
|
|
118
|
+
cmsApi('/seo/index-status'),
|
|
119
|
+
])
|
|
120
|
+
.then(([settingsRes, seoRes, statusRes]) => {
|
|
121
|
+
if (cancelled)
|
|
122
|
+
return;
|
|
123
|
+
if (settingsRes.error && settingsRes.status !== 404) {
|
|
124
|
+
setLoadError(settingsRes.error);
|
|
125
|
+
}
|
|
126
|
+
const settingsData = settingsRes.data ?? {};
|
|
127
|
+
const seo = seoRes.data ?? {};
|
|
128
|
+
const effectiveSite = seo.effective?.site ?? {};
|
|
129
|
+
const staticSite = seo.static?.site ?? {};
|
|
130
|
+
const overSite = seo.overrides?.site ?? {};
|
|
131
|
+
const robots = effectiveSite.robots ?? {};
|
|
132
|
+
const loaded = {
|
|
133
|
+
siteTitle: String(settingsData.siteTitle ?? ''),
|
|
134
|
+
tagline: String(settingsData.tagline ?? ''),
|
|
135
|
+
siteUrl: String(effectiveSite.siteUrl ?? settingsData.siteUrl ?? ''),
|
|
136
|
+
language: String(settingsData.language ?? 'en'),
|
|
137
|
+
timezone: String(settingsData.timezone ?? 'UTC'),
|
|
138
|
+
defaultNoIndex: robots.defaultNoIndex === true,
|
|
139
|
+
defaultNoFollow: robots.defaultNoFollow === true,
|
|
140
|
+
noIndexNonProduction: robots.noIndexNonProduction === true,
|
|
141
|
+
};
|
|
142
|
+
// Site URL is config-managed when actuate.config.ts supplies it.
|
|
143
|
+
const siteUrlFromConfig = Boolean(staticSite.siteUrl);
|
|
144
|
+
setSources({
|
|
145
|
+
siteUrl: siteUrlFromConfig
|
|
146
|
+
? { source: 'config', editable: false, reason: 'Set in actuate.config.ts' }
|
|
147
|
+
: { source: 'database', editable: canEdit },
|
|
148
|
+
siteTitle: { source: 'database', editable: canEdit },
|
|
149
|
+
tagline: { source: 'database', editable: canEdit },
|
|
150
|
+
language: { source: 'database', editable: canEdit },
|
|
151
|
+
timezone: { source: 'database', editable: canEdit },
|
|
152
|
+
seoDefaults: {
|
|
153
|
+
source: 'plugin',
|
|
154
|
+
editable: canEdit,
|
|
155
|
+
reason: 'Shared with the SEO section',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
setBaseline(loaded);
|
|
159
|
+
setForm(loaded);
|
|
160
|
+
setSettingsBlob(settingsData);
|
|
161
|
+
setOverrideSite(overSite ?? {});
|
|
162
|
+
if (!seoRes.error && statusRes.data)
|
|
163
|
+
setIndexStatus(statusRes.data);
|
|
164
|
+
})
|
|
165
|
+
.catch(() => {
|
|
166
|
+
if (!cancelled)
|
|
167
|
+
setLoadError('Failed to load settings.');
|
|
168
|
+
})
|
|
169
|
+
.finally(() => {
|
|
170
|
+
if (!cancelled)
|
|
171
|
+
setLoading(false);
|
|
172
|
+
});
|
|
173
|
+
return () => {
|
|
174
|
+
cancelled = true;
|
|
175
|
+
};
|
|
176
|
+
}, [reloadKey, canEdit]);
|
|
177
|
+
const setField = useCallback((key, value) => {
|
|
178
|
+
setForm((prev) => ({ ...prev, [key]: value }));
|
|
179
|
+
setSaveState('idle');
|
|
180
|
+
}, []);
|
|
181
|
+
const isProduction = (indexStatus?.environment ?? '') === 'production';
|
|
182
|
+
const dirty = useMemo(() => Object.keys(form).some((k) => form[k] !== baseline[k]), [form, baseline]);
|
|
183
|
+
const issues = useMemo(() => validate(form, isProduction), [form, isProduction]);
|
|
184
|
+
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
185
|
+
const refetchStatus = useCallback(async () => {
|
|
186
|
+
const res = await cmsApi('/seo/index-status');
|
|
187
|
+
if (!res.error && res.data)
|
|
188
|
+
setIndexStatus(res.data);
|
|
189
|
+
}, []);
|
|
190
|
+
const save = useCallback(async () => {
|
|
191
|
+
if (!canEdit || hasErrors)
|
|
192
|
+
return;
|
|
193
|
+
setSaveState('saving');
|
|
194
|
+
setSaveError(null);
|
|
195
|
+
const siteUrlEditable = sources.siteUrl?.editable !== false;
|
|
196
|
+
const siteInfoChanged = form.siteTitle !== baseline.siteTitle ||
|
|
197
|
+
form.tagline !== baseline.tagline ||
|
|
198
|
+
form.language !== baseline.language ||
|
|
199
|
+
form.timezone !== baseline.timezone;
|
|
200
|
+
const siteUrlChanged = siteUrlEditable && form.siteUrl !== baseline.siteUrl;
|
|
201
|
+
const robotsChanged = form.defaultNoIndex !== baseline.defaultNoIndex ||
|
|
202
|
+
form.defaultNoFollow !== baseline.defaultNoFollow ||
|
|
203
|
+
form.noIndexNonProduction !== baseline.noIndexNonProduction;
|
|
204
|
+
try {
|
|
205
|
+
// 1. Site info → the `settings` global (preserve AI/security/layout keys,
|
|
206
|
+
// and drop legacy robots/siteUrl duplicates so SEO config owns them).
|
|
207
|
+
// Read-modify-write against a fresh fetch so a concurrent save from the
|
|
208
|
+
// Security/AI tabs (same global blob) is never clobbered.
|
|
209
|
+
if (siteInfoChanged) {
|
|
210
|
+
const current = await cmsApi('/globals/settings');
|
|
211
|
+
const blob = { ...(current.data ?? settingsBlob) };
|
|
212
|
+
delete blob.defaultNoIndex;
|
|
213
|
+
delete blob.defaultNoFollow;
|
|
214
|
+
delete blob.noIndexNonProduction;
|
|
215
|
+
delete blob.siteUrl;
|
|
216
|
+
const payload = {
|
|
217
|
+
...blob,
|
|
218
|
+
siteTitle: form.siteTitle.trim(),
|
|
219
|
+
tagline: form.tagline.trim(),
|
|
220
|
+
language: form.language,
|
|
221
|
+
timezone: form.timezone,
|
|
222
|
+
};
|
|
223
|
+
const res = await cmsApi('/globals/settings', {
|
|
224
|
+
method: 'PUT',
|
|
225
|
+
body: JSON.stringify(payload),
|
|
226
|
+
});
|
|
227
|
+
if (res.error)
|
|
228
|
+
throw new Error(res.error);
|
|
229
|
+
setSettingsBlob(payload);
|
|
230
|
+
}
|
|
231
|
+
// 2. Site URL + robots defaults → the SHARED SEO config (merge over the
|
|
232
|
+
// current override site so SEO-tab fields aren't lost).
|
|
233
|
+
if (siteUrlChanged || robotsChanged) {
|
|
234
|
+
const nextSite = { ...overrideSite };
|
|
235
|
+
if (siteUrlChanged)
|
|
236
|
+
nextSite.siteUrl = form.siteUrl.trim() || undefined;
|
|
237
|
+
if (robotsChanged) {
|
|
238
|
+
nextSite.robots = {
|
|
239
|
+
...(overrideSite.robots ?? {}),
|
|
240
|
+
defaultNoIndex: form.defaultNoIndex,
|
|
241
|
+
defaultNoFollow: form.defaultNoFollow,
|
|
242
|
+
noIndexNonProduction: form.noIndexNonProduction,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const res = await cmsApi('/seo/config', {
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
body: JSON.stringify({ site: nextSite }),
|
|
248
|
+
});
|
|
249
|
+
if (res.error)
|
|
250
|
+
throw new Error(res.error);
|
|
251
|
+
setOverrideSite(nextSite);
|
|
252
|
+
}
|
|
253
|
+
setBaseline(form);
|
|
254
|
+
setSaveState('success');
|
|
255
|
+
await refetchStatus();
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
setSaveState('error');
|
|
259
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save settings.');
|
|
260
|
+
}
|
|
261
|
+
}, [canEdit, hasErrors, form, baseline, settingsBlob, overrideSite, sources, refetchStatus]);
|
|
262
|
+
const reset = useCallback(() => {
|
|
263
|
+
setForm(baseline);
|
|
264
|
+
setSaveState('idle');
|
|
265
|
+
setSaveError(null);
|
|
266
|
+
}, [baseline]);
|
|
267
|
+
const refetch = useCallback(() => setReloadKey((k) => k + 1), []);
|
|
268
|
+
return {
|
|
269
|
+
loading,
|
|
270
|
+
loadError,
|
|
271
|
+
form,
|
|
272
|
+
setField,
|
|
273
|
+
dirty,
|
|
274
|
+
issues,
|
|
275
|
+
hasErrors,
|
|
276
|
+
indexStatus,
|
|
277
|
+
sources,
|
|
278
|
+
isProduction,
|
|
279
|
+
saveState,
|
|
280
|
+
saveError,
|
|
281
|
+
save,
|
|
282
|
+
reset,
|
|
283
|
+
refetch,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=useGeneralSettings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGeneralSettings.js","sourceRoot":"","sources":["../../../src/views/settings/useGeneralSettings.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cms-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"happy-dom": "^20.9.0",
|
|
87
87
|
"tailwindcss": "^4.0.0",
|
|
88
88
|
"typescript": "^5.7.0",
|
|
89
|
-
"vitest": "^
|
|
90
|
-
"@actuate-media/cms-core": "0.
|
|
89
|
+
"vitest": "^4.1.0",
|
|
90
|
+
"@actuate-media/cms-core": "0.36.0",
|
|
91
91
|
"@actuate-media/component-blocks": "0.2.0"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
package/src/AdminRoot.tsx
CHANGED
|
@@ -424,15 +424,17 @@ function AdminShell({
|
|
|
424
424
|
// Users + API Keys moved into the Settings view as tabs. The legacy
|
|
425
425
|
// top-level routes still resolve — they just land on the matching tab.
|
|
426
426
|
if (matchRoute('/users')) {
|
|
427
|
-
return <Settings onNavigate={navigate} config={config} initialTab="users" />
|
|
427
|
+
return <Settings onNavigate={navigate} config={config} session={session} initialTab="users" />
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
if (matchRoute('/api-keys')) {
|
|
431
|
-
return
|
|
431
|
+
return (
|
|
432
|
+
<Settings onNavigate={navigate} config={config} session={session} initialTab="api-keys" />
|
|
433
|
+
)
|
|
432
434
|
}
|
|
433
435
|
|
|
434
436
|
if (matchRoute('/settings')) {
|
|
435
|
-
return <Settings onNavigate={navigate} config={config} />
|
|
437
|
+
return <Settings onNavigate={navigate} config={config} session={session} />
|
|
436
438
|
}
|
|
437
439
|
|
|
438
440
|
// Page Builder routes — explicit /page-builder/* takes precedence over
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
// The Appearance tab reads `/appearance` (settings + resolved brand), then on
|
|
6
|
+
// save read-modify-writes `/globals/settings`. Uploads POST `/media/upload`.
|
|
7
|
+
let appearanceData: any
|
|
8
|
+
|
|
9
|
+
const cmsApi = vi.fn(async (endpoint: string, options?: { method?: string; body?: unknown }) => {
|
|
10
|
+
const method = options?.method ?? 'GET'
|
|
11
|
+
if (endpoint === '/appearance' && method === 'GET') return { data: appearanceData, status: 200 }
|
|
12
|
+
if (endpoint === '/globals/settings' && method === 'GET')
|
|
13
|
+
return { data: { siteTitle: 'Actuate Media' }, status: 200 }
|
|
14
|
+
if (endpoint === '/globals/settings' && method === 'PUT') return { data: {}, status: 200 }
|
|
15
|
+
if (endpoint === '/media/upload' && method === 'POST')
|
|
16
|
+
return {
|
|
17
|
+
data: { id: 'new-logo', url: 'https://cdn.example.com/x.svg', mimeType: 'image/svg+xml' },
|
|
18
|
+
status: 200,
|
|
19
|
+
}
|
|
20
|
+
return { data: {}, status: 200 }
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
vi.mock('../../lib/api.js', () => ({ cmsApi: (e: string, o?: unknown) => cmsApi(e, o as any) }))
|
|
24
|
+
|
|
25
|
+
const { AppearanceTab } = await import('../../views/settings/AppearanceTab.js')
|
|
26
|
+
|
|
27
|
+
function baseData(overrides: Record<string, unknown> = {}) {
|
|
28
|
+
return {
|
|
29
|
+
appearance: {
|
|
30
|
+
adminColorModeDefault: 'system',
|
|
31
|
+
adminAccentColor: 'purple',
|
|
32
|
+
sidebarDensity: 'default',
|
|
33
|
+
contentFont: 'DM Sans',
|
|
34
|
+
reduceMotion: false,
|
|
35
|
+
},
|
|
36
|
+
brand: { mode: 'inheritPublicBrand' },
|
|
37
|
+
resolved: { admin: { initials: 'AM' } },
|
|
38
|
+
assets: [],
|
|
39
|
+
siteTitle: 'Actuate Media',
|
|
40
|
+
...overrides,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
cmsApi.mockClear()
|
|
46
|
+
appearanceData = baseData()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('AppearanceTab', () => {
|
|
50
|
+
it('renders the branding/theme/typography cards with a clean (disabled) save bar', async () => {
|
|
51
|
+
render(<AppearanceTab role="ADMIN" />)
|
|
52
|
+
|
|
53
|
+
await screen.findByText('Branding')
|
|
54
|
+
expect(screen.getByText('Admin Theme')).toBeTruthy()
|
|
55
|
+
expect(screen.getByText('Typography & Motion')).toBeTruthy()
|
|
56
|
+
expect(screen.getByText('Brand Preview')).toBeTruthy()
|
|
57
|
+
expect(
|
|
58
|
+
(screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
|
|
59
|
+
).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('enables Save when the accent changes and persists appearance to the settings global', async () => {
|
|
63
|
+
render(<AppearanceTab role="ADMIN" />)
|
|
64
|
+
await screen.findByText('Branding')
|
|
65
|
+
|
|
66
|
+
fireEvent.click(screen.getByRole('radio', { name: 'Blue' }))
|
|
67
|
+
const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
|
|
68
|
+
expect(save.disabled).toBe(false)
|
|
69
|
+
|
|
70
|
+
fireEvent.click(save)
|
|
71
|
+
|
|
72
|
+
await waitFor(() =>
|
|
73
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
74
|
+
'/globals/settings',
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
method: 'PUT',
|
|
77
|
+
body: expect.stringContaining('"adminAccentColor":"blue"'),
|
|
78
|
+
}),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('warns when dark mode is the default but no dark-mode logo is set', async () => {
|
|
84
|
+
appearanceData = baseData({
|
|
85
|
+
appearance: {
|
|
86
|
+
adminColorModeDefault: 'dark',
|
|
87
|
+
adminAccentColor: 'purple',
|
|
88
|
+
sidebarDensity: 'default',
|
|
89
|
+
contentFont: 'DM Sans',
|
|
90
|
+
reduceMotion: false,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
render(<AppearanceTab role="ADMIN" />)
|
|
94
|
+
|
|
95
|
+
expect(await screen.findByText(/no dark-mode logo is set/i)).toBeTruthy()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('uploads a logo through the Media Center and enables Save', async () => {
|
|
99
|
+
render(<AppearanceTab role="ADMIN" />)
|
|
100
|
+
await screen.findByText('Branding')
|
|
101
|
+
|
|
102
|
+
const input = screen.getByLabelText('Primary Logo') as HTMLInputElement
|
|
103
|
+
const file = new File(
|
|
104
|
+
['<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg"></svg>'],
|
|
105
|
+
'logo.svg',
|
|
106
|
+
{ type: 'image/svg+xml' },
|
|
107
|
+
)
|
|
108
|
+
fireEvent.change(input, { target: { files: [file] } })
|
|
109
|
+
|
|
110
|
+
await waitFor(() =>
|
|
111
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
112
|
+
'/media/upload',
|
|
113
|
+
expect.objectContaining({ method: 'POST' }),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
await waitFor(() =>
|
|
117
|
+
expect(
|
|
118
|
+
(screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
|
|
119
|
+
).toBe(false),
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('is read-only for non-admins', async () => {
|
|
124
|
+
render(<AppearanceTab role="EDITOR" />)
|
|
125
|
+
|
|
126
|
+
expect(await screen.findByText(/read-only access to appearance/i)).toBeTruthy()
|
|
127
|
+
// Upload control disabled.
|
|
128
|
+
expect(
|
|
129
|
+
(screen.getByRole('button', { name: /upload primary logo/i }) as HTMLButtonElement).disabled,
|
|
130
|
+
).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
// Own the three backends the General tab reads: the `settings` global
|
|
6
|
+
// (site info / locale), the shared `/seo/config` (site URL + robots), and the
|
|
7
|
+
// computed `/seo/index-status` (the status card).
|
|
8
|
+
const STATUS = {
|
|
9
|
+
status: 'protected',
|
|
10
|
+
severity: 'warning',
|
|
11
|
+
label: 'Protected',
|
|
12
|
+
description: 'Non-production environment is noindexed. Production remains unaffected.',
|
|
13
|
+
environment: 'preview',
|
|
14
|
+
checks: [
|
|
15
|
+
{ key: 'environment', label: 'Environment', value: 'Preview', status: 'pass' },
|
|
16
|
+
{ key: 'siteUrl', label: 'Site URL configured', value: 'Yes', status: 'pass' },
|
|
17
|
+
{ key: 'sitemap', label: 'Sitemap enabled', value: 'Yes', status: 'pass' },
|
|
18
|
+
],
|
|
19
|
+
actions: [
|
|
20
|
+
{ label: 'Run SEO Audit', href: '/seo?tab=audit', action: 'run_seo_audit' },
|
|
21
|
+
{ label: 'View robots.txt', href: '/robots.txt' },
|
|
22
|
+
],
|
|
23
|
+
lastAuditAt: null,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cmsApi = vi.fn(async (endpoint: string, options?: { method?: string }) => {
|
|
27
|
+
const method = options?.method ?? 'GET'
|
|
28
|
+
if (endpoint === '/globals/settings' && method === 'GET') {
|
|
29
|
+
return {
|
|
30
|
+
data: {
|
|
31
|
+
siteTitle: 'Actuate Media',
|
|
32
|
+
tagline: 'A lightweight content management system',
|
|
33
|
+
language: 'en-US',
|
|
34
|
+
timezone: 'America/Los_Angeles',
|
|
35
|
+
aiProvider: 'anthropic',
|
|
36
|
+
},
|
|
37
|
+
status: 200,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (endpoint === '/seo/config' && method === 'GET') {
|
|
41
|
+
return {
|
|
42
|
+
data: {
|
|
43
|
+
static: { site: null },
|
|
44
|
+
overrides: { site: {} },
|
|
45
|
+
effective: {
|
|
46
|
+
site: {
|
|
47
|
+
siteUrl: 'https://actuatemedia.com',
|
|
48
|
+
robots: { defaultNoIndex: false, defaultNoFollow: false, noIndexNonProduction: true },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
status: 200,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (endpoint === '/seo/index-status' && method === 'GET') {
|
|
56
|
+
return { data: STATUS, status: 200 }
|
|
57
|
+
}
|
|
58
|
+
return { data: {}, status: 200 }
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
vi.mock('../../lib/api.js', () => ({ cmsApi: (e: string, o?: unknown) => cmsApi(e, o as any) }))
|
|
62
|
+
|
|
63
|
+
const { GeneralSettingsTab } = await import('../../views/settings/GeneralSettingsTab.js')
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
cmsApi.mockClear()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('GeneralSettingsTab', () => {
|
|
70
|
+
it('renders the index status pill, loaded site info, and a clean (disabled) save bar', async () => {
|
|
71
|
+
render(<GeneralSettingsTab role="ADMIN" />)
|
|
72
|
+
|
|
73
|
+
await screen.findByLabelText('Site Title')
|
|
74
|
+
const statusCard = screen.getByRole('region', { name: 'Site Index Status' })
|
|
75
|
+
expect(within(statusCard).getByText('Protected')).toBeTruthy()
|
|
76
|
+
expect((screen.getByLabelText('Site Title') as HTMLInputElement).value).toBe('Actuate Media')
|
|
77
|
+
expect((screen.getByLabelText('Site URL') as HTMLInputElement).value).toBe(
|
|
78
|
+
'https://actuatemedia.com',
|
|
79
|
+
)
|
|
80
|
+
// Nothing dirty yet → Save disabled.
|
|
81
|
+
expect(
|
|
82
|
+
(screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
|
|
83
|
+
).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('enables Save when a field changes and persists site info to the settings global', async () => {
|
|
87
|
+
render(<GeneralSettingsTab role="ADMIN" />)
|
|
88
|
+
const title = (await screen.findByLabelText('Site Title')) as HTMLInputElement
|
|
89
|
+
|
|
90
|
+
fireEvent.change(title, { target: { value: 'Actuate Media Co' } })
|
|
91
|
+
const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
|
|
92
|
+
expect(save.disabled).toBe(false)
|
|
93
|
+
|
|
94
|
+
fireEvent.click(save)
|
|
95
|
+
|
|
96
|
+
await waitFor(() =>
|
|
97
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
98
|
+
'/globals/settings',
|
|
99
|
+
expect.objectContaining({ method: 'PUT' }),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('blocks save and shows an error for an invalid Site URL', async () => {
|
|
105
|
+
render(<GeneralSettingsTab role="ADMIN" />)
|
|
106
|
+
await screen.findByLabelText('Site Title')
|
|
107
|
+
const url = screen.getByLabelText('Site URL') as HTMLInputElement
|
|
108
|
+
|
|
109
|
+
fireEvent.change(url, { target: { value: 'not a url' } })
|
|
110
|
+
|
|
111
|
+
expect((await screen.findAllByText(/valid absolute URL/i)).length).toBeGreaterThan(0)
|
|
112
|
+
expect(
|
|
113
|
+
(screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
|
|
114
|
+
).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('confirms before turning OFF non-production noindex', async () => {
|
|
118
|
+
render(<GeneralSettingsTab role="ADMIN" />)
|
|
119
|
+
await screen.findByLabelText('Site Title')
|
|
120
|
+
|
|
121
|
+
// The non-production noindex toggle is ON; clicking it should request confirmation.
|
|
122
|
+
fireEvent.click(screen.getByRole('switch', { name: 'Noindex Non-Production Environments' }))
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
await screen.findByText(/Preview and staging environments may become indexable/i),
|
|
126
|
+
).toBeTruthy()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('is read-only for non-admins', async () => {
|
|
130
|
+
render(<GeneralSettingsTab role="EDITOR" />)
|
|
131
|
+
|
|
132
|
+
expect(await screen.findByText(/read-only access to settings/i)).toBeTruthy()
|
|
133
|
+
expect((screen.getByLabelText('Site Title') as HTMLInputElement).disabled).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
})
|