@cardinali-studio/adminkit 2.2.0 → 2.2.2

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/api.d.ts CHANGED
@@ -69,6 +69,8 @@ export type ContentDocument = {
69
69
  kind: string;
70
70
  locale: string;
71
71
  draftRevisionId: string | null;
72
+ draftPayload: string | null;
73
+ draftTombstone: boolean | null;
72
74
  publishedRevisionId: string | null;
73
75
  publishedPayload: string | null;
74
76
  publishedTombstone: boolean | null;
@@ -10,6 +10,8 @@
10
10
  */
11
11
  import { useCallback, useEffect, useState } from "react";
12
12
  import { ApiError } from "../../index.js";
13
+ import { api } from "../../api.js";
14
+ import { contentPersistenceMode, site } from "../site.js";
13
15
  async function call(url, init) {
14
16
  const res = await fetch(url, { cache: "no-store", ...init });
15
17
  const body = await res.json().catch(() => null);
@@ -24,9 +26,20 @@ export function useHome(active) {
24
26
  const [dirty, setDirty] = useState(false);
25
27
  const [saving, setSaving] = useState(false);
26
28
  const [notice, setNotice] = useState(null);
29
+ const [revision, setRevision] = useState(null);
27
30
  const load = useCallback(async () => {
28
31
  try {
29
- setData((await call("/admin/api/singleton?name=home")).data);
32
+ const mapped = site().homeDocument;
33
+ if (contentPersistenceMode() === "d1" && mapped) {
34
+ const { document } = await api.contentDocument(mapped.documentId);
35
+ const payload = document.draftPayload ?? document.publishedPayload;
36
+ if (!payload || (document.draftPayload !== null ? document.draftTombstone : document.publishedTombstone))
37
+ throw new Error("content_unavailable");
38
+ setData(JSON.parse(payload));
39
+ setRevision({ base: document.draftRevisionId ?? document.publishedRevisionId, expected: document.draftRevisionId });
40
+ }
41
+ else
42
+ setData((await call("/admin/api/singleton?name=home")).data);
30
43
  setError(null);
31
44
  setDirty(false);
32
45
  }
@@ -44,6 +57,15 @@ export function useHome(active) {
44
57
  setSaving(true);
45
58
  setNotice(null);
46
59
  try {
60
+ const mapped = site().homeDocument;
61
+ if (contentPersistenceMode() === "d1" && mapped && revision) {
62
+ const revisionId = crypto.randomUUID();
63
+ await api.saveContent({ revisionId, documentId: mapped.documentId, baseRevisionId: revision.base, expectedDraftRevisionId: revision.expected, kind: mapped.kind, locale: mapped.locale, schemaVersion: mapped.schemaVersion, payload: data, tombstone: false });
64
+ setRevision({ base: revisionId, expected: revisionId });
65
+ setNotice("Bozza D1 salvata");
66
+ setDirty(false);
67
+ return;
68
+ }
47
69
  await call("/admin/api/singleton", {
48
70
  method: "POST",
49
71
  headers: { "Content-Type": "application/json" },
@@ -61,7 +83,7 @@ export function useHome(active) {
61
83
  finally {
62
84
  setSaving(false);
63
85
  }
64
- }, [data]);
86
+ }, [data, revision]);
65
87
  const change = useCallback((next) => { setData(next); setDirty(true); }, []);
66
88
  return { data, error, dirty, saving, notice, load, change, save, setNotice };
67
89
  }
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import { useCallback, useEffect, useState } from "react";
8
8
  import { ApiError } from "../../index.js";
9
+ import { api } from "../../api.js";
10
+ import { contentPersistenceMode, site } from "../site.js";
9
11
  const empty = {
10
12
  siteTitle: "",
11
13
  shortDescription: "",
@@ -34,10 +36,22 @@ export function useSiteSettings(active) {
34
36
  const [dirty, setDirty] = useState(false);
35
37
  const [saving, setSaving] = useState(false);
36
38
  const [notice, setNotice] = useState(null);
39
+ const [revision, setRevision] = useState(null);
37
40
  const load = useCallback(async () => {
38
41
  try {
39
- const result = await call("/admin/api/settings");
40
- setData({ ...empty, ...result.settings, password: "" });
42
+ const mapped = site().siteSettingsDocument;
43
+ if (contentPersistenceMode() === "d1" && mapped) {
44
+ const { document } = await api.contentDocument(mapped.documentId);
45
+ const payload = document.draftPayload ?? document.publishedPayload;
46
+ if (!payload || (document.draftPayload !== null ? document.draftTombstone : document.publishedTombstone))
47
+ throw new Error("content_unavailable");
48
+ setData({ ...empty, ...JSON.parse(payload), password: "" });
49
+ setRevision({ base: document.draftRevisionId ?? document.publishedRevisionId, expected: document.draftRevisionId });
50
+ }
51
+ else {
52
+ const result = await call("/admin/api/settings");
53
+ setData({ ...empty, ...result.settings, password: "" });
54
+ }
41
55
  setError(null);
42
56
  setDirty(false);
43
57
  }
@@ -55,6 +69,15 @@ export function useSiteSettings(active) {
55
69
  setSaving(true);
56
70
  setNotice(null);
57
71
  try {
72
+ const mapped = site().siteSettingsDocument;
73
+ if (contentPersistenceMode() === "d1" && mapped && revision) {
74
+ const revisionId = crypto.randomUUID();
75
+ await api.saveContent({ revisionId, documentId: mapped.documentId, baseRevisionId: revision.base, expectedDraftRevisionId: revision.expected, kind: mapped.kind, locale: mapped.locale, schemaVersion: mapped.schemaVersion, payload: data, tombstone: false });
76
+ setRevision({ base: revisionId, expected: revisionId });
77
+ setDirty(false);
78
+ setNotice("Bozza D1 salvata");
79
+ return;
80
+ }
58
81
  const result = await call("/admin/api/settings", {
59
82
  method: "POST",
60
83
  headers: { "Content-Type": "application/json" },
@@ -73,7 +96,7 @@ export function useSiteSettings(active) {
73
96
  finally {
74
97
  setSaving(false);
75
98
  }
76
- }, [data]);
99
+ }, [data, revision]);
77
100
  const change = useCallback((next) => {
78
101
  setData(next);
79
102
  setDirty(true);
@@ -49,6 +49,19 @@ export interface SiteAdminConfig {
49
49
  modules?: Record<string, boolean>;
50
50
  /** Git remains the compatibility default. D1 is an explicit, later site cutover. */
51
51
  contentPersistence?: "github" | "d1";
52
+ /** Lossless D1 identity for the singleton home; required before its editor may leave Git. */
53
+ homeDocument?: {
54
+ documentId: string;
55
+ kind: string;
56
+ locale: string;
57
+ schemaVersion: number;
58
+ };
59
+ siteSettingsDocument?: {
60
+ documentId: string;
61
+ kind: string;
62
+ locale: string;
63
+ schemaVersion: number;
64
+ };
52
65
  }
53
66
  /** Chiamata da `mountAdmin` prima del primo render. */
54
67
  export declare function setSiteConfig(config: SiteAdminConfig): void;
@@ -86,6 +86,8 @@ export type EffectiveDocument = {
86
86
  locale: string;
87
87
  draftRevisionId: string | null;
88
88
  publishedRevisionId: string | null;
89
+ draftPayload: string | null;
90
+ draftTombstone: boolean | null;
89
91
  publishedPayload: string | null;
90
92
  publishedTombstone: boolean | null;
91
93
  };
@@ -113,8 +113,10 @@ export class D1ContentRepository {
113
113
  }
114
114
  async getEffectiveDocument(documentId) {
115
115
  return this.db.prepare(`SELECT d.document_id AS documentId, d.kind, d.locale, d.draft_revision_id AS draftRevisionId,
116
+ draft.payload_json AS draftPayload, draft.is_tombstone AS draftTombstone,
116
117
  published.revision_id AS publishedRevisionId, published.payload_json AS publishedPayload, published.is_tombstone AS publishedTombstone
117
118
  FROM content_documents d
119
+ LEFT JOIN content_revisions draft ON draft.document_id = d.document_id AND draft.revision_id = d.draft_revision_id
118
120
  LEFT JOIN content_site_state state ON state.singleton = 1
119
121
  LEFT JOIN content_release_entries entry ON entry.release_id = state.current_release_id AND entry.document_id = d.document_id
120
122
  LEFT JOIN content_revisions published ON published.document_id = entry.document_id AND published.revision_id = entry.revision_id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "type": "module",
5
5
  "description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
6
6
  "license": "UNLICENSED",