@cardinali-studio/adminkit 2.2.33 → 2.2.35
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 +20 -0
- package/dist/api.js +3 -0
- package/dist/app/App.js +38 -9
- package/dist/app/BugReport.d.ts +7 -0
- package/dist/app/BugReport.js +212 -0
- package/dist/app/app.css +124 -0
- package/dist/app/index.js +3 -0
- 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 +7 -0
- package/dist/app/lib/publish.js +64 -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 +18 -0
- package/dist/direct-publication.d.ts +2 -0
- package/dist/direct-publication.js +1 -0
- package/dist/form/media/MediaFolders.js +6 -1
- package/dist/form/media/MediaGrid.js +1 -1
- package/package.json +3 -2
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,24 @@ export declare const api: {
|
|
|
204
206
|
enabled: boolean;
|
|
205
207
|
} | null;
|
|
206
208
|
activeReleaseId?: string | null;
|
|
209
|
+
previousReleaseId?: string | null;
|
|
210
|
+
verified?: boolean;
|
|
211
|
+
}>;
|
|
212
|
+
contentPreview: (target: ContentDocumentPublishTarget) => Promise<{
|
|
213
|
+
ok: true;
|
|
214
|
+
previewUrl: string;
|
|
215
|
+
expectedBaseReleaseId: string;
|
|
216
|
+
expectedDraftRevisionId: string;
|
|
217
|
+
previewChecksum: string;
|
|
218
|
+
}>;
|
|
219
|
+
contentRollback: (expectedCurrentReleaseId: string, targetReleaseId: string) => Promise<{
|
|
220
|
+
ok: true;
|
|
221
|
+
releaseId: string;
|
|
222
|
+
}>;
|
|
223
|
+
contentVerification: (releaseId: string) => Promise<{
|
|
224
|
+
ok: true;
|
|
225
|
+
publicOriginVerified: boolean;
|
|
226
|
+
reason: string;
|
|
207
227
|
}>;
|
|
208
228
|
contentPublish: (target: ContentPublishTarget) => Promise<{
|
|
209
229
|
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
|
@@ -14,14 +14,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
14
14
|
* separati (una collezione e un singleton), ma da qui dentro sono lo stesso gesto e non c'è
|
|
15
15
|
* ragione di farli sembrare due funzioni diverse.
|
|
16
16
|
*/
|
|
17
|
-
import { useEffect, useMemo, useState } from "react";
|
|
17
|
+
import { useEffect, useMemo, useState, useRef } from "react";
|
|
18
18
|
/* L'ordine di questi tre import È la separazione motore/cliente, e non va scambiato:
|
|
19
19
|
* i default del kit, poi l'identità di questo sito che li sovrascrive, poi l'impaginazione. */
|
|
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";
|
|
@@ -59,6 +61,8 @@ export function App() {
|
|
|
59
61
|
}
|
|
60
62
|
function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
61
63
|
const [route, go] = useHashRoute();
|
|
64
|
+
const [releaseOpen, setReleaseOpen] = useState(false);
|
|
65
|
+
const [rollbackConfirm, setRollbackConfirm] = useState(false);
|
|
62
66
|
const { theme, toggle: toggleTheme } = useTheme();
|
|
63
67
|
/* Si entra dalla Panoramica, non dalle Pagine: aprendo il pannello la domanda è quasi sempre
|
|
64
68
|
«è successo qualcosa?», e la risposta stava sparsa in tre sezioni da visitare una per una. */
|
|
@@ -71,6 +75,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
71
75
|
const pages = usePages();
|
|
72
76
|
const home = useHome(section === "home");
|
|
73
77
|
const HomeEditorView = site().homeEditor ?? HomeEditor;
|
|
78
|
+
const MediaEditorView = site().mediaEditor;
|
|
74
79
|
/* Anche la Panoramica: le sue card devono sapere quali funzioni aggiuntive sono attive, e
|
|
75
80
|
lo stato è quello — non una seconda copia. È una lettura sola, e la Panoramica è la
|
|
76
81
|
schermata da cui si entra, quindi arriva prima che serva. */
|
|
@@ -78,7 +83,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
78
83
|
/* La libreria media vive QUI, non dentro un componente: le cartelle stanno nella seconda
|
|
79
84
|
colonna e la griglia nella terza, e sono due punti dello schermo lontani. Uno stato per
|
|
80
85
|
ciascuno vorrebbe dire due idee diverse di quale cartella si sta guardando. */
|
|
81
|
-
const media = useMediaLibrary({ mode: "manage", active: section === "media" });
|
|
86
|
+
const media = useMediaLibrary({ mode: "manage", active: section === "media" && !MediaEditorView });
|
|
82
87
|
const mediaSettingsOpen = view === "media" && route[1] === "settings";
|
|
83
88
|
/* Le richieste si caricano SUBITO, non entrando nella sezione: il numero «da gestire» nella
|
|
84
89
|
prima colonna è il motivo per cui uno ci entra, e un badge che compare solo dopo essere
|
|
@@ -96,6 +101,20 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
96
101
|
finito uno, non all'avanzamento — altrimenti il pannello si ridisegnerebbe due volte al
|
|
97
102
|
secondo per tutta la durata dell'upload. */
|
|
98
103
|
const uploadCompletions = useUploadCompletions();
|
|
104
|
+
useEffect(() => { if (site().directPublication && section === "media")
|
|
105
|
+
void publish.reload(); }, [media.mediaOperations]);
|
|
106
|
+
const lastObservedRelease = useRef(null);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (lastObservedRelease.current && publish.activeReleaseId && lastObservedRelease.current !== publish.activeReleaseId) {
|
|
109
|
+
if (!pages.dirty)
|
|
110
|
+
void pages.load();
|
|
111
|
+
if (home.data && !home.dirty)
|
|
112
|
+
void home.load();
|
|
113
|
+
if (settings.data && !settings.dirty)
|
|
114
|
+
void settings.load();
|
|
115
|
+
}
|
|
116
|
+
lastObservedRelease.current = publish.activeReleaseId;
|
|
117
|
+
}, [publish.activeReleaseId]);
|
|
99
118
|
useEffect(() => {
|
|
100
119
|
if (section === "deliveries")
|
|
101
120
|
void deliveries.load();
|
|
@@ -258,7 +277,7 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
258
277
|
|| (section === "requests" && !openReq)
|
|
259
278
|
|| (section === "bookings" && !openBooking)
|
|
260
279
|
|| (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 })
|
|
280
|
+
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: [site().directPublication && _jsxs("div", { className: "ak-release-control", children: [_jsxs("button", { className: "ak-release-trigger", "aria-haspopup": "dialog", "aria-expanded": releaseOpen, onClick: () => { setReleaseOpen(!releaseOpen); setRollbackConfirm(false); }, children: [_jsx("span", { className: "ak-release-dot" + (publish.verified ? " is-ok" : "") }), publish.configured ? (publish.verified ? "Online" : "In attesa") : "Non disponibile"] }), releaseOpen && _jsxs("section", { className: "ak-release-menu", role: "dialog", "aria-label": "Stato della pubblicazione", children: [_jsx("strong", { children: publish.verified ? "Online" : "Verifica in attesa" }), _jsx("p", { children: publish.verified ? "Il sito mostra gli ultimi contenuti pubblicati." : "I contenuti sono salvati in D1; verifica la versione visibile sul dominio pubblico." }), _jsx("dl", { children: _jsxs("div", { children: [_jsx("dt", { children: "Versione" }), _jsx("dd", { children: publish.activeReleaseId?.slice(0, 8) || "—" })] }) }), _jsx("button", { className: "ak-btn", onClick: () => void publish.verify(), children: "Verifica sito pubblico" }), publish.previousReleaseId && !rollbackConfirm && _jsx("button", { className: "ak-btn", disabled: publish.publishing || pages.dirty || home.dirty || settings.dirty, onClick: () => setRollbackConfirm(true), children: "Ripristina la versione precedente" }), rollbackConfirm && _jsxs("div", { children: [_jsx("p", { children: "Ripristinare la versione precedente? Le bozze restano salvate." }), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: publish.publishing, onClick: () => void publish.rollback().then(async () => { await pages.load(); await home.load(); await settings.load(); setRollbackConfirm(false); }), children: "Conferma ripristino" }), _jsx("button", { className: "ak-btn", onClick: () => setRollbackConfirm(false), children: "Annulla" })] }), _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setReleaseOpen(false), children: "Chiudi" })] })] }), _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
281
|
: !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
263
282
|
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
|
|
264
283
|
if (editing?.slug === from)
|
|
@@ -283,13 +302,23 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
283
302
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
284
303
|
: _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
304
|
: !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,
|
|
305
|
+
: _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
306
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
288
307
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
289
308
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
|
290
309
|
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
|
-
|
|
310
|
+
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")
|
|
311
|
+
await pages.load(); if (section === "home")
|
|
312
|
+
await home.load(); if (section === "settings")
|
|
313
|
+
await settings.load(); await publish.reload(); }), children: "Scarta bozza" })] }), ((contentPersistenceMode() === "d1" && d1ContextPending)
|
|
314
|
+
|| (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) {
|
|
315
|
+
if (section === "pages")
|
|
316
|
+
await pages.load();
|
|
317
|
+
if (section === "home")
|
|
318
|
+
await home.load();
|
|
319
|
+
if (section === "settings")
|
|
320
|
+
await settings.load();
|
|
321
|
+
} }), title: publish.commits.map((c) => c.message).join("\n"), children: publish.publishing ? "Pubblico…"
|
|
293
322
|
: contentPersistenceMode() === "d1" ? contextualPublishLabel
|
|
294
323
|
: publish.ahead === 1 ? "Pubblica (1 modifica)"
|
|
295
324
|
: `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 +333,9 @@ function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
|
304
333
|
se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
|
|
305
334
|
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
335
|
: 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"
|
|
336
|
+
: _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
337
|
? _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
|
|
338
|
+
: _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
339
|
? _jsx(MediaSettingsEditor, { lib: media })
|
|
311
340
|
: _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
312
341
|
: openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { domToBlob } from "modern-screenshot";
|
|
3
|
+
import { StrictMode, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
function BugIcon() {
|
|
6
|
+
return _jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M9 4.5h6M10 2.5v2m4-2v2M7.5 9H4m3.5 4H4m12.5-4H20m-3.5 4H20M8 6.5h8v7.25a4 4 0 0 1-8 0V6.5Z" }), _jsx("path", { d: "M8 10.5h8M12 10.5v7" })] });
|
|
7
|
+
}
|
|
8
|
+
function DownloadIcon() {
|
|
9
|
+
return _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 3v11m0 0 4-4m-4 4-4-4M5 17v3h14v-3" }) });
|
|
10
|
+
}
|
|
11
|
+
function CloseIcon() {
|
|
12
|
+
return _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) });
|
|
13
|
+
}
|
|
14
|
+
function MailIcon() {
|
|
15
|
+
return _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M3.5 6.5h17v12h-17zM4 7l8 6 8-6" }) });
|
|
16
|
+
}
|
|
17
|
+
function WhatsAppIcon() {
|
|
18
|
+
return _jsxs("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M5.2 18.8 3.8 22l3.5-1.2A9 9 0 1 0 5.2 18.8Z" }), _jsx("path", { d: "M9 7.7c.2-.5.5-.5.8-.5h.5c.2 0 .4.1.5.4l.8 1.9c.1.3 0 .5-.2.7l-.6.7c-.2.2-.1.4 0 .6.7 1.2 1.6 2.1 2.8 2.7.2.1.4.1.6-.1l.8-1c.2-.2.4-.3.7-.2l1.8.9c.3.1.4.3.4.5 0 .4-.2 1.5-1.1 2.1-.8.5-1.8.7-3.4.2-1.2-.4-2.7-1.1-4.2-2.7-1.2-1.2-2.1-2.8-2.4-4-.3-1.2.2-1.9.5-2.2.3-.4.6-.5.8-.5" })] });
|
|
19
|
+
}
|
|
20
|
+
function IconButton({ children, label, onClick, disabled = false }) {
|
|
21
|
+
return _jsx("button", { type: "button", className: "ak-bug-icon-button", "aria-label": label, title: label, onClick: onClick, disabled: disabled, children: children });
|
|
22
|
+
}
|
|
23
|
+
async function capturePage() {
|
|
24
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
25
|
+
const scale = Math.min(1, 1600 / window.innerWidth, 1200 / window.innerHeight);
|
|
26
|
+
const blob = await domToBlob(document.body, {
|
|
27
|
+
type: "image/webp",
|
|
28
|
+
quality: .68,
|
|
29
|
+
backgroundColor: getComputedStyle(document.body).backgroundColor,
|
|
30
|
+
scale,
|
|
31
|
+
width: window.innerWidth,
|
|
32
|
+
height: window.innerHeight,
|
|
33
|
+
filter: (node) => !(node instanceof Element && node.closest("#ak-bug-report-root")),
|
|
34
|
+
features: { restoreScrollPosition: true },
|
|
35
|
+
fetch: { requestInit: { cache: "force-cache", credentials: "same-origin" } },
|
|
36
|
+
});
|
|
37
|
+
if (!blob)
|
|
38
|
+
throw new Error("Screenshot non disponibile");
|
|
39
|
+
const extension = blob.type === "image/webp" ? "webp" : "jpg";
|
|
40
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
41
|
+
return { blob, objectUrl: URL.createObjectURL(blob), filename: `bug-report-${day}.${extension}` };
|
|
42
|
+
}
|
|
43
|
+
function currentPageLabel() {
|
|
44
|
+
const hash = location.hash.replace(/^#\/?/, "");
|
|
45
|
+
return hash ? `${location.pathname}#/${hash}` : location.pathname;
|
|
46
|
+
}
|
|
47
|
+
function BugReport({ siteName, email, whatsapp }) {
|
|
48
|
+
const [enabled, setEnabled] = useState(() => document.documentElement.dataset.adminInterface === "next");
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
const [capturing, setCapturing] = useState(false);
|
|
51
|
+
const [sending, setSending] = useState(null);
|
|
52
|
+
const [message, setMessage] = useState("");
|
|
53
|
+
const [screenshot, setScreenshot] = useState(null);
|
|
54
|
+
const [uploaded, setUploaded] = useState(null);
|
|
55
|
+
const [error, setError] = useState(null);
|
|
56
|
+
const dialogRef = useRef(null);
|
|
57
|
+
const messageRef = useRef(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const observer = new MutationObserver(() => {
|
|
60
|
+
setEnabled(document.documentElement.dataset.adminInterface === "next");
|
|
61
|
+
});
|
|
62
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-admin-interface"] });
|
|
63
|
+
return () => observer.disconnect();
|
|
64
|
+
}, []);
|
|
65
|
+
useEffect(() => () => {
|
|
66
|
+
if (screenshot)
|
|
67
|
+
URL.revokeObjectURL(screenshot.objectUrl);
|
|
68
|
+
}, [screenshot]);
|
|
69
|
+
/* Ogni apertura è una nuova segnalazione. Lo screenshot viene già rigenerato; anche il testo
|
|
70
|
+
* deve perdere il report precedente quando il dialog viene chiuso, qualunque sia il gesto. */
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!open)
|
|
73
|
+
setMessage("");
|
|
74
|
+
}, [open]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!open)
|
|
77
|
+
return;
|
|
78
|
+
const app = document.getElementById("root");
|
|
79
|
+
if (app)
|
|
80
|
+
app.inert = true;
|
|
81
|
+
messageRef.current?.focus();
|
|
82
|
+
const keydown = (event) => {
|
|
83
|
+
if (event.key === "Escape" && !sending)
|
|
84
|
+
setOpen(false);
|
|
85
|
+
if (event.key !== "Tab")
|
|
86
|
+
return;
|
|
87
|
+
const focusable = [...(dialogRef.current?.querySelectorAll("button:not(:disabled), a[href], textarea:not(:disabled)") || [])];
|
|
88
|
+
if (!focusable.length)
|
|
89
|
+
return;
|
|
90
|
+
const first = focusable[0];
|
|
91
|
+
const last = focusable[focusable.length - 1];
|
|
92
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
last.focus();
|
|
95
|
+
}
|
|
96
|
+
else if (!event.shiftKey && document.activeElement === last) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
first.focus();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
addEventListener("keydown", keydown);
|
|
102
|
+
return () => {
|
|
103
|
+
if (app)
|
|
104
|
+
app.inert = false;
|
|
105
|
+
removeEventListener("keydown", keydown);
|
|
106
|
+
};
|
|
107
|
+
}, [open, sending]);
|
|
108
|
+
const begin = async () => {
|
|
109
|
+
if (capturing)
|
|
110
|
+
return;
|
|
111
|
+
setCapturing(true);
|
|
112
|
+
setError(null);
|
|
113
|
+
setUploaded(null);
|
|
114
|
+
try {
|
|
115
|
+
const next = await capturePage();
|
|
116
|
+
setScreenshot(next);
|
|
117
|
+
setOpen(true);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
setScreenshot(null);
|
|
121
|
+
setOpen(true);
|
|
122
|
+
setError("Non sono riuscito a creare lo screenshot. Riprova.");
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
setCapturing(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const retry = async () => {
|
|
129
|
+
setOpen(false);
|
|
130
|
+
await begin();
|
|
131
|
+
};
|
|
132
|
+
const upload = async () => {
|
|
133
|
+
if (uploaded)
|
|
134
|
+
return uploaded;
|
|
135
|
+
if (!screenshot)
|
|
136
|
+
throw new Error("Crea prima lo screenshot.");
|
|
137
|
+
const form = new FormData();
|
|
138
|
+
form.set("image", screenshot.blob, screenshot.filename);
|
|
139
|
+
form.set("site", siteName);
|
|
140
|
+
form.set("page", location.href);
|
|
141
|
+
form.set("title", document.title);
|
|
142
|
+
const response = await fetch("/admin/api/bug-report", { method: "POST", body: form });
|
|
143
|
+
const result = await response.json().catch(() => null);
|
|
144
|
+
if (!response.ok || !result?.url)
|
|
145
|
+
throw new Error(result?.error || "Non sono riuscito a caricare lo screenshot.");
|
|
146
|
+
setUploaded(result);
|
|
147
|
+
return result;
|
|
148
|
+
};
|
|
149
|
+
const reportText = (screenshotUrl) => [
|
|
150
|
+
message.trim(),
|
|
151
|
+
"",
|
|
152
|
+
`Screenshot: ${screenshotUrl}`,
|
|
153
|
+
`Pagina: ${currentPageLabel()}`,
|
|
154
|
+
`AdminKit · nuova interfaccia`,
|
|
155
|
+
`Data: ${new Intl.DateTimeFormat("it-IT", { dateStyle: "medium", timeStyle: "short" }).format(new Date())}`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
const send = async (channel) => {
|
|
158
|
+
if (!message.trim()) {
|
|
159
|
+
setError("Scrivi una breve descrizione del problema.");
|
|
160
|
+
messageRef.current?.focus();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!screenshot) {
|
|
164
|
+
setError("Crea prima lo screenshot.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const whatsappWindow = channel === "whatsapp" ? window.open("", "_blank") : null;
|
|
168
|
+
if (whatsappWindow)
|
|
169
|
+
whatsappWindow.opener = null;
|
|
170
|
+
setSending(channel);
|
|
171
|
+
setError(null);
|
|
172
|
+
try {
|
|
173
|
+
const saved = await upload();
|
|
174
|
+
const text = reportText(saved.url);
|
|
175
|
+
if (channel === "email") {
|
|
176
|
+
const subject = `Report Bug ${siteName}`;
|
|
177
|
+
location.href = `mailto:${encodeURIComponent(email)}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(text)}`;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const target = `https://wa.me/${whatsapp.replace(/\D/g, "")}?text=${encodeURIComponent(text)}`;
|
|
181
|
+
if (whatsappWindow)
|
|
182
|
+
whatsappWindow.location.replace(target);
|
|
183
|
+
else
|
|
184
|
+
window.open(target, "_blank", "noopener,noreferrer");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (reason) {
|
|
188
|
+
whatsappWindow?.close();
|
|
189
|
+
setError(reason instanceof Error ? reason.message : "Non sono riuscito a preparare il report.");
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
setSending(null);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
if (!enabled)
|
|
196
|
+
return null;
|
|
197
|
+
return _jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "ak-bug-trigger", "aria-label": "Segnala un bug", title: "Segnala un bug", onClick: () => void begin(), disabled: capturing, children: [_jsx(BugIcon, {}), capturing && _jsx("span", { className: "ak-bug-capturing", "aria-hidden": "true" })] }), open && _jsx("div", { className: "ak-bug-backdrop", onMouseDown: (event) => {
|
|
198
|
+
if (event.target === event.currentTarget && !sending)
|
|
199
|
+
setOpen(false);
|
|
200
|
+
}, children: _jsxs("div", { className: "ak-bug-dialog", ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-labelledby": "ak-bug-title", children: [_jsxs("header", { className: "ak-bug-head", children: [_jsxs("div", { children: [_jsx("span", { className: "ak-bug-kicker", children: "Segnalazione" }), _jsx("h2", { id: "ak-bug-title", children: "Riporta un problema" }), _jsx("p", { children: "Descrivi cosa non ha funzionato. La schermata ci aiuter\u00E0 a capirlo." })] }), _jsx(IconButton, { label: "Chiudi", onClick: () => setOpen(false), disabled: Boolean(sending), children: _jsx(CloseIcon, {}) })] }), _jsxs("div", { className: "ak-bug-content", children: [_jsxs("section", { className: "ak-bug-shot-card", "aria-label": "Screenshot del problema", children: [_jsxs("div", { className: "ak-bug-shot-head", children: [_jsx("strong", { children: "Schermata" }), screenshot && _jsxs("a", { className: "ak-bug-download", href: screenshot.objectUrl, download: screenshot.filename, children: [_jsx(DownloadIcon, {}), " Scarica"] })] }), screenshot
|
|
201
|
+
? _jsx("img", { src: screenshot.objectUrl, alt: "Anteprima della schermata segnalata" })
|
|
202
|
+
: _jsxs("div", { className: "ak-bug-shot-missing", children: [_jsx("span", { children: "Screenshot non disponibile" }), _jsx("button", { type: "button", onClick: () => void retry(), children: "Riprova" })] }), screenshot && _jsxs("small", { children: [Math.max(1, Math.round(screenshot.blob.size / 1024)), " KB \u00B7 il link scade dopo 30 giorni"] })] }), _jsxs("label", { className: "ak-bug-message", children: [_jsx("span", { children: "Cosa \u00E8 successo?" }), _jsx("textarea", { ref: messageRef, value: message, onChange: (event) => setMessage(event.target.value), placeholder: "Es. Ho premuto Salva, ma il titolo \u00E8 tornato come prima\u2026", rows: 8, maxLength: 3000 }), _jsxs("small", { children: [message.length, "/3000"] })] })] }), error && _jsx("p", { className: "ak-bug-error", role: "alert", children: error }), _jsxs("footer", { className: "ak-bug-actions", children: [_jsx("span", { children: sending ? "Carico lo screenshot e preparo il messaggio…" : "Lo screenshot sarà caricato solo quando invii." }), _jsxs("div", { children: [_jsxs("button", { type: "button", className: "ak-bug-send is-email", disabled: Boolean(sending) || !screenshot, onClick: () => void send("email"), children: [_jsx(MailIcon, {}), sending === "email" ? "Preparo…" : "Invia email"] }), whatsapp && _jsxs("button", { type: "button", className: "ak-bug-send is-whatsapp", disabled: Boolean(sending) || !screenshot, onClick: () => void send("whatsapp"), children: [_jsx(WhatsAppIcon, {}), sending === "whatsapp" ? "Preparo…" : "Invia WhatsApp"] })] })] })] }) })] });
|
|
203
|
+
}
|
|
204
|
+
export function mountBugReport(options) {
|
|
205
|
+
let host = document.getElementById("ak-bug-report-root");
|
|
206
|
+
if (!host) {
|
|
207
|
+
host = document.createElement("div");
|
|
208
|
+
host.id = "ak-bug-report-root";
|
|
209
|
+
document.body.append(host);
|
|
210
|
+
}
|
|
211
|
+
createRoot(host).render(_jsx(StrictMode, { children: _jsx(BugReport, { ...options }) }));
|
|
212
|
+
}
|
package/dist/app/app.css
CHANGED
|
@@ -2229,3 +2229,127 @@ body:has(.uk) .pane-body { padding-bottom: 190px; }
|
|
|
2229
2229
|
.ak-conflict-image { display: grid; gap: 6px; margin-top: 5px; }.ak-conflict-image img { width: 72px; height: 48px; object-fit: cover; border-radius: 3px; }.ak-conflict-image code { overflow-wrap: anywhere; font-size: 11px; }
|
|
2230
2230
|
.ak-conflict-dialog footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
|
|
2231
2231
|
@media (max-width: 620px) { .ak-conflict-options { grid-template-columns: 1fr; } .ak-conflict-backdrop { padding: 12px; } }
|
|
2232
|
+
|
|
2233
|
+
/* Direct SSR publication status lives with the global site controls. */
|
|
2234
|
+
.ak-release-control{position:relative;flex:none}.ak-release-trigger{min-width:104px;height:48px;display:inline-flex;align-items:center;justify-content:center;gap:9px;padding:0 14px;border:0;border-radius:14px;background:color-mix(in srgb,var(--ak-rail-bg) 74%,var(--ak-line));color:var(--ak-rail-ink-on);cursor:pointer;font:600 12px/1 var(--ak-font);white-space:nowrap}.ak-release-dot{width:8px;height:8px;border-radius:50%;background:#c68b3c}.ak-release-dot.is-ok{background:#35a66f}.ak-release-menu{position:absolute;z-index:100;top:calc(100% + 10px);right:0;width:min(330px,calc(100vw - 24px));display:flex;flex-direction:column;gap:14px;padding:18px;border:0;border-radius:18px;background:var(--ak-panel);color:var(--ak-ink);box-shadow:var(--ak-lift-3);font:400 13px/1.5 var(--ak-font)}.ak-release-menu p{margin:0;color:var(--ak-ink-quiet)}.ak-release-menu dl{margin:0;padding:12px;border-radius:12px;background:var(--ak-inset)}.ak-release-menu dl>div{display:flex;justify-content:space-between;gap:16px}.ak-release-menu dd{margin:0;font-family:var(--ak-font-mono)}:root[data-admin-interface="next"] .rail{overflow:visible}
|
|
2235
|
+
|
|
2236
|
+
/* Bug report della nuova interfaccia: il root vive fuori dall'app per poter sfocare l'intero
|
|
2237
|
+
* pannello senza includere il dialog nello screenshot. Nella UI corrente non viene renderizzato. */
|
|
2238
|
+
.ak-bug-trigger {
|
|
2239
|
+
position: fixed; z-index: 70; right: 18px; bottom: 18px;
|
|
2240
|
+
width: 46px; height: 46px; display: grid; place-items: center;
|
|
2241
|
+
border: 0; border-radius: 15px; background: var(--ak-ink); color: var(--ak-panel);
|
|
2242
|
+
box-shadow: 0 5px 18px rgba(35, 30, 32, .18); cursor: pointer;
|
|
2243
|
+
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
|
2244
|
+
}
|
|
2245
|
+
.ak-bug-trigger:hover { transform: translateY(-2px); box-shadow: 0 9px 25px rgba(35, 30, 32, .23); }
|
|
2246
|
+
.ak-bug-trigger:focus-visible { outline: 0; box-shadow: var(--ak-ring), 0 9px 25px rgba(35, 30, 32, .23); }
|
|
2247
|
+
.ak-bug-trigger:disabled { cursor: wait; opacity: .78; }
|
|
2248
|
+
.ak-bug-trigger svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
2249
|
+
.ak-bug-capturing {
|
|
2250
|
+
position: absolute; inset: 5px; border: 2px solid transparent; border-top-color: currentColor;
|
|
2251
|
+
border-radius: 12px; animation: ak-bug-spin .8s linear infinite;
|
|
2252
|
+
}
|
|
2253
|
+
@keyframes ak-bug-spin { to { transform: rotate(1turn); } }
|
|
2254
|
+
|
|
2255
|
+
.ak-bug-backdrop {
|
|
2256
|
+
position: fixed; z-index: 200; inset: 0; display: grid; place-items: center; padding: 24px;
|
|
2257
|
+
background: rgba(31, 29, 30, .2); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
|
|
2258
|
+
animation: ak-bug-backdrop-in 180ms ease-out both;
|
|
2259
|
+
}
|
|
2260
|
+
@keyframes ak-bug-backdrop-in { from { opacity: 0; } }
|
|
2261
|
+
.ak-bug-dialog {
|
|
2262
|
+
width: min(780px, 100%); max-height: min(760px, calc(100vh - 48px)); overflow: auto;
|
|
2263
|
+
display: flex; flex-direction: column; gap: 20px; padding: 24px;
|
|
2264
|
+
border: 0; border-radius: 24px; background: var(--ak-panel); color: var(--ak-ink);
|
|
2265
|
+
box-shadow: 0 28px 80px -28px rgba(18, 15, 16, .48); font-family: var(--ak-font);
|
|
2266
|
+
animation: ak-bug-dialog-in 220ms cubic-bezier(.2, .8, .2, 1) both;
|
|
2267
|
+
}
|
|
2268
|
+
@keyframes ak-bug-dialog-in {
|
|
2269
|
+
from { opacity: 0; transform: translateY(12px) scale(.985); }
|
|
2270
|
+
}
|
|
2271
|
+
.ak-bug-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; }
|
|
2272
|
+
.ak-bug-head > div { min-width: 0; }
|
|
2273
|
+
.ak-bug-kicker {
|
|
2274
|
+
display: block; margin-bottom: 6px; color: var(--ak-accent);
|
|
2275
|
+
font-size: 10px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
|
|
2276
|
+
}
|
|
2277
|
+
.ak-bug-head h2 { margin: 0; font-size: 22px; line-height: 1.15; letter-spacing: -.025em; }
|
|
2278
|
+
.ak-bug-head p { max-width: 560px; margin: 7px 0 0; color: var(--ak-ink-quiet); font-size: 13px; line-height: 1.45; }
|
|
2279
|
+
.ak-bug-icon-button {
|
|
2280
|
+
width: 40px; height: 40px; flex: none; display: grid; place-items: center;
|
|
2281
|
+
border: 0; border-radius: 12px; background: var(--ak-inset); color: var(--ak-ink-quiet); cursor: pointer;
|
|
2282
|
+
}
|
|
2283
|
+
.ak-bug-icon-button:hover { color: var(--ak-ink); background: var(--ak-inset-deep); }
|
|
2284
|
+
.ak-bug-icon-button:focus-visible { outline: 0; box-shadow: var(--ak-ring); }
|
|
2285
|
+
.ak-bug-icon-button:disabled { opacity: .5; cursor: default; }
|
|
2286
|
+
.ak-bug-icon-button svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
|
|
2287
|
+
|
|
2288
|
+
.ak-bug-content { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: 18px; }
|
|
2289
|
+
.ak-bug-shot-card {
|
|
2290
|
+
min-width: 0; display: flex; flex-direction: column; gap: 10px; padding: 12px;
|
|
2291
|
+
border-radius: 17px; background: var(--ak-inset);
|
|
2292
|
+
}
|
|
2293
|
+
.ak-bug-shot-head { min-height: 30px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
2294
|
+
.ak-bug-shot-head strong { font-size: 12px; font-weight: 650; }
|
|
2295
|
+
.ak-bug-download {
|
|
2296
|
+
min-height: 30px; display: inline-flex; align-items: center; gap: 6px; padding: 0 9px;
|
|
2297
|
+
border-radius: 9px; background: var(--ak-panel); color: var(--ak-ink); font-size: 11px; font-weight: 600;
|
|
2298
|
+
text-decoration: none;
|
|
2299
|
+
}
|
|
2300
|
+
.ak-bug-download:hover { background: var(--ak-inset-deep); }
|
|
2301
|
+
.ak-bug-download:focus-visible { outline: 0; box-shadow: var(--ak-ring); }
|
|
2302
|
+
.ak-bug-download svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
2303
|
+
.ak-bug-shot-card img {
|
|
2304
|
+
width: 100%; aspect-ratio: 16 / 10; display: block; object-fit: contain;
|
|
2305
|
+
border-radius: 11px; background: var(--ak-inset-deep);
|
|
2306
|
+
}
|
|
2307
|
+
.ak-bug-shot-card small { color: var(--ak-ink-faint); font-size: 10px; line-height: 1.35; }
|
|
2308
|
+
.ak-bug-shot-missing {
|
|
2309
|
+
min-height: 185px; display: grid; place-items: center; align-content: center; gap: 10px;
|
|
2310
|
+
border-radius: 11px; color: var(--ak-ink-quiet); font-size: 12px; text-align: center;
|
|
2311
|
+
}
|
|
2312
|
+
.ak-bug-shot-missing button { border: 0; background: transparent; color: var(--ak-accent); font: 600 12px var(--ak-font); cursor: pointer; }
|
|
2313
|
+
|
|
2314
|
+
.ak-bug-message { min-width: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
2315
|
+
.ak-bug-message > span { font-size: 12px; font-weight: 650; }
|
|
2316
|
+
.ak-bug-message textarea {
|
|
2317
|
+
min-height: 218px; width: 100%; resize: vertical; padding: 13px 14px;
|
|
2318
|
+
border: 1px solid transparent; border-radius: 15px; background: var(--ak-inset); color: var(--ak-ink);
|
|
2319
|
+
font: 400 13px/1.5 var(--ak-font);
|
|
2320
|
+
}
|
|
2321
|
+
.ak-bug-message textarea::placeholder { color: var(--ak-ink-faint); }
|
|
2322
|
+
.ak-bug-message textarea:focus { outline: 0; border-color: color-mix(in srgb, var(--ak-accent) 30%, transparent); box-shadow: var(--ak-ring); background: var(--ak-panel); }
|
|
2323
|
+
.ak-bug-message small { align-self: flex-end; color: var(--ak-ink-faint); font-size: 10px; }
|
|
2324
|
+
.ak-bug-error { margin: -5px 0 0; padding: 10px 12px; border-radius: 11px; background: color-mix(in srgb, var(--ak-danger) 11%, transparent); color: var(--ak-danger); font-size: 12px; }
|
|
2325
|
+
|
|
2326
|
+
.ak-bug-actions { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding-top: 18px; border-top: 1px solid var(--ak-line); }
|
|
2327
|
+
.ak-bug-actions > span { color: var(--ak-ink-faint); font-size: 10px; line-height: 1.4; }
|
|
2328
|
+
.ak-bug-actions > div { flex: none; display: flex; gap: 8px; }
|
|
2329
|
+
.ak-bug-send {
|
|
2330
|
+
min-height: 42px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
2331
|
+
padding: 0 14px; border: 0; border-radius: 12px; background: var(--ak-inset); color: var(--ak-ink);
|
|
2332
|
+
font: 650 12px/1 var(--ak-font); cursor: pointer;
|
|
2333
|
+
}
|
|
2334
|
+
.ak-bug-send.is-whatsapp { background: var(--ak-accent); color: var(--ak-accent-ink); }
|
|
2335
|
+
.ak-bug-send:hover { filter: brightness(.97); }
|
|
2336
|
+
.ak-bug-send:focus-visible { outline: 0; box-shadow: var(--ak-ring); }
|
|
2337
|
+
.ak-bug-send:disabled { opacity: .5; cursor: wait; }
|
|
2338
|
+
.ak-bug-send svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
2339
|
+
|
|
2340
|
+
@media (max-width: 680px) {
|
|
2341
|
+
.ak-bug-trigger { right: 12px; bottom: 12px; }
|
|
2342
|
+
.ak-bug-backdrop { padding: 10px; place-items: end center; }
|
|
2343
|
+
.ak-bug-dialog { max-height: calc(100vh - 20px); padding: 20px; border-radius: 22px; }
|
|
2344
|
+
.ak-bug-content { grid-template-columns: minmax(0, 1fr); }
|
|
2345
|
+
.ak-bug-shot-card img { max-height: 220px; }
|
|
2346
|
+
.ak-bug-message textarea { min-height: 140px; }
|
|
2347
|
+
.ak-bug-actions { align-items: stretch; flex-direction: column; }
|
|
2348
|
+
.ak-bug-actions > div { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2352
|
+
.ak-bug-trigger, .ak-bug-backdrop, .ak-bug-dialog { animation: none; transition: none; }
|
|
2353
|
+
}
|
|
2354
|
+
:root[data-admin-interface="next"] .rail-foot > .ak-release-control{width:auto;height:auto;overflow:visible}
|
|
2355
|
+
:root[data-admin-interface="next"] .rail-foot > .ak-release-control > .ak-release-trigger{width:auto;min-width:104px;display:inline-flex;gap:9px;padding:0 14px;color:var(--ak-rail-ink-on);font:600 12px/1 var(--ak-font);overflow:visible}
|
package/dist/app/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
15
15
|
import { StrictMode } from "react";
|
|
16
16
|
import { createRoot } from "react-dom/client";
|
|
17
17
|
import { setStoragePrefix } from "../index.js";
|
|
18
|
+
import { mountBugReport } from "./BugReport.js";
|
|
18
19
|
import { App } from "./App.js";
|
|
19
20
|
import { applyTheme, initialTheme } from "./lib/theme.js";
|
|
20
21
|
import { setSiteConfig } from "./site.js";
|
|
@@ -31,4 +32,6 @@ export function mountAdmin({ site, root, storagePrefix, workspace }) {
|
|
|
31
32
|
throw new Error("adminkit: elemento di montaggio non trovato (atteso #root)");
|
|
32
33
|
const Workspace = workspace ?? App;
|
|
33
34
|
createRoot(el).render(_jsx(StrictMode, { children: _jsx(Workspace, {}) }));
|
|
35
|
+
if (site.bugReport)
|
|
36
|
+
mountBugReport({ siteName: site.name, email: site.bugReport.email, whatsapp: site.bugReport.whatsapp || "" });
|
|
34
37
|
}
|
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,13 @@ 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
|
+
activeReleaseId: string | null;
|
|
24
|
+
verified: boolean;
|
|
25
|
+
rollback(): Promise<void>;
|
|
26
|
+
verify(): Promise<void>;
|
|
20
27
|
}
|
|
21
28
|
export declare function contentPublicationNotice(result: {
|
|
22
29
|
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,67 @@ 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 [verified, setVerified] = useState(false);
|
|
29
|
+
const [approval, setApproval] = useState(null);
|
|
30
|
+
const [activeReleaseId, setActiveReleaseId] = useState(null);
|
|
31
|
+
const [previousReleaseId, setPreviousReleaseId] = useState(null);
|
|
32
|
+
const previewApproved = (target) => !!target && "documentId" in target && !!approval && approval.documentId === target.documentId && approval.expectedDraftRevisionId === target.expectedDraftRevisionId && approval.expectedBaseReleaseId === activeReleaseId;
|
|
33
|
+
const preview = async (target) => {
|
|
34
|
+
if (!("documentId" in target))
|
|
35
|
+
return;
|
|
36
|
+
setPublishing(true);
|
|
37
|
+
setNotice(null);
|
|
38
|
+
try {
|
|
39
|
+
const result = await api.contentPreview(target);
|
|
40
|
+
setApproval({ ...result, documentId: target.documentId });
|
|
41
|
+
window.open(result.previewUrl, "_blank", "noopener");
|
|
42
|
+
setNotice("Anteprima della bozza salvata pronta.");
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
setApproval(null);
|
|
46
|
+
setNotice(error.message);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
setPublishing(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const rollback = async () => {
|
|
53
|
+
if (!activeReleaseId || !previousReleaseId)
|
|
54
|
+
return;
|
|
55
|
+
setPublishing(true);
|
|
56
|
+
setNotice(null);
|
|
57
|
+
try {
|
|
58
|
+
await api.contentRollback(activeReleaseId, previousReleaseId);
|
|
59
|
+
setApproval(null);
|
|
60
|
+
await reload();
|
|
61
|
+
setNotice("Ripristinata la release precedente. Le bozze sono conservate.");
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
setNotice(error.message);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
setPublishing(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const verify = async () => {
|
|
71
|
+
if (!activeReleaseId)
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
const result = await api.contentVerification(activeReleaseId);
|
|
75
|
+
setVerified(result.publicOriginVerified);
|
|
76
|
+
setNotice(result.publicOriginVerified ? "Release verificata sul dominio pubblico." : result.reason);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
setNotice(error.message);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
28
82
|
const reload = useCallback(async () => {
|
|
29
83
|
if (contentPersistenceMode() === "d1") {
|
|
30
84
|
try {
|
|
31
85
|
const d = await api.contentPublishState();
|
|
86
|
+
setActiveReleaseId(d.activeReleaseId ?? null);
|
|
87
|
+
setPreviousReleaseId(d.previousReleaseId ?? null);
|
|
88
|
+
setVerified(d.verified === true);
|
|
32
89
|
setAhead(d.ahead ?? 0);
|
|
33
90
|
setDocuments(d.documents ?? []);
|
|
34
91
|
setSiteAccessPending(Boolean(d.siteAccess));
|
|
@@ -78,7 +135,10 @@ export function usePublish() {
|
|
|
78
135
|
try {
|
|
79
136
|
if (!target)
|
|
80
137
|
throw new ApiError("Apri il contenuto che vuoi pubblicare.", "invalid_request", 400);
|
|
81
|
-
|
|
138
|
+
if (site().directPublication && !previewApproved(target))
|
|
139
|
+
throw new ApiError("Apri una nuova anteprima della bozza prima di pubblicare.", "stale_preview", 409);
|
|
140
|
+
const r = await api.contentPublish(site().directPublication ? { ...target, ...approval } : target);
|
|
141
|
+
setApproval(null);
|
|
82
142
|
setNotice(contentPublicationNotice(r));
|
|
83
143
|
await reload();
|
|
84
144
|
}
|
|
@@ -113,9 +173,9 @@ export function usePublish() {
|
|
|
113
173
|
finally {
|
|
114
174
|
setPublishing(false);
|
|
115
175
|
}
|
|
116
|
-
}, [reload]);
|
|
176
|
+
}, [reload, approval, activeReleaseId]);
|
|
117
177
|
return {
|
|
118
178
|
ahead, commits, documents, siteAccessPending, configured, publishing, notice, error,
|
|
119
|
-
reload, publish, clearNotice: () => setNotice(null),
|
|
179
|
+
reload, publish, preview, previewApproved, previousReleaseId, activeReleaseId, verified, rollback, verify, clearNotice: () => setNotice(null),
|
|
120
180
|
};
|
|
121
181
|
}
|
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,18 @@ 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;
|
|
55
|
+
/** Optional support workflow: screenshot stays local until the user sends. */
|
|
56
|
+
bugReport?: {
|
|
57
|
+
email: string;
|
|
58
|
+
whatsapp?: string;
|
|
59
|
+
};
|
|
42
60
|
/** Optional site-owned Home surface. The kit keeps its legacy visual editor
|
|
43
61
|
* as a fallback, while a site with a different document shape can render its
|
|
44
62
|
* own schema-driven editor without teaching the shared package its fields. */
|
|
@@ -17,4 +17,6 @@ export declare function commitDirectRelease(db: D1DatabaseLike, input: {
|
|
|
17
17
|
}[];
|
|
18
18
|
authorSubject: string;
|
|
19
19
|
now: number;
|
|
20
|
+
/** Trusted site-owned transactional preconditions; never from an HTTP payload. */
|
|
21
|
+
guards?: readonly D1Statement[];
|
|
20
22
|
}): Promise<void>;
|
|
@@ -33,6 +33,7 @@ export async function commitDirectRelease(db, input) {
|
|
|
33
33
|
const statements = [
|
|
34
34
|
transactionAssertion(db, `EXISTS(SELECT 1 FROM content_site_state WHERE singleton=1 AND current_release_id=?) AND NOT EXISTS(SELECT 1 FROM content_release_site_access WHERE release_id=?) AND NOT EXISTS(SELECT 1 FROM production_deployment_attempts WHERE phase NOT IN ('succeeded','failed','superseded'))`, [expectedBaseReleaseId, expectedBaseReleaseId]),
|
|
35
35
|
];
|
|
36
|
+
statements.push(...(input.guards ?? []));
|
|
36
37
|
for (const d of selectedDrafts)
|
|
37
38
|
statements.push(transactionAssertion(db, "EXISTS(SELECT 1 FROM content_documents WHERE document_id=? AND draft_revision_id=?)", [d.documentId, d.revisionId]));
|
|
38
39
|
for (const e of release.entries)
|
|
@@ -81,6 +81,12 @@ export function MediaFolders({ lib, trash, settings }) {
|
|
|
81
81
|
trash.leave(); lib.go(p); };
|
|
82
82
|
const drop = (dest) => (e) => {
|
|
83
83
|
setOver(null);
|
|
84
|
+
const ids = readDragged(e.dataTransfer);
|
|
85
|
+
if (canMove && ids.length) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
void lib.move(ids, dest);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
84
90
|
if (canUpload && e.dataTransfer.files.length) {
|
|
85
91
|
e.preventDefault();
|
|
86
92
|
if (lib.mode === "pick")
|
|
@@ -92,7 +98,6 @@ export function MediaFolders({ lib, trash, settings }) {
|
|
|
92
98
|
if (!canMove)
|
|
93
99
|
return;
|
|
94
100
|
e.preventDefault();
|
|
95
|
-
const ids = readDragged(e.dataTransfer);
|
|
96
101
|
if (ids.length)
|
|
97
102
|
void lib.move(ids, dest);
|
|
98
103
|
};
|
|
@@ -173,7 +173,7 @@ export function MediaGrid({ lib, notice, onPick, onCancel }) {
|
|
|
173
173
|
e.dataTransfer.setData(DRAG_FOLDERS, JSON.stringify(dirs));
|
|
174
174
|
e.dataTransfer.effectAllowed = "move";
|
|
175
175
|
}, children: [_jsxs("div", { className: "mg-frame", onClick: () => { if (!locked)
|
|
176
|
-
(manage ? lib.view(i) : lib.toggle(f.key)); }, children: [_jsx("img", { className: "mg-img", src: f.url, alt: "", loading: "lazy",
|
|
176
|
+
(manage ? lib.view(i) : lib.toggle(f.key)); }, children: [_jsx("img", { className: "mg-img", draggable: false, src: f.url, alt: "", loading: "lazy",
|
|
177
177
|
/* Il rapporto è noto prima che l'immagine arrivi (lo dà l'elenco): la cella
|
|
178
178
|
riserva subito l'ingombro giusto e la griglia non salta mentre si carica. */
|
|
179
179
|
style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), manage && altStatus && (_jsx("span", { className: `mg-alt-badge is-${altStatus}`, children: altStatus === "missing" ? "ALT da completare" : altStatus === "decorative" ? "Decorativa" : "ALT completo" })), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardinali-studio/adminkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.35",
|
|
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",
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"zod": "^4.4.3"
|
|
109
109
|
},
|
|
110
110
|
"dependencies": {
|
|
111
|
-
"heic-to": "^1.3.0"
|
|
111
|
+
"heic-to": "^1.3.0",
|
|
112
|
+
"modern-screenshot": "4.7.0"
|
|
112
113
|
}
|
|
113
114
|
}
|