@cardinali-studio/adminkit 2.2.25 → 2.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +19 -0
- package/dist/api.js +3 -0
- package/dist/app/App.js +22 -8
- package/dist/app/app.css +3 -3
- package/dist/app/delivery.d.ts +11 -0
- package/dist/app/delivery.js +46 -0
- package/dist/app/index.d.ts +4 -1
- package/dist/app/index.js +3 -2
- package/dist/app/lib/bookings.js +5 -0
- package/dist/app/lib/deliveries.d.ts +8 -0
- package/dist/app/lib/deliveries.js +7 -0
- package/dist/app/lib/delivery-network.d.ts +25 -0
- package/dist/app/lib/delivery-network.js +159 -0
- package/dist/app/lib/delivery-upload.d.ts +29 -11
- package/dist/app/lib/delivery-upload.js +408 -131
- package/dist/app/lib/home.js +7 -4
- package/dist/app/lib/pages.js +2 -1
- package/dist/app/lib/publish.d.ts +5 -0
- package/dist/app/lib/publish.js +61 -4
- package/dist/app/lib/requests.js +6 -0
- package/dist/app/lib/settings.js +7 -3
- package/dist/app/lib/upload-queue.d.ts +21 -3
- package/dist/app/lib/upload-queue.js +76 -19
- package/dist/app/routes/Deliveries.js +108 -29
- 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/routes/UploadDock.js +6 -2
- package/dist/app/site.d.ts +13 -0
- package/dist/direct-publication.d.ts +20 -0
- package/dist/direct-publication.js +75 -0
- package/dist/functions/_shared/b2.js +22 -3
- package/dist/functions/_shared/delivery-crypto.js +5 -0
- package/dist/functions/_shared/delivery-limits.d.ts +9 -0
- package/dist/functions/_shared/delivery-limits.js +29 -0
- package/dist/functions/_shared/delivery-manifest-v2.sql +49 -0
- package/dist/functions/_shared/delivery-manifest.js +392 -0
- package/dist/functions/_shared/delivery-purge.js +17 -1
- package/dist/functions/api/deliveries.js +177 -49
- package/dist/functions/api/delivery/image.js +11 -6
- package/dist/functions/api/delivery/multipart.js +196 -64
- package/dist/functions/delivery-public/assets/client.js +246 -0
- package/dist/functions/delivery-public/assets/delivery.css +297 -0
- package/dist/functions/delivery-public/assets/gallery.js +243 -0
- package/dist/functions/delivery-public/assets/stage.js +107 -0
- package/dist/functions/delivery-public/index.js +637 -0
- package/dist/functions/delivery-public/migrations/0002_deliveries.sql +128 -0
- package/dist/functions/delivery-public/migrations/0003_delivery_quick_hero.sql +34 -0
- package/dist/functions/delivery-public/migrations/0004_delivery_file_expiry.sql +9 -0
- package/dist/functions/delivery-public/migrations/0006_delivery_manifest_v2.sql +49 -0
- package/dist/functions/delivery-public/migrations/0007_delivery_link_aliases.sql +8 -0
- package/dist/functions/delivery-public/migrations/0008_delivery_web_format.sql +2 -0
- package/dist/functions/delivery-public/migrations/0009_delivery_download_labels.sql +2 -0
- package/dist/functions/index.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +10 -3
|
@@ -22,7 +22,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
22
22
|
* Il vocabolario completo (e il perché di ogni parola) sta in DELIVERY-REDESIGN.md §2.
|
|
23
23
|
*/
|
|
24
24
|
import { useEffect, useRef, useState } from "react";
|
|
25
|
-
import { deliverySelectionName, deliverySourceFingerprint, uploadDeliveryHeroObject, } from "../lib/delivery-upload";
|
|
25
|
+
import { deliverySelectionName, deliverySourceFingerprint, uploadDeliveryHeroObject, preflightDeliverySelection, } from "../lib/delivery-upload";
|
|
26
26
|
import { isBusy, startUpload, uploadFor, useUploadCompletions, useUploads, } from "../lib/upload-queue";
|
|
27
27
|
import { filesFromDrop } from "../lib/delivery-drop";
|
|
28
28
|
/* ============================================================================================
|
|
@@ -56,12 +56,30 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
56
56
|
const [heroes, setHeroes] = useState(fromServer?.heroes || []);
|
|
57
57
|
const [heroesClean, setHeroesClean] = useState(fromServer?.heroes || []);
|
|
58
58
|
const [saving, setSaving] = useState(false);
|
|
59
|
+
const [jpegEnabled, setJpegEnabled] = useState(false);
|
|
60
|
+
const [downloadLabelsEnabled, setDownloadLabelsEnabled] = useState(false);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
fetch("/admin/api/deliveries?capabilities=1", { signal: controller.signal, credentials: "same-origin" })
|
|
64
|
+
.then(response => response.ok ? response.json() : null)
|
|
65
|
+
.then(value => {
|
|
66
|
+
if (!controller.signal.aborted) {
|
|
67
|
+
setJpegEnabled(value?.jpegEnabled === true);
|
|
68
|
+
setDownloadLabelsEnabled(value?.downloadLabelsEnabled === true);
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.catch(() => { });
|
|
72
|
+
return () => controller.abort();
|
|
73
|
+
}, []);
|
|
59
74
|
const [failure, setFailure] = useState(null);
|
|
60
75
|
const [notice, setNotice] = useState(null);
|
|
61
76
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
62
77
|
/* Anche «Rimuovi» su un pacchetto cancella i byte all'istante: chiede conferma
|
|
63
78
|
* come il fondo pagina, ma dentro la riga, che è dove si è guardato. */
|
|
64
79
|
const [confirmPackage, setConfirmPackage] = useState(null);
|
|
80
|
+
const [moreImages, setMoreImages] = useState({});
|
|
81
|
+
const [loadingImages, setLoadingImages] = useState(false);
|
|
82
|
+
const [previewAttempt, setPreviewAttempt] = useState(0);
|
|
65
83
|
const [copied, setCopied] = useState(null);
|
|
66
84
|
const folderRef = useRef(null);
|
|
67
85
|
const filesRef = useRef(null);
|
|
@@ -76,8 +94,34 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
76
94
|
const jobs = useUploads();
|
|
77
95
|
const job = delivery ? uploadFor(delivery.id, jobs) : undefined;
|
|
78
96
|
const uploading = isBusy(job) || startingRef.current;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
97
|
+
const heroesDirty = !sameHeroes(heroes, heroesClean);
|
|
98
|
+
const dirty = !same(draft, clean) || heroesDirty;
|
|
99
|
+
const photos = packages.filter(pkg => pkg.state === "ready").flatMap((pkg) => [...pkg.images, ...(moreImages[pkg.id]?.images || [])].map((image) => ({ pkg, image })));
|
|
100
|
+
async function loadMoreImages() {
|
|
101
|
+
if (loadingImages || !delivery)
|
|
102
|
+
return;
|
|
103
|
+
setLoadingImages(true);
|
|
104
|
+
try {
|
|
105
|
+
for (const pkg of packages) {
|
|
106
|
+
const cursor = moreImages[pkg.id] ? moreImages[pkg.id].nextCursor : pkg.nextCursor;
|
|
107
|
+
if (cursor == null)
|
|
108
|
+
continue;
|
|
109
|
+
const page = await onAction({
|
|
110
|
+
action: "manifestPage", id: delivery.id, packageId: pkg.id, after: cursor,
|
|
111
|
+
}, { reload: false });
|
|
112
|
+
setMoreImages(current => ({ ...current, [pkg.id]: {
|
|
113
|
+
images: [...(current[pkg.id]?.images || []), ...page.images], nextCursor: page.nextCursor,
|
|
114
|
+
} }));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
setFailure(errorText(error));
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
setLoadingImages(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
useEffect(() => { setMoreImages({}); }, [delivery?.id]);
|
|
81
125
|
useEffect(() => {
|
|
82
126
|
if (!fromServer)
|
|
83
127
|
return;
|
|
@@ -105,7 +149,8 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
105
149
|
const change = (patch) => setDraft((current) => ({ ...current, ...patch }));
|
|
106
150
|
/* ---- caricamento ------------------------------------------------------------------- */
|
|
107
151
|
/* Qui si prepara e si consegna alla coda: transfer, pacchetto, impronta. Poi si esce
|
|
108
|
-
|
|
152
|
+
dalla preparazione; la promessa resta aperta per conservare il file input fino a lettura finita.
|
|
153
|
+
Il caricamento vero, la chiusura del pacchetto e l'attivazione li fa la coda,
|
|
109
154
|
che non muore quando questa schermata sparisce. */
|
|
110
155
|
const beginUpload = async (files, resume = null) => {
|
|
111
156
|
if (!files.length || startingRef.current)
|
|
@@ -115,16 +160,23 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
115
160
|
setNotice(null);
|
|
116
161
|
let target = delivery;
|
|
117
162
|
let packageId = resume?.id || "";
|
|
118
|
-
let options = { galleryEnabled: draft.gallery, webEnabled: draft.light };
|
|
163
|
+
let options = resume ? { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled, webFormat: resume.webFormat || "webp" } : { galleryEnabled: draft.gallery, webEnabled: draft.light, webFormat: draft.light ? draft.webFormat : "webp" };
|
|
119
164
|
try {
|
|
165
|
+
// Verifica i limiti fisici prima della richiesta delle capacità del sito.
|
|
166
|
+
preflightDeliverySelection(files, { ...options, manifestVersion: 2 });
|
|
167
|
+
const capabilities = resume ? { manifestVersion: resume.manifestVersion || 1 } :
|
|
168
|
+
await onAction({ action: "capabilities" }, { reload: false });
|
|
169
|
+
options.manifestVersion = capabilities.manifestVersion;
|
|
170
|
+
const { sourceBytes } = preflightDeliverySelection(files, options);
|
|
120
171
|
/* L'impronta si legge PRIMA di creare qualsiasi cosa: se il browser ha perso il
|
|
121
172
|
permesso su un file, non resta un transfer a metà né un oggetto orfano. */
|
|
122
|
-
const fingerprint = await deliverySourceFingerprint(files);
|
|
173
|
+
const fingerprint = await deliverySourceFingerprint(files, options.manifestVersion);
|
|
123
174
|
const title = draft.title.trim() || deliverySelectionName(files) || "Transfer";
|
|
124
175
|
if (!target) {
|
|
125
176
|
change({ title });
|
|
126
177
|
target = (await onAction({
|
|
127
178
|
action: "quickCreate", title, filesExpiresAt: expiryFrom(draft.duration),
|
|
179
|
+
...(downloadLabelsEnabled ? { downloadOriginalsLabel: draft.downloadOriginalsLabel, downloadWebLabel: draft.downloadWebLabel } : {}),
|
|
128
180
|
}, { reload: false })).delivery;
|
|
129
181
|
setMade(target);
|
|
130
182
|
}
|
|
@@ -133,18 +185,25 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
133
185
|
throw new Error("Questi non sono gli stessi file di prima. Riseleziona esattamente la cartella " +
|
|
134
186
|
"(o i file) del caricamento interrotto.");
|
|
135
187
|
}
|
|
136
|
-
options = { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled };
|
|
188
|
+
options = { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled, webFormat: resume.webFormat || "webp", manifestVersion: resume.manifestVersion || 1 };
|
|
137
189
|
}
|
|
138
190
|
else {
|
|
139
191
|
const created = await onAction({
|
|
140
192
|
action: "createPackage", id: target.id, label: title,
|
|
141
193
|
...options, sourceFingerprint: fingerprint, fileCount: files.length,
|
|
142
|
-
sourceBytes
|
|
194
|
+
sourceBytes,
|
|
143
195
|
}, { reload: false });
|
|
144
196
|
packageId = created.package.id;
|
|
197
|
+
options.webFormat = created.package.webFormat || "webp";
|
|
198
|
+
}
|
|
199
|
+
const completion = startUpload({ deliveryId: target.id, packageId, title, files, options });
|
|
200
|
+
try {
|
|
201
|
+
await refreshAll(target.id);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
startingRef.current = false;
|
|
205
|
+
await completion;
|
|
145
206
|
}
|
|
146
|
-
startUpload({ deliveryId: target.id, packageId, title, files, options });
|
|
147
|
-
await refreshAll(target.id);
|
|
148
207
|
}
|
|
149
208
|
catch (error) {
|
|
150
209
|
setFailure(uploadFailure(error) + (packageId
|
|
@@ -184,7 +243,7 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
184
243
|
if (files.length)
|
|
185
244
|
void beginUpload(files, resume).finally(() => { input.value = ""; });
|
|
186
245
|
};
|
|
187
|
-
const choose = (input, resume =
|
|
246
|
+
const choose = (input, resume = resumeRef.current) => {
|
|
188
247
|
resumeRef.current = resume;
|
|
189
248
|
if (input)
|
|
190
249
|
input.value = "";
|
|
@@ -203,7 +262,8 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
203
262
|
clientName: draft.clientName, description: draft.description,
|
|
204
263
|
licenseNotes: draft.licenseNotes, ctaEnabled: draft.ctaEnabled,
|
|
205
264
|
ctaLabel: draft.ctaLabel, ctaUrl: draft.ctaUrl, syncPackageLabel: true,
|
|
206
|
-
|
|
265
|
+
...(downloadLabelsEnabled ? { downloadOriginalsLabel: draft.downloadOriginalsLabel, downloadWebLabel: draft.downloadWebLabel } : {}),
|
|
266
|
+
expiresAt: delivery.expiresAt,
|
|
207
267
|
filesExpiresAt: draft.duration === "keep"
|
|
208
268
|
? delivery.filesExpiresAt : expiryFrom(draft.duration),
|
|
209
269
|
}, { reload: false })).delivery;
|
|
@@ -223,8 +283,9 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
223
283
|
action: "createLink", id: delivery.id, label: "Link del cliente", pin: draft.pin,
|
|
224
284
|
}, { reload: false });
|
|
225
285
|
}
|
|
226
|
-
if (ready)
|
|
227
|
-
await onAction({ action: "activate", id: delivery.id }, { reload: false })
|
|
286
|
+
if (ready && delivery.status !== "archived") {
|
|
287
|
+
await onAction({ action: "activate", id: delivery.id }, { reload: false });
|
|
288
|
+
}
|
|
228
289
|
setClean({ ...draft, duration: "keep" });
|
|
229
290
|
setDraft((current) => ({ ...current, duration: "keep" }));
|
|
230
291
|
setHeroesClean(heroes);
|
|
@@ -324,35 +385,47 @@ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefr
|
|
|
324
385
|
const state = delivery ? transferState(delivery) : null;
|
|
325
386
|
const expiry = draft.duration === "keep"
|
|
326
387
|
? delivery?.filesExpiresAt || null : expiryFrom(draft.duration);
|
|
327
|
-
return (_jsxs("div", { className: "dl-work", children: [_jsxs("header", { className: "dl-work-head", children: [_jsxs("div", { className: "dl-work-title", children: [state && _jsx("span", { className: "dl-pill is-" + state.kind, children: state.label }), _jsx("h2", { children: draft.title.trim() || (delivery ? delivery.title : "Nuovo transfer") })] }), _jsxs("div", { className: "dl-work-actions", children: [link && (_jsx("a", { className: "ak-btn", href: link.url, target: "_blank", rel: "noreferrer", children: "Guarda come lo vede il cliente" })), link && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva" }))] })] }), (failure || notice) && (_jsxs("p", { className: failure ? "ak-error dl-feedback" : "dl-feedback is-ok", children: [failure || notice, _jsx("button", { type: "button", "aria-label": "Chiudi", onClick: () => { setFailure(null); setNotice(null); }, children: "\u00D7" })] })), _jsxs("div", { className: "dl-grid", children: [_jsxs("div", { className: "dl-main", children: [_jsxs("section", { className: "dl-card", children: [_jsxs("div", { className: "dl-pick", children: [_jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(filesRef.current), children: [_jsx(FileIcon, {}), "Aggiungi file"] }), _jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(folderRef.current), children: [_jsx(FolderIcon, {}), "Aggiungi una cartella"] }), _jsx("input", { ref: folderRef, type: "file", multiple: true, hidden: true, onChange: picked }), _jsx("input", { ref: filesRef, type: "file", multiple: true, hidden: true, onChange: picked })] }), _jsx(DropArea, { disabled: uploading, resuming: !!resumeRef.current, onFiles: (files) => {
|
|
388
|
+
return (_jsxs("div", { className: "dl-work", children: [_jsxs("header", { className: "dl-work-head", children: [_jsxs("div", { className: "dl-work-title", children: [state && _jsx("span", { className: "dl-pill is-" + state.kind, children: state.label }), _jsx("h2", { children: draft.title.trim() || (delivery ? delivery.title : "Nuovo transfer") })] }), _jsxs("div", { className: "dl-work-actions", children: [link && (_jsx("a", { className: "ak-btn", href: link.url, target: "_blank", rel: "noreferrer", children: "Guarda come lo vede il cliente" })), link && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva" }))] })] }), (failure || notice) && (_jsxs("p", { className: failure ? "ak-error dl-feedback" : "dl-feedback is-ok", children: [failure || notice, _jsx("button", { type: "button", "aria-label": "Chiudi", onClick: () => { setFailure(null); setNotice(null); }, children: "\u00D7" })] })), _jsxs("div", { className: "dl-grid", children: [_jsxs("div", { className: "dl-main", children: [_jsxs("section", { className: "dl-card", children: [_jsxs("div", { className: "dl-pick", children: [_jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(filesRef.current), children: [_jsx(FileIcon, {}), "Aggiungi file"] }), _jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(folderRef.current), children: [_jsx(FolderIcon, {}), "Aggiungi una cartella"] }), _jsx("input", { ref: folderRef, type: "file", multiple: true, hidden: true, onChange: picked }), _jsx("input", { ref: filesRef, type: "file", multiple: true, hidden: true, onChange: picked })] }), _jsx("p", { className: "ak-help", children: "Fino a 150 GB di file per invio." }), _jsx(DropArea, { disabled: uploading, resuming: !!resumeRef.current, onFiles: (files) => {
|
|
328
389
|
const resume = resumeRef.current;
|
|
329
390
|
resumeRef.current = null;
|
|
330
391
|
void beginUpload(files, resume);
|
|
331
|
-
}, onError: (error) => setFailure(errorText(error)) }), job && isBusy(job) && _jsx(Progress, { job: job }), !!packages.length && (_jsx("ul", { className: "dl-files", children: packages.map((pkg) => (_jsxs("li", { className: "dl-file is-" + pkg.state + (confirmPackage === pkg.id ? " is-confirming" : ""), children: [_jsx("span", { className: "dl-file-icon", "aria-hidden": "true", children: pkg.state === "ready" ? "✓" : "…" }), confirmPackage === pkg.id ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: ["Rimuovo ", pkg.fileCount || "
|
|
392
|
+
}, onError: (error) => setFailure(errorText(error)) }), job && isBusy(job) && _jsx(Progress, { job: job }), !!packages.length && (_jsx("ul", { className: "dl-files", children: packages.map((pkg) => (_jsxs("li", { className: "dl-file is-" + pkg.state + (confirmPackage === pkg.id ? " is-confirming" : ""), children: [_jsx("span", { className: "dl-file-icon", "aria-hidden": "true", children: pkg.state === "ready" ? "✓" : "…" }), confirmPackage === pkg.id ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: ["Rimuovo ", pkg.fileCount || "questi", " file?"] }), _jsx("span", { children: "Spariscono adesso, e non si torna indietro." })] }), _jsxs("span", { className: "dl-file-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => setConfirmPackage(null), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", disabled: uploading, onClick: () => {
|
|
332
393
|
setConfirmPackage(null);
|
|
333
394
|
void run({ action: "deletePackage", id: delivery?.id, packageId: pkg.id }, "File rimossi");
|
|
334
|
-
}, children: "S\u00EC, rimuovi" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: [pkg.fileCount || "—", "
|
|
395
|
+
}, children: "S\u00EC, rimuovi" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: [pkg.fileCount || "—", " file \u00B7 ", formatBytes(pkg.sourceBytes)] }), _jsx("span", { children: pkg.state === "ready"
|
|
335
396
|
? [pkg.galleryEnabled ? "si vedono sulla pagina" : "", pkg.webEnabled ? "versione leggera" : ""]
|
|
336
397
|
.filter(Boolean).join(" · ") || "solo originali"
|
|
337
398
|
: job?.id === pkg.id && isBusy(job)
|
|
338
399
|
? (job.state === "paused" ? "caricamento in pausa" : "caricamento in corso")
|
|
339
|
-
: "caricamento interrotto" })] }), _jsxs("span", { className: "dl-file-actions", children: [pkg.state !== "ready" && !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn", disabled: uploading, onClick: () => { resumeRef.current = pkg; setNotice(resumeHint); }, children: "Riprendi" })), !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: uploading, onClick: () => setConfirmPackage(pkg.id), children: "Rimuovi" }))] })] }))] }, pkg.id))) }))] }), _jsxs("section", { className: "dl-card dl-about", children: [_jsx(Field, { label: "Nome del transfer", hint: "Lo vede il cliente, ed \u00E8 il nome dello ZIP che scarica.", children: _jsx("input", { value: draft.title, placeholder: "Es. Matrimonio Rossi", onChange: (e) => change({ title: e.target.value }) }) }), _jsx(Field, { label: "Per", hint: "Il nome del cliente, grande in cima alla pagina.", children: _jsx("input", { value: draft.clientName, placeholder: "Es. Marta Rossi", onChange: (e) => change({ clientName: e.target.value }) }) }), _jsx(Field, { label: "Messaggio", wide: true, hint: "Due righe di accompagnamento. Si pu\u00F2 lasciare vuoto.", children: _jsx("textarea", { rows: 3, value: draft.description, onChange: (e) => change({ description: e.target.value }) }) })] }), link ? (_jsxs("section", { className: "dl-card dl-link", children: [_jsxs("div", { className: "dl-link-head", children: [_jsx("span", { className: "dl-step", children: "Il link per il cliente" }), _jsx("code", { children: link.url })] }), _jsxs("div", { className: "dl-link-actions", children: [
|
|
340
|
-
|
|
400
|
+
: "caricamento interrotto" })] }), _jsxs("span", { className: "dl-file-actions", children: [pkg.state !== "ready" && !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn", disabled: uploading, onClick: () => { resumeRef.current = pkg; setNotice(resumeHint); }, children: "Riprendi" })), !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: uploading, onClick: () => setConfirmPackage(pkg.id), children: "Rimuovi" }))] })] }))] }, pkg.id))) }))] }), _jsxs("section", { className: "dl-card dl-about", children: [_jsx(Field, { label: "Nome del transfer", hint: "Lo vede il cliente, ed \u00E8 il nome dello ZIP che scarica.", children: _jsx("input", { value: draft.title, placeholder: "Es. Matrimonio Rossi", onChange: (e) => change({ title: e.target.value }) }) }), _jsx(Field, { label: "Per", hint: "Il nome del cliente, grande in cima alla pagina.", children: _jsx("input", { value: draft.clientName, placeholder: "Es. Marta Rossi", onChange: (e) => change({ clientName: e.target.value }) }) }), _jsx(Field, { label: "Messaggio", wide: true, hint: "Due righe di accompagnamento. Si pu\u00F2 lasciare vuoto.", children: _jsx("textarea", { rows: 3, value: draft.description, onChange: (e) => change({ description: e.target.value }) }) })] }), link ? (_jsxs("section", { className: "dl-card dl-link", children: [_jsxs("div", { className: "dl-link-head", children: [_jsx("span", { className: "dl-step", children: "Il link per il cliente" }), _jsx("code", { children: link.url })] }), _jsxs("div", { className: "dl-link-actions", children: [delivery?.canShortenLinks && link.token.length > 16 && (_jsx("button", { className: "ak-btn", disabled: dirty || saving || busy, title: dirty ? "Salva prima le modifiche del transfer" : "Il vecchio indirizzo continuerà a funzionare", onClick: async () => {
|
|
401
|
+
setSaving(true);
|
|
402
|
+
try {
|
|
403
|
+
await run({ action: "shortenLink", id: delivery.id, linkId: link.id }, "Link accorciato. Anche il vecchio indirizzo resta valido.");
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
setSaving(false);
|
|
407
|
+
}
|
|
408
|
+
}, children: "Accorcia link" })), _jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => void copy(link.url, "link"), children: copied === "link" ? "Copiato" : "Copia il link" }), _jsx("button", { className: "ak-btn", onClick: () => void copy(messageFor(draft, link, expiry), "messaggio"), children: copied === "messaggio" ? "Copiato" : "Copia il messaggio" }), _jsx("a", { className: "ak-btn", target: "_blank", rel: "noreferrer", href: `https://wa.me/?text=${encodeURIComponent(messageFor(draft, link, expiry))}`, children: "Apri in WhatsApp" })] }), _jsx("p", { className: "dl-link-stats", children: linkStory(link) }), draft.pin && (_jsxs("p", { className: "dl-link-stats", children: ["Codice di accesso ", _jsx("strong", { children: draft.pin }), " \u2014 \u00E8 gi\u00E0 dentro il messaggio."] }))] })) : (_jsxs("section", { className: "dl-card dl-get", children: [_jsx("button", { className: "ak-btn ak-btn-primary dl-get-btn", disabled: !delivery || saving || busy, onClick: () => void save(), children: saving ? "Preparo…" : "Ottieni il link" }), _jsx("p", { children: delivery
|
|
409
|
+
? "Il link vale già mentre l'upload continua: chi lo apre vede che i file stanno arrivando."
|
|
341
410
|
: "Aggiungi i file: il caricamento parte subito e il resto si compila mentre va." })] }))] }), _jsxs("div", { className: "dl-side", children: [_jsxs(Group, { title: "Chi pu\u00F2 aprirlo", children: [_jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !draft.pin, onChange: () => change({ pin: "" }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiunque abbia il link" }), _jsx("small", { children: "Il link \u00E8 lungo e casuale: non si indovina." })] })] }), _jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !!draft.pin, onChange: () => change({ pin: draft.pin || makeCode() }) }), _jsxs("span", { children: [_jsx("strong", { children: "Serve anche un codice" }), _jsx("small", { children: "Da mandare insieme al link. Dopo dieci errori si blocca per un'ora." })] })] }), !!draft.pin && (_jsxs("div", { className: "dl-code", children: [_jsx("input", { value: draft.pin, spellCheck: false, onChange: (e) => change({ pin: e.target.value.toUpperCase() }) }), _jsx("button", { className: "ak-btn", onClick: () => change({ pin: makeCode() }), children: "Altro" })] }))] }), _jsxs(Group, { title: "Cosa riceve", note: packages.length
|
|
342
411
|
? "Si decide prima di caricare: per cambiarle bisognerebbe ricaricare i file."
|
|
343
|
-
: undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e un Transfer." })] })] }), _jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.light, disabled: !!packages.length, onChange: (e) => change({ light: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Pu\u00F2 scaricare una versione leggera" }), _jsx("small", { children: "Un secondo download, adatto a social e web. Gli originali ci sono sempre." })] })] })] }), _jsxs(Group, { title: "Per quanto resta scaricabile", children: [_jsxs("select", { className: "dl-select", value: draft.duration, onChange: (e) => change({ duration: e.target.value }), children: [delivery && _jsx("option", { value: "keep", children: delivery.filesExpiresAt
|
|
412
|
+
: undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e un Transfer." })] })] }), _jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.light, disabled: !!packages.length, onChange: (e) => change({ light: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Pu\u00F2 scaricare una versione leggera" }), _jsx("small", { children: "Un secondo download, adatto a social e web. Gli originali ci sono sempre." })] })] }), draft.light && (_jsxs("label", { className: "dl-field", children: [_jsx("span", { children: "Formato della versione leggera" }), _jsxs("select", { className: "dl-select", "aria-label": "Formato della versione leggera", value: draft.webFormat, disabled: !!packages.length || uploading, onChange: (e) => change({ webFormat: e.target.value === "jpg" ? "jpg" : "webp" }), children: [_jsx("option", { value: "webp", children: "WebP \u2014 file pi\u00F9 piccoli" }), _jsx("option", { value: "jpg", disabled: !jpegEnabled && draft.webFormat !== "jpg", children: "JPG \u2014 massima compatibilit\u00E0" })] }), _jsxs("small", { children: ["2048 px sul lato lungo, senza ingrandire le foto pi\u00F9 piccole. Qualit\u00E0 ", draft.webFormat === "jpg" ? "82%" : "80%", "."] })] }))] }), downloadLabelsEnabled && (_jsxs(Group, { title: "Testi dei pulsanti di download", note: "Lascia vuoto per usare il testo predefinito. Puoi cambiarli anche dopo l\u2019invio, senza ricaricare i file.", children: [delivery?.status !== "archived" && (_jsx(Field, { label: "Pulsante degli originali", children: _jsx("input", { value: draft.downloadOriginalsLabel, maxLength: 80, placeholder: "Scarica i file", onChange: (e) => change({ downloadOriginalsLabel: e.target.value }) }) })), (packages.length ? packages.some(pkg => pkg.webEnabled) : draft.light) && (_jsx(Field, { label: "Pulsante della versione leggera", children: _jsx("input", { value: draft.downloadWebLabel, maxLength: 80, placeholder: "Versione leggera", onChange: (e) => change({ downloadWebLabel: e.target.value }) }) }))] })), _jsxs(Group, { title: "Per quanto resta scaricabile", children: [_jsxs("select", { className: "dl-select", value: draft.duration, onChange: (e) => change({ duration: e.target.value }), children: [delivery && _jsx("option", { value: "keep", children: delivery.filesExpiresAt
|
|
344
413
|
? `Fino al ${formatDate(delivery.filesExpiresAt)}`
|
|
345
414
|
: "Per sempre" }), _jsx("option", { value: "7", children: "7 giorni" }), _jsx("option", { value: "30", children: "30 giorni" }), _jsx("option", { value: "180", children: "6 mesi" }), _jsx("option", { value: "365", children: "1 anno" }), _jsx("option", { value: "forever", children: "Per sempre" })] }), _jsx("p", { className: "dl-note", children: expiry
|
|
346
415
|
? `Dopo il ${formatDate(expiry)} il link continua ad aprirsi, ma i file non si scaricano più.`
|
|
347
416
|
: "I file restano finché non li togli tu." })] }), _jsxs(Group, { title: "Alla fine della pagina", children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.ctaEnabled, onChange: (e) => change({ ctaEnabled: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiedi una recensione" }), _jsx("small", { children: "Un pulsante in fondo, dopo le fotografie." })] })] }), draft.ctaEnabled && (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Testo del pulsante", children: _jsx("input", { value: draft.ctaLabel, onChange: (e) => change({ ctaLabel: e.target.value }) }) }), _jsx(Field, { label: "Indirizzo", hint: "Il link alla tua pagina delle recensioni.", children: _jsx("input", { type: "url", value: draft.ctaUrl, placeholder: "https://\u2026", onChange: (e) => change({ ctaUrl: e.target.value }) }) })] })), _jsx(Field, { label: "Note d'uso", hint: "Cosa pu\u00F2 farci il cliente. Appaiono in fondo alla pagina.", children: _jsx("textarea", { rows: 3, value: draft.licenseNotes, onChange: (e) => change({ licenseNotes: e.target.value }) }) })] })] })] }), !!delivery && (_jsxs("section", { className: "dl-card dl-cover", children: [_jsxs("div", { className: "dl-cover-head", children: [_jsxs("div", { children: [_jsx("span", { className: "dl-step", children: "Copertina" }), _jsxs("p", { children: ["Le foto grandi in cima alla pagina del cliente, fino a otto.", " ", heroes.length
|
|
348
417
|
? `Ne hai scelte ${heroes.length}.`
|
|
349
|
-
: "Nessuna scelta: si userà quella predefinita."] })
|
|
350
|
-
|
|
418
|
+
: "Nessuna scelta: si userà quella predefinita."] }), _jsx("p", { role: "status", children: heroesDirty
|
|
419
|
+
? "Scelta non ancora salvata: premi Salva modifiche per mostrarla al cliente."
|
|
420
|
+
: heroes.length
|
|
421
|
+
? "Copertine salvate. Una sola immagine resta fissa; più immagini compongono la sequenza."
|
|
422
|
+
: "Scegli le copertine qui: la galleria sotto nella pagina del cliente è separata." })] }), _jsxs("div", { className: "dl-cover-actions", children: [!!photos.length && (_jsx("button", { className: "ak-btn", onClick: () => setPreviewAttempt(value => value + 1), title: "Riprova a caricare le miniature senza cambiare le copertine selezionate", children: "Ricarica anteprime" })), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva modifiche" }), _jsx("button", { className: "ak-btn", disabled: saving || !ready, onClick: () => { if (coverRef.current)
|
|
423
|
+
coverRef.current.value = ""; coverRef.current?.click(); }, children: "Carica un'immagine" }), !!heroes.length && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: saving, onClick: () => setHeroes([]), children: "Usa quella predefinita" })), _jsx("input", { ref: coverRef, type: "file", accept: "image/jpeg,image/png,image/webp", multiple: true, hidden: true, onChange: (event) => void uploadCovers(event) })] })] }), packages.some(pkg => (moreImages[pkg.id] ? moreImages[pkg.id].nextCursor : pkg.nextCursor) != null) && (_jsx("button", { className: "ak-btn", disabled: loadingImages, onClick: () => void loadMoreImages(), children: loadingImages ? "Caricamento…" : "Mostra altre foto" })), photos.length ? (_jsx("div", { className: "dl-cover-grid", children: photos.map(({ pkg, image }) => {
|
|
351
424
|
const chosen = heroes.some((item) => item.type === "package" && item.packageId === pkg.id && item.index === image.index);
|
|
352
|
-
return (_jsxs("button", { type: "button", className: chosen ? "is-chosen" : "", "aria-pressed": chosen, onClick: () => toggleCover(pkg.id, image.index, image.previewUrl), children: [_jsx("img", { src: image.previewUrl, alt: image.name, loading: "lazy" }), chosen && _jsx("em", { "aria-hidden": "true", children: "\u2713" })] }, `${pkg.id}-${image.index}`));
|
|
425
|
+
return (_jsxs("button", { type: "button", className: chosen ? "is-chosen" : "", disabled: saving, "aria-pressed": chosen, onClick: () => toggleCover(pkg.id, image.index, image.previewUrl), children: [_jsx("img", { src: previewAttempt ? `${image.previewUrl}&previewRetry=${previewAttempt}` : image.previewUrl, alt: image.name, width: image.width, height: image.height, loading: "lazy" }), chosen && _jsx("em", { "aria-hidden": "true", children: "\u2713" })] }, `${pkg.id}-${image.index}`));
|
|
353
426
|
}) })) : (_jsx("p", { className: "dl-note dl-cover-empty", children: heroes.some((item) => item.type === "object")
|
|
354
427
|
? `${heroes.length} immagini caricate a mano.`
|
|
355
|
-
: "Questo transfer non ha anteprime da cui scegliere: carica un'immagine, oppure resta quella predefinita." }))] })), !!delivery && (_jsxs("section", { className: "dl-danger", children: [_jsxs("div", { children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void duplicate(), children: "Usa come modello" }), delivery.status === "active" && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void run({ action: "archive", id: delivery.id }, "Originali tolti: recuperabili per sette giorni"), children: "Libera spazio: togli gli originali" }))] }), confirmDelete ? (_jsxs("div", { className: "dl-confirm", children: [_jsxs("p", { children: ["Elimino ", _jsx("strong", { children: delivery.title }), photos.length ? ` (${countPhotos(packages)}
|
|
428
|
+
: "Questo transfer non ha anteprime da cui scegliere: carica un'immagine, oppure resta quella predefinita." }))] })), !!delivery && (_jsxs("section", { className: "dl-danger", children: [_jsxs("div", { children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void duplicate(), children: "Usa come modello" }), delivery.status === "active" && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void run({ action: "archive", id: delivery.id }, "Originali tolti: recuperabili per sette giorni"), children: "Libera spazio: togli gli originali" }))] }), confirmDelete ? (_jsxs("div", { className: "dl-confirm", children: [_jsxs("p", { children: ["Elimino ", _jsx("strong", { children: delivery.title }), photos.length ? ` (${countPhotos(packages)} file, ${formatBytes(delivery.storedBytes)})` : "", "? Il link smette subito di funzionare e i file vengono cancellati adesso. Non si torna indietro."] }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmDelete(false), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => void remove(), children: "S\u00EC, elimina" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmDelete(true), children: "Elimina il transfer" }))] }))] }));
|
|
356
429
|
}
|
|
357
430
|
/* ============================================================================================
|
|
358
431
|
Impostazioni
|
|
@@ -419,9 +492,11 @@ function Progress({ job }) {
|
|
|
419
492
|
}
|
|
420
493
|
const done = progress.totalFiles ? Math.min(1, progress.doneFiles / progress.totalFiles) : 0;
|
|
421
494
|
const phase = job.state === "paused" ? "In pausa"
|
|
422
|
-
:
|
|
423
|
-
:
|
|
424
|
-
: "
|
|
495
|
+
: job.state === "cancelling" ? "Annullamento in corso"
|
|
496
|
+
: job.waitingForNetwork ? "Attendo la connessione e riprovo…"
|
|
497
|
+
: progress.phase === "originals" ? "Carico gli originali"
|
|
498
|
+
: progress.phase === "derivatives" ? "Preparo le anteprime"
|
|
499
|
+
: "Ultimi controlli";
|
|
425
500
|
const seconds = progress.transferStartedAt
|
|
426
501
|
? Math.max(1, (Date.now() - progress.transferStartedAt) / 1000) : 0;
|
|
427
502
|
const mbps = seconds ? progress.transferredBytes * 8 / seconds / 1_000_000 : 0;
|
|
@@ -440,12 +515,15 @@ function draftOf(delivery, keep) {
|
|
|
440
515
|
return {
|
|
441
516
|
title: "", clientName: "", description: "", licenseNotes: "",
|
|
442
517
|
ctaEnabled: false, ctaLabel: "Lascia una recensione", ctaUrl: "",
|
|
443
|
-
duration: "30", pin: "", gallery: true, light: true,
|
|
518
|
+
duration: "30", pin: "", gallery: true, light: true, webFormat: "webp",
|
|
519
|
+
downloadOriginalsLabel: "", downloadWebLabel: "",
|
|
444
520
|
};
|
|
445
521
|
}
|
|
446
522
|
const link = primaryLink(delivery);
|
|
447
523
|
return {
|
|
448
524
|
title: delivery.title,
|
|
525
|
+
downloadOriginalsLabel: delivery.downloadOriginalsLabel || "",
|
|
526
|
+
downloadWebLabel: delivery.downloadWebLabel || "",
|
|
449
527
|
clientName: delivery.clientName,
|
|
450
528
|
description: delivery.description,
|
|
451
529
|
licenseNotes: delivery.licenseNotes,
|
|
@@ -458,6 +536,7 @@ function draftOf(delivery, keep) {
|
|
|
458
536
|
transfer che ha già i file, la verità è quella dei file. */
|
|
459
537
|
gallery: delivery.packages[0]?.galleryEnabled ?? keep?.gallery ?? true,
|
|
460
538
|
light: delivery.packages[0]?.webEnabled ?? keep?.light ?? true,
|
|
539
|
+
webFormat: delivery.packages.length ? (delivery.packages[0].webFormat || "webp") : keep?.webFormat || "webp",
|
|
461
540
|
};
|
|
462
541
|
}
|
|
463
542
|
function same(a, b) {
|
|
@@ -506,7 +585,7 @@ function makeCode() {
|
|
|
506
585
|
function messageFor(draft, link, expiry) {
|
|
507
586
|
const who = draft.clientName.trim().split(/\s+/)[0];
|
|
508
587
|
return [
|
|
509
|
-
`Ciao${who ? ` ${who}` : ""},
|
|
588
|
+
`Ciao${who ? ` ${who}` : ""}, i file di ${draft.title.trim() || "questo lavoro"} sono pronti:`,
|
|
510
589
|
link.url,
|
|
511
590
|
draft.pin ? `Codice di accesso: ${draft.pin}` : "",
|
|
512
591
|
expiry ? `Puoi scaricarle fino al ${formatDate(expiry)}.` : "",
|
|
@@ -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)]);
|
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
11
11
|
* sempre.
|
|
12
12
|
*/
|
|
13
13
|
import { useEffect, useState } from "react";
|
|
14
|
-
import { cancelUpload, dismissUpload, isBusy, pauseUpload, resumeUpload, useUploads, } from "../lib/upload-queue";
|
|
14
|
+
import { cancelUpload, dismissUpload, isBusy, pauseUpload, resumeUpload, retryUpload, useUploads, } from "../lib/upload-queue";
|
|
15
15
|
import { dismissMediaUpload, useMediaUploads, } from "../../form/media/media-upload-queue.js";
|
|
16
16
|
export function UploadDock() {
|
|
17
17
|
const jobs = useUploads();
|
|
@@ -28,7 +28,7 @@ export function UploadDock() {
|
|
|
28
28
|
}, [busy]);
|
|
29
29
|
if (!jobs.length && !mediaJobs.length)
|
|
30
30
|
return null;
|
|
31
|
-
return (_jsxs("aside", { className: "uk" + (open ? "" : " is-closed"), "aria-label": "Caricamenti", children: [_jsxs("button", { className: "uk-head", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [_jsx("span", { className: "uk-head-text", children: summary(jobs, mediaJobs) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (_jsxs("ul", { className: "uk-list", children: [mediaJobs.map((job) => _jsx(MediaJob, { job: job }, job.id)), jobs.map((job) => (_jsxs("li", { className: "uk-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: line(job) })] }), isBusy(job) && (_jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${percent(job)}%` } }) })), job.error && _jsx("p", { className: "uk-error", children: job.error }), _jsxs("div", { className: "uk-job-actions", children: [job.state === "running" && (_jsx("button", { className: "ak-btn", onClick: () => pauseUpload(job.id), children: "Pausa" })), job.state === "paused" && (_jsx("button", { className: "ak-btn", onClick: () => resumeUpload(job.id), children: "Riprendi" })), isBusy(job) && (confirmStop === job.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "uk-confirm", children: "I file gi\u00E0 caricati verranno buttati." }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmStop(null), children: "No" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => { cancelUpload(job.id); setConfirmStop(null); }, children: "Annulla il caricamento" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmStop(job.id), children: "Annulla" }))), !isBusy(job) && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissUpload(job.id), children: "Togli dall'elenco" }))] })] }, job.id)))] }))] }));
|
|
31
|
+
return (_jsxs("aside", { className: "uk" + (open ? "" : " is-closed"), "aria-label": "Caricamenti", children: [_jsxs("button", { className: "uk-head", onClick: () => setOpen((value) => !value), "aria-expanded": open, children: [_jsx("span", { className: "uk-head-text", children: summary(jobs, mediaJobs) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (_jsxs("ul", { className: "uk-list", children: [mediaJobs.map((job) => _jsx(MediaJob, { job: job }, job.id)), jobs.map((job) => (_jsxs("li", { className: "uk-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: line(job) })] }), isBusy(job) && (_jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${percent(job)}%` } }) })), job.error && _jsx("p", { className: "uk-error", children: job.error }), _jsxs("div", { className: "uk-job-actions", children: [job.state === "running" && (_jsx("button", { className: "ak-btn", onClick: () => pauseUpload(job.id), children: "Pausa" })), job.state === "paused" && (_jsx("button", { className: "ak-btn", onClick: () => resumeUpload(job.id), children: "Riprendi" })), isBusy(job) && job.state !== "cancelling" && (confirmStop === job.id ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "uk-confirm", children: "I file gi\u00E0 caricati verranno buttati." }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmStop(null), children: "No" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => { cancelUpload(job.id); setConfirmStop(null); }, children: "Annulla il caricamento" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmStop(job.id), children: "Annulla" }))), job.state === "failed" && job.retryable && _jsx("button", { className: "ak-btn", onClick: () => void retryUpload(job.id), children: "Riprova" }), !isBusy(job) && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissUpload(job.id), children: "Togli dall'elenco" }))] })] }, job.id)))] }))] }));
|
|
32
32
|
}
|
|
33
33
|
function MediaJob({ job }) {
|
|
34
34
|
const conversionPercent = job.total ? (job.conversionDone / job.total) * 100 : 0;
|
|
@@ -68,10 +68,14 @@ function line(job) {
|
|
|
68
68
|
? `Pronto · ${job.skipped} senza anteprima`
|
|
69
69
|
: "Pronto";
|
|
70
70
|
}
|
|
71
|
+
if (job.state === "cancelling")
|
|
72
|
+
return "Annullamento in corso";
|
|
71
73
|
if (job.state === "failed")
|
|
72
74
|
return "Interrotto";
|
|
73
75
|
if (job.state === "paused")
|
|
74
76
|
return "In pausa";
|
|
77
|
+
if (job.waitingForNetwork)
|
|
78
|
+
return "Attendo la connessione e riprovo…";
|
|
75
79
|
const progress = job.progress;
|
|
76
80
|
if (!progress)
|
|
77
81
|
return "Preparo…";
|
package/dist/app/site.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface PageTypeConfig {
|
|
|
10
10
|
object: z.ZodType;
|
|
11
11
|
payload: z.ZodType;
|
|
12
12
|
hasBody: boolean;
|
|
13
|
+
/** Fixed routes cannot be renamed, hidden, duplicated or removed. */
|
|
14
|
+
fixed?: boolean;
|
|
15
|
+
menuEditable?: boolean;
|
|
16
|
+
creatable?: boolean;
|
|
17
|
+
create?: (title: string, slug: string) => Record<string, unknown>;
|
|
18
|
+
seoShape?: Shape;
|
|
13
19
|
}
|
|
14
20
|
/** Lingue dei contenuti, assenti nei siti monolingui. I codici sono quelli dei sibling Hugo
|
|
15
21
|
* (`index.md`, `index.en.md`): la lingua predefinita non porta alcun suffisso. */
|
|
@@ -39,6 +45,13 @@ export interface SiteAdminConfig {
|
|
|
39
45
|
languages?: LanguagesConfig;
|
|
40
46
|
/** Forma del singleton home: i suoi `group` diventano le voci della seconda colonna. */
|
|
41
47
|
home: Shape;
|
|
48
|
+
/** Schema-owned fields rendered inside the native Settings surface. */
|
|
49
|
+
settingsShape?: Shape;
|
|
50
|
+
pagesReorderable?: boolean;
|
|
51
|
+
/** Optional storage-policy surface inside the native Media pane. */
|
|
52
|
+
mediaEditor?: ComponentType;
|
|
53
|
+
/** Preview approval, public observation and rollback for direct SSR publishing. */
|
|
54
|
+
directPublication?: boolean;
|
|
42
55
|
/** Optional site-owned Home surface. The kit keeps its legacy visual editor
|
|
43
56
|
* as a fallback, while a site with a different document shape can render its
|
|
44
57
|
* own schema-driven editor without teaching the shared package its fields. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type D1DatabaseLike, type D1Statement } from "./persistence.js";
|
|
2
|
+
import { type ImmutableReleaseExport } from "./release.js";
|
|
3
|
+
/** A SELECT guard belongs inside the batch: a failed conditional must abort
|
|
4
|
+
* every mutation, including when an UPDATE would silently affect zero rows. */
|
|
5
|
+
export declare function transactionAssertion(db: D1DatabaseLike, condition: string, values?: unknown[]): D1Statement;
|
|
6
|
+
export declare function previewCandidateChecksum(baseReleaseId: string, entries: readonly {
|
|
7
|
+
documentId: string;
|
|
8
|
+
revisionId: string;
|
|
9
|
+
payloadChecksum: string;
|
|
10
|
+
}[]): Promise<string>;
|
|
11
|
+
export declare function commitDirectRelease(db: D1DatabaseLike, input: {
|
|
12
|
+
release: ImmutableReleaseExport;
|
|
13
|
+
expectedBaseReleaseId: string;
|
|
14
|
+
selectedDrafts: readonly {
|
|
15
|
+
documentId: string;
|
|
16
|
+
revisionId: string;
|
|
17
|
+
}[];
|
|
18
|
+
authorSubject: string;
|
|
19
|
+
now: number;
|
|
20
|
+
}): Promise<void>;
|