@actuate-media/cms-admin 0.67.0 → 0.68.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/CHANGELOG.md +16 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +29 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.js +88 -0
- package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
- package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
- package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +25 -6
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.js +105 -0
- package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
- package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
- package/dist/__tests__/views/security-settings.render.test.js +85 -4
- package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/collections/EntryDetailPane.d.ts +10 -0
- package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
- package/dist/components/collections/EntryDetailPane.js +92 -0
- package/dist/components/collections/EntryDetailPane.js.map +1 -0
- package/dist/components/forms/FieldsPanel.js +1 -1
- package/dist/components/forms/FieldsPanel.js.map +1 -1
- package/dist/components/forms/FormSchemaPane.d.ts +8 -0
- package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +95 -0
- package/dist/components/forms/FormSchemaPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +9 -0
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +175 -0
- package/dist/components/seo/SeoEditorPane.js.map +1 -0
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/components/ui/StatusBadge.d.ts +1 -1
- package/dist/hooks/useIdleLogout.d.ts +18 -0
- package/dist/hooks/useIdleLogout.d.ts.map +1 -0
- package/dist/hooks/useIdleLogout.js +66 -0
- package/dist/hooks/useIdleLogout.js.map +1 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +7 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/post-types-service.d.ts +44 -0
- package/dist/lib/post-types-service.d.ts.map +1 -0
- package/dist/lib/post-types-service.js +83 -0
- package/dist/lib/post-types-service.js.map +1 -0
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +23 -4
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +25 -21
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
- package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
- package/dist/views/Posts/PostTypeEditor.js +159 -0
- package/dist/views/Posts/PostTypeEditor.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +3 -2
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +51 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +113 -6
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +22 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
- package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
- package/dist/views/settings/AccessProtectionCard.js +13 -10
- package/dist/views/settings/AccessProtectionCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +3 -3
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
- package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
- package/dist/views/settings/SessionPolicyCard.js +12 -5
- package/dist/views/settings/SessionPolicyCard.js.map +1 -1
- package/dist/views/settings/useSecuritySettings.d.ts +13 -1
- package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
- package/dist/views/settings/useSecuritySettings.js +47 -4
- package/dist/views/settings/useSecuritySettings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +36 -7
- package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
- package/src/__tests__/lib/post-types-service.test.ts +112 -0
- package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
- package/src/__tests__/views/forms-list.render.test.tsx +34 -6
- package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
- package/src/__tests__/views/page-templates.render.test.tsx +142 -0
- package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
- package/src/__tests__/views/security-settings.render.test.tsx +111 -4
- package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
- package/src/components/collections/EntryDetailPane.tsx +182 -0
- package/src/components/forms/FieldsPanel.tsx +3 -1
- package/src/components/forms/FormSchemaPane.tsx +192 -0
- package/src/components/seo/SeoEditorPane.tsx +533 -0
- package/src/hooks/useIdleLogout.ts +77 -0
- package/src/lib/api.ts +8 -2
- package/src/lib/post-types-service.ts +129 -0
- package/src/views/CollectionList.tsx +158 -110
- package/src/views/Forms.tsx +154 -126
- package/src/views/Posts/PostTypeEditor.tsx +568 -0
- package/src/views/Posts/PostTypesView.tsx +5 -4
- package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
- package/src/views/page-builder/PageBuilder.tsx +69 -2
- package/src/views/page-builder/PageTemplates.tsx +340 -57
- package/src/views/seo/ContentTab.tsx +113 -80
- package/src/views/settings/AccessProtectionCard.tsx +83 -53
- package/src/views/settings/SecurityTab.tsx +23 -2
- package/src/views/settings/SessionPolicyCard.tsx +45 -9
- package/src/views/settings/useSecuritySettings.ts +78 -22
- package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
- package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
- package/dist/components/forms/FormSchemaDrawer.js +0 -99
- package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.js +0 -172
- package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
- package/src/components/forms/FormSchemaDrawer.tsx +0 -192
- package/src/components/seo/SeoEditorDrawer.tsx +0 -529
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionPolicyCard.js","sourceRoot":"","sources":["../../../src/views/settings/SessionPolicyCard.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,
|
|
1
|
+
{"version":3,"file":"SessionPolicyCard.js","sourceRoot":"","sources":["../../../src/views/settings/SessionPolicyCard.tsx"],"names":[],"mappings":"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"}
|
|
@@ -5,8 +5,15 @@ export interface SecurityForm {
|
|
|
5
5
|
mfaRequired: boolean;
|
|
6
6
|
mfaGracePeriodDays: number;
|
|
7
7
|
maxConcurrentSessions: number;
|
|
8
|
+
timeoutHours: number;
|
|
9
|
+
idleLogoutEnabled: boolean;
|
|
10
|
+
/** Raw textarea contents — one IP/CIDR entry per line. */
|
|
11
|
+
ipAllowlistText: string;
|
|
12
|
+
auditLoggingEnabled: boolean;
|
|
8
13
|
}
|
|
9
14
|
type SaveState = 'idle' | 'saving' | 'success' | 'error';
|
|
15
|
+
/** Split the textarea into trimmed, de-duplicated entries (one per line). */
|
|
16
|
+
export declare function parseAllowlistText(text: string): string[];
|
|
10
17
|
export interface UseSecuritySettings {
|
|
11
18
|
loading: boolean;
|
|
12
19
|
loadError: string | null;
|
|
@@ -19,9 +26,14 @@ export interface UseSecuritySettings {
|
|
|
19
26
|
canEdit: boolean;
|
|
20
27
|
saveState: SaveState;
|
|
21
28
|
saveError: string | null;
|
|
22
|
-
save: (
|
|
29
|
+
save: (options?: {
|
|
30
|
+
confirmLockout?: boolean;
|
|
31
|
+
}) => Promise<void>;
|
|
23
32
|
reset: () => void;
|
|
24
33
|
refetch: () => void;
|
|
34
|
+
/** Server rejected the save with IP_LOCKOUT_RISK — message to confirm. */
|
|
35
|
+
lockoutPrompt: string | null;
|
|
36
|
+
dismissLockoutPrompt: () => void;
|
|
25
37
|
revokeState: 'idle' | 'working' | 'done' | 'error';
|
|
26
38
|
revokeAllOtherSessions: () => Promise<number | null>;
|
|
27
39
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSecuritySettings.d.ts","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAE9D,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"useSecuritySettings.d.ts","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAE9D,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAA;IACvB,mBAAmB,EAAE,OAAO,CAAA;CAC7B;AAED,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAA;AAYxD,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AA6BD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC/B,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,YAAY,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAChF,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,SAAS,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,0EAA0E;IAC1E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,oBAAoB,EAAE,MAAM,IAAI,CAAA;IAChC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;IAClD,sBAAsB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrD;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAuJzE"}
|
|
@@ -6,18 +6,44 @@ const EMPTY_FORM = {
|
|
|
6
6
|
mfaRequired: false,
|
|
7
7
|
mfaGracePeriodDays: 7,
|
|
8
8
|
maxConcurrentSessions: 5,
|
|
9
|
+
timeoutHours: 168,
|
|
10
|
+
idleLogoutEnabled: false,
|
|
11
|
+
ipAllowlistText: '',
|
|
12
|
+
auditLoggingEnabled: true,
|
|
9
13
|
};
|
|
14
|
+
/** Split the textarea into trimmed, de-duplicated entries (one per line). */
|
|
15
|
+
export function parseAllowlistText(text) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const line of text.split('\n')) {
|
|
18
|
+
const entry = line.trim();
|
|
19
|
+
if (entry && !out.includes(entry))
|
|
20
|
+
out.push(entry);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
10
24
|
function formFromSettings(s) {
|
|
11
25
|
return {
|
|
12
26
|
mfaRequired: s.mfa.required,
|
|
13
27
|
mfaGracePeriodDays: s.mfa.gracePeriodDays,
|
|
14
28
|
maxConcurrentSessions: s.session.maxConcurrentSessions,
|
|
29
|
+
timeoutHours: s.session.timeoutHours ?? 168,
|
|
30
|
+
idleLogoutEnabled: s.session.idleLogoutEnabled ?? false,
|
|
31
|
+
ipAllowlistText: (s.access?.ipAllowlist ?? []).join('\n'),
|
|
32
|
+
auditLoggingEnabled: s.access?.auditLoggingEnabled ?? true,
|
|
15
33
|
};
|
|
16
34
|
}
|
|
17
35
|
function formToSettings(f) {
|
|
18
36
|
return {
|
|
19
37
|
mfa: { required: f.mfaRequired, gracePeriodDays: f.mfaGracePeriodDays },
|
|
20
|
-
session: {
|
|
38
|
+
session: {
|
|
39
|
+
maxConcurrentSessions: f.maxConcurrentSessions,
|
|
40
|
+
timeoutHours: f.timeoutHours,
|
|
41
|
+
idleLogoutEnabled: f.idleLogoutEnabled,
|
|
42
|
+
},
|
|
43
|
+
access: {
|
|
44
|
+
ipAllowlist: parseAllowlistText(f.ipAllowlistText),
|
|
45
|
+
auditLoggingEnabled: f.auditLoggingEnabled,
|
|
46
|
+
},
|
|
21
47
|
};
|
|
22
48
|
}
|
|
23
49
|
/**
|
|
@@ -35,6 +61,7 @@ export function useSecuritySettings(canEdit) {
|
|
|
35
61
|
const [form, setForm] = useState(EMPTY_FORM);
|
|
36
62
|
const [saveState, setSaveState] = useState('idle');
|
|
37
63
|
const [saveError, setSaveError] = useState(null);
|
|
64
|
+
const [lockoutPrompt, setLockoutPrompt] = useState(null);
|
|
38
65
|
const [revokeState, setRevokeState] = useState('idle');
|
|
39
66
|
const [reloadKey, setReloadKey] = useState(0);
|
|
40
67
|
useEffect(() => {
|
|
@@ -85,18 +112,30 @@ export function useSecuritySettings(canEdit) {
|
|
|
85
112
|
return list;
|
|
86
113
|
}, [form, baseline.mfaRequired, data]);
|
|
87
114
|
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
88
|
-
const save = useCallback(async () => {
|
|
115
|
+
const save = useCallback(async (options) => {
|
|
89
116
|
if (!canEdit || hasErrors || !dirty)
|
|
90
117
|
return;
|
|
91
118
|
setSaveState('saving');
|
|
92
119
|
setSaveError(null);
|
|
120
|
+
setLockoutPrompt(null);
|
|
93
121
|
try {
|
|
94
122
|
const res = await cmsApi('/security', {
|
|
95
123
|
method: 'PUT',
|
|
96
|
-
body: JSON.stringify(
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
...formToSettings(form),
|
|
126
|
+
...(options?.confirmLockout ? { confirmLockout: true } : {}),
|
|
127
|
+
}),
|
|
97
128
|
});
|
|
98
|
-
|
|
129
|
+
// The server's lockout guard: surface a confirm dialog instead of a
|
|
130
|
+
// generic failure so the operator can retry with confirmLockout.
|
|
131
|
+
if (res.code === 'IP_LOCKOUT_RISK') {
|
|
132
|
+
setSaveState('idle');
|
|
133
|
+
setLockoutPrompt(res.error ?? 'This allowlist may lock you out. Save anyway?');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (res.error || !res.data) {
|
|
99
137
|
throw new Error(res.error ?? 'Failed to save security settings.');
|
|
138
|
+
}
|
|
100
139
|
setData(res.data);
|
|
101
140
|
const f = formFromSettings(res.data.settings);
|
|
102
141
|
setBaseline(f);
|
|
@@ -108,10 +147,12 @@ export function useSecuritySettings(canEdit) {
|
|
|
108
147
|
setSaveError(err instanceof Error ? err.message : 'Failed to save security settings.');
|
|
109
148
|
}
|
|
110
149
|
}, [canEdit, hasErrors, dirty, form]);
|
|
150
|
+
const dismissLockoutPrompt = useCallback(() => setLockoutPrompt(null), []);
|
|
111
151
|
const reset = useCallback(() => {
|
|
112
152
|
setForm(baseline);
|
|
113
153
|
setSaveState('idle');
|
|
114
154
|
setSaveError(null);
|
|
155
|
+
setLockoutPrompt(null);
|
|
115
156
|
}, [baseline]);
|
|
116
157
|
const refetch = useCallback(() => setReloadKey((k) => k + 1), []);
|
|
117
158
|
const revokeAllOtherSessions = useCallback(async () => {
|
|
@@ -147,6 +188,8 @@ export function useSecuritySettings(canEdit) {
|
|
|
147
188
|
save,
|
|
148
189
|
reset,
|
|
149
190
|
refetch,
|
|
191
|
+
lockoutPrompt,
|
|
192
|
+
dismissLockoutPrompt,
|
|
150
193
|
revokeState,
|
|
151
194
|
revokeAllOtherSessions,
|
|
152
195
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSecuritySettings.js","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EACL,wBAAwB,GAGzB,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"useSecuritySettings.js","sourceRoot":"","sources":["../../../src/views/settings/useSecuritySettings.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.68.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"tailwindcss": "^4.0.0",
|
|
96
96
|
"typescript": "^5.7.0",
|
|
97
97
|
"vitest": "^4.1.0",
|
|
98
|
-
"@actuate-media/cms-core": "0.
|
|
98
|
+
"@actuate-media/cms-core": "0.88.0",
|
|
99
99
|
"@actuate-media/component-blocks": "0.2.1"
|
|
100
100
|
},
|
|
101
101
|
"scripts": {
|
package/src/AdminRoot.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import { ThemeProvider } from './components/ThemeProvider.js'
|
|
|
38
38
|
import { LocaleProvider } from './components/LocaleProvider.js'
|
|
39
39
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js'
|
|
40
40
|
import { useAppearanceBootstrap } from './hooks/useAppearanceBootstrap.js'
|
|
41
|
+
import { useIdleLogout } from './hooks/useIdleLogout.js'
|
|
41
42
|
import { PageBuilder } from './views/page-builder/PageBuilder.js'
|
|
42
43
|
import { PageSectionEditor } from './views/page-editor/PageSectionEditor.js'
|
|
43
44
|
import { PagePreview } from './views/page-editor/PagePreview.js'
|
|
@@ -52,6 +53,7 @@ import { PagesListView } from './views/Pages/PagesListView.js'
|
|
|
52
53
|
import { PageTagManager } from './views/Pages/PageTagManager.js'
|
|
53
54
|
import { PostsListView } from './views/Posts/PostsListView.js'
|
|
54
55
|
import { PostTypesView } from './views/Posts/PostTypesView.js'
|
|
56
|
+
import { PostTypeEditor } from './views/Posts/PostTypeEditor.js'
|
|
55
57
|
import { NewPostPicker } from './views/Posts/NewPostPicker.js'
|
|
56
58
|
import { NewPostTypeInstructions } from './views/Posts/NewPostTypeInstructions.js'
|
|
57
59
|
import { PostSectionEditor } from './views/post-editor/PostSectionEditor.js'
|
|
@@ -179,6 +181,39 @@ function AdminShell({
|
|
|
179
181
|
[buildHref],
|
|
180
182
|
)
|
|
181
183
|
|
|
184
|
+
// Idle auto-logout (Settings → Security). The policy is read from the
|
|
185
|
+
// non-admin-gated /auth/session-policy endpoint so every member's client
|
|
186
|
+
// enforces it, not just admins. When enabled, `timeoutHours` of inactivity
|
|
187
|
+
// signs the user out through the normal logout flow (server revocation +
|
|
188
|
+
// cookie clear + local state reset).
|
|
189
|
+
const [sessionPolicy, setSessionPolicy] = useState<{
|
|
190
|
+
timeoutHours: number
|
|
191
|
+
idleLogoutEnabled: boolean
|
|
192
|
+
} | null>(null)
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!session) {
|
|
195
|
+
setSessionPolicy(null)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
let cancelled = false
|
|
199
|
+
void cmsApi<{ timeoutHours: number; idleLogoutEnabled: boolean }>('/auth/session-policy').then(
|
|
200
|
+
(res) => {
|
|
201
|
+
if (!cancelled && res.data && typeof res.data.timeoutHours === 'number') {
|
|
202
|
+
setSessionPolicy(res.data)
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
return () => {
|
|
207
|
+
cancelled = true
|
|
208
|
+
}
|
|
209
|
+
}, [session])
|
|
210
|
+
|
|
211
|
+
useIdleLogout({
|
|
212
|
+
enabled: Boolean(session && sessionPolicy?.idleLogoutEnabled),
|
|
213
|
+
timeoutMs: (sessionPolicy?.timeoutHours ?? 0) * 3_600_000,
|
|
214
|
+
onIdle: handleLogout,
|
|
215
|
+
})
|
|
216
|
+
|
|
182
217
|
useBranding(config)
|
|
183
218
|
useAppearanceBootstrap(session)
|
|
184
219
|
useAdminPageTitle(config, currentPath)
|
|
@@ -358,13 +393,7 @@ function AdminShell({
|
|
|
358
393
|
}
|
|
359
394
|
const postTypeEdit = matchRoute('/posts/types/:slug/edit')
|
|
360
395
|
if (postTypeEdit?.slug) {
|
|
361
|
-
|
|
362
|
-
// PR introduces a dedicated PostTypeEditor with field-level
|
|
363
|
-
// metadata. The list view is the natural per-type management
|
|
364
|
-
// surface today (it already shows posts in that type).
|
|
365
|
-
return (
|
|
366
|
-
<CollectionList collectionSlug={postTypeEdit.slug} config={config} onNavigate={navigate} />
|
|
367
|
-
)
|
|
396
|
+
return <PostTypeEditor slug={postTypeEdit.slug} onNavigate={navigate} />
|
|
368
397
|
}
|
|
369
398
|
if (matchRoute('/posts/types')) {
|
|
370
399
|
return <PostTypesView onNavigate={navigate} />
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { render } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
import { useIdleLogout } from '../../hooks/useIdleLogout.js'
|
|
6
|
+
|
|
7
|
+
function Harness({
|
|
8
|
+
enabled,
|
|
9
|
+
timeoutMs,
|
|
10
|
+
onIdle,
|
|
11
|
+
}: {
|
|
12
|
+
enabled: boolean
|
|
13
|
+
timeoutMs: number
|
|
14
|
+
onIdle: () => void
|
|
15
|
+
}) {
|
|
16
|
+
useIdleLogout({ enabled, timeoutMs, onIdle })
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TIMEOUT_MS = 60_000
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.useFakeTimers()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.useRealTimers()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('useIdleLogout', () => {
|
|
31
|
+
it('fires onIdle exactly once after the timeout with no activity', () => {
|
|
32
|
+
const onIdle = vi.fn()
|
|
33
|
+
render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(TIMEOUT_MS - 1_000)
|
|
36
|
+
expect(onIdle).not.toHaveBeenCalled()
|
|
37
|
+
|
|
38
|
+
vi.advanceTimersByTime(TIMEOUT_MS)
|
|
39
|
+
expect(onIdle).toHaveBeenCalledTimes(1)
|
|
40
|
+
|
|
41
|
+
// The idle callback must not re-fire on subsequent ticks.
|
|
42
|
+
vi.advanceTimersByTime(TIMEOUT_MS * 3)
|
|
43
|
+
expect(onIdle).toHaveBeenCalledTimes(1)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('resets the idle deadline on user activity', () => {
|
|
47
|
+
const onIdle = vi.fn()
|
|
48
|
+
render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
|
|
49
|
+
|
|
50
|
+
// Just before the deadline the user types — the clock restarts.
|
|
51
|
+
vi.advanceTimersByTime(TIMEOUT_MS - 5_000)
|
|
52
|
+
window.dispatchEvent(new Event('keydown'))
|
|
53
|
+
vi.advanceTimersByTime(TIMEOUT_MS - 5_000)
|
|
54
|
+
expect(onIdle).not.toHaveBeenCalled()
|
|
55
|
+
|
|
56
|
+
// A full quiet timeout after the last activity signs the user out.
|
|
57
|
+
vi.advanceTimersByTime(TIMEOUT_MS)
|
|
58
|
+
expect(onIdle).toHaveBeenCalledTimes(1)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('does nothing when disabled', () => {
|
|
62
|
+
const onIdle = vi.fn()
|
|
63
|
+
render(<Harness enabled={false} timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
|
|
64
|
+
vi.advanceTimersByTime(TIMEOUT_MS * 10)
|
|
65
|
+
expect(onIdle).not.toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('tears everything down on unmount', () => {
|
|
69
|
+
const onIdle = vi.fn()
|
|
70
|
+
const { unmount } = render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
|
|
71
|
+
unmount()
|
|
72
|
+
vi.advanceTimersByTime(TIMEOUT_MS * 10)
|
|
73
|
+
expect(onIdle).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('checks the deadline immediately when a hidden tab becomes visible', () => {
|
|
77
|
+
const onIdle = vi.fn()
|
|
78
|
+
render(<Harness enabled timeoutMs={TIMEOUT_MS} onIdle={onIdle} />)
|
|
79
|
+
|
|
80
|
+
// Simulate the deadline passing while the tab was hidden (the interval is
|
|
81
|
+
// shared with visibility, so freeze it by only moving the mocked clock).
|
|
82
|
+
vi.setSystemTime(Date.now() + TIMEOUT_MS + 1_000)
|
|
83
|
+
document.dispatchEvent(new Event('visibilitychange'))
|
|
84
|
+
expect(onIdle).toHaveBeenCalledTimes(1)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const cmsApiMock = vi.fn()
|
|
4
|
+
vi.mock('../../lib/api.js', () => ({
|
|
5
|
+
cmsApi: (...args: unknown[]) => cmsApiMock(...args),
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
fetchPostTypeDetail,
|
|
10
|
+
fieldKeyFromName,
|
|
11
|
+
fieldTypeLabel,
|
|
12
|
+
humanizeFieldName,
|
|
13
|
+
renderFieldSnippet,
|
|
14
|
+
} = await import('../../lib/post-types-service.js')
|
|
15
|
+
|
|
16
|
+
const COLLECTIONS = [
|
|
17
|
+
{
|
|
18
|
+
slug: 'blog',
|
|
19
|
+
labels: { singular: 'Blog Post', plural: 'Blog' },
|
|
20
|
+
type: 'post',
|
|
21
|
+
urlPrefix: 'blog',
|
|
22
|
+
description: 'Long-form articles',
|
|
23
|
+
icon: 'file-text',
|
|
24
|
+
color: 'purple',
|
|
25
|
+
hidden: false,
|
|
26
|
+
fields: [
|
|
27
|
+
{ name: 'title', type: 'text', required: true },
|
|
28
|
+
{ name: 'body', type: 'richText', required: false },
|
|
29
|
+
{ name: 'author', type: 'relationship', required: false, relationTo: 'users' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
describe('fetchPostTypeDetail', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
cmsApiMock.mockReset()
|
|
37
|
+
cmsApiMock.mockResolvedValue({ data: COLLECTIONS })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('maps the matching collection row onto PostTypeDetail', async () => {
|
|
41
|
+
const detail = await fetchPostTypeDetail('blog')
|
|
42
|
+
expect(detail).toEqual({
|
|
43
|
+
slug: 'blog',
|
|
44
|
+
singular: 'Blog Post',
|
|
45
|
+
plural: 'Blog',
|
|
46
|
+
description: 'Long-form articles',
|
|
47
|
+
urlPrefix: 'blog',
|
|
48
|
+
icon: 'file-text',
|
|
49
|
+
color: 'purple',
|
|
50
|
+
fields: COLLECTIONS[0]!.fields,
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns null when the slug is unknown', async () => {
|
|
55
|
+
expect(await fetchPostTypeDetail('missing')).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('throws on an API error envelope', async () => {
|
|
59
|
+
cmsApiMock.mockResolvedValue({ error: 'boom' })
|
|
60
|
+
await expect(fetchPostTypeDetail('blog')).rejects.toThrow('boom')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('field name helpers', () => {
|
|
65
|
+
it('humanizes camelCase and snake_case keys', () => {
|
|
66
|
+
expect(humanizeFieldName('clientName')).toBe('Client Name')
|
|
67
|
+
expect(humanizeFieldName('meta_description')).toBe('Meta Description')
|
|
68
|
+
expect(humanizeFieldName('seo-score')).toBe('Seo Score')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('derives camelCase keys from display names', () => {
|
|
72
|
+
expect(fieldKeyFromName('Client Name')).toBe('clientName')
|
|
73
|
+
expect(fieldKeyFromName(' Featured Image! ')).toBe('featuredImage')
|
|
74
|
+
expect(fieldKeyFromName('')).toBe('')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('labels known field types and passes through unknown ones', () => {
|
|
78
|
+
expect(fieldTypeLabel('text')).toBe('Short Text')
|
|
79
|
+
expect(fieldTypeLabel('relationship')).toBe('Relation')
|
|
80
|
+
expect(fieldTypeLabel('customThing')).toBe('customThing')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('renderFieldSnippet', () => {
|
|
85
|
+
it('renders a minimal optional field', () => {
|
|
86
|
+
expect(
|
|
87
|
+
renderFieldSnippet({ key: 'subtitle', label: 'Subtitle', type: 'text', required: false }),
|
|
88
|
+
).toBe("subtitle: { type: 'text', label: 'Subtitle' },")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('includes required, help text, and escapes quotes', () => {
|
|
92
|
+
const snippet = renderFieldSnippet({
|
|
93
|
+
key: 'clientName',
|
|
94
|
+
label: "Client's Name",
|
|
95
|
+
type: 'text',
|
|
96
|
+
required: true,
|
|
97
|
+
helpText: "Editor's hint",
|
|
98
|
+
})
|
|
99
|
+
expect(snippet).toContain("label: 'Client\\'s Name'")
|
|
100
|
+
expect(snippet).toContain('required: true')
|
|
101
|
+
expect(snippet).toContain("admin: { description: 'Editor\\'s hint' }")
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('seeds placeholder options for select and relationTo for relationship', () => {
|
|
105
|
+
expect(
|
|
106
|
+
renderFieldSnippet({ key: 'tone', label: 'Tone', type: 'select', required: false }),
|
|
107
|
+
).toContain("options: [{ label: 'Option A', value: 'option-a' }]")
|
|
108
|
+
expect(
|
|
109
|
+
renderFieldSnippet({ key: 'owner', label: 'Owner', type: 'relationship', required: false }),
|
|
110
|
+
).toContain("relationTo: 'posts'")
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -3,9 +3,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
3
3
|
import { fireEvent, render, screen, within } from '@testing-library/react'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* CollectionList row interactions with the inline entry detail pane
|
|
7
|
+
* (design Entries split-pane):
|
|
8
|
+
* - row / title click toggles the 400px detail pane (no navigation)
|
|
9
|
+
* - "Open full editor" and row double-click open the correct editor route
|
|
10
|
+
*
|
|
11
|
+
* Regression (post-list navigation bug): post-type rows must open the section
|
|
12
|
+
* editor (`/posts/:type/:id/edit`), never the generic `/:collection/:id`
|
|
13
|
+
* route — which 404s when the slug is `posts`.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
vi.mock('../../lib/api.js', () => ({
|
|
@@ -40,17 +45,58 @@ beforeEach(() => {
|
|
|
40
45
|
listDocs = []
|
|
41
46
|
})
|
|
42
47
|
|
|
43
|
-
describe('CollectionList navigation (post collection)', () => {
|
|
44
|
-
it('
|
|
48
|
+
describe('CollectionList entry detail pane + navigation (post collection)', () => {
|
|
49
|
+
it('row click opens the detail pane instead of navigating', () => {
|
|
50
|
+
listDocs = [
|
|
51
|
+
{
|
|
52
|
+
id: '1',
|
|
53
|
+
title: 'Hello',
|
|
54
|
+
status: 'PUBLISHED',
|
|
55
|
+
updatedAt: '2026-06-01T00:00:00Z',
|
|
56
|
+
data: { title: 'Hello' },
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
const onNavigate = vi.fn()
|
|
60
|
+
render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
|
|
61
|
+
|
|
62
|
+
const titleButton = within(screen.getByRole('table')).getByRole('button', { name: 'Hello' })
|
|
63
|
+
expect(titleButton.getAttribute('aria-expanded')).toBe('false')
|
|
64
|
+
fireEvent.click(titleButton)
|
|
65
|
+
|
|
66
|
+
expect(onNavigate).not.toHaveBeenCalled()
|
|
67
|
+
expect(titleButton.getAttribute('aria-expanded')).toBe('true')
|
|
68
|
+
expect(screen.getByRole('region', { name: 'Post details' })).toBeTruthy()
|
|
69
|
+
|
|
70
|
+
// Clicking again toggles the pane closed.
|
|
71
|
+
fireEvent.click(titleButton)
|
|
72
|
+
expect(screen.queryByRole('region', { name: 'Post details' })).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('"Open full editor" opens the section editor, not the generic route', () => {
|
|
45
76
|
listDocs = [{ id: '1', title: 'Hello', status: 'PUBLISHED', updatedAt: '2026-06-01T00:00:00Z' }]
|
|
46
77
|
const onNavigate = vi.fn()
|
|
47
78
|
render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
|
|
48
79
|
|
|
49
80
|
fireEvent.click(within(screen.getByRole('table')).getByRole('button', { name: 'Hello' }))
|
|
81
|
+
fireEvent.click(screen.getByRole('button', { name: 'Open full editor' }))
|
|
82
|
+
|
|
50
83
|
expect(onNavigate).toHaveBeenCalledWith('/posts/posts/1/edit')
|
|
51
84
|
expect(onNavigate).not.toHaveBeenCalledWith('/posts/1')
|
|
52
85
|
})
|
|
53
86
|
|
|
87
|
+
it('row double-click opens the section editor directly', () => {
|
|
88
|
+
listDocs = [{ id: '1', title: 'Hello', status: 'PUBLISHED', updatedAt: '2026-06-01T00:00:00Z' }]
|
|
89
|
+
const onNavigate = vi.fn()
|
|
90
|
+
render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
|
|
91
|
+
|
|
92
|
+
const row = within(screen.getByRole('table'))
|
|
93
|
+
.getByRole('button', { name: 'Hello' })
|
|
94
|
+
.closest('tr')!
|
|
95
|
+
fireEvent.doubleClick(row)
|
|
96
|
+
|
|
97
|
+
expect(onNavigate).toHaveBeenCalledWith('/posts/posts/1/edit')
|
|
98
|
+
})
|
|
99
|
+
|
|
54
100
|
it('starts a new post in the section editor', () => {
|
|
55
101
|
const onNavigate = vi.fn()
|
|
56
102
|
render(<CollectionList collectionSlug="posts" config={POST_CONFIG} onNavigate={onNavigate} />)
|
|
@@ -3,8 +3,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
3
3
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
4
|
|
|
5
5
|
// Drive the Forms list through a mocked forms-service so the test owns the
|
|
6
|
-
// data and can assert filtering, search, the notify toggle, and the
|
|
7
|
-
//
|
|
6
|
+
// data and can assert filtering, search, the notify toggle, and the inline
|
|
7
|
+
// schema pane without a real backend.
|
|
8
8
|
function makeForm(over: Record<string, unknown> = {}) {
|
|
9
9
|
return {
|
|
10
10
|
id: 'f1',
|
|
@@ -56,6 +56,7 @@ const fetchFormSchema = vi.fn(async () => ({
|
|
|
56
56
|
activeVersionId: null,
|
|
57
57
|
versions: [],
|
|
58
58
|
}))
|
|
59
|
+
const saveFormSchema = vi.fn(async (_id: string, _fields: unknown[]) => ({ data: {} }))
|
|
59
60
|
const fetchEmbedSettings = vi.fn(async () => ({}))
|
|
60
61
|
const fetchNotificationSettings = vi.fn(async () => ({ enabled: true, recipients: [] }))
|
|
61
62
|
|
|
@@ -64,7 +65,7 @@ vi.mock('../../lib/forms-service.js', () => ({
|
|
|
64
65
|
fetchFormStats: () => fetchFormStats(),
|
|
65
66
|
setNotifyEnabled: (id: string, v: boolean) => setNotifyEnabled(id, v),
|
|
66
67
|
fetchFormSchema: () => fetchFormSchema(),
|
|
67
|
-
saveFormSchema:
|
|
68
|
+
saveFormSchema: (id: string, fields: unknown[]) => saveFormSchema(id, fields),
|
|
68
69
|
fetchEmbedSettings: () => fetchEmbedSettings(),
|
|
69
70
|
updateEmbedSettings: vi.fn(async () => ({ data: {} })),
|
|
70
71
|
fetchNotificationSettings: () => fetchNotificationSettings(),
|
|
@@ -79,6 +80,8 @@ beforeEach(() => {
|
|
|
79
80
|
fetchForms.mockClear()
|
|
80
81
|
fetchFormStats.mockClear()
|
|
81
82
|
setNotifyEnabled.mockClear()
|
|
83
|
+
fetchFormSchema.mockClear()
|
|
84
|
+
saveFormSchema.mockClear()
|
|
82
85
|
})
|
|
83
86
|
|
|
84
87
|
describe('Forms list', () => {
|
|
@@ -128,14 +131,39 @@ describe('Forms list', () => {
|
|
|
128
131
|
await waitFor(() => expect(setNotifyEnabled).toHaveBeenCalledWith('f1', false))
|
|
129
132
|
})
|
|
130
133
|
|
|
131
|
-
it('opens the schema
|
|
134
|
+
it('opens the inline schema pane from the Schema action and saves through the service', async () => {
|
|
132
135
|
render(<Forms onNavigate={() => {}} />)
|
|
133
136
|
await screen.findByText('Contact Us')
|
|
134
137
|
|
|
135
|
-
|
|
138
|
+
const schemaButton = screen.getAllByRole('button', { name: 'Schema' })[0]!
|
|
139
|
+
expect(schemaButton.getAttribute('aria-expanded')).toBe('false')
|
|
140
|
+
fireEvent.click(schemaButton)
|
|
136
141
|
|
|
137
142
|
await waitFor(() => expect(fetchFormSchema).toHaveBeenCalled())
|
|
138
|
-
//
|
|
143
|
+
// Pane renders as a labelled inline region (not an overlay dialog).
|
|
144
|
+
expect(screen.getByRole('region', { name: 'Form schema' })).toBeTruthy()
|
|
145
|
+
expect(schemaButton.getAttribute('aria-expanded')).toBe('true')
|
|
139
146
|
expect(await screen.findByRole('tab', { name: 'Fields' })).toBeTruthy()
|
|
147
|
+
|
|
148
|
+
// Saving flows through the same saveFormSchema service call.
|
|
149
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Save Schema' }))
|
|
150
|
+
await waitFor(() =>
|
|
151
|
+
expect(saveFormSchema).toHaveBeenCalledWith(
|
|
152
|
+
'f1',
|
|
153
|
+
expect.arrayContaining([expect.objectContaining({ key: 'name' })]),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('clicking Schema again toggles the pane closed', async () => {
|
|
159
|
+
render(<Forms onNavigate={() => {}} />)
|
|
160
|
+
await screen.findByText('Contact Us')
|
|
161
|
+
|
|
162
|
+
const schemaButton = screen.getAllByRole('button', { name: 'Schema' })[0]!
|
|
163
|
+
fireEvent.click(schemaButton)
|
|
164
|
+
await waitFor(() => expect(screen.getByRole('region', { name: 'Form schema' })).toBeTruthy())
|
|
165
|
+
|
|
166
|
+
fireEvent.click(schemaButton)
|
|
167
|
+
await waitFor(() => expect(screen.queryByRole('region', { name: 'Form schema' })).toBeNull())
|
|
140
168
|
})
|
|
141
169
|
})
|