@cardinali-studio/adminkit 2.2.33 → 2.2.34
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 +19 -0
- package/dist/api.js +3 -0
- package/dist/app/App.js +22 -8
- package/dist/app/lib/bookings.js +5 -0
- package/dist/app/lib/deliveries.js +7 -0
- package/dist/app/lib/home.js +7 -4
- package/dist/app/lib/pages.js +2 -1
- package/dist/app/lib/publish.d.ts +5 -0
- package/dist/app/lib/publish.js +61 -4
- package/dist/app/lib/requests.js +6 -0
- package/dist/app/lib/settings.js +7 -3
- package/dist/app/routes/PageActions.d.ts +1 -1
- package/dist/app/routes/PageActions.js +5 -2
- package/dist/app/routes/PageEditor.js +6 -4
- package/dist/app/routes/PageSeoEditor.js +6 -1
- package/dist/app/routes/PagesList.js +5 -1
- package/dist/app/site.d.ts +13 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export type ContentDocumentPublishTarget = {
|
|
|
71
71
|
documentId: string;
|
|
72
72
|
expectedDraftRevisionId: string | null;
|
|
73
73
|
includeSiteAccess?: boolean;
|
|
74
|
+
expectedBaseReleaseId?: string;
|
|
75
|
+
previewChecksum?: string;
|
|
74
76
|
};
|
|
75
77
|
export type ContentMediaPublishTarget = {
|
|
76
78
|
mediaOperationId: string;
|
|
@@ -204,6 +206,23 @@ export declare const api: {
|
|
|
204
206
|
enabled: boolean;
|
|
205
207
|
} | null;
|
|
206
208
|
activeReleaseId?: string | null;
|
|
209
|
+
previousReleaseId?: string | null;
|
|
210
|
+
}>;
|
|
211
|
+
contentPreview: (target: ContentDocumentPublishTarget) => Promise<{
|
|
212
|
+
ok: true;
|
|
213
|
+
previewUrl: string;
|
|
214
|
+
expectedBaseReleaseId: string;
|
|
215
|
+
expectedDraftRevisionId: string;
|
|
216
|
+
previewChecksum: string;
|
|
217
|
+
}>;
|
|
218
|
+
contentRollback: (expectedCurrentReleaseId: string, targetReleaseId: string) => Promise<{
|
|
219
|
+
ok: true;
|
|
220
|
+
releaseId: string;
|
|
221
|
+
}>;
|
|
222
|
+
contentVerification: (releaseId: string) => Promise<{
|
|
223
|
+
ok: true;
|
|
224
|
+
publicOriginVerified: boolean;
|
|
225
|
+
reason: string;
|
|
207
226
|
}>;
|
|
208
227
|
contentPublish: (target: ContentPublishTarget) => Promise<{
|
|
209
228
|
ok: true;
|
package/dist/api.js
CHANGED
|
@@ -73,6 +73,9 @@ export const api = {
|
|
|
73
73
|
/** D1 publication is site-owned: the site chooses which documents it can
|
|
74
74
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
75
75
|
contentPublishState: () => call("/admin/api/content/publish"),
|
|
76
|
+
contentPreview: (target) => post("/admin/api/content/preview", target),
|
|
77
|
+
contentRollback: (expectedCurrentReleaseId, targetReleaseId) => post("/admin/api/content/rollback", { expectedCurrentReleaseId, targetReleaseId }),
|
|
78
|
+
contentVerification: (releaseId) => call("/admin/api/publication-status?releaseId=" + encodeURIComponent(releaseId)),
|
|
76
79
|
contentPublish: (target) => post("/admin/api/content/publish", target),
|
|
77
80
|
contentPublishMedia: (mediaOperationId) => post("/admin/api/content/publish", { mediaOperationId }),
|
|
78
81
|
// ---- pubblicazione
|
package/dist/app/App.js
CHANGED
|
@@ -20,8 +20,10 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
20
20
|
import "../theme.css";
|
|
21
21
|
import "./app.css";
|
|
22
22
|
import "./next.css";
|
|
23
|
-
import { MediaFolders, MediaGrid, ProsePages, Trash, useMediaLibrary, usePicker } from "../form/index.js";
|
|
23
|
+
import { MediaFolders, MediaGrid, ProsePages, Trash, SchemaForm, useMediaLibrary, usePicker } from "../form/index.js";
|
|
24
24
|
import { Preview, isLocalPreview } from "../preview.js";
|
|
25
|
+
import { api } from "../api.js";
|
|
26
|
+
import { ui } from "../registries.js";
|
|
25
27
|
import { contentPersistenceMode, site } from "./site.js";
|
|
26
28
|
import { useHashRoute } from "./lib/router";
|
|
27
29
|
import { usePages, legacyDraft, legacyDraftTitles, discardLegacyDraft } from "./lib/pages";
|
|
@@ -71,6 +73,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
71
73
|
const pages = usePages();
|
|
72
74
|
const home = useHome(section === "home");
|
|
73
75
|
const HomeEditorView = site().homeEditor ?? HomeEditor;
|
|
76
|
+
const MediaEditorView = site().mediaEditor;
|
|
74
77
|
/* Anche la Panoramica: le sue card devono sapere quali funzioni aggiuntive sono attive, e
|
|
75
78
|
lo stato è quello — non una seconda copia. È una lettura sola, e la Panoramica è la
|
|
76
79
|
schermata da cui si entra, quindi arriva prima che serva. */
|
|
@@ -78,7 +81,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
78
81
|
/* La libreria media vive QUI, non dentro un componente: le cartelle stanno nella seconda
|
|
79
82
|
colonna e la griglia nella terza, e sono due punti dello schermo lontani. Uno stato per
|
|
80
83
|
ciascuno vorrebbe dire due idee diverse di quale cartella si sta guardando. */
|
|
81
|
-
const media = useMediaLibrary({ mode: "manage", active: section === "media" });
|
|
84
|
+
const media = useMediaLibrary({ mode: "manage", active: section === "media" && !MediaEditorView });
|
|
82
85
|
const mediaSettingsOpen = view === "media" && route[1] === "settings";
|
|
83
86
|
/* Le richieste si caricano SUBITO, non entrando nella sezione: il numero «da gestire» nella
|
|
84
87
|
prima colonna è il motivo per cui uno ci entra, e un badge che compare solo dopo essere
|
|
@@ -258,7 +261,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
258
261
|
|| (section === "requests" && !openReq)
|
|
259
262
|
|| (section === "bookings" && !openBooking)
|
|
260
263
|
|| (section === "media" && (view === "trash" || mediaSettingsOpen || mediaCrumbs.length === 0));
|
|
261
|
-
return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), "aria-label": n.label, disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", "aria-label": "Vedi il sito", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, "aria-label": theme === "dark" ? "Tema chiaro" : "Tema scuro", children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), "aria-label": "Impostazioni", disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Pagine" }), list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) })] }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
|
|
264
|
+
return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !(section === "media" && MediaEditorView) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), "aria-label": n.label, disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", "aria-label": "Vedi il sito", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, "aria-label": theme === "dark" ? "Tema chiaro" : "Tema scuro", children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), "aria-label": "Impostazioni", disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !(section === "media" && MediaEditorView) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Pagine" }), list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) })] }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
|
|
262
265
|
: !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
263
266
|
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
|
|
264
267
|
if (editing?.slug === from)
|
|
@@ -283,13 +286,24 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
283
286
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
284
287
|
: _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
|
|
285
288
|
: !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
286
|
-
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen && mediaCrumbs.length > 0 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : !redundantPaneContext ? (_jsx("h1", { className: "pane-title", children: heading })) : null, view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
289
|
+
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen && mediaCrumbs.length > 0 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : !redundantPaneContext ? (_jsx("h1", { className: "pane-title", children: heading })) : null, view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && !MediaEditorView && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
287
290
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
288
291
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
289
292
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
|
290
293
|
qualcosa da pubblicare. */
|
|
291
|
-
onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), contentPersistenceMode() === "d1" && d1ContextPending && otherPendingCount > 0 && (_jsx("span", { className: "pane-count", title: "Queste modifiche restano in bozza", children: otherPendingCount === 1 ? "1 altra bozza" : `${otherPendingCount} altre bozze` })), ((
|
|
292
|
-
|
|
294
|
+
onClick: () => void saver.save().then(() => publish.reload()), children: saver.saving ? "Salvo…" : "Salva" })] })), contentPersistenceMode() === "d1" && d1ContextPending && otherPendingCount > 0 && (_jsx("span", { className: "pane-count", title: "Queste modifiche restano in bozza", children: otherPendingCount === 1 ? "1 altra bozza" : `${otherPendingCount} altre bozze` })), site().directPublication && _jsxs(_Fragment, { children: [d1ContextPending && d1PublishTarget && _jsx("button", { className: "ak-btn", disabled: publish.publishing || !!saver?.dirty || !!saver?.saving, onClick: () => void publish.preview(d1PublishTarget), children: "Anteprima" }), d1ContextPending && d1PublishTarget && _jsx("button", { className: "ak-btn", disabled: publish.publishing || !!saver?.dirty || !!saver?.saving, onClick: () => void api.discardContent(d1PublishTarget.documentId, d1PublishTarget.expectedDraftRevisionId, null).then(async () => { if (section === "pages")
|
|
295
|
+
await pages.load(); if (section === "home")
|
|
296
|
+
await home.load(); if (section === "settings")
|
|
297
|
+
await settings.load(); await publish.reload(); }), children: "Scarta bozza" }), _jsx("button", { className: "ak-btn", onClick: () => void publish.verify(), children: "Verifica sito pubblico" }), publish.previousReleaseId && _jsx("button", { className: "ak-btn", disabled: publish.publishing || pages.dirty || home.dirty || settings.dirty, onClick: () => { if (confirm("Ripristinare la release precedente del sito? Le bozze restano salvate."))
|
|
298
|
+
void publish.rollback().then(async () => { await pages.load(); await home.load(); await settings.load(); }); }, children: "Ripristina precedente" })] }), ((contentPersistenceMode() === "d1" && d1ContextPending)
|
|
299
|
+
|| (contentPersistenceMode() !== "d1" && publish.ahead > 0)) && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing || !!saver?.dirty || !!saver?.saving || (site().directPublication === true && !publish.previewApproved(d1PublishTarget)), onClick: () => void publish.publish(d1PublishTarget ?? undefined).then(async () => { if (!saver?.dirty) {
|
|
300
|
+
if (section === "pages")
|
|
301
|
+
await pages.load();
|
|
302
|
+
if (section === "home")
|
|
303
|
+
await home.load();
|
|
304
|
+
if (section === "settings")
|
|
305
|
+
await settings.load();
|
|
306
|
+
} }), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
|
|
293
307
|
: contentPersistenceMode() === "d1" ? contextualPublishLabel
|
|
294
308
|
: publish.ahead === 1 ? "Pubblica (1 modifica)"
|
|
295
309
|
: `Pubblica (${publish.ahead} modifiche)` }))] }), offModule ? _jsx(FeatureOff, { info: offModule }) : _jsxs(_Fragment, { children: [draft !== null && (_jsxs("div", { className: "app-legacy", children: [_jsxs("p", { className: "app-legacy-row", children: [_jsxs("span", { children: ["Questo browser conserva una bozza del ", _jsx("strong", { children: "vecchio" }), " pannello (", draft, " pagine). Non viene usata: quello che si vede qui \u00E8 il sito pubblicato."] }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setDraftOpen((v) => !v), children: draftOpen ? "Nascondi" : "Cosa contiene" }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => { discardLegacyDraft(); setDraft(null); }, children: "Butta" })] }), draftOpen && _jsx("p", { className: "app-legacy-list", children: legacyDraftTitles().join(" · ") })] })), _jsxs("div", { className: "pane-body" + (wide ? " is-wide" : ""), children: [section === "overview" && (
|
|
@@ -304,9 +318,9 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
304
318
|
se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
|
|
305
319
|
home.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: home.error }), _jsx("button", { className: "ak-btn", onClick: () => void home.load(), children: "Riprova" })] }))
|
|
306
320
|
: home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
|
|
307
|
-
: _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (settingsView === "site" ? (_jsx(SiteSettingsUnavailable, { error: settings.error, onRetry: () => void settings.load(), adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface })) : (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] }))) : settings.data ? (settingsView === "seo"
|
|
321
|
+
: _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (settingsView === "site" ? (_jsx(SiteSettingsUnavailable, { error: settings.error, onRetry: () => void settings.load(), adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface })) : (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] }))) : settings.data ? site().settingsShape ? _jsx(SchemaForm, { shape: site().settingsShape, omit: Object.entries(site().settingsShape).filter(([, field]) => settingsView === "seo" ? ui.get(field)?.group !== "seo" : ui.get(field)?.group === "seo").map(([key]) => key), value: settings.data, onChange: (next) => settings.change(next), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles }) : (settingsView === "seo"
|
|
308
322
|
? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
|
|
309
|
-
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
|
|
323
|
+
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (MediaEditorView ? _jsx(MediaEditorView, {}) : mediaSettingsOpen
|
|
310
324
|
? _jsx(MediaSettingsEditor, { lib: media })
|
|
311
325
|
: _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
312
326
|
: openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
|
package/dist/app/lib/bookings.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { site } from "../site.js";
|
|
1
2
|
import { useCallback, useEffect, useState } from "react";
|
|
2
3
|
async function call(path, init) {
|
|
3
4
|
const response = await fetch(`/admin/api/booking${path}`, {
|
|
@@ -17,6 +18,10 @@ export function useBookings() {
|
|
|
17
18
|
loading: true, busy: false, error: null, notice: null,
|
|
18
19
|
});
|
|
19
20
|
const load = useCallback(async () => {
|
|
21
|
+
if (site().modules?.booking !== true) {
|
|
22
|
+
setState(value => ({ ...value, rows: [], pending: 0, upcoming: 0, loading: false, error: null }));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
20
25
|
setState((value) => ({ ...value, loading: true, error: null }));
|
|
21
26
|
try {
|
|
22
27
|
const [list, types, settings, availability, google, activation, blocks] = await Promise.all([
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { site } from "../site.js";
|
|
1
2
|
import { useCallback, useEffect, useState } from "react";
|
|
2
3
|
import { ApiError } from "../../index.js";
|
|
3
4
|
export function useDeliveries(activeId) {
|
|
@@ -8,6 +9,12 @@ export function useDeliveries(activeId) {
|
|
|
8
9
|
const [error, setError] = useState(null);
|
|
9
10
|
const [busy, setBusy] = useState(false);
|
|
10
11
|
const load = useCallback(async () => {
|
|
12
|
+
if (site().modules?.delivery !== true) {
|
|
13
|
+
setRows([]);
|
|
14
|
+
setExpiring(0);
|
|
15
|
+
setError(null);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
11
18
|
try {
|
|
12
19
|
const out = await getJson("/admin/api/deliveries");
|
|
13
20
|
setRows(out.deliveries);
|
package/dist/app/lib/home.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* l'admin per riordinare le pagine non ha motivo di pagare una chiamata in più — e senza
|
|
9
9
|
* GITHUB_TOKEN, in anteprima locale, quella chiamata è un 500.
|
|
10
10
|
*/
|
|
11
|
-
import { useCallback, useEffect, useState } from "react";
|
|
11
|
+
import { useCallback, useEffect, useState, useRef } from "react";
|
|
12
12
|
import { ApiError } from "../../index.js";
|
|
13
13
|
import { api } from "../../api.js";
|
|
14
14
|
import { contentPersistenceMode, site } from "../site.js";
|
|
@@ -27,6 +27,7 @@ export function useHome(active) {
|
|
|
27
27
|
const [saving, setSaving] = useState(false);
|
|
28
28
|
const [notice, setNotice] = useState(null);
|
|
29
29
|
const [revision, setRevision] = useState(null);
|
|
30
|
+
const pending = useRef(null);
|
|
30
31
|
const load = useCallback(async () => {
|
|
31
32
|
try {
|
|
32
33
|
const mapped = site().homeDocument;
|
|
@@ -59,12 +60,14 @@ export function useHome(active) {
|
|
|
59
60
|
try {
|
|
60
61
|
const mapped = site().homeDocument;
|
|
61
62
|
if (contentPersistenceMode() === "d1" && mapped && revision) {
|
|
62
|
-
const revisionId = crypto.randomUUID();
|
|
63
|
+
const revisionId = pending.current?.revisionId ?? crypto.randomUUID();
|
|
63
64
|
/* The Worker validates and may normalize a document before checking its signed
|
|
64
65
|
* representation. Sign the site-owned canonical payload here too, otherwise an
|
|
65
66
|
* obsolete/unknown client field becomes a misleading "documento non valido". */
|
|
66
67
|
const payload = mapped.toPayload ? mapped.toPayload(data) : data;
|
|
67
|
-
|
|
68
|
+
pending.current ||= { revisionId, documentId: mapped.documentId, baseRevisionId: revision.base, expectedDraftRevisionId: revision.expected, kind: mapped.kind, locale: mapped.locale, schemaVersion: mapped.schemaVersion, payload: payload, tombstone: false };
|
|
69
|
+
await api.saveContent(pending.current);
|
|
70
|
+
pending.current = null;
|
|
68
71
|
setRevision({ base: revisionId, expected: revisionId });
|
|
69
72
|
setNotice("Bozza D1 salvata");
|
|
70
73
|
setDirty(false);
|
|
@@ -88,7 +91,7 @@ export function useHome(active) {
|
|
|
88
91
|
setSaving(false);
|
|
89
92
|
}
|
|
90
93
|
}, [data, revision]);
|
|
91
|
-
const change = useCallback((next) => { setData(next); setDirty(true); }, []);
|
|
94
|
+
const change = useCallback((next) => { pending.current = null; setData(next); setDirty(true); }, []);
|
|
92
95
|
const mapped = site().homeDocument;
|
|
93
96
|
return { data, error, dirty, saving, notice, load, change, save, setNotice,
|
|
94
97
|
publishTarget: contentPersistenceMode() === "d1" && mapped && revision
|
package/dist/app/lib/pages.js
CHANGED
|
@@ -48,7 +48,8 @@ export function usePages() {
|
|
|
48
48
|
}, []);
|
|
49
49
|
useEffect(() => { void load(); }, [load]);
|
|
50
50
|
const update = useCallback((next) => {
|
|
51
|
-
|
|
51
|
+
if (site().pagesReorderable !== false)
|
|
52
|
+
next.forEach((p, i) => { p.weight = site().positionWeight(p, i, next); });
|
|
52
53
|
setPages(next);
|
|
53
54
|
setDirty(true);
|
|
54
55
|
}, []);
|
|
@@ -17,6 +17,11 @@ export interface PublishState {
|
|
|
17
17
|
reload(): Promise<void>;
|
|
18
18
|
publish(target?: ContentPublishTarget): Promise<void>;
|
|
19
19
|
clearNotice(): void;
|
|
20
|
+
preview(target: ContentPublishTarget): Promise<void>;
|
|
21
|
+
previewApproved(target: ContentPublishTarget | null): boolean;
|
|
22
|
+
previousReleaseId: string | null;
|
|
23
|
+
rollback(): Promise<void>;
|
|
24
|
+
verify(): Promise<void>;
|
|
20
25
|
}
|
|
21
26
|
export declare function contentPublicationNotice(result: {
|
|
22
27
|
published: boolean;
|
package/dist/app/lib/publish.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useEffect, useState } from "react";
|
|
9
9
|
import { api, ApiError } from "../../index.js";
|
|
10
|
-
import { contentPersistenceMode } from "../site.js";
|
|
10
|
+
import { contentPersistenceMode, site } from "../site.js";
|
|
11
11
|
export function contentPublicationNotice(result) {
|
|
12
12
|
if (result.published === false) {
|
|
13
13
|
return result.message ?? "Non c'erano modifiche salvate da pubblicare.";
|
|
@@ -25,10 +25,64 @@ export function usePublish() {
|
|
|
25
25
|
const [publishing, setPublishing] = useState(false);
|
|
26
26
|
const [notice, setNotice] = useState(null);
|
|
27
27
|
const [error, setError] = useState(null);
|
|
28
|
+
const [approval, setApproval] = useState(null);
|
|
29
|
+
const [activeReleaseId, setActiveReleaseId] = useState(null);
|
|
30
|
+
const [previousReleaseId, setPreviousReleaseId] = useState(null);
|
|
31
|
+
const previewApproved = (target) => !!target && "documentId" in target && !!approval && approval.documentId === target.documentId && approval.expectedDraftRevisionId === target.expectedDraftRevisionId && approval.expectedBaseReleaseId === activeReleaseId;
|
|
32
|
+
const preview = async (target) => {
|
|
33
|
+
if (!("documentId" in target))
|
|
34
|
+
return;
|
|
35
|
+
setPublishing(true);
|
|
36
|
+
setNotice(null);
|
|
37
|
+
try {
|
|
38
|
+
const result = await api.contentPreview(target);
|
|
39
|
+
setApproval({ ...result, documentId: target.documentId });
|
|
40
|
+
window.open(result.previewUrl, "_blank", "noopener");
|
|
41
|
+
setNotice("Anteprima della bozza salvata pronta.");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
setApproval(null);
|
|
45
|
+
setNotice(error.message);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
setPublishing(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const rollback = async () => {
|
|
52
|
+
if (!activeReleaseId || !previousReleaseId)
|
|
53
|
+
return;
|
|
54
|
+
setPublishing(true);
|
|
55
|
+
setNotice(null);
|
|
56
|
+
try {
|
|
57
|
+
await api.contentRollback(activeReleaseId, previousReleaseId);
|
|
58
|
+
setApproval(null);
|
|
59
|
+
await reload();
|
|
60
|
+
setNotice("Ripristinata la release precedente. Le bozze sono conservate.");
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
setNotice(error.message);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
setPublishing(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const verify = async () => {
|
|
70
|
+
if (!activeReleaseId)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
const result = await api.contentVerification(activeReleaseId);
|
|
74
|
+
setNotice(result.publicOriginVerified ? "Release verificata sul dominio pubblico." : result.reason);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
setNotice(error.message);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
28
80
|
const reload = useCallback(async () => {
|
|
29
81
|
if (contentPersistenceMode() === "d1") {
|
|
30
82
|
try {
|
|
31
83
|
const d = await api.contentPublishState();
|
|
84
|
+
setActiveReleaseId(d.activeReleaseId ?? null);
|
|
85
|
+
setPreviousReleaseId(d.previousReleaseId ?? null);
|
|
32
86
|
setAhead(d.ahead ?? 0);
|
|
33
87
|
setDocuments(d.documents ?? []);
|
|
34
88
|
setSiteAccessPending(Boolean(d.siteAccess));
|
|
@@ -78,7 +132,10 @@ export function usePublish() {
|
|
|
78
132
|
try {
|
|
79
133
|
if (!target)
|
|
80
134
|
throw new ApiError("Apri il contenuto che vuoi pubblicare.", "invalid_request", 400);
|
|
81
|
-
|
|
135
|
+
if (site().directPublication && !previewApproved(target))
|
|
136
|
+
throw new ApiError("Apri una nuova anteprima della bozza prima di pubblicare.", "stale_preview", 409);
|
|
137
|
+
const r = await api.contentPublish(site().directPublication ? { ...target, ...approval } : target);
|
|
138
|
+
setApproval(null);
|
|
82
139
|
setNotice(contentPublicationNotice(r));
|
|
83
140
|
await reload();
|
|
84
141
|
}
|
|
@@ -113,9 +170,9 @@ export function usePublish() {
|
|
|
113
170
|
finally {
|
|
114
171
|
setPublishing(false);
|
|
115
172
|
}
|
|
116
|
-
}, [reload]);
|
|
173
|
+
}, [reload, approval, activeReleaseId]);
|
|
117
174
|
return {
|
|
118
175
|
ahead, commits, documents, siteAccessPending, configured, publishing, notice, error,
|
|
119
|
-
reload, publish, clearNotice: () => setNotice(null),
|
|
176
|
+
reload, publish, preview, previewApproved, previousReleaseId, rollback, verify, clearNotice: () => setNotice(null),
|
|
120
177
|
};
|
|
121
178
|
}
|
package/dist/app/lib/requests.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { site } from "../site.js";
|
|
1
2
|
/* Le richieste dal form di contatto.
|
|
2
3
|
*
|
|
3
4
|
* L'archivio è la SORGENTE, non l'email: `functions/api/contact.js` salva PRIMA e manda dopo, e
|
|
@@ -15,6 +16,11 @@ export function useRequests() {
|
|
|
15
16
|
const [error, setError] = useState(null);
|
|
16
17
|
const [busy, setBusy] = useState(false);
|
|
17
18
|
const load = useCallback(async () => {
|
|
19
|
+
if (site().modules?.requests !== true) {
|
|
20
|
+
setRows([]);
|
|
21
|
+
setError(null);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
18
24
|
try {
|
|
19
25
|
setRows((await api.requests()).requests);
|
|
20
26
|
setError(null);
|
package/dist/app/lib/settings.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* protegge il sito. La password digitata resta soltanto nello stato del form: il backend
|
|
5
5
|
* restituisce sempre il campo vuoto e conserva esclusivamente la verifica crittografica.
|
|
6
6
|
*/
|
|
7
|
-
import { useCallback, useEffect, useState } from "react";
|
|
7
|
+
import { useCallback, useEffect, useState, useRef } from "react";
|
|
8
8
|
import { ApiError } from "../../index.js";
|
|
9
9
|
import { api } from "../../api.js";
|
|
10
10
|
import { contentPersistenceMode, site } from "../site.js";
|
|
@@ -43,6 +43,7 @@ export function useSiteSettings(active) {
|
|
|
43
43
|
const { password: _password, passwordEnabled: _passwordEnabled, ...editorial } = settings;
|
|
44
44
|
return JSON.stringify(editorial);
|
|
45
45
|
};
|
|
46
|
+
const pending = useRef(null);
|
|
46
47
|
const load = useCallback(async () => {
|
|
47
48
|
try {
|
|
48
49
|
const mapped = site().siteSettingsDocument;
|
|
@@ -88,9 +89,11 @@ export function useSiteSettings(active) {
|
|
|
88
89
|
const mapped = site().siteSettingsDocument;
|
|
89
90
|
if (contentPersistenceMode() === "d1" && mapped && revision) {
|
|
90
91
|
if (editorialSnapshot(data) !== editorialBaseline) {
|
|
91
|
-
const revisionId = crypto.randomUUID();
|
|
92
|
+
const revisionId = pending.current?.revisionId ?? crypto.randomUUID();
|
|
92
93
|
const payload = mapped.toPayload ? mapped.toPayload(data) : data;
|
|
93
|
-
|
|
94
|
+
pending.current ||= { revisionId, documentId: mapped.documentId, baseRevisionId: revision.base, expectedDraftRevisionId: revision.expected, kind: mapped.kind, locale: mapped.locale, schemaVersion: mapped.schemaVersion, payload: payload, tombstone: false };
|
|
95
|
+
await api.saveContent(pending.current);
|
|
96
|
+
pending.current = null;
|
|
94
97
|
setRevision({ base: revisionId, expected: revisionId });
|
|
95
98
|
setEditorialBaseline(editorialSnapshot(data));
|
|
96
99
|
}
|
|
@@ -124,6 +127,7 @@ export function useSiteSettings(active) {
|
|
|
124
127
|
}
|
|
125
128
|
}, [data, revision, accessState, editorialBaseline]);
|
|
126
129
|
const change = useCallback((next) => {
|
|
130
|
+
pending.current = null;
|
|
127
131
|
setData(next);
|
|
128
132
|
setDirty(true);
|
|
129
133
|
}, []);
|
|
@@ -32,7 +32,7 @@ import { Icon } from "../ui/icons";
|
|
|
32
32
|
* modulo andava bene: è il genere di cosa che cambia quando una dipendenza smette di essere
|
|
33
33
|
* statica e diventa iniettata. */
|
|
34
34
|
function tipiDisponibili() {
|
|
35
|
-
const tipi = Object.entries(site().pages).map(([value, page]) => ({ value, label: page.label }));
|
|
35
|
+
const tipi = Object.entries(site().pages).filter(([, page]) => page.creatable !== false).map(([value, page]) => ({ value, label: page.label }));
|
|
36
36
|
/* La preselezione: "Pagina" se c'è (è la scelta di gran lunga più frequente), altrimenti la
|
|
37
37
|
* prima opzione — così questo elenco non può restare senza un valore valido. */
|
|
38
38
|
const iniziale = tipi.some((t) => t.value === "page") ? "page" : (tipi[0]?.value ?? "page");
|
|
@@ -56,6 +56,7 @@ export function NewPageButton({ pages, onCreate }) {
|
|
|
56
56
|
isHome: false,
|
|
57
57
|
inMenu: false,
|
|
58
58
|
r2Prefix: "",
|
|
59
|
+
...site().pages[type]?.create?.(t, uniqueSlug(t, pages.map((p) => p.slug))),
|
|
59
60
|
_orig: null, // pagina nuova: nessuna sorgente da cui conservare
|
|
60
61
|
});
|
|
61
62
|
setTitle("");
|
|
@@ -73,10 +74,12 @@ export function RowMenu({ page, pages, onChange, onRemove, onDuplicate, }) {
|
|
|
73
74
|
const [open, setOpen] = useState(false);
|
|
74
75
|
const [renaming, setRenaming] = useState(false);
|
|
75
76
|
const [title, setTitle] = useState(String(page.title ?? ""));
|
|
77
|
+
if (site().pages[String(page.type)]?.fixed)
|
|
78
|
+
return null;
|
|
76
79
|
const taken = pages.filter((p) => p.slug !== page.slug).map((p) => p.slug);
|
|
77
80
|
const nuovo = slugify(title);
|
|
78
81
|
const cambia = nuovo !== page.slug;
|
|
79
|
-
return (_jsxs("span", { className: "pa", children: [_jsx("button", { className: "pa-more", onClick: () => { setOpen(!open); setRenaming(false); }, title: "Altro", children: _jsx(Icon.more, {}) }), open && !renaming && (_jsxs("div", { className: "pa-menu", onMouseLeave: () => setOpen(false), children: [_jsx("button", { onClick: () => { setTitle(String(page.title ?? "")); setRenaming(true); }, children: "Rinomina\u2026" }), _jsx("button", { onClick: () => { onChange({ ...page, inMenu: !page.inMenu }); setOpen(false); }, children: page.inMenu ? "Togli dal menu" : "Aggiungi al menu" }), _jsx("button", { onClick: () => { onChange({ ...page, published: page.published === false }); setOpen(false); }, children: page.published === false ? "Pubblica" : "Nascondi" }), _jsx("button", { onClick: () => { onDuplicate(); setOpen(false); }, children: "Duplica" }), _jsx("button", { className: "pa-danger", onClick: () => {
|
|
82
|
+
return (_jsxs("span", { className: "pa", children: [_jsx("button", { className: "pa-more", onClick: () => { setOpen(!open); setRenaming(false); }, title: "Altro", children: _jsx(Icon.more, {}) }), open && !renaming && (_jsxs("div", { className: "pa-menu", onMouseLeave: () => setOpen(false), children: [_jsx("button", { onClick: () => { setTitle(String(page.title ?? "")); setRenaming(true); }, children: "Rinomina\u2026" }), site().pages[String(page.type)]?.menuEditable !== false && _jsx("button", { onClick: () => { onChange({ ...page, inMenu: !page.inMenu }); setOpen(false); }, children: page.inMenu ? "Togli dal menu" : "Aggiungi al menu" }), _jsx("button", { onClick: () => { onChange({ ...page, published: page.published === false }); setOpen(false); }, children: page.published === false ? "Pubblica" : "Nascondi" }), _jsx("button", { onClick: () => { onDuplicate(); setOpen(false); }, children: "Duplica" }), _jsx("button", { className: "pa-danger", onClick: () => {
|
|
80
83
|
if (confirm(`Eliminare "${page.title}"?\n\nLa pagina e il suo testo spariscono dal sito al prossimo salvataggio. ` +
|
|
81
84
|
`Le foto restano nel bucket.`))
|
|
82
85
|
onRemove();
|
|
@@ -69,14 +69,15 @@ export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, lang
|
|
|
69
69
|
});
|
|
70
70
|
setTitleOpen(false);
|
|
71
71
|
};
|
|
72
|
+
const fixed = pageType(page.type)?.fixed === true;
|
|
72
73
|
const online = page.published !== false;
|
|
73
74
|
const translationActive = !!languages && activeLanguage !== languages.default;
|
|
74
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pe-head-main", children: [_jsx("button", { type: "button", className: "pe-title", title: "Modifica nome e indirizzo", disabled: translationActive, onClick: () => {
|
|
75
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pe-head-main", children: [_jsx("button", { type: "button", className: "pe-title", title: "Modifica nome e indirizzo", disabled: translationActive || fixed, onClick: () => {
|
|
75
76
|
setTitle(String(page.title ?? ""));
|
|
76
77
|
setSlug(page.slug);
|
|
77
78
|
setVisibilityOpen(false);
|
|
78
79
|
setTitleOpen(true);
|
|
79
|
-
}, children: String(page.title ?? page.slug) }), languages && (_jsx("div", { className: "pe-language", "aria-label": "Lingua del contenuto", children: languages.available.map((language) => (_jsx("button", { type: "button", className: activeLanguage === language ? "is-active" : "", "aria-pressed": activeLanguage === language, onClick: () => onLanguageChange?.(language), children: language.toUpperCase() }, language))) })), _jsxs("button", { type: "button", className: "pe-seo-button" + (seoActive ? " is-active" : ""), "aria-pressed": seoActive, disabled: translationActive, onClick: onSeoToggle, children: [_jsx(Icon.search, {}), "SEO"] }), _jsxs("div", { className: "pe-visibility", ref: visibilityRef, children: [_jsxs("button", { type: "button", className: "pe-status" + (online ? " is-online" : " is-offline"), "aria-haspopup": "menu", "aria-expanded": visibilityOpen, disabled: translationActive, onClick: () => setVisibilityOpen((open) => !open), children: [_jsx("span", { className: "pe-status-dot" }), online ? "Online" : "Offline", _jsx("span", { className: "pe-chevron", "aria-hidden": "true", children: "\u2304" })] }), visibilityOpen && (_jsxs("div", { className: "pe-status-menu", role: "menu", children: [_jsx("span", { className: "pe-status-label", children: "Visibilit\u00E0 della pagina" }), _jsxs("button", { type: "button", className: online ? "is-selected" : "", onClick: () => {
|
|
80
|
+
}, children: String(page.title ?? page.slug) }), languages && (_jsx("div", { className: "pe-language", "aria-label": "Lingua del contenuto", children: languages.available.map((language) => (_jsx("button", { type: "button", className: activeLanguage === language ? "is-active" : "", "aria-pressed": activeLanguage === language, onClick: () => onLanguageChange?.(language), children: language.toUpperCase() }, language))) })), _jsxs("button", { type: "button", className: "pe-seo-button" + (seoActive ? " is-active" : ""), "aria-pressed": seoActive, disabled: translationActive, onClick: onSeoToggle, children: [_jsx(Icon.search, {}), "SEO"] }), _jsxs("div", { className: "pe-visibility", ref: visibilityRef, children: [_jsxs("button", { type: "button", className: "pe-status" + (online ? " is-online" : " is-offline"), "aria-haspopup": "menu", "aria-expanded": visibilityOpen, disabled: translationActive || fixed, onClick: () => setVisibilityOpen((open) => !open), children: [_jsx("span", { className: "pe-status-dot" }), online ? "Online" : "Offline", _jsx("span", { className: "pe-chevron", "aria-hidden": "true", children: "\u2304" })] }), visibilityOpen && (_jsxs("div", { className: "pe-status-menu", role: "menu", children: [_jsx("span", { className: "pe-status-label", children: "Visibilit\u00E0 della pagina" }), _jsxs("button", { type: "button", className: online ? "is-selected" : "", onClick: () => {
|
|
80
81
|
if (!online)
|
|
81
82
|
onChange({ ...page, published: true });
|
|
82
83
|
setVisibilityOpen(false);
|
|
@@ -84,7 +85,7 @@ export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, lang
|
|
|
84
85
|
if (online)
|
|
85
86
|
onChange({ ...page, published: false });
|
|
86
87
|
setVisibilityOpen(false);
|
|
87
|
-
}, children: [_jsx("span", { className: "pe-radio", children: _jsx("span", {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Offline" }), _jsx("small", { children: "La pagina non \u00E8 visibile sul sito." })] })] })] }))] }), _jsxs("label", { className: "pe-menu-toggle", children: [_jsx("input", { type: "checkbox", checked: !!page.inMenu, disabled: translationActive, onChange: (event) => onChange({ ...page, inMenu: event.target.checked }) }), _jsx("span", { children: "Aggiungi al menu" })] })] }), titleOpen && (_jsx("div", { className: "pe-modal", role: "presentation", onMouseDown: (event) => {
|
|
88
|
+
}, children: [_jsx("span", { className: "pe-radio", children: _jsx("span", {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Offline" }), _jsx("small", { children: "La pagina non \u00E8 visibile sul sito." })] })] })] }))] }), pageType(page.type)?.menuEditable !== false && _jsxs("label", { className: "pe-menu-toggle", children: [_jsx("input", { type: "checkbox", checked: !!page.inMenu, disabled: translationActive || fixed, onChange: (event) => onChange({ ...page, inMenu: event.target.checked }) }), _jsx("span", { children: "Aggiungi al menu" })] })] }), titleOpen && (_jsx("div", { className: "pe-modal", role: "presentation", onMouseDown: (event) => {
|
|
88
89
|
if (event.target === event.currentTarget)
|
|
89
90
|
setTitleOpen(false);
|
|
90
91
|
}, children: _jsxs("div", { className: "pe-modal-card", role: "dialog", "aria-modal": "true", "aria-labelledby": "pe-title-dialog", children: [_jsxs("header", { children: [_jsx("h2", { id: "pe-title-dialog", children: "Modifica pagina" }), _jsx("button", { type: "button", onClick: () => setTitleOpen(false), "aria-label": "Chiudi", children: "\u00D7" })] }), _jsxs("div", { className: "pe-modal-fields", children: [_jsxs("label", { children: [_jsx("span", { children: "Nome pagina" }), _jsx("input", { autoFocus: true, value: title, onChange: (event) => setTitle(event.target.value), onKeyDown: (event) => {
|
|
@@ -340,7 +341,8 @@ export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pic
|
|
|
340
341
|
...(translationActive ? [] : ["title"]),
|
|
341
342
|
"published", "inMenu",
|
|
342
343
|
"seoTitle", "seoDescription", "ogTitle", "ogDescription", "ogImage",
|
|
343
|
-
|
|
344
|
+
...(definition?.seoShape ? ["seo"] : []),
|
|
345
|
+
], [translationActive, definition]);
|
|
344
346
|
/* Come la gallery: una pagina portfolio È la sua selezione di lavori, quindi la schermata
|
|
345
347
|
mostra quella e basta. Il `SchemaForm` generico qui elencava anche «Foto», «Edizioni»,
|
|
346
348
|
«Ritratto» e «Riconoscimenti» — campi di altri tipi di pagina, che compaiono perché il
|
|
@@ -8,7 +8,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
import { useEffect, useState } from "react";
|
|
9
9
|
import { api } from "../../index.js";
|
|
10
10
|
import { Icon } from "../ui/icons";
|
|
11
|
-
import {
|
|
11
|
+
import { SchemaForm } from "../../form/index.js";
|
|
12
|
+
import { pageType, site } from "../site.js";
|
|
12
13
|
/* ⚠️ Non costanti di modulo: `site()` è disponibile solo dopo `mountAdmin`, e un `const` qui
|
|
13
14
|
* verrebbe valutato all'import — cioè prima. Si leggono dentro il componente.
|
|
14
15
|
*
|
|
@@ -93,6 +94,10 @@ export function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFi
|
|
|
93
94
|
const socialImage = explicitImage || automaticImage(page);
|
|
94
95
|
const pageUrl = `${site().publicBase}/${page.slug}/`;
|
|
95
96
|
const complete = !!seoTitle.trim() && !!seoDescription.trim() && !!socialImage;
|
|
97
|
+
const customShape = pageType(page.type)?.seoShape;
|
|
98
|
+
if (customShape)
|
|
99
|
+
return _jsxs("section", { className: "seo-editor", children: [_jsx("h2", { children: "SEO e GEO" }), loadError && _jsx("p", { className: "ak-error", children: "Non sono riuscito a rileggere la pagina." }), _jsx(SchemaForm, { shape: customShape, value: (page.seo ?? {}), onChange: (seo) => { if (!readOnly)
|
|
100
|
+
onChange({ ...page, seo }); }, pickMediaFiles: pickMediaFiles })] });
|
|
96
101
|
const chooseImage = async () => {
|
|
97
102
|
const selected = await pickMediaFiles?.({ multiple: false });
|
|
98
103
|
if (selected?.[0])
|
|
@@ -14,6 +14,7 @@ import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
|
14
14
|
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
|
|
15
15
|
import { KeyboardSensor } from "@dnd-kit/core";
|
|
16
16
|
import { CSS } from "@dnd-kit/utilities";
|
|
17
|
+
import { site } from "../site.js";
|
|
17
18
|
import { RowMenu } from "./PageActions";
|
|
18
19
|
import { uniqueSlug } from "../lib/slug";
|
|
19
20
|
import { Icon } from "../ui/icons";
|
|
@@ -23,7 +24,7 @@ const ICONS = {
|
|
|
23
24
|
function Row({ p, pages, current, onEdit, onChange, onRemove, onDuplicate, languages }) {
|
|
24
25
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: p.slug });
|
|
25
26
|
const TypeIcon = ICONS[String(p.type)] ?? Icon.doc;
|
|
26
|
-
return (_jsxs("li", { ref: setNodeRef, className: "pg-row" + (isDragging ? " is-dragging" : "") + (p.slug === current ? " is-on" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsx("button", { className: "pg-handle", ...attributes, ...listeners, title: "Trascina per riordinare", children: _jsx(Icon.grip, {}) }), _jsxs("button", { className: "pg-open", onClick: () => onEdit(p.slug), children: [_jsx("span", { className: "pg-icon", children: _jsx(TypeIcon, {}) }), _jsx("span", { className: "pg-title", children: String(p.title ?? p.slug) }), languages?.available.filter((language) => language !== languages.default).map((language) => {
|
|
27
|
+
return (_jsxs("li", { ref: setNodeRef, className: "pg-row" + (isDragging ? " is-dragging" : "") + (p.slug === current ? " is-on" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsx("button", { disabled: site().pagesReorderable === false, className: "pg-handle", ...attributes, ...listeners, title: "Trascina per riordinare", children: _jsx(Icon.grip, {}) }), _jsxs("button", { className: "pg-open", onClick: () => onEdit(p.slug), children: [_jsx("span", { className: "pg-icon", children: _jsx(TypeIcon, {}) }), _jsx("span", { className: "pg-title", children: String(p.title ?? p.slug) }), languages?.available.filter((language) => language !== languages.default).map((language) => {
|
|
27
28
|
const saved = p.translations?.[language]?.exists;
|
|
28
29
|
const draft = !!p._translations?.[language];
|
|
29
30
|
return (_jsx("span", { className: "pg-language" + (saved ? " is-saved" : draft ? " is-draft" : " is-inherited"), title: saved ? `${language.toUpperCase()} tradotta` : draft
|
|
@@ -37,6 +38,8 @@ function Group({ title, items, all, current, onEdit, onReorder, onChange, onRemo
|
|
|
37
38
|
const { active, over } = e;
|
|
38
39
|
if (!over || active.id === over.id)
|
|
39
40
|
return;
|
|
41
|
+
if (site().pagesReorderable === false)
|
|
42
|
+
return;
|
|
40
43
|
onReorder(items.findIndex((p) => p.slug === active.id), items.findIndex((p) => p.slug === over.id));
|
|
41
44
|
};
|
|
42
45
|
return (_jsxs("section", { className: "pg-group", children: [_jsx("h2", { className: "pg-group-title", children: title }), items.length === 0 && _jsx("p", { className: "pg-empty", children: "nessuna" }), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, modifiers: [restrictToVerticalAxis], onDragEnd: onDragEnd, children: _jsx(SortableContext, { items: items.map((p) => p.slug), strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "pg-list", children: items.map((p) => (_jsx(Row, { p: p, pages: all, current: current, onEdit: onEdit, onChange: onChange, languages: languages, onRemove: () => onRemove(p.slug), onDuplicate: () => onDuplicate(p.slug) }, p.slug))) }) }) })] }));
|
|
@@ -73,6 +76,7 @@ export function PagesList({ pages, current, onEdit, onChange, onRenamed, languag
|
|
|
73
76
|
inMenu: false, // una copia non entra nel menu da sola
|
|
74
77
|
published: false, // né va online prima che qualcuno l'abbia guardata
|
|
75
78
|
_orig: null, // pagina nuova: niente da cui conservare
|
|
79
|
+
_sha: undefined, _documentId: undefined, _draftRevisionId: null, _publishedRevisionId: null,
|
|
76
80
|
};
|
|
77
81
|
const i = pages.indexOf(src);
|
|
78
82
|
onChange([...pages.slice(0, i + 1), copia, ...pages.slice(i + 1)]);
|
package/dist/app/site.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface PageTypeConfig {
|
|
|
10
10
|
object: z.ZodType;
|
|
11
11
|
payload: z.ZodType;
|
|
12
12
|
hasBody: boolean;
|
|
13
|
+
/** Fixed routes cannot be renamed, hidden, duplicated or removed. */
|
|
14
|
+
fixed?: boolean;
|
|
15
|
+
menuEditable?: boolean;
|
|
16
|
+
creatable?: boolean;
|
|
17
|
+
create?: (title: string, slug: string) => Record<string, unknown>;
|
|
18
|
+
seoShape?: Shape;
|
|
13
19
|
}
|
|
14
20
|
/** Lingue dei contenuti, assenti nei siti monolingui. I codici sono quelli dei sibling Hugo
|
|
15
21
|
* (`index.md`, `index.en.md`): la lingua predefinita non porta alcun suffisso. */
|
|
@@ -39,6 +45,13 @@ export interface SiteAdminConfig {
|
|
|
39
45
|
languages?: LanguagesConfig;
|
|
40
46
|
/** Forma del singleton home: i suoi `group` diventano le voci della seconda colonna. */
|
|
41
47
|
home: Shape;
|
|
48
|
+
/** Schema-owned fields rendered inside the native Settings surface. */
|
|
49
|
+
settingsShape?: Shape;
|
|
50
|
+
pagesReorderable?: boolean;
|
|
51
|
+
/** Optional storage-policy surface inside the native Media pane. */
|
|
52
|
+
mediaEditor?: ComponentType;
|
|
53
|
+
/** Preview approval, public observation and rollback for direct SSR publishing. */
|
|
54
|
+
directPublication?: boolean;
|
|
42
55
|
/** Optional site-owned Home surface. The kit keeps its legacy visual editor
|
|
43
56
|
* as a fallback, while a site with a different document shape can render its
|
|
44
57
|
* own schema-driven editor without teaching the shared package its fields. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinali-studio/adminkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.34",
|
|
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": "0BSD",
|