@pramen/cms-editor 0.0.56 → 0.0.57
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/editor.js +114 -114
- package/package.json +1 -1
- package/src/api.ts +17 -4
- package/src/app-context.tsx +43 -8
- package/src/components.tsx +128 -17
- package/src/routes/_layout.tsx +44 -13
- package/src/routes/collection-item.tsx +4 -5
- package/src/routes/collection.tsx +4 -5
- package/src/routes/home.tsx +30 -31
- package/src/routes/page.tsx +23 -15
- package/src/routes/type.tsx +21 -28
- package/src/types.ts +7 -1
package/src/routes/page.tsx
CHANGED
|
@@ -6,14 +6,14 @@ import { createPage, useNavigate } from "@buzola/router";
|
|
|
6
6
|
import { Button } from "@podoba/react";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { useApp } from "../app-context";
|
|
9
|
-
import { PageEditor, errMsg, visibleTabs, type InspectorTab } from "../components";
|
|
9
|
+
import { Notice, PageEditor, errMsg, splitsByType, visibleTabs, type InspectorTab } from "../components";
|
|
10
10
|
import type { BlockType, Page } from "../types";
|
|
11
11
|
|
|
12
12
|
export default createPage()
|
|
13
13
|
.params({ pageId: "string", tab: "?string" })
|
|
14
14
|
.route("/pages/:pageId")
|
|
15
15
|
.render(function PageEditorRoute({ params }) {
|
|
16
|
-
const { api, cms, setError, setNavGuard } = useApp();
|
|
16
|
+
const { api, cms, contentTypes, setError, setNavGuard } = useApp();
|
|
17
17
|
const navigate = useNavigate();
|
|
18
18
|
const [page, setPage] = useState<Page | null>(null);
|
|
19
19
|
const [blockTypes, setBlockTypes] = useState<BlockType[]>([]);
|
|
@@ -22,12 +22,14 @@ export default createPage()
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
let live = true;
|
|
24
24
|
setMissing(false);
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// By id. Resolving it against `listPages` instead meant the editor could not open a row
|
|
26
|
+
// the list had just shown it: that list is capped, and since the editor lists ONE type
|
|
27
|
+
// per tab it isn't even the same list — 150 newer pages of another type were enough to
|
|
28
|
+
// push an article out of the pooled fetch and turn a click into "Page not found."
|
|
29
|
+
api.getPageById(params.pageId)
|
|
30
|
+
.then((found) => {
|
|
28
31
|
if (!live) return;
|
|
29
|
-
|
|
30
|
-
setPage(found);
|
|
32
|
+
setPage(found ?? null);
|
|
31
33
|
setMissing(!found);
|
|
32
34
|
})
|
|
33
35
|
.catch((e) => setError(errMsg(e)));
|
|
@@ -35,6 +37,16 @@ export default createPage()
|
|
|
35
37
|
return () => { live = false; };
|
|
36
38
|
}, [api, params.pageId, setError]);
|
|
37
39
|
|
|
40
|
+
// Back goes to the list this page actually belongs to. `home` redirects to whichever type
|
|
41
|
+
// sorts FIRST BY NAME on a split deployment, so "← all pages" from an article landed the
|
|
42
|
+
// editor in some other type's list — with the `replace` overwriting `/`, so Back could not
|
|
43
|
+
// undo it either. Falls back to `home` when the type is unknown (not loaded, or gone).
|
|
44
|
+
const ownType = (contentTypes ?? []).find((t) => t.id === page?.typeId);
|
|
45
|
+
const backToList = () => {
|
|
46
|
+
if (splitsByType(contentTypes, cms) && ownType) navigate("type", { params: { slug: ownType.slug } });
|
|
47
|
+
else navigate("home");
|
|
48
|
+
};
|
|
49
|
+
|
|
38
50
|
// The visible set comes from the server (`visibleTabs`), so a deep link to `?tab=i18n`
|
|
39
51
|
// on a single-locale deployment falls back to settings — and the URL is REWRITTEN to
|
|
40
52
|
// match. Rendering one tab under another tab's address means a refresh, a Back, or a
|
|
@@ -48,14 +60,10 @@ export default createPage()
|
|
|
48
60
|
}, [params.tab, tab]);
|
|
49
61
|
|
|
50
62
|
if (missing) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<p className="text-fg-subtle">Page not found.</p>
|
|
54
|
-
<Button variant="ghost" size="sm" onPress={() => navigate("home")}>← all pages</Button>
|
|
55
|
-
</div>
|
|
56
|
-
);
|
|
63
|
+
// No page ⇒ no type to go back to; `home` lands on the first list either way.
|
|
64
|
+
return <Notice action={<Button variant="ghost" size="sm" onPress={() => navigate("home")}>← all pages</Button>}>Page not found.</Notice>;
|
|
57
65
|
}
|
|
58
|
-
if (!page) return <
|
|
66
|
+
if (!page) return <Notice>Loading…</Notice>;
|
|
59
67
|
|
|
60
68
|
return (
|
|
61
69
|
<PageEditor
|
|
@@ -64,7 +72,7 @@ export default createPage()
|
|
|
64
72
|
blockTypes={blockTypes}
|
|
65
73
|
tab={tab}
|
|
66
74
|
onTab={setTab}
|
|
67
|
-
onBack={
|
|
75
|
+
onBack={backToList}
|
|
68
76
|
onChange={setPage}
|
|
69
77
|
registerGuard={setNavGuard}
|
|
70
78
|
/>
|
package/src/routes/type.tsx
CHANGED
|
@@ -6,50 +6,43 @@
|
|
|
6
6
|
|
|
7
7
|
import { createPage, useNavigate } from "@buzola/router";
|
|
8
8
|
import { Button } from "@podoba/react";
|
|
9
|
-
import { useEffect, useState } from "react";
|
|
10
9
|
import { useApp } from "../app-context";
|
|
11
|
-
import { PageList,
|
|
12
|
-
import type { BlockType, Page } from "../types";
|
|
10
|
+
import { Notice, PageList, pagesHidden } from "../components";
|
|
13
11
|
|
|
14
12
|
export default createPage()
|
|
15
13
|
.params({ slug: "string" })
|
|
16
14
|
.route("/types/:slug")
|
|
17
15
|
.render(function ContentTypeRoute({ params }) {
|
|
18
|
-
const {
|
|
16
|
+
const { contentTypes, contentTypesFailed, refreshContentTypes, api, setError } = useApp();
|
|
19
17
|
const navigate = useNavigate();
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const type = contentTypes.find((t) => t.slug === params.slug);
|
|
18
|
+
const type = (contentTypes ?? []).find((t) => t.slug === params.slug);
|
|
19
|
+
const back = <Button variant="ghost" size="sm" onPress={() => navigate("home")}>← Back</Button>;
|
|
23
20
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
const refreshPages = () => api.listPages(params.slug).then(setPages).catch((e) => setError(errMsg(e)));
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
refreshPages();
|
|
30
|
-
api.listBlockTypes().then(setBlockTypes).catch((e) => setError(errMsg(e)));
|
|
31
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
-
}, [api, params.slug]);
|
|
21
|
+
// This route IS the block/page builder, so a deployment that hides it hides this too —
|
|
22
|
+
// otherwise the flag is bypassed by typing the URL, or by a stale link.
|
|
23
|
+
if (pagesHidden()) return <Notice action={back}>The page builder is not enabled on this deployment.</Notice>;
|
|
33
24
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{contentTypes.length > 0 ? <Button variant="ghost" size="sm" onPress={() => navigate("home")}>← Back</Button> : null}
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
25
|
+
// `null` = listContentTypes has not answered yet; `failed` = it answered with an error and
|
|
26
|
+
// this screen cannot name its own type. The two used to collapse into an empty array, so a
|
|
27
|
+
// 5xx (or a session outside `editorRoles ∪ reviewerRoles`, which cannot call
|
|
28
|
+
// listContentTypes at all) sat on "Loading…" forever with the way back suppressed.
|
|
29
|
+
if (contentTypesFailed) {
|
|
30
|
+
return <Notice action={<Button variant="ghost" size="sm" onPress={refreshContentTypes}>Retry</Button>}>Couldn't load the content types.</Notice>;
|
|
43
31
|
}
|
|
32
|
+
if (contentTypes === null) return <Notice>Loading…</Notice>;
|
|
33
|
+
// Nothing is fetched for a slug that is not a content type — the list would be empty and
|
|
34
|
+
// the heading unlabelled, and both round trips are wasted.
|
|
35
|
+
if (!type) return <Notice action={back}>Unknown content type: {params.slug}</Notice>;
|
|
44
36
|
|
|
45
37
|
return (
|
|
46
38
|
<PageList
|
|
47
39
|
api={api}
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
// Keyed on the slug so switching tabs REMOUNTS the list: buzola renders the same
|
|
41
|
+
// component instance across a params-only change, and PageList holds the rows,
|
|
42
|
+
// the paging offset and any open create-modal — all of which belong to one type.
|
|
43
|
+
key={type.slug}
|
|
50
44
|
type={type}
|
|
51
45
|
onOpen={(p) => navigate("page", { params: { pageId: p.id } })}
|
|
52
|
-
onCreated={refreshPages}
|
|
53
46
|
onError={setError}
|
|
54
47
|
/>
|
|
55
48
|
);
|
package/src/types.ts
CHANGED
|
@@ -146,12 +146,18 @@ export interface CmsCapabilities {
|
|
|
146
146
|
defaultLocale: string;
|
|
147
147
|
/** More than one declared locale. Gates the whole i18n surface. */
|
|
148
148
|
multilingual: boolean;
|
|
149
|
+
/** The server's `listPages` understands `contentType` — the licence to give each content
|
|
150
|
+
* type its own tab and its own list. An older server ACCEPTS the argument and ignores it
|
|
151
|
+
* (an unknown input key is passed through, not rejected), answering with the pooled list:
|
|
152
|
+
* without this probe the editor renders N tabs that all show every type's pages under a
|
|
153
|
+
* heading claiming otherwise, and "New page" from any of them stamps that tab's type. */
|
|
154
|
+
pagesByType: boolean;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
/** Used until `listCmsCapabilities` answers, and when it cannot (an older server). Assumes
|
|
152
158
|
* MONOLINGUAL: a hidden i18n surface on a multilingual site is recoverable by reloading,
|
|
153
159
|
* where a half-rendered one on a single-locale site is what this replaced. */
|
|
154
|
-
export const DEFAULT_CAPABILITIES: CmsCapabilities = { locales: ["en"], defaultLocale: "en", multilingual: false };
|
|
160
|
+
export const DEFAULT_CAPABILITIES: CmsCapabilities = { locales: ["en"], defaultLocale: "en", multilingual: false, pagesByType: false };
|
|
155
161
|
|
|
156
162
|
/** Mirror of @pramen/cms `CollectionFeature`. */
|
|
157
163
|
export type CollectionFeature = "drafts" | "scheduling" | "revisions" | "preview";
|