@actuate-media/cms-admin 0.75.0 → 0.76.1
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 +34 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.js +46 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +2 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
- package/dist/__tests__/router/navigation-blocker.test.d.ts +2 -0
- package/dist/__tests__/router/navigation-blocker.test.d.ts.map +1 -0
- package/dist/__tests__/router/navigation-blocker.test.js +69 -0
- package/dist/__tests__/router/navigation-blocker.test.js.map +1 -0
- package/dist/__tests__/views/article-body-section.render.test.d.ts +2 -0
- package/dist/__tests__/views/article-body-section.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/article-body-section.render.test.js +24 -0
- package/dist/__tests__/views/article-body-section.render.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +12 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/post-fields-modal.render.test.js +2 -0
- package/dist/__tests__/views/post-fields-modal.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +17 -0
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +2 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +4 -0
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/post-editor-service.d.ts +3 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +4 -0
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/router/index.d.ts +13 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +49 -0
- package/dist/router/index.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +4 -9
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +3 -0
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +7 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +35 -4
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +38 -13
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.js +1 -1
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +38 -12
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +3 -10
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/layout/sidebar-plugin-nav.test.ts +50 -0
- package/src/__tests__/lib/post-editor-service.test.ts +2 -0
- package/src/__tests__/router/navigation-blocker.test.ts +80 -0
- package/src/__tests__/views/article-body-section.render.test.tsx +32 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +24 -0
- package/src/__tests__/views/post-fields-modal.render.test.tsx +2 -0
- package/src/layout/Sidebar.tsx +19 -0
- package/src/lib/page-editor-service.ts +8 -0
- package/src/lib/post-editor-service.ts +8 -0
- package/src/router/index.ts +61 -0
- package/src/styles/theme.css +91 -0
- package/src/views/DocumentEdit.tsx +4 -8
- package/src/views/page-builder/PageBuilder.tsx +4 -0
- package/src/views/page-editor/EditorTopBar.tsx +69 -0
- package/src/views/page-editor/PageSectionEditor.tsx +58 -10
- package/src/views/page-editor/sections/ArticleBodySection.tsx +1 -4
- package/src/views/post-editor/PostSectionEditor.tsx +58 -10
- package/src/views/post-editor/PostTemplateEditor.tsx +3 -9
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
4
4
|
import { AlertTriangle, ArrowLeft, Layout, Loader2, Save } from 'lucide-react';
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js';
|
|
7
|
+
import { useNavigationBlocker } from '../../router/index.js';
|
|
7
8
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js';
|
|
8
9
|
import { addSection, createSection, duplicateSection, fetchTemplateForEditor, reorderSections, removeSection, saveTemplate, toggleSectionVisibility, updateSectionContent, updateSectionMeta, updateSectionSettings, validateTemplate, } from '../../lib/post-editor-service.js';
|
|
9
10
|
import { DEFAULT_VIEWPORT } from '../page-editor/viewports.js';
|
|
@@ -64,16 +65,8 @@ export function PostTemplateEditor({ postType, config, session, onNavigate, }) {
|
|
|
64
65
|
cancelled = true;
|
|
65
66
|
};
|
|
66
67
|
}, [postType]);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
function onBeforeUnload(e) {
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
e.returnValue = '';
|
|
73
|
-
}
|
|
74
|
-
window.addEventListener('beforeunload', onBeforeUnload);
|
|
75
|
-
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
|
76
|
-
}, [dirty]);
|
|
68
|
+
// Unsaved-changes guard (client-side navigation + browser unload).
|
|
69
|
+
useNavigationBlocker(dirty);
|
|
77
70
|
const mutateSections = useCallback((fn) => {
|
|
78
71
|
setTemplate((prev) => (prev ? { ...prev, sections: fn(prev.sections) } : prev));
|
|
79
72
|
setDirty(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostTemplateEditor.js","sourceRoot":"","sources":["../../../src/views/post-editor/PostTemplateEditor.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"PostTemplateEditor.js","sourceRoot":"","sources":["../../../src/views/post-editor/PostTemplateEditor.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACjE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAC9E,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAA;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAA;AACjE,OAAO,EACL,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,uBAAuB,EACvB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,GAGjB,MAAM,kCAAkC,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAmB,MAAM,6BAA6B,CAAA;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAStD,SAAS,aAAa,CAAC,OAAY;IACjC,MAAM,IAAI,GAAuB,OAAO,EAAE,IAAI,EAAE,IAAI,CAAA;IACpD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC3C,CAAC;AAED,MAAM,cAAc,GAAsB;IACxC,KAAK,EAAE,mBAAmB;IAC1B,OAAO,EAAE,iEAAiE;IAC1E,IAAI,EAAE,2IAA2I;IACjJ,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;IACrC,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,6CAA6C,EAAE;CACpF,CAAA;AAED,MAAM,UAAU,kBAAkB,CAAC,EACjC,QAAQ,EACR,MAAM,EACN,OAAO,EACP,UAAU,GACc;IACxB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAA4B,IAAI,CAAC,CAAA;IACzE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAE/D,mEAAmE;IACnE,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAEtC,IAAI,CAAC,CAAA;IACP,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAa,gBAAgB,CAAC,CAAA;IACtE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACzC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE3C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC7C,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACzE,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAEnE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAChE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE;QAC7B,MAAM,GAAG,GAAG,MAAM,EAAE,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAA;QAC3C,OAAO,GAAG,EAAE,MAAM,EAAE,QAAQ,IAAI,QAAQ,CAAA;IAC1C,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEtB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,sBAAsB,CAAC,QAAQ,CAAC;aAC7B,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACV,IAAI,CAAC,SAAS;gBAAE,WAAW,CAAC,CAAC,CAAC,CAAA;QAChC,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,IAAI,CAAC,SAAS;gBAAE,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAA;QAC9F,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,CAAC,SAAS;gBAAE,UAAU,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC,CAAC,CAAA;QACJ,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,mEAAmE;IACnE,oBAAoB,CAAC,KAAK,CAAC,CAAA;IAE3B,MAAM,cAAc,GAAG,WAAW,CAChC,CAAC,EAAgF,EAAE,EAAE;QACnF,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QAC/E,QAAQ,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC,EACD,EAAE,CACH,CAAA;IAED,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,KAAgC,EAAE,EAAE;QAC1E,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QACxF,QAAQ,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,IAAmB,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;QACnC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAA;QAC7C,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAA;QAChD,UAAU,CAAC,KAAK,CAAC,CAAA;IACnB,CAAC,EACD,CAAC,cAAc,CAAC,CACjB,CAAA;IAED,MAAM,iBAAiB,GAAG,QAAQ,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IAE3E,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACxC,IAAI,CAAC,QAAQ;YAAE,OAAM;QACrB,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAA;QACzC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAA;YAC9B,OAAM;QACR,CAAC;QACD,SAAS,CAAC,IAAI,CAAC,CAAA;QACf,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAA;YAC1C,WAAW,CAAC,KAAK,CAAC,CAAA;YAClB,QAAQ,CAAC,KAAK,CAAC,CAAA;YACf,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;QACjC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,KAAK,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAA;QAC7E,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,CAAC,CAAA;QAClB,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,MAAM,eAAe,GAAG,WAAW,CACjC,CAAC,IAAY,EAAE,EAAE;QACf,IAAI,KAAK;YAAE,cAAc,CAAC,IAAI,CAAC,CAAA;;YAC1B,UAAU,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC,EACD,CAAC,KAAK,EAAE,UAAU,CAAC,CACpB,CAAA;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,eAAK,SAAS,EAAC,uDAAuD,aACpE,KAAC,OAAO,IAAC,SAAS,EAAC,4CAA4C,wBAAe,EAC9E,eAAM,SAAS,EAAC,SAAS,8CAAgC,IACrD,CACP,CAAA;IACH,CAAC;IAED,IAAI,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC3B,OAAO,CACL,cAAK,SAAS,EAAC,2DAA2D,YACxE,eAAK,SAAS,EAAC,+FAA+F,aAC5G,KAAC,aAAa,IAAC,SAAS,EAAC,2BAA2B,EAAC,IAAI,EAAE,EAAE,wBAAgB,EAC7E,eAAK,SAAS,EAAC,SAAS,aACtB,YAAG,SAAS,EAAC,qCAAqC,wCAA4B,EAC9E,YAAG,SAAS,EAAC,oCAAoC,YAAE,SAAS,IAAI,YAAY,GAAK,EACjF,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EACzC,SAAS,EAAC,uDAAuD,mCAG1D,IACL,IACF,GACF,CACP,CAAA;IACH,CAAC;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,iBAAiB,CAAC,IAAI,IAAI,CAAA;IAEzF,OAAO,CACL,MAAC,aAAa,eACZ,eAAK,SAAS,EAAC,oDAAoD,aAEjE,kBAAQ,SAAS,EAAC,2FAA2F,aAC3G,6BAAgB,YAAY,EAAC,SAAS,EAAC,2CAA2C,aAChF,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC,EAC9C,SAAS,EAAC,uFAAuF,aAEjG,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,wBAAe,kBAEtC,EACT,eAAM,SAAS,EAAC,uBAAuB,uCAEhC,EACP,gBAAM,SAAS,EAAC,sCAAsC,aAAE,SAAS,iBAAiB,EACjF,KAAK,IAAI,CACR,eAAM,SAAS,EAAC,oCAAoC,eAAW,QAAQ,gCAEhE,CACR,IACG,EAEN,eAAK,SAAS,EAAC,yBAAyB,aACtC,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAC9C,SAAS,EAAE,wGACT,QAAQ,EAAE,IAAI,KAAK,QAAQ;4CACzB,CAAC,CAAC,0CAA0C;4CAC5C,CAAC,CAAC,+CACN,EAAE,aAEF,KAAC,MAAM,IAAC,SAAS,EAAC,SAAS,wBAAe,cAEnC,EACT,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,OAAO,IAAI,MAAM,IAAI,CAAC,KAAK,EACtC,SAAS,EAAC,wMAAwM,aAEjN,MAAM,CAAC,CAAC,CAAC,CACR,KAAC,OAAO,IAAC,SAAS,EAAC,0BAA0B,wBAAe,CAC7D,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,SAAS,EAAC,aAAa,wBAAe,CAC7C,YAEM,IACL,IACC,EAET,eAAK,SAAS,EAAC,sCAAsC,aACnD,KAAC,aAAa,IACZ,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAC3B,UAAU,EAAE,iBAAiB,EAC7B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EACtD,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAClE,kBAAkB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,uBAAuB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EACjF,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EACnE,eAAe,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC,EAC9C,KAAK,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAC7B,EAEF,cAAK,SAAS,EAAC,wBAAwB,YACrC,KAAC,gBAAgB,IACf,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM,EACvB,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,iBAAiB,EAC7B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EACtD,YAAY,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GACpC,GACE,EAEL,QAAQ,EAAE,IAAI,KAAK,QAAQ,IAAI,CAC9B,KAAC,eAAe,IACd,MAAM,EAAE,QAAQ,CAAC,MAAM,EACvB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAChC,QAAQ,EAAE,kBAAkB,GAC5B,CACH,EAEA,eAAe,IAAI,CAClB,KAAC,gBAAgB,IACf,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAChC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CACzB,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,EAAE,eAAe,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,EAE3E,gBAAgB,EAAE,CAAC,KAA+B,EAAE,EAAE,CACpD,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,qBAAqB,CAAC,CAAC,EAAE,eAAe,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,EAE5E,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CACtB,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,eAAe,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,EAExE,kBAAkB,EAAE,GAAG,EAAE,CACvB,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,uBAAuB,CAAC,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,GAEvE,CACH,IACG,IACF,EAEN,KAAC,eAAe,IACd,IAAI,EAAE,OAAO,EACb,KAAK,EAAC,MAAM,EACZ,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAChC,KAAK,EAAE,SAAS,GAChB,EAEF,KAAC,aAAa,IACZ,IAAI,EAAE,cAAc,KAAK,IAAI,EAC7B,OAAO,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,EACtC,SAAS,EAAE,GAAG,EAAE;oBACd,IAAI,cAAc,EAAE,CAAC;wBACnB,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAA;wBACvD,IAAI,iBAAiB,KAAK,cAAc;4BAAE,WAAW,CAAC,IAAI,CAAC,CAAA;oBAC7D,CAAC;oBACD,iBAAiB,CAAC,IAAI,CAAC,CAAA;gBACzB,CAAC,EACD,KAAK,EAAC,iBAAiB,EACvB,WAAW,EAAC,qFAAqF,EACjG,YAAY,EAAC,QAAQ,EACrB,WAAW,SACX,EAEF,KAAC,aAAa,IACZ,IAAI,EAAE,WAAW,KAAK,IAAI,EAC1B,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,EACnC,SAAS,EAAE,GAAG,EAAE;oBACd,MAAM,MAAM,GAAG,WAAW,CAAA;oBAC1B,cAAc,CAAC,IAAI,CAAC,CAAA;oBACpB,QAAQ,CAAC,KAAK,CAAC,CAAA;oBACf,IAAI,MAAM;wBAAE,UAAU,CAAC,MAAM,CAAC,CAAA;gBAChC,CAAC,EACD,KAAK,EAAC,0BAA0B,EAChC,WAAW,EAAC,8DAA8D,EAC1E,YAAY,EAAC,OAAO,EACpB,WAAW,EAAC,MAAM,EAClB,WAAW,SACX,IACY,CACjB,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cms-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.1",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"repository": {
|
|
5
6
|
"type": "git",
|
|
6
7
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -95,8 +96,8 @@
|
|
|
95
96
|
"tailwindcss": "^4.0.0",
|
|
96
97
|
"typescript": "^5.7.0",
|
|
97
98
|
"vitest": "^4.1.0",
|
|
98
|
-
"@actuate-media/cms-core": "0.
|
|
99
|
-
"@actuate-media/component-blocks": "0.2.
|
|
99
|
+
"@actuate-media/cms-core": "0.91.0",
|
|
100
|
+
"@actuate-media/component-blocks": "0.2.2"
|
|
100
101
|
},
|
|
101
102
|
"scripts": {
|
|
102
103
|
"build": "tsc --project tsconfig.json && npx @tailwindcss/cli -i src/styles/build-input.css -o dist/actuate-admin.css --minify",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildNavItems } from '../../layout/Sidebar.js'
|
|
3
|
+
|
|
4
|
+
// Plugin adminNavItems (collected onto `_pluginNavItems` by cms-core's
|
|
5
|
+
// applyPlugins) must surface in the admin sidebar, without doubling a built-in
|
|
6
|
+
// destination or one of the plugin's own collections.
|
|
7
|
+
const baseCollections = {
|
|
8
|
+
collections: {
|
|
9
|
+
pages: { slug: 'pages', labels: { singular: 'Page', plural: 'Pages' }, type: 'page' },
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('buildNavItems — plugin nav items', () => {
|
|
14
|
+
it('adds a plugin-contributed custom nav link', () => {
|
|
15
|
+
const items = buildNavItems({
|
|
16
|
+
...baseCollections,
|
|
17
|
+
_pluginNavItems: [{ label: 'Redirects', icon: 'arrow-right-left', path: '/redirects' }],
|
|
18
|
+
})
|
|
19
|
+
const redirects = items.find((i) => i.path === '/redirects')
|
|
20
|
+
expect(redirects).toBeDefined()
|
|
21
|
+
expect(redirects?.label).toBe('Redirects')
|
|
22
|
+
expect(redirects?.group).toBe('Content')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('does not double a built-in destination path', () => {
|
|
26
|
+
const items = buildNavItems({
|
|
27
|
+
...baseCollections,
|
|
28
|
+
_pluginNavItems: [{ label: 'Media Library', icon: 'image', path: '/media' }],
|
|
29
|
+
})
|
|
30
|
+
expect(items.filter((i) => i.path === '/media')).toHaveLength(1)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('falls back to a default icon for an unknown icon string and still renders', () => {
|
|
34
|
+
const items = buildNavItems({
|
|
35
|
+
...baseCollections,
|
|
36
|
+
_pluginNavItems: [{ label: 'Custom', icon: 'no-such-icon', path: '/custom' }],
|
|
37
|
+
})
|
|
38
|
+
const custom = items.find((i) => i.path === '/custom')
|
|
39
|
+
expect(custom).toBeDefined()
|
|
40
|
+
expect(custom?.icon).toBeTruthy()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('ignores malformed plugin nav entries', () => {
|
|
44
|
+
const items = buildNavItems({
|
|
45
|
+
...baseCollections,
|
|
46
|
+
_pluginNavItems: [{ label: 'No path' }, null, { path: '/ok', label: 'Ok', icon: 'menu' }],
|
|
47
|
+
})
|
|
48
|
+
expect(items.find((i) => i.path === '/ok')).toBeDefined()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { act, renderHook } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
hasBlockingNavigation,
|
|
7
|
+
NAVIGATION_CONFIRM_MESSAGE,
|
|
8
|
+
registerNavigationBlocker,
|
|
9
|
+
useAdminRouter,
|
|
10
|
+
useNavigationBlocker,
|
|
11
|
+
} from '../../router/index.js'
|
|
12
|
+
|
|
13
|
+
// Guards against silent data loss: an in-app route change (Cancel, sidebar /
|
|
14
|
+
// breadcrumb links, command palette) must confirm before discarding an editor's
|
|
15
|
+
// unsaved changes — beforeunload only covers hard reloads, never SPA navigation.
|
|
16
|
+
describe('navigation blocker', () => {
|
|
17
|
+
let confirmMock: ReturnType<typeof vi.fn>
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset the URL so each test's router mounts at '/' (pushState from a prior
|
|
21
|
+
// test would otherwise leak into window.location and skew currentPath).
|
|
22
|
+
window.history.replaceState(null, '', '/admin')
|
|
23
|
+
confirmMock = vi.fn(() => true)
|
|
24
|
+
;(window as unknown as { confirm: typeof window.confirm }).confirm =
|
|
25
|
+
confirmMock as unknown as typeof window.confirm
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
// Ensure no blocker leaks into the next test.
|
|
30
|
+
expect(hasBlockingNavigation()).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('navigates freely and never prompts when nothing is dirty', () => {
|
|
34
|
+
const { result } = renderHook(() => useAdminRouter('/admin', '/'))
|
|
35
|
+
act(() => result.current.navigate('/collections/posts'))
|
|
36
|
+
expect(result.current.currentPath).toBe('/collections/posts')
|
|
37
|
+
expect(confirmMock).not.toHaveBeenCalled()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('aborts the navigation when a dirty editor is registered and the user cancels', () => {
|
|
41
|
+
confirmMock.mockReturnValue(false)
|
|
42
|
+
const unregister = registerNavigationBlocker(() => true)
|
|
43
|
+
try {
|
|
44
|
+
const { result } = renderHook(() => useAdminRouter('/admin', '/'))
|
|
45
|
+
act(() => result.current.navigate('/collections/posts'))
|
|
46
|
+
expect(confirmMock).toHaveBeenCalledWith(NAVIGATION_CONFIRM_MESSAGE)
|
|
47
|
+
expect(result.current.currentPath).toBe('/')
|
|
48
|
+
} finally {
|
|
49
|
+
unregister()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('proceeds with the navigation when the user confirms leaving', () => {
|
|
54
|
+
confirmMock.mockReturnValue(true)
|
|
55
|
+
const unregister = registerNavigationBlocker(() => true)
|
|
56
|
+
try {
|
|
57
|
+
const { result } = renderHook(() => useAdminRouter('/admin', '/'))
|
|
58
|
+
act(() => result.current.navigate('/settings'))
|
|
59
|
+
expect(confirmMock).toHaveBeenCalledOnce()
|
|
60
|
+
expect(result.current.currentPath).toBe('/settings')
|
|
61
|
+
} finally {
|
|
62
|
+
unregister()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('useNavigationBlocker only blocks while dirty and cleans up on unmount', () => {
|
|
67
|
+
const { rerender, unmount } = renderHook(({ dirty }) => useNavigationBlocker(dirty), {
|
|
68
|
+
initialProps: { dirty: false },
|
|
69
|
+
})
|
|
70
|
+
expect(hasBlockingNavigation()).toBe(false)
|
|
71
|
+
rerender({ dirty: true })
|
|
72
|
+
expect(hasBlockingNavigation()).toBe(true)
|
|
73
|
+
rerender({ dirty: false })
|
|
74
|
+
expect(hasBlockingNavigation()).toBe(false)
|
|
75
|
+
rerender({ dirty: true })
|
|
76
|
+
expect(hasBlockingNavigation()).toBe(true)
|
|
77
|
+
unmount()
|
|
78
|
+
expect(hasBlockingNavigation()).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { ArticleBodySection } from '../../views/page-editor/sections/ArticleBodySection.js'
|
|
6
|
+
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
7
|
+
|
|
8
|
+
function section(content: Record<string, unknown> = {}): PageSection {
|
|
9
|
+
return {
|
|
10
|
+
id: 's1',
|
|
11
|
+
sectionType: 'article-body',
|
|
12
|
+
name: 'Article Body',
|
|
13
|
+
visible: true,
|
|
14
|
+
content,
|
|
15
|
+
settings: {},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ArticleBodySection', () => {
|
|
20
|
+
it('renders post body HTML with rich-text preview class', () => {
|
|
21
|
+
const { container } = render(
|
|
22
|
+
<ArticleBodySection
|
|
23
|
+
section={section({ source: 'field' })}
|
|
24
|
+
context={{ body: '<h2>What is Lorem Ipsum?</h2><p>Body copy.</p>' }}
|
|
25
|
+
/>,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(screen.getByRole('heading', { level: 2, name: 'What is Lorem Ipsum?' })).toBeTruthy()
|
|
29
|
+
expect(container.querySelector('.actuate-rich-text')).toBeTruthy()
|
|
30
|
+
expect(container.querySelector('.prose')).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -62,4 +62,28 @@ describe('EditorTopBar publish gating', () => {
|
|
|
62
62
|
fireEvent.click(screen.getByRole('button', { name: 'Publish' }))
|
|
63
63
|
expect(onPublish).toHaveBeenCalledOnce()
|
|
64
64
|
})
|
|
65
|
+
|
|
66
|
+
it('shows Schedule when onSchedule is provided and calls it when ready', () => {
|
|
67
|
+
const onSchedule = vi.fn()
|
|
68
|
+
render(
|
|
69
|
+
<EditorTopBar
|
|
70
|
+
{...baseProps}
|
|
71
|
+
publishReady
|
|
72
|
+
scheduleReady
|
|
73
|
+
onSchedule={onSchedule}
|
|
74
|
+
scheduledAt="2026-07-11T16:00:00.000Z"
|
|
75
|
+
/>,
|
|
76
|
+
)
|
|
77
|
+
const btn = screen.getByRole('button', { name: 'Reschedule publish' })
|
|
78
|
+
expect((btn as HTMLButtonElement).disabled).toBe(false)
|
|
79
|
+
fireEvent.click(btn)
|
|
80
|
+
expect(onSchedule).toHaveBeenCalledOnce()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('disables Schedule when scheduleReady is false', () => {
|
|
84
|
+
render(<EditorTopBar {...baseProps} publishReady scheduleReady={false} onSchedule={vi.fn()} />)
|
|
85
|
+
expect(
|
|
86
|
+
(screen.getByRole('button', { name: 'Schedule publish' }) as HTMLButtonElement).disabled,
|
|
87
|
+
).toBe(true)
|
|
88
|
+
})
|
|
65
89
|
})
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -876,6 +876,25 @@ export function buildNavItems(config: any): NavItem[] {
|
|
|
876
876
|
}
|
|
877
877
|
for (const bucket of groupBuckets.values()) items.push(...bucket)
|
|
878
878
|
|
|
879
|
+
// Plugin-contributed custom nav links (PluginDefinition.adminNavItems,
|
|
880
|
+
// collected onto `_pluginNavItems` by cms-core's applyPlugins). Deduped by
|
|
881
|
+
// path against everything already present so a plugin can't double a built-in
|
|
882
|
+
// destination or one of its own collections.
|
|
883
|
+
const pluginNav: Array<{ label?: string; icon?: string; path?: string }> = Array.isArray(
|
|
884
|
+
config?._pluginNavItems,
|
|
885
|
+
)
|
|
886
|
+
? config._pluginNavItems
|
|
887
|
+
: []
|
|
888
|
+
for (const item of pluginNav) {
|
|
889
|
+
if (!item?.path || items.some((n) => n.path === item.path)) continue
|
|
890
|
+
items.push({
|
|
891
|
+
label: item.label ?? item.path,
|
|
892
|
+
path: item.path,
|
|
893
|
+
icon: (item.icon && ICON_MAP[item.icon]) || FileText,
|
|
894
|
+
group: 'Content',
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
|
|
879
898
|
// Settings destinations — always last. Users + API Keys now live inside
|
|
880
899
|
// the Settings view as tabs (see Settings.tsx), so only Settings is anchored
|
|
881
900
|
// here.
|
|
@@ -75,6 +75,8 @@ export interface EditorPage {
|
|
|
75
75
|
seoDescription: string
|
|
76
76
|
publishedAt: string | null
|
|
77
77
|
updatedAt: string | null
|
|
78
|
+
scheduledAt: string | null
|
|
79
|
+
scheduledUnpublishAt: string | null
|
|
78
80
|
/**
|
|
79
81
|
* True when the document was authored in the visual Page Builder (it carries
|
|
80
82
|
* a `_layout` tree). Such a page's `sections` are *compiled* from the layout
|
|
@@ -176,6 +178,8 @@ interface DocumentApiRow {
|
|
|
176
178
|
slug: string | null
|
|
177
179
|
status: PageStatus
|
|
178
180
|
publishedAt: string | null
|
|
181
|
+
scheduledAt?: string | null
|
|
182
|
+
scheduledUnpublishAt?: string | null
|
|
179
183
|
updatedAt: string | null
|
|
180
184
|
data: Record<string, unknown> | null
|
|
181
185
|
/** The page-builder tree, lifted out of `data` onto the wire envelope by the API. */
|
|
@@ -207,6 +211,8 @@ function rowToEditorPage(doc: DocumentApiRow): EditorPage {
|
|
|
207
211
|
seoDescription: typeof data.metaDescription === 'string' ? data.metaDescription : '',
|
|
208
212
|
publishedAt: doc.publishedAt ?? null,
|
|
209
213
|
updatedAt: doc.updatedAt ?? null,
|
|
214
|
+
scheduledAt: doc.scheduledAt ?? null,
|
|
215
|
+
scheduledUnpublishAt: doc.scheduledUnpublishAt ?? null,
|
|
210
216
|
builderOwned,
|
|
211
217
|
}
|
|
212
218
|
}
|
|
@@ -302,6 +308,8 @@ export function emptyPage(parentPageId: string | null = null): EditorPage {
|
|
|
302
308
|
seoDescription: '',
|
|
303
309
|
publishedAt: null,
|
|
304
310
|
updatedAt: null,
|
|
311
|
+
scheduledAt: null,
|
|
312
|
+
scheduledUnpublishAt: null,
|
|
305
313
|
builderOwned: false,
|
|
306
314
|
}
|
|
307
315
|
}
|
|
@@ -78,6 +78,9 @@ export interface EditorPost {
|
|
|
78
78
|
tags: string[]
|
|
79
79
|
status: PostStatus
|
|
80
80
|
publishDate: string | null
|
|
81
|
+
/** When set and status is SCHEDULED, the cron publishes at this time. */
|
|
82
|
+
scheduledAt: string | null
|
|
83
|
+
scheduledUnpublishAt: string | null
|
|
81
84
|
sections: PageSection[]
|
|
82
85
|
seoTitle: string
|
|
83
86
|
seoDescription: string
|
|
@@ -189,6 +192,7 @@ interface DocumentApiRow {
|
|
|
189
192
|
status: PostStatus
|
|
190
193
|
publishedAt: string | null
|
|
191
194
|
scheduledAt?: string | null
|
|
195
|
+
scheduledUnpublishAt?: string | null
|
|
192
196
|
updatedAt: string | null
|
|
193
197
|
data: Record<string, unknown> | null
|
|
194
198
|
}
|
|
@@ -235,6 +239,8 @@ function rowToEditorPost(postType: string, doc: DocumentApiRow): EditorPost {
|
|
|
235
239
|
tags: parseTags(data.tags),
|
|
236
240
|
status: doc.status ?? 'DRAFT',
|
|
237
241
|
publishDate: (data.publishDate as string) ?? doc.scheduledAt ?? null,
|
|
242
|
+
scheduledAt: doc.scheduledAt ?? null,
|
|
243
|
+
scheduledUnpublishAt: doc.scheduledUnpublishAt ?? null,
|
|
238
244
|
// Preserve unknown (externally-managed) sections so the editor can show
|
|
239
245
|
// them read-only and round-trip them on save instead of dropping them.
|
|
240
246
|
sections: coerceSections(data.sections, { onUnknown: 'preserve' }),
|
|
@@ -417,6 +423,8 @@ export async function emptyPostFromTemplate(postType: string): Promise<EditorPos
|
|
|
417
423
|
tags: [],
|
|
418
424
|
status: 'DRAFT',
|
|
419
425
|
publishDate: null,
|
|
426
|
+
scheduledAt: null,
|
|
427
|
+
scheduledUnpublishAt: null,
|
|
420
428
|
sections,
|
|
421
429
|
seoTitle: '',
|
|
422
430
|
seoDescription: '',
|
package/src/router/index.ts
CHANGED
|
@@ -23,6 +23,61 @@ export function parseRouteQuery(path: string): URLSearchParams {
|
|
|
23
23
|
return new URLSearchParams(path.split('#')[0]!.split('?')[1] ?? '')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Confirmation shown when navigating away from an editor with unsaved changes. */
|
|
27
|
+
export const NAVIGATION_CONFIRM_MESSAGE =
|
|
28
|
+
'You have unsaved changes that will be lost. Leave this page anyway?'
|
|
29
|
+
|
|
30
|
+
// Module-level registry of "is there unsaved work?" predicates. Editors register
|
|
31
|
+
// via useNavigationBlocker; the SPA router's navigate() consults them before an
|
|
32
|
+
// in-app route change so Cancel / sidebar / breadcrumb / command-palette links
|
|
33
|
+
// can't silently discard an editor's unsaved changes (beforeunload only covers
|
|
34
|
+
// hard reloads, never client-side navigation).
|
|
35
|
+
type BlockerPredicate = () => boolean
|
|
36
|
+
const navigationBlockers = new Set<BlockerPredicate>()
|
|
37
|
+
|
|
38
|
+
export function registerNavigationBlocker(predicate: BlockerPredicate): () => void {
|
|
39
|
+
navigationBlockers.add(predicate)
|
|
40
|
+
return () => {
|
|
41
|
+
navigationBlockers.delete(predicate)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True when any registered editor currently has unsaved changes. */
|
|
46
|
+
export function hasBlockingNavigation(): boolean {
|
|
47
|
+
for (const predicate of navigationBlockers) {
|
|
48
|
+
try {
|
|
49
|
+
if (predicate()) return true
|
|
50
|
+
} catch {
|
|
51
|
+
// A misbehaving predicate must not wedge navigation.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register an unsaved-changes guard for an editor. While `isDirty` is true, an
|
|
59
|
+
* in-app navigation prompts for confirmation (via navigate()) and a browser
|
|
60
|
+
* unload/refresh shows the native "leave site?" prompt. The registration and
|
|
61
|
+
* the beforeunload listener are torn down on unmount.
|
|
62
|
+
*/
|
|
63
|
+
export function useNavigationBlocker(isDirty: boolean): void {
|
|
64
|
+
const dirtyRef = useRef(isDirty)
|
|
65
|
+
dirtyRef.current = isDirty
|
|
66
|
+
|
|
67
|
+
useEffect(() => registerNavigationBlocker(() => dirtyRef.current), [])
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!isDirty || typeof window === 'undefined') return
|
|
71
|
+
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
// Required by Chrome/Safari to actually trigger the native prompt.
|
|
74
|
+
e.returnValue = ''
|
|
75
|
+
}
|
|
76
|
+
window.addEventListener('beforeunload', onBeforeUnload)
|
|
77
|
+
return () => window.removeEventListener('beforeunload', onBeforeUnload)
|
|
78
|
+
}, [isDirty])
|
|
79
|
+
}
|
|
80
|
+
|
|
26
81
|
export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
27
82
|
const [currentPath, setCurrentPath] = useState(serverPath)
|
|
28
83
|
const baseRef = useRef(basePath)
|
|
@@ -52,6 +107,12 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
52
107
|
|
|
53
108
|
const navigate = useCallback((path: string) => {
|
|
54
109
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
110
|
+
|
|
111
|
+
// Guard against discarding unsaved editor work on client-side navigation.
|
|
112
|
+
if (hasBlockingNavigation() && typeof window !== 'undefined') {
|
|
113
|
+
if (!window.confirm(NAVIGATION_CONFIRM_MESSAGE)) return
|
|
114
|
+
}
|
|
115
|
+
|
|
55
116
|
setCurrentPath(normalizedPath)
|
|
56
117
|
|
|
57
118
|
const fullUrl = normalizedPath === '/' ? baseRef.current : `${baseRef.current}${normalizedPath}`
|
package/src/styles/theme.css
CHANGED
|
@@ -224,6 +224,97 @@
|
|
|
224
224
|
letter-spacing: revert-layer;
|
|
225
225
|
line-height: revert-layer;
|
|
226
226
|
}
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* Rich-text HTML in the canvas preview (article body, etc.). The TipTap
|
|
230
|
+
* editor styles headings in `.ProseMirror`; the canvas cannot rely on
|
|
231
|
+
* `prose` because @tailwindcss/typography is not bundled, and the revert
|
|
232
|
+
* above strips admin heading scales without substituting site typography.
|
|
233
|
+
*/
|
|
234
|
+
.actuate-admin .actuate-canvas .actuate-rich-text {
|
|
235
|
+
color: var(--foreground);
|
|
236
|
+
font-size: 1rem;
|
|
237
|
+
line-height: 1.625;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.actuate-admin .actuate-canvas .actuate-rich-text :is(h1, h2, h3, h4, h5, h6) {
|
|
241
|
+
color: var(--foreground);
|
|
242
|
+
font-weight: 600;
|
|
243
|
+
line-height: 1.5;
|
|
244
|
+
margin-top: 1.25em;
|
|
245
|
+
margin-bottom: 0.5em;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.actuate-admin .actuate-canvas .actuate-rich-text h1 {
|
|
249
|
+
font-size: 2em;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.actuate-admin .actuate-canvas .actuate-rich-text h2 {
|
|
253
|
+
font-size: 1.5em;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.actuate-admin .actuate-canvas .actuate-rich-text h3 {
|
|
257
|
+
font-size: 1.25em;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.actuate-admin .actuate-canvas .actuate-rich-text h4 {
|
|
261
|
+
font-size: 1.125em;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.actuate-admin .actuate-canvas .actuate-rich-text h5 {
|
|
265
|
+
font-size: 1em;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.actuate-admin .actuate-canvas .actuate-rich-text h6 {
|
|
269
|
+
font-size: 0.875em;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.actuate-admin .actuate-canvas .actuate-rich-text p {
|
|
273
|
+
margin-top: 0.75em;
|
|
274
|
+
margin-bottom: 0.75em;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.actuate-admin .actuate-canvas .actuate-rich-text :is(ul, ol) {
|
|
278
|
+
margin-top: 0.75em;
|
|
279
|
+
margin-bottom: 0.75em;
|
|
280
|
+
padding-left: 1.5rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.actuate-admin .actuate-canvas .actuate-rich-text ul {
|
|
284
|
+
list-style-type: disc;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.actuate-admin .actuate-canvas .actuate-rich-text ol {
|
|
288
|
+
list-style-type: decimal;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.actuate-admin .actuate-canvas .actuate-rich-text li {
|
|
292
|
+
margin-top: 0.25em;
|
|
293
|
+
margin-bottom: 0.25em;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.actuate-admin .actuate-canvas .actuate-rich-text a {
|
|
297
|
+
color: var(--primary);
|
|
298
|
+
text-decoration: underline;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.actuate-admin .actuate-canvas .actuate-rich-text :is(strong, b) {
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.actuate-admin .actuate-canvas .actuate-rich-text blockquote {
|
|
306
|
+
border-left: 3px solid var(--border);
|
|
307
|
+
margin: 1em 0;
|
|
308
|
+
padding-left: 1em;
|
|
309
|
+
color: var(--muted-foreground);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.actuate-admin .actuate-canvas .actuate-rich-text img {
|
|
313
|
+
max-width: 100%;
|
|
314
|
+
height: auto;
|
|
315
|
+
border-radius: var(--radius-md);
|
|
316
|
+
margin: 1em 0;
|
|
317
|
+
}
|
|
227
318
|
}
|
|
228
319
|
|
|
229
320
|
.actuate-admin.reduce-motion,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
XCircle,
|
|
15
15
|
} from 'lucide-react'
|
|
16
16
|
import { toast } from 'sonner'
|
|
17
|
+
import { useNavigationBlocker } from '../router/index.js'
|
|
17
18
|
import { FieldRenderer } from '../fields/FieldRenderer.js'
|
|
18
19
|
import { RelationshipField } from '../fields/RelationshipField.js'
|
|
19
20
|
import { Button } from '../components/ui/Button.js'
|
|
@@ -101,14 +102,9 @@ export function DocumentEdit({
|
|
|
101
102
|
}
|
|
102
103
|
}, [documentId, isNew])
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
e.preventDefault()
|
|
108
|
-
}
|
|
109
|
-
window.addEventListener('beforeunload', handler)
|
|
110
|
-
return () => window.removeEventListener('beforeunload', handler)
|
|
111
|
-
}, [isDirty])
|
|
105
|
+
// Warn before discarding unsaved changes on client-side navigation (Cancel,
|
|
106
|
+
// sidebar/breadcrumb links) and on browser unload/refresh.
|
|
107
|
+
useNavigationBlocker(isDirty)
|
|
112
108
|
|
|
113
109
|
const SEO_FIELDS: (keyof SEOData)[] = [
|
|
114
110
|
'metaTitle',
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
|
|
4
4
|
import { Loader2, AlertTriangle } from 'lucide-react'
|
|
5
5
|
import { toast } from 'sonner'
|
|
6
6
|
import { cmsApi } from '../../lib/api.js'
|
|
7
|
+
import { useNavigationBlocker } from '../../router/index.js'
|
|
7
8
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
8
9
|
import {
|
|
9
10
|
useBuilderState,
|
|
@@ -169,6 +170,9 @@ export function PageBuilder({
|
|
|
169
170
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- replaceTree/setPageSettings are stable builder actions
|
|
170
171
|
}, [documentId, templateId, collectionSlug])
|
|
171
172
|
|
|
173
|
+
// Warn before discarding unsaved builder changes on navigation / unload.
|
|
174
|
+
useNavigationBlocker(dirty)
|
|
175
|
+
|
|
172
176
|
const handleSave = useCallback(async () => {
|
|
173
177
|
setSaving(true)
|
|
174
178
|
const body = JSON.stringify({
|