@pramen/cms-editor 0.0.39 → 0.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms-editor",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "Visual block/page editor for @pramen/cms — a standalone React SPA that talks to the CMS handlers over HTTP.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,6 +13,10 @@ declare global {
13
13
  /** Runtime config set by the host's /config.js (see @pramen/cms-editor build). */
14
14
  PRAMEN_CMS_EDITOR?: {
15
15
  signInUrl?: string;
16
+ /** Hide the Pages tab for deployments that use collections only — no block/page
17
+ * building. The tab is otherwise always shown and lands on an empty list, which
18
+ * reads as "the CMS is broken" rather than "this site has no pages". */
19
+ hidePages?: boolean;
16
20
  /** Extra top-nav links to companion tools the host serves (e.g. a curation page).
17
21
  * Rendered as plain external `<a>` links after the built-in tabs. */
18
22
  extraNav?: { label: string; href: string }[];
@@ -187,23 +187,45 @@ function cellText(v: unknown): string {
187
187
  return String(v);
188
188
  }
189
189
 
190
+ /** Page size for collection lists. collectionList defaults to 100 server-side, so a
191
+ * request without an explicit limit silently truncates — and the header then reports the
192
+ * truncated count as if it were the total, which reads as "that is all there is". */
193
+ const COLLECTION_PAGE_SIZE = 50;
194
+
190
195
  export function CollectionList({ api, def, onOpen, onNew, onError }: { api: Api; def: CollectionMeta; onOpen: (id: string) => void; onNew: () => void; onError: (s: string) => void }) {
191
196
  const [rows, setRows] = useState<Record<string, unknown>[]>([]);
197
+ const [offset, setOffset] = useState(0);
198
+ const [hasMore, setHasMore] = useState(false);
192
199
  const [loading, setLoading] = useState(true);
200
+
201
+ const load = useCallback(
202
+ (off: number) => {
203
+ setLoading(true);
204
+ return api
205
+ .call<Record<string, unknown>[]>("collectionList", { collection: def.slug, limit: COLLECTION_PAGE_SIZE, offset: off })
206
+ .then((r) => {
207
+ setRows((prev) => (off === 0 ? r : [...prev, ...r]));
208
+ // A full page means there is probably more; a short one is definitely the end.
209
+ setHasMore(r.length === COLLECTION_PAGE_SIZE);
210
+ setOffset(off + r.length);
211
+ })
212
+ .catch((e) => onError(errMsg(e)))
213
+ .finally(() => setLoading(false));
214
+ },
215
+ [api, def.slug, onError],
216
+ );
217
+
193
218
  useEffect(() => {
194
- let live = true;
195
- setLoading(true);
196
- api.call<Record<string, unknown>[]>("collectionList", { collection: def.slug })
197
- .then((r) => { if (live) setRows(r); })
198
- .catch((e) => onError(errMsg(e)))
199
- .finally(() => { if (live) setLoading(false); });
200
- return () => { live = false; };
201
- }, [api, def.slug, onError]);
219
+ setRows([]);
220
+ setOffset(0);
221
+ setHasMore(false);
222
+ void load(0);
223
+ }, [load]);
202
224
 
203
225
  const labelOf = (col: string) => def.fields.find((f) => f.name === col)?.label ?? col;
204
226
  return (
205
227
  <>
206
- <Hero lead={def.pluralLabel} em={rows.length === 0 ? "None yet" : rows.length === 1 ? `1 ${def.label.toLowerCase()}` : `${rows.length} ${def.pluralLabel.toLowerCase()}`}>
228
+ <Hero lead={def.pluralLabel} em={rows.length === 0 ? "None yet" : rows.length === 1 ? `1 ${def.label.toLowerCase()}` : `${rows.length}${hasMore ? "+" : ""} ${def.pluralLabel.toLowerCase()}`}>
207
229
  <Cta text="Let's" em={`add a ${def.label.toLowerCase()}`}>
208
230
  <Button className="shrink-0" onPress={onNew}>+ New {def.label.toLowerCase()}</Button>
209
231
  </Cta>
@@ -225,6 +247,11 @@ export function CollectionList({ api, def, onOpen, onNew, onError }: { api: Api;
225
247
  {!loading && rows.length === 0 ? <p className="text-fg-subtle">No {def.pluralLabel.toLowerCase()} yet. Create one.</p> : null}
226
248
  {loading ? <p className="text-fg-subtle">Loading…</p> : null}
227
249
  </div>
250
+ {hasMore && !loading ? (
251
+ <div className="mt-3.5 text-center">
252
+ <Button variant="secondary" size="sm" onPress={() => void load(offset)}>Load more</Button>
253
+ </div>
254
+ ) : null}
228
255
  </div>
229
256
  </>
230
257
  );
@@ -27,6 +27,8 @@ export default function RootLayout() {
27
27
 
28
28
  // Host-configured links to companion tools (e.g. a curation page), from /config.js.
29
29
  const extraNav = typeof window !== "undefined" ? window.PRAMEN_CMS_EDITOR?.extraNav ?? [] : [];
30
+ // Collections-only deployments hide the block/page builder entirely.
31
+ const hidePages = typeof window !== "undefined" ? window.PRAMEN_CMS_EDITOR?.hidePages === true : false;
30
32
 
31
33
  return (
32
34
  <>
@@ -36,7 +38,9 @@ export default function RootLayout() {
36
38
  </span>
37
39
  <span className="flex-1" />
38
40
  <nav className="flex items-center gap-0.5">
39
- <Button variant="ghost" size="sm" className={tabCls("pages")} onPress={() => navigate("home")}>Pages</Button>
41
+ {hidePages ? null : (
42
+ <Button variant="ghost" size="sm" className={tabCls("pages")} onPress={() => navigate("home")}>Pages</Button>
43
+ )}
40
44
  {collections.map((c) => (
41
45
  <Button key={c.slug} variant="ghost" size="sm" className={tabCls(`col:${c.slug}`)} onPress={() => navigate("collection", { params: { slug: c.slug } })}>
42
46
  {c.icon ? `${c.icon} ` : ""}{c.pluralLabel}
@@ -9,17 +9,31 @@ import type { BlockType, Page } from "../types";
9
9
  export default createPage()
10
10
  .route("/")
11
11
  .render(function Home() {
12
- const { api, setError } = useApp();
12
+ const { api, setError, collections } = useApp();
13
13
  const navigate = useNavigate();
14
14
  const [pages, setPages] = useState<Page[]>([]);
15
15
  const [blockTypes, setBlockTypes] = useState<BlockType[]>([]);
16
16
 
17
+ // Collections-only deployment: `/` is the Pages list, so land on the first
18
+ // collection instead. Without this, hiding the tab still leaves the landing page
19
+ // showing an empty page list — and still fetching pages, which errors outright on a
20
+ // deployment that never spread cmsSchema.
21
+ const hidePages = typeof window !== "undefined" && window.PRAMEN_CMS_EDITOR?.hidePages === true;
22
+ const firstCollection = collections[0]?.slug;
23
+
24
+ useEffect(() => {
25
+ if (hidePages && firstCollection) navigate("collection", { params: { slug: firstCollection }, replace: true });
26
+ }, [hidePages, firstCollection, navigate]);
27
+
17
28
  const refreshPages = () => api.listPages().then(setPages).catch((e) => setError(errMsg(e)));
18
29
  useEffect(() => {
30
+ if (hidePages) return;
19
31
  refreshPages();
20
32
  api.listBlockTypes().then(setBlockTypes).catch((e) => setError(errMsg(e)));
21
33
  // eslint-disable-next-line react-hooks/exhaustive-deps
22
- }, [api]);
34
+ }, [api, hidePages]);
35
+
36
+ if (hidePages && firstCollection) return null;
23
37
 
24
38
  return (
25
39
  <PageList