@cardinali-studio/adminkit 2.2.10 → 2.2.12
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 +58 -2
- package/dist/api.js +39 -5
- package/dist/app/App.js +10 -4
- package/dist/app/app.css +5 -0
- package/dist/app/lib/publish.js +9 -1
- package/dist/app/lib/settings.d.ts +2 -0
- package/dist/app/lib/settings.js +54 -8
- package/dist/app/routes/MediaSettingsEditor.d.ts +4 -0
- package/dist/app/routes/MediaSettingsEditor.js +4 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +3 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/routes/UploadDock.js +31 -9
- package/dist/app/site.d.ts +4 -0
- package/dist/deployment.js +14 -1
- package/dist/form/media/MediaFolders.d.ts +5 -1
- package/dist/form/media/MediaFolders.js +3 -2
- package/dist/form/media/MediaGrid.js +10 -1
- package/dist/form/media/image-conversion.d.ts +14 -0
- package/dist/form/media/image-conversion.js +163 -0
- package/dist/form/media/media-upload-queue.d.ts +31 -0
- package/dist/form/media/media-upload-queue.js +230 -0
- package/dist/form/media/useMediaLibrary.d.ts +7 -0
- package/dist/form/media/useMediaLibrary.js +123 -22
- package/dist/functions/_shared/assets.js +3 -2
- package/dist/functions/_shared/d1-media.js +317 -0
- package/dist/functions/api/assets/delete.js +19 -1
- package/dist/functions/api/assets/folders.js +5 -2
- package/dist/functions/api/assets/media.js +30 -0
- package/dist/functions/api/assets/move.js +22 -6
- package/dist/functions/api/assets/settings.js +40 -0
- package/dist/functions/api/assets/upload.js +4 -1
- package/dist/functions/api/assets/usage.js +5 -1
- package/dist/functions/api/assets.js +7 -4
- package/dist/functions/api/content-ci.js +8 -1
- package/dist/functions/api/site-access.js +78 -0
- package/dist/functions/index.js +6 -0
- package/dist/functions/site.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +52 -0
- package/dist/media.js +103 -1
- package/dist/migrations/0004_site_access.sql +41 -0
- package/dist/persistence.d.ts +10 -0
- package/dist/persistence.js +129 -8
- package/dist/release.d.ts +3 -0
- package/dist/release.js +7 -0
- package/dist/site-access.d.ts +48 -0
- package/dist/site-access.js +180 -0
- package/dist/theme.css +13 -0
- package/package.json +4 -1
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type JsonValue } from "./persistence.js";
|
|
2
|
+
import type { MediaOperationStatus } from "./media.js";
|
|
2
3
|
export declare class ApiError extends Error {
|
|
3
4
|
code: string;
|
|
4
5
|
status: number;
|
|
@@ -75,6 +76,16 @@ export type ContentDocument = {
|
|
|
75
76
|
publishedPayload: string | null;
|
|
76
77
|
publishedTombstone: boolean | null;
|
|
77
78
|
};
|
|
79
|
+
export type SiteAccessPublicVersion = {
|
|
80
|
+
versionId: string;
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
formatVersion: 1;
|
|
83
|
+
};
|
|
84
|
+
export type SiteAccessPublicState = {
|
|
85
|
+
active: SiteAccessPublicVersion | null;
|
|
86
|
+
draft: SiteAccessPublicVersion | null;
|
|
87
|
+
effective: SiteAccessPublicVersion | null;
|
|
88
|
+
};
|
|
78
89
|
/** Builds the only D1 Save wire shape. The Worker recomputes these hashes too,
|
|
79
90
|
* so retries are deterministic rather than client-trusted. */
|
|
80
91
|
export declare function contentSaveRequest(command: ContentSaveCommand): Promise<{
|
|
@@ -132,6 +143,20 @@ export declare const api: {
|
|
|
132
143
|
ok: true;
|
|
133
144
|
discarded: true;
|
|
134
145
|
}>;
|
|
146
|
+
siteAccessState: () => Promise<{
|
|
147
|
+
ok: true;
|
|
148
|
+
state: SiteAccessPublicState;
|
|
149
|
+
}>;
|
|
150
|
+
saveSiteAccess: (enabled: boolean, password: string, expectedDraftVersionId: string | null) => Promise<{
|
|
151
|
+
ok: true;
|
|
152
|
+
saved: true;
|
|
153
|
+
state: SiteAccessPublicState;
|
|
154
|
+
}>;
|
|
155
|
+
discardSiteAccess: (expectedDraftVersionId: string) => Promise<{
|
|
156
|
+
ok: true;
|
|
157
|
+
discarded: true;
|
|
158
|
+
state: SiteAccessPublicState;
|
|
159
|
+
}>;
|
|
135
160
|
/** D1 publication is site-owned: the site chooses which documents it can
|
|
136
161
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
137
162
|
contentPublishState: () => Promise<{
|
|
@@ -143,6 +168,10 @@ export declare const api: {
|
|
|
143
168
|
kind: string;
|
|
144
169
|
updatedAt: number;
|
|
145
170
|
}[];
|
|
171
|
+
mediaOperations?: MediaOperationStatus[];
|
|
172
|
+
siteAccess?: {
|
|
173
|
+
enabled: boolean;
|
|
174
|
+
} | null;
|
|
146
175
|
activeReleaseId?: string | null;
|
|
147
176
|
}>;
|
|
148
177
|
contentPublish: () => Promise<{
|
|
@@ -219,6 +248,19 @@ export declare const api: {
|
|
|
219
248
|
}[];
|
|
220
249
|
warning?: string;
|
|
221
250
|
}>;
|
|
251
|
+
mediaOperations: () => Promise<{
|
|
252
|
+
ok: true;
|
|
253
|
+
operations: MediaOperationStatus[];
|
|
254
|
+
}>;
|
|
255
|
+
resumeMediaOperation: (operationId: string, releaseId: string) => Promise<{
|
|
256
|
+
ok: true;
|
|
257
|
+
completed: number;
|
|
258
|
+
failed: number;
|
|
259
|
+
}>;
|
|
260
|
+
cancelMediaOperation: (operationId: string) => Promise<{
|
|
261
|
+
ok: true;
|
|
262
|
+
cancelled: true;
|
|
263
|
+
}>;
|
|
222
264
|
usage: (target: {
|
|
223
265
|
keys?: string[];
|
|
224
266
|
prefixes?: string[];
|
|
@@ -258,7 +300,21 @@ export declare const api: {
|
|
|
258
300
|
ok: true;
|
|
259
301
|
}>;
|
|
260
302
|
/** L'upload NON è JSON: corpo grezzo e metadati negli header (vedi assets/upload.js). */
|
|
261
|
-
upload: (prefix: string, file: File
|
|
303
|
+
upload: (prefix: string, file: File, metadata?: {
|
|
304
|
+
capturedAt?: string | null;
|
|
305
|
+
}) => Promise<AssetFile>;
|
|
306
|
+
mediaSettings: () => Promise<{
|
|
307
|
+
ok: true;
|
|
308
|
+
settings: {
|
|
309
|
+
autoWebp: boolean;
|
|
310
|
+
};
|
|
311
|
+
}>;
|
|
312
|
+
saveMediaSettings: (autoWebp: boolean) => Promise<{
|
|
313
|
+
ok: true;
|
|
314
|
+
settings: {
|
|
315
|
+
autoWebp: boolean;
|
|
316
|
+
};
|
|
317
|
+
}>;
|
|
262
318
|
trash: () => Promise<{
|
|
263
319
|
ok: true;
|
|
264
320
|
items: TrashItem[];
|
|
@@ -280,4 +336,4 @@ export declare const api: {
|
|
|
280
336
|
removed: number;
|
|
281
337
|
}>;
|
|
282
338
|
};
|
|
283
|
-
export declare function uploadQueue(prefix: string, files: File[], onProgress: (done: number, total: number, last?: AssetFile, error?: string) => void, concurrency?: number): Promise<AssetFile[]>;
|
|
339
|
+
export declare function uploadQueue(prefix: string, files: File[], onProgress: (done: number, total: number, last?: AssetFile, error?: string) => void, concurrency?: number, prepare?: (file: File) => Promise<File>): Promise<AssetFile[]>;
|
package/dist/api.js
CHANGED
|
@@ -60,6 +60,9 @@ export const api = {
|
|
|
60
60
|
contentDocument: (documentId) => call(`/admin/api/content/document?id=${encodeURIComponent(documentId)}`),
|
|
61
61
|
saveContent: async (command) => post("/admin/api/content/save", await contentSaveRequest(command)),
|
|
62
62
|
discardContent: (documentId, expectedDraftRevisionId, baselineRevisionId) => post("/admin/api/content/discard", { documentId, expectedDraftRevisionId, baselineRevisionId }),
|
|
63
|
+
siteAccessState: () => call("/admin/api/content/site-access"),
|
|
64
|
+
saveSiteAccess: (enabled, password, expectedDraftVersionId) => post("/admin/api/content/site-access", { action: "save", enabled, password, expectedDraftVersionId }),
|
|
65
|
+
discardSiteAccess: (expectedDraftVersionId) => post("/admin/api/content/site-access", { action: "discard", expectedDraftVersionId }),
|
|
63
66
|
/** D1 publication is site-owned: the site chooses which documents it can
|
|
64
67
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
65
68
|
contentPublishState: () => call("/admin/api/content/publish"),
|
|
@@ -78,14 +81,38 @@ export const api = {
|
|
|
78
81
|
/** `label` è il nome per esteso; lo slug della chiave lo calcola il server e lo restituisce. */
|
|
79
82
|
createFolder: (parent, name, opts = {}) => post("/admin/api/assets/folder", { parent, name, ...opts }),
|
|
80
83
|
/** Con `skipRefs` copia senza cancellare gli originali e restituisce la mappa degli URL. */
|
|
81
|
-
move: (keys, dest, opts = {}) =>
|
|
84
|
+
move: async (keys, dest, opts = {}) => {
|
|
85
|
+
const operationId = crypto.randomUUID();
|
|
86
|
+
const destinationPrefix = dest.trim().endsWith("/") ? dest.trim() : `${dest.trim()}/`;
|
|
87
|
+
const sourceKeys = [...new Set(keys)].filter((key) => {
|
|
88
|
+
const slash = key.lastIndexOf("/");
|
|
89
|
+
const parent = slash < 0 ? "" : key.slice(0, slash + 1);
|
|
90
|
+
return parent !== destinationPrefix;
|
|
91
|
+
});
|
|
92
|
+
if (!sourceKeys.length)
|
|
93
|
+
return {
|
|
94
|
+
ok: true, moved: 0, finalized: 0, urlMap: {}, pagesUpdated: [], failed: [],
|
|
95
|
+
};
|
|
96
|
+
const requestChecksum = await canonicalChecksum({ action: "media.operation", operationId, type: "move", sourceKeys, sourcePrefix: null, destinationPrefix });
|
|
97
|
+
return post("/admin/api/assets/move", { keys: sourceKeys, dest: destinationPrefix, operationId, requestChecksum, ...opts });
|
|
98
|
+
},
|
|
82
99
|
/** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
|
|
83
100
|
moveApplyRefs: (applyRefs) => post("/admin/api/assets/move", { applyRefs }),
|
|
101
|
+
mediaOperations: () => call("/admin/api/assets/media"),
|
|
102
|
+
resumeMediaOperation: (operationId, releaseId) => post("/admin/api/assets/media", { action: "resume", operationId, releaseId }),
|
|
103
|
+
cancelMediaOperation: (operationId) => post("/admin/api/assets/media", { action: "cancel", operationId }),
|
|
84
104
|
usage: (target) => post("/admin/api/assets/usage", target),
|
|
85
105
|
/* `refsOnly` pulisce solo i riferimenti nelle pagine (un commit, niente file spostati);
|
|
86
106
|
* `skipRefs` sposta i file e non tocca il repo. Servono a spezzare una cancellazione grande
|
|
87
107
|
* in più richieste senza fare un commit — cioè una build — per ogni gruppo. */
|
|
88
|
-
remove: (target, opts = {}) =>
|
|
108
|
+
remove: async (target, opts = {}) => {
|
|
109
|
+
const operationId = crypto.randomUUID();
|
|
110
|
+
const sourceKeys = target.prefixes?.length ? null : [...new Set(target.keys ?? [])];
|
|
111
|
+
const requestChecksum = sourceKeys?.length
|
|
112
|
+
? await canonicalChecksum({ action: "media.operation", operationId, type: "delete", sourceKeys, sourcePrefix: null, destinationPrefix: null })
|
|
113
|
+
: undefined;
|
|
114
|
+
return post("/admin/api/assets/delete", { ...target, operationId, ...(requestChecksum ? { requestChecksum } : {}), ...opts });
|
|
115
|
+
},
|
|
89
116
|
// ---- richieste dal form di contatto
|
|
90
117
|
requests: () => call("/admin/api/requests"),
|
|
91
118
|
markRequest: (id, handled) => call("/admin/api/requests", {
|
|
@@ -99,15 +126,18 @@ export const api = {
|
|
|
99
126
|
body: JSON.stringify({ id }),
|
|
100
127
|
}),
|
|
101
128
|
/** L'upload NON è JSON: corpo grezzo e metadati negli header (vedi assets/upload.js). */
|
|
102
|
-
upload: (prefix, file) => call("/admin/api/assets/upload", {
|
|
129
|
+
upload: (prefix, file, metadata = {}) => call("/admin/api/assets/upload", {
|
|
103
130
|
method: "POST",
|
|
104
131
|
headers: {
|
|
105
132
|
"Content-Type": file.type || "application/octet-stream",
|
|
106
133
|
"X-Prefix": prefix,
|
|
107
134
|
"X-Filename": encodeURIComponent(file.name),
|
|
135
|
+
...(metadata.capturedAt ? { "X-Captured-At": metadata.capturedAt } : {}),
|
|
108
136
|
},
|
|
109
137
|
body: file,
|
|
110
138
|
}),
|
|
139
|
+
mediaSettings: () => call("/admin/api/assets/settings"),
|
|
140
|
+
saveMediaSettings: (autoWebp) => post("/admin/api/assets/settings", { autoWebp }),
|
|
111
141
|
// ---- cestino
|
|
112
142
|
trash: () => call("/admin/api/assets/trash"),
|
|
113
143
|
trashRestore: (trashKeys) => post("/admin/api/assets/trash/restore", { trashKeys }),
|
|
@@ -118,14 +148,18 @@ export const api = {
|
|
|
118
148
|
* (app.js:922), il che su quaranta foto è tedioso senza motivo; tutte insieme invece saturano
|
|
119
149
|
* la connessione e rendono il progresso illeggibile. Tre è il compromesso.
|
|
120
150
|
* Un file che fallisce non ferma gli altri: si segnala e si prosegue. */
|
|
121
|
-
export async function uploadQueue(prefix, files, onProgress, concurrency = 3) {
|
|
151
|
+
export async function uploadQueue(prefix, files, onProgress, concurrency = 3, prepare = async (file) => file) {
|
|
122
152
|
const out = [];
|
|
123
153
|
let done = 0, next = 0;
|
|
124
154
|
const worker = async () => {
|
|
125
155
|
while (next < files.length) {
|
|
126
156
|
const file = files[next++];
|
|
127
157
|
try {
|
|
128
|
-
|
|
158
|
+
/* Preparazione e rete stanno nello STESSO worker: tre immagini vengono convertite in
|
|
159
|
+
* parallelo, e appena una è pronta parte il suo upload mentre le altre continuano. Non
|
|
160
|
+
* teniamo in memoria quaranta WebP aspettando che l'intero batch sia convertito. */
|
|
161
|
+
const ready = await prepare(file);
|
|
162
|
+
const r = await api.upload(prefix, ready);
|
|
129
163
|
out.push(r);
|
|
130
164
|
onProgress(++done, files.length, r);
|
|
131
165
|
}
|
package/dist/app/App.js
CHANGED
|
@@ -32,6 +32,7 @@ import { PagesList } from "./routes/PagesList";
|
|
|
32
32
|
import { PageEditor, PageHeader } from "./routes/PageEditor";
|
|
33
33
|
import { PageSeoEditor } from "./routes/PageSeoEditor";
|
|
34
34
|
import { SiteSettingsEditor, WebsiteSeoSettingsEditor } from "./routes/SiteSettingsEditor";
|
|
35
|
+
import { MediaSettingsEditor } from "./routes/MediaSettingsEditor";
|
|
35
36
|
import { HomeEditor } from "./routes/HomeEditor";
|
|
36
37
|
import { RequestsList, RequestDetail } from "./routes/Requests";
|
|
37
38
|
import { useRequests } from "./lib/requests";
|
|
@@ -67,6 +68,7 @@ export function App() {
|
|
|
67
68
|
colonna e la griglia nella terza, e sono due punti dello schermo lontani. Uno stato per
|
|
68
69
|
ciascuno vorrebbe dire due idee diverse di quale cartella si sta guardando. */
|
|
69
70
|
const media = useMediaLibrary({ mode: "manage", active: section === "media" });
|
|
71
|
+
const mediaSettingsOpen = view === "media" && route[1] === "settings";
|
|
70
72
|
/* Le richieste si caricano SUBITO, non entrando nella sezione: il numero «da gestire» nella
|
|
71
73
|
prima colonna è il motivo per cui uno ci entra, e un badge che compare solo dopo essere
|
|
72
74
|
andati a guardare non serve a niente. */
|
|
@@ -195,7 +197,9 @@ export function App() {
|
|
|
195
197
|
: section === "pages" ? (editing ? String(editing.title ?? editing.slug) : "Pagine")
|
|
196
198
|
: section === "home" ? (homeGroup || "Home")
|
|
197
199
|
: view === "trash" ? "Cestino"
|
|
198
|
-
: section === "media" ? (
|
|
200
|
+
: section === "media" ? (mediaSettingsOpen
|
|
201
|
+
? "Impostazioni Media"
|
|
202
|
+
: media.prefix ? media.labelOf(media.prefix) : "Senza cartella")
|
|
199
203
|
/* Consegne non ripete il titolo qui: il nome del transfer sta nella sua schermata, dove
|
|
200
204
|
si modifica, e si aggiorna mentre lo si scrive. Averlo in due posti significa averne
|
|
201
205
|
uno dei due sempre vecchio. */
|
|
@@ -220,7 +224,7 @@ export function App() {
|
|
|
220
224
|
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
|
|
221
225
|
if (editing?.slug === from)
|
|
222
226
|
go(`/pages/${to}${seoEditing ? "/seo" : ""}`);
|
|
223
|
-
} }) })] })), section === "media" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Cartelle" }) }), _jsx("div", { className: "col-body", children: _jsx(MediaFolders, { lib: media, trash: { active: view === "trash", open: () => go("/trash"), leave: () => go("/media") } }) })] })), section === "requests" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: pickedReq.size === 0 ? (_jsx("span", { className: "col-title", children: requests.rows ? `${requests.rows.length} richieste` : "Richieste" })) : (_jsxs("span", { className: "rq-bulk", children: [_jsxs("span", { className: "col-title", children: [pickedReq.size, " scelte"] }), _jsx("button", { className: "mg-act", onClick: () => { void requests.mark([...pickedReq], true); setPickedReq(new Set()); }, children: "Gestite" }), _jsx("button", { className: "mg-act is-danger", onClick: () => { void requests.remove([...pickedReq]); setPickedReq(new Set()); }, children: "Elimina" }), _jsx("button", { className: "mg-act", onClick: () => setPickedReq(new Set()), children: "\u00D7" })] })) }), _jsx("div", { className: "col-body", children: requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
227
|
+
} }) })] })), section === "media" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Cartelle" }) }), _jsx("div", { className: "col-body", children: _jsx(MediaFolders, { lib: media, trash: { active: view === "trash", open: () => go("/trash"), leave: () => go("/media") }, settings: { active: mediaSettingsOpen, open: () => go("/media/settings") } }) })] })), section === "requests" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: pickedReq.size === 0 ? (_jsx("span", { className: "col-title", children: requests.rows ? `${requests.rows.length} richieste` : "Richieste" })) : (_jsxs("span", { className: "rq-bulk", children: [_jsxs("span", { className: "col-title", children: [pickedReq.size, " scelte"] }), _jsx("button", { className: "mg-act", onClick: () => { void requests.mark([...pickedReq], true); setPickedReq(new Set()); }, children: "Gestite" }), _jsx("button", { className: "mg-act is-danger", onClick: () => { void requests.remove([...pickedReq]); setPickedReq(new Set()); }, children: "Elimina" }), _jsx("button", { className: "mg-act", onClick: () => setPickedReq(new Set()), children: "\u00D7" })] })) }), _jsx("div", { className: "col-body", children: requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
224
228
|
: !requests.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
225
229
|
: requests.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessuna richiesta, per ora." })
|
|
226
230
|
: _jsx(RequestsList, { rows: requests.rows, current: openReq?.id, picked: pickedReq, onOpen: (id) => go(`/requests/${id}`), onPick: (id) => setPickedReq((prev) => {
|
|
@@ -240,7 +244,7 @@ export function App() {
|
|
|
240
244
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
241
245
|
: _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 })
|
|
242
246
|
: !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
243
|
-
: _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" ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), 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" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && (_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => media.setUploadOpen(true), 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,
|
|
247
|
+
: _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 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), 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" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), 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", onClick: () => media.setUploadOpen(true), 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,
|
|
244
248
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
245
249
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
246
250
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
|
@@ -261,7 +265,9 @@ export function App() {
|
|
|
261
265
|
: home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
|
|
262
266
|
: _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (_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"
|
|
263
267
|
? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
|
|
264
|
-
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, focus: route[1] })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" &&
|
|
268
|
+
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, 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
|
|
269
|
+
? _jsx(MediaSettingsEditor, { lib: media })
|
|
270
|
+
: _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
265
271
|
: openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
|
|
266
272
|
: _jsx("p", { className: "pane-empty ak-hint", children: requests.rows?.length ? "Scegli una richiesta dall'elenco." : "" })), section === "deliveries" && (deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
|
|
267
273
|
: deliverySettingsOpen ? (_jsx(DeliverySettings, { settings: deliveries.settings, onAction: deliveries.action, pickMedia: pickMedia })) : activeDeliveryId && !deliveries.detail ? (_jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })) : (
|
package/dist/app/app.css
CHANGED
|
@@ -235,6 +235,7 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
235
235
|
.st-inline-help {
|
|
236
236
|
margin: -3px 0 0; color: var(--ak-ink-quiet); font: 400 11px/1.45 var(--ak-font);
|
|
237
237
|
}
|
|
238
|
+
.media-settings-note { margin-top: 13px; max-width: 520px; }
|
|
238
239
|
.st-favicon { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
239
240
|
.st-favicon-preview {
|
|
240
241
|
width: 48px; height: 48px; display: grid; place-items: center; flex: none;
|
|
@@ -1165,6 +1166,10 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
1165
1166
|
.uk-job.is-failed .uk-job-top span { color: #a33; }
|
|
1166
1167
|
.uk-track { height: 4px; overflow: hidden; border-radius: 3px; background: var(--ak-line); }
|
|
1167
1168
|
.uk-track i { display: block; height: 100%; background: var(--ak-accent); transition: width .25s; }
|
|
1169
|
+
.uk-phase { display: grid; gap: 4px; }
|
|
1170
|
+
.uk-phase > span { color: var(--ak-ink-quiet); font: 500 10px/1.2 var(--ak-font); }
|
|
1171
|
+
.uk-media-job .uk-phase:nth-of-type(3) .uk-track i { opacity: .65; }
|
|
1172
|
+
.uk-warning { margin: 0; color: #8a5a12; font-size: 11px; line-height: 1.45; }
|
|
1168
1173
|
.uk-job.is-paused .uk-track i { background: var(--ak-ink-quiet); }
|
|
1169
1174
|
.uk-error { margin: 0; color: #a33; font-size: 11px; line-height: 1.45; }
|
|
1170
1175
|
.uk-job-actions { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
|
package/dist/app/lib/publish.js
CHANGED
|
@@ -22,7 +22,15 @@ export function usePublish() {
|
|
|
22
22
|
setAhead(d.ahead ?? 0);
|
|
23
23
|
setCommits((d.documents ?? []).map((document) => ({
|
|
24
24
|
message: `Modifica salvata (${document.kind})`, date: document.updatedAt ? new Date(document.updatedAt).toISOString() : null,
|
|
25
|
-
})))
|
|
25
|
+
})).concat((d.mediaOperations ?? [])
|
|
26
|
+
.filter((operation) => operation.state === "ready")
|
|
27
|
+
.map((operation) => ({
|
|
28
|
+
message: operation.type === "move" ? "Spostamento Media pronto" : "Eliminazione Media pronta",
|
|
29
|
+
date: operation.updatedAt ? new Date(operation.updatedAt).toISOString() : null,
|
|
30
|
+
}))).concat(d.siteAccess ? [{
|
|
31
|
+
message: d.siteAccess.enabled ? "Attivazione o cambio password del sito" : "Disattivazione password del sito",
|
|
32
|
+
date: null,
|
|
33
|
+
}] : []));
|
|
26
34
|
setConfigured(true);
|
|
27
35
|
setError(null);
|
|
28
36
|
}
|
|
@@ -26,4 +26,6 @@ export declare function useSiteSettings(active: boolean): {
|
|
|
26
26
|
change: (next: SiteSettings) => void;
|
|
27
27
|
save: () => Promise<void>;
|
|
28
28
|
setNotice: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
29
|
+
accessDraftPending: boolean;
|
|
30
|
+
discardSiteAccess: () => Promise<void>;
|
|
29
31
|
};
|
package/dist/app/lib/settings.js
CHANGED
|
@@ -37,6 +37,12 @@ export function useSiteSettings(active) {
|
|
|
37
37
|
const [saving, setSaving] = useState(false);
|
|
38
38
|
const [notice, setNotice] = useState(null);
|
|
39
39
|
const [revision, setRevision] = useState(null);
|
|
40
|
+
const [accessState, setAccessState] = useState(null);
|
|
41
|
+
const [editorialBaseline, setEditorialBaseline] = useState("");
|
|
42
|
+
const editorialSnapshot = (settings) => {
|
|
43
|
+
const { password: _password, passwordEnabled: _passwordEnabled, ...editorial } = settings;
|
|
44
|
+
return JSON.stringify(editorial);
|
|
45
|
+
};
|
|
40
46
|
const load = useCallback(async () => {
|
|
41
47
|
try {
|
|
42
48
|
const mapped = site().siteSettingsDocument;
|
|
@@ -49,12 +55,18 @@ export function useSiteSettings(active) {
|
|
|
49
55
|
const settings = mapped.fromPayload ? mapped.fromPayload(stored) : stored;
|
|
50
56
|
if (!settings || typeof settings !== "object" || Array.isArray(settings))
|
|
51
57
|
throw new Error("content_unavailable");
|
|
52
|
-
|
|
58
|
+
const access = site().siteAccess ? (await api.siteAccessState()).state : null;
|
|
59
|
+
const loaded = { ...empty, ...settings, passwordEnabled: access?.effective?.enabled ?? false, password: "" };
|
|
60
|
+
setData(loaded);
|
|
61
|
+
setAccessState(access);
|
|
62
|
+
setEditorialBaseline(editorialSnapshot(loaded));
|
|
53
63
|
setRevision({ base: document.draftRevisionId ?? document.publishedRevisionId, expected: document.draftRevisionId });
|
|
54
64
|
}
|
|
55
65
|
else {
|
|
56
66
|
const result = await call("/admin/api/settings");
|
|
57
|
-
|
|
67
|
+
const loaded = { ...empty, ...result.settings, password: "" };
|
|
68
|
+
setData(loaded);
|
|
69
|
+
setEditorialBaseline(editorialSnapshot(loaded));
|
|
58
70
|
}
|
|
59
71
|
setError(null);
|
|
60
72
|
setDirty(false);
|
|
@@ -75,10 +87,19 @@ export function useSiteSettings(active) {
|
|
|
75
87
|
try {
|
|
76
88
|
const mapped = site().siteSettingsDocument;
|
|
77
89
|
if (contentPersistenceMode() === "d1" && mapped && revision) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
if (editorialSnapshot(data) !== editorialBaseline) {
|
|
91
|
+
const revisionId = crypto.randomUUID();
|
|
92
|
+
const payload = mapped.toPayload ? mapped.toPayload(data) : data;
|
|
93
|
+
await api.saveContent({ revisionId, documentId: mapped.documentId, baseRevisionId: revision.base, expectedDraftRevisionId: revision.expected, kind: mapped.kind, locale: mapped.locale, schemaVersion: mapped.schemaVersion, payload: payload, tombstone: false });
|
|
94
|
+
setRevision({ base: revisionId, expected: revisionId });
|
|
95
|
+
setEditorialBaseline(editorialSnapshot(data));
|
|
96
|
+
}
|
|
97
|
+
const effectiveEnabled = accessState?.effective?.enabled ?? false;
|
|
98
|
+
if (site().siteAccess && (data.password !== "" || data.passwordEnabled !== effectiveEnabled)) {
|
|
99
|
+
const result = await api.saveSiteAccess(data.passwordEnabled, data.password, accessState?.draft?.versionId ?? null);
|
|
100
|
+
setAccessState(result.state);
|
|
101
|
+
setData({ ...data, password: "", passwordEnabled: result.state.effective?.enabled ?? false });
|
|
102
|
+
}
|
|
82
103
|
setDirty(false);
|
|
83
104
|
setNotice("Bozza D1 salvata");
|
|
84
105
|
return;
|
|
@@ -101,10 +122,35 @@ export function useSiteSettings(active) {
|
|
|
101
122
|
finally {
|
|
102
123
|
setSaving(false);
|
|
103
124
|
}
|
|
104
|
-
}, [data, revision]);
|
|
125
|
+
}, [data, revision, accessState, editorialBaseline]);
|
|
105
126
|
const change = useCallback((next) => {
|
|
106
127
|
setData(next);
|
|
107
128
|
setDirty(true);
|
|
108
129
|
}, []);
|
|
109
|
-
|
|
130
|
+
const discardSiteAccess = useCallback(async () => {
|
|
131
|
+
if (!accessState?.draft)
|
|
132
|
+
return;
|
|
133
|
+
setSaving(true);
|
|
134
|
+
setNotice(null);
|
|
135
|
+
try {
|
|
136
|
+
const result = await api.discardSiteAccess(accessState.draft.versionId);
|
|
137
|
+
setAccessState(result.state);
|
|
138
|
+
setData((current) => {
|
|
139
|
+
if (!current)
|
|
140
|
+
return current;
|
|
141
|
+
const next = { ...current, password: "", passwordEnabled: result.state.active?.enabled ?? false };
|
|
142
|
+
setDirty(editorialSnapshot(next) !== editorialBaseline);
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
setNotice("Modifica della password annullata");
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
setNotice(error.message);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
setSaving(false);
|
|
152
|
+
}
|
|
153
|
+
}, [accessState, editorialBaseline]);
|
|
154
|
+
return { data, error, dirty, saving, notice, load, change, save, setNotice,
|
|
155
|
+
accessDraftPending: Boolean(accessState?.draft), discardSiteAccess };
|
|
110
156
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function MediaSettingsEditor({ lib }) {
|
|
3
|
+
return (_jsxs("section", { className: "settings-editor", children: [_jsxs("header", { className: "st-intro", children: [_jsx("span", { children: "Impostazioni Media" }), _jsx("h2", { children: "Caricamento immagini" }), _jsx("p", { children: "Ottimizza le immagini prima che vengano inviate alla libreria del sito." })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Ottimizzazione automatica" }), _jsxs("div", { className: "st-row", children: [_jsxs("div", { className: "st-copy", children: [_jsx("h3", { children: "Converti le immagini in WebP" }), _jsx("p", { children: "JPG, JPEG, PNG, WebP, HEIC e gli altri formati immagine compatibili vengono convertiti in WebP, qualit\u00E0 80, con lato lungo massimo di 2500 px. I file pi\u00F9 piccoli non vengono ingranditi. Video e file non immagine restano invariati." })] }), _jsxs("div", { className: "st-control", children: [_jsxs("label", { className: "st-toggle" + (lib.settingsBusy ? " is-locked" : ""), children: [_jsx("input", { type: "checkbox", checked: lib.autoWebp, disabled: lib.settingsBusy, onChange: (event) => void lib.setAutoWebp(event.target.checked) }), _jsx("span", { className: "st-toggle-track", "aria-hidden": "true", children: _jsx("span", {}) }), _jsxs("span", { children: [lib.autoWebp ? "On" : "Off", " \u00B7 Conversione automatica"] })] }), _jsx("p", { className: "st-inline-help media-settings-note", children: "La conversione (da una a tre immagini) e l\u2019upload (da due a quattro file) usano pool adattivi e procedono in parallelo in base al dispositivo e alla connessione. L\u2019impostazione vale per questo sito e per tutti gli utenti del pannello." })] })] })] }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error })] }));
|
|
4
|
+
}
|
|
@@ -7,13 +7,15 @@ export declare const EXTRAS: {
|
|
|
7
7
|
card: string;
|
|
8
8
|
short: string;
|
|
9
9
|
}[];
|
|
10
|
-
export declare function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, }: {
|
|
10
|
+
export declare function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, passwordDraftPending, onDiscardPasswordDraft, }: {
|
|
11
11
|
value: SiteSettings;
|
|
12
12
|
onChange: (next: SiteSettings) => void;
|
|
13
13
|
pickMediaFiles: (o: {
|
|
14
14
|
multiple: boolean;
|
|
15
15
|
}) => Promise<AssetFile[] | null>;
|
|
16
16
|
focus?: string;
|
|
17
|
+
passwordDraftPending?: boolean;
|
|
18
|
+
onDiscardPasswordDraft?: () => void;
|
|
17
19
|
}): import("react").JSX.Element;
|
|
18
20
|
export declare function WebsiteSeoSettingsEditor({ value, onChange, }: {
|
|
19
21
|
value: SiteSettings;
|
|
@@ -35,8 +35,9 @@ function SettingRow({ title, help, children, }) {
|
|
|
35
35
|
function Field({ value, onChange, maxLength, placeholder, type = "text", disabled = false, }) {
|
|
36
36
|
return (_jsx("input", { className: "st-input", type: type, value: value, maxLength: maxLength, placeholder: placeholder, disabled: disabled, onChange: (event) => onChange(event.target.value) }));
|
|
37
37
|
}
|
|
38
|
-
export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, }) {
|
|
38
|
+
export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, passwordDraftPending = false, onDiscardPasswordDraft, }) {
|
|
39
39
|
const d1Content = contentPersistenceMode() === "d1";
|
|
40
|
+
const managedSiteAccess = d1Content && Boolean(site().siteAccess);
|
|
40
41
|
const update = (key, next) => onChange({ ...value, [key]: next });
|
|
41
42
|
const extras = useRef(null);
|
|
42
43
|
/* Salto SECCO (`behavior: "auto"`), non fluido, e al frame successivo.
|
|
@@ -58,7 +59,7 @@ export function SiteSettingsEditor({ value, onChange, pickMediaFiles, focus, })
|
|
|
58
59
|
if (selected?.[0])
|
|
59
60
|
update("favicon", selected[0].url);
|
|
60
61
|
};
|
|
61
|
-
return (_jsxs("section", { className: "settings-editor", children: [_jsxs("header", { className: "st-intro", children: [_jsx("span", { children: "Impostazioni generali" }), _jsx("h2", { children: "Sito" }), _jsx("p", { children: "Identit\u00E0, descrizione e accesso al sito pubblico." })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Identit\u00E0 del sito" }), _jsx(SettingRow, { title: "Titolo del sito", help: "Il nome generale del sito, usato anche come nome del publisher nei metadati.", children: _jsx(Field, { value: value.siteTitle, maxLength: 140, placeholder: site().name, onChange: (next) => update("siteTitle", next) }) }), _jsxs(SettingRow, { title: "Descrizione breve", help: "Una frase sintetica usata come descrizione predefinita quando una pagina non ne possiede una specifica.", children: [_jsx("textarea", { className: "st-input st-textarea", rows: 3, value: value.shortDescription, maxLength: 255, onChange: (event) => update("shortDescription", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.shortDescription.length, "/255"] })] }), _jsx(SettingRow, { title: "Favicon", help: "L\u2019icona mostrata nella scheda del browser. \u00C8 preferibile un\u2019immagine quadrata.", children: _jsxs("div", { className: "st-favicon", children: [_jsx("span", { className: "st-favicon-preview", children: value.favicon ? _jsx("img", { src: value.favicon, alt: "Anteprima favicon" }) : _jsx(Icon.pages, {}) }), _jsx("button", { className: "ak-btn", type: "button", onClick: () => void chooseFavicon(), children: value.favicon ? "Cambia favicon" : "Carica favicon" }), value.favicon && (_jsx("button", { className: "ak-btn ak-btn-quiet", type: "button", onClick: () => update("favicon", ""), children: "Rimuovi" }))] }) })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Accesso al sito" }), _jsx(SettingRow, { title: "Password del sito", help: "Quando \u00E8 attiva, ogni pagina e ogni immagine pubblica richiedono la stessa password. L\u2019admin resta raggiungibile tramite il suo accesso protetto.", children: _jsxs("div", { className: "st-stack", children: [_jsx(Field, { type: "password", value: value.password, maxLength: 200, placeholder: value.passwordEnabled ? "Password già impostata · inseriscine una per cambiarla" : "Almeno 8 caratteri", disabled: d1Content, onChange: (next) => update("password", next) }), _jsx(Toggle, { checked: value.passwordEnabled, label: "Protezione con password", disabled: d1Content, onChange: (next) => update("passwordEnabled", next) }), d1Content && (_jsx("p", { className: "st-inline-help", children: "La protezione esistente resta attiva e viene gestita dall\u2019operatore durante la migrazione D1." })), value.passwordEnabled && (_jsx("p", { className: "st-inline-help", children: "Lascia il campo vuoto per mantenere la password attuale." }))] }) })] }), _jsxs("section", { className: "st-section" + (focus === "funzioni" ? " is-focused" : ""), ref: extras, children: [_jsx("h2", { children: "Funzioni aggiuntive" }), EXTRAS.map((extra) => (_jsx(SettingRow, { title: extra.title, help: extra.help, children: _jsx(Toggle, { checked: value.features[extra.key], disabled: true, label: value.features[extra.key] ? "Attiva su questo sito" : "Non attiva", onChange: () => { } }) }, extra.key))), _jsxs("p", { className: "st-inline-help st-extras-note", children: ["Queste funzioni si attivano separatamente. Per accenderle su questo sito scrivi a", " ", _jsx("strong", { children: site().support.name }), ":", " ", _jsx("a", { href: `mailto:${site().support.email}`, children: site().support.email }), "."] })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Statistiche" }), _jsx(SettingRow, { title: "Tag Analytics", help: _jsxs(_Fragment, { children: ["Il codice fornito dal servizio di statistiche (Umami, Google Analytics, altro). Si incolla qui cos\u00EC com\u2019\u00E8: finisce dentro ", _jsx("code", { children: "<head>" }), " di ogni pagina del sito alla prossima pubblicazione. Le pagine di consegna ai clienti restano escluse. Per svuotarlo, cancella tutto e salva."] }), children: _jsxs("div", { className: "st-stack", children: [_jsx("textarea", { className: "st-input st-textarea st-code", rows: 6, value: value.analyticsTag, maxLength: 4000, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", placeholder: '<script defer src="https://cloud.umami.is/script.js" data-website-id="…"></script>', onChange: (event) => update("analyticsTag", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.analyticsTag.length, "/4000"] }), _jsxs("p", { className: "st-inline-help", children: ["\u00C8 codice eseguito su ogni pagina: incolla solo quello che ti ha dato il servizio. Un tag incompleto (per esempio senza ", _jsx("code", { children: "</script>" }), ") pu\u00F2 rompere il sito."] })] }) })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Profilo" }), _jsx(SettingRow, { title: "Nome", help: "Il nome della persona proprietaria del sito. Sar\u00E0 disponibile per future funzioni del pannello.", children: _jsx(Field, { value: value.ownerName, maxLength: 120, placeholder: site().name, onChange: (next) => update("ownerName", next) }) }), _jsxs(SettingRow, { title: "Descrizione del sito", help: "Una descrizione generale pi\u00F9 estesa, conservata come informazione del profilo del sito.", children: [_jsx("textarea", { className: "st-input st-textarea", rows: 5, value: value.siteDescription, maxLength: 1200, onChange: (event) => update("siteDescription", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.siteDescription.length, "/1200"] })] })] })] }));
|
|
62
|
+
return (_jsxs("section", { className: "settings-editor", children: [_jsxs("header", { className: "st-intro", children: [_jsx("span", { children: "Impostazioni generali" }), _jsx("h2", { children: "Sito" }), _jsx("p", { children: "Identit\u00E0, descrizione e accesso al sito pubblico." })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Identit\u00E0 del sito" }), _jsx(SettingRow, { title: "Titolo del sito", help: "Il nome generale del sito, usato anche come nome del publisher nei metadati.", children: _jsx(Field, { value: value.siteTitle, maxLength: 140, placeholder: site().name, onChange: (next) => update("siteTitle", next) }) }), _jsxs(SettingRow, { title: "Descrizione breve", help: "Una frase sintetica usata come descrizione predefinita quando una pagina non ne possiede una specifica.", children: [_jsx("textarea", { className: "st-input st-textarea", rows: 3, value: value.shortDescription, maxLength: 255, onChange: (event) => update("shortDescription", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.shortDescription.length, "/255"] })] }), _jsx(SettingRow, { title: "Favicon", help: "L\u2019icona mostrata nella scheda del browser. \u00C8 preferibile un\u2019immagine quadrata.", children: _jsxs("div", { className: "st-favicon", children: [_jsx("span", { className: "st-favicon-preview", children: value.favicon ? _jsx("img", { src: value.favicon, alt: "Anteprima favicon" }) : _jsx(Icon.pages, {}) }), _jsx("button", { className: "ak-btn", type: "button", onClick: () => void chooseFavicon(), children: value.favicon ? "Cambia favicon" : "Carica favicon" }), value.favicon && (_jsx("button", { className: "ak-btn ak-btn-quiet", type: "button", onClick: () => update("favicon", ""), children: "Rimuovi" }))] }) })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Accesso al sito" }), _jsx(SettingRow, { title: "Password del sito", help: "Quando \u00E8 attiva, ogni pagina e ogni immagine pubblica richiedono la stessa password. L\u2019admin resta raggiungibile tramite il suo accesso protetto.", children: _jsxs("div", { className: "st-stack", children: [_jsx(Field, { type: "password", value: value.password, maxLength: 200, placeholder: value.passwordEnabled ? "Password già impostata · inseriscine una per cambiarla" : "Almeno 8 caratteri", disabled: d1Content && !managedSiteAccess, onChange: (next) => update("password", next) }), _jsx(Toggle, { checked: value.passwordEnabled, label: "Protezione con password", disabled: d1Content && !managedSiteAccess, onChange: (next) => update("passwordEnabled", next) }), d1Content && !managedSiteAccess && (_jsx("p", { className: "st-inline-help", children: "La protezione esistente resta attiva e viene gestita dall\u2019operatore durante la migrazione D1." })), managedSiteAccess && (_jsx("p", { className: "st-inline-help", children: "La modifica resta in bozza e diventa effettiva solo dopo una pubblicazione completata." })), managedSiteAccess && passwordDraftPending && onDiscardPasswordDraft && (_jsx("button", { className: "ak-btn ak-btn-quiet", type: "button", onClick: onDiscardPasswordDraft, children: "Annulla la modifica salvata" })), value.passwordEnabled && (_jsx("p", { className: "st-inline-help", children: "Lascia il campo vuoto per mantenere la password attuale." }))] }) })] }), _jsxs("section", { className: "st-section" + (focus === "funzioni" ? " is-focused" : ""), ref: extras, children: [_jsx("h2", { children: "Funzioni aggiuntive" }), EXTRAS.map((extra) => (_jsx(SettingRow, { title: extra.title, help: extra.help, children: _jsx(Toggle, { checked: value.features[extra.key], disabled: true, label: value.features[extra.key] ? "Attiva su questo sito" : "Non attiva", onChange: () => { } }) }, extra.key))), _jsxs("p", { className: "st-inline-help st-extras-note", children: ["Queste funzioni si attivano separatamente. Per accenderle su questo sito scrivi a", " ", _jsx("strong", { children: site().support.name }), ":", " ", _jsx("a", { href: `mailto:${site().support.email}`, children: site().support.email }), "."] })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Statistiche" }), _jsx(SettingRow, { title: "Tag Analytics", help: _jsxs(_Fragment, { children: ["Il codice fornito dal servizio di statistiche (Umami, Google Analytics, altro). Si incolla qui cos\u00EC com\u2019\u00E8: finisce dentro ", _jsx("code", { children: "<head>" }), " di ogni pagina del sito alla prossima pubblicazione. Le pagine di consegna ai clienti restano escluse. Per svuotarlo, cancella tutto e salva."] }), children: _jsxs("div", { className: "st-stack", children: [_jsx("textarea", { className: "st-input st-textarea st-code", rows: 6, value: value.analyticsTag, maxLength: 4000, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", placeholder: '<script defer src="https://cloud.umami.is/script.js" data-website-id="…"></script>', onChange: (event) => update("analyticsTag", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.analyticsTag.length, "/4000"] }), _jsxs("p", { className: "st-inline-help", children: ["\u00C8 codice eseguito su ogni pagina: incolla solo quello che ti ha dato il servizio. Un tag incompleto (per esempio senza ", _jsx("code", { children: "</script>" }), ") pu\u00F2 rompere il sito."] })] }) })] }), _jsxs("section", { className: "st-section", children: [_jsx("h2", { children: "Profilo" }), _jsx(SettingRow, { title: "Nome", help: "Il nome della persona proprietaria del sito. Sar\u00E0 disponibile per future funzioni del pannello.", children: _jsx(Field, { value: value.ownerName, maxLength: 120, placeholder: site().name, onChange: (next) => update("ownerName", next) }) }), _jsxs(SettingRow, { title: "Descrizione del sito", help: "Una descrizione generale pi\u00F9 estesa, conservata come informazione del profilo del sito.", children: [_jsx("textarea", { className: "st-input st-textarea", rows: 5, value: value.siteDescription, maxLength: 1200, onChange: (event) => update("siteDescription", event.target.value) }), _jsxs("span", { className: "st-count", children: [value.siteDescription.length, "/1200"] })] })] })] }));
|
|
62
63
|
}
|
|
63
64
|
export function WebsiteSeoSettingsEditor({ value, onChange, }) {
|
|
64
65
|
const update = (key, next) => onChange({ ...value, [key]: next });
|
|
@@ -12,11 +12,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
*/
|
|
13
13
|
import { useEffect, useState } from "react";
|
|
14
14
|
import { cancelUpload, dismissUpload, isBusy, pauseUpload, resumeUpload, useUploads, } from "../lib/upload-queue";
|
|
15
|
+
import { dismissMediaUpload, useMediaUploads, } from "../../form/media/media-upload-queue.js";
|
|
15
16
|
export function UploadDock() {
|
|
16
17
|
const jobs = useUploads();
|
|
18
|
+
const mediaJobs = useMediaUploads();
|
|
17
19
|
const [open, setOpen] = useState(true);
|
|
18
20
|
const [confirmStop, setConfirmStop] = useState(null);
|
|
19
|
-
const busy = jobs.some((job) => job.state === "running");
|
|
21
|
+
const busy = jobs.some(isBusy) || mediaJobs.some((job) => job.state === "running");
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
if (!busy)
|
|
22
24
|
return;
|
|
@@ -24,21 +26,41 @@ export function UploadDock() {
|
|
|
24
26
|
window.addEventListener("beforeunload", warn);
|
|
25
27
|
return () => window.removeEventListener("beforeunload", warn);
|
|
26
28
|
}, [busy]);
|
|
27
|
-
if (!jobs.length)
|
|
29
|
+
if (!jobs.length && !mediaJobs.length)
|
|
28
30
|
return null;
|
|
29
|
-
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) }), _jsx("span", { className: "uk-chevron", "aria-hidden": "true", children: open ? "▾" : "▴" })] }), open && (
|
|
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)))] }))] }));
|
|
30
32
|
}
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
+
function MediaJob({ job }) {
|
|
34
|
+
const conversionPercent = job.total ? (job.conversionDone / job.total) * 100 : 0;
|
|
35
|
+
const uploadPercent = job.total ? (job.uploadDone / job.total) * 100 : 0;
|
|
36
|
+
return (_jsxs("li", { className: "uk-job uk-media-job is-" + job.state, children: [_jsxs("div", { className: "uk-job-top", children: [_jsx("strong", { children: job.title }), _jsx("span", { children: mediaLine(job) })] }), _jsxs("div", { className: "uk-phase", children: [_jsxs("span", { children: [job.autoWebp ? "Conversione" : "Preparazione", " \u00B7 ", job.conversionDone, "/", job.total] }), _jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${Math.max(3, conversionPercent)}%` } }) })] }), _jsxs("div", { className: "uk-phase", children: [_jsxs("span", { children: ["Upload \u00B7 ", job.uploadDone, "/", job.total] }), _jsx("div", { className: "uk-track", children: _jsx("i", { style: { width: `${Math.max(3, uploadPercent)}%` } }) })] }), job.originals > 0 && (_jsx("p", { className: "uk-warning", children: job.originals === 1 ? "1 originale caricato dopo tre tentativi di conversione." :
|
|
37
|
+
`${job.originals} originali caricati dopo tre tentativi di conversione.` })), job.errors.map((error, index) => _jsx("p", { className: "uk-error", children: error }, `${index}-${error}`)), job.state !== "running" && (_jsx("div", { className: "uk-job-actions", children: _jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => dismissMediaUpload(job.id), children: "Togli dall\u2019elenco" }) }))] }));
|
|
38
|
+
}
|
|
39
|
+
function mediaLine(job) {
|
|
40
|
+
if (job.state === "failed")
|
|
41
|
+
return `Completato con ${job.failed} ${job.failed === 1 ? "errore" : "errori"}`;
|
|
42
|
+
if (job.state === "done")
|
|
43
|
+
return job.originals ? "Completato con originali" : "Completato";
|
|
44
|
+
if (job.uploadDone < job.conversionDone)
|
|
45
|
+
return "Conversione e upload in parallelo";
|
|
46
|
+
return job.autoWebp ? "Converto le immagini" : "Preparo i file";
|
|
47
|
+
}
|
|
48
|
+
function summary(jobs, mediaJobs) {
|
|
49
|
+
const running = jobs.filter((job) => job.state === "running").length +
|
|
50
|
+
mediaJobs.filter((job) => job.state === "running").length;
|
|
33
51
|
const paused = jobs.filter((job) => job.state === "paused").length;
|
|
34
|
-
const
|
|
52
|
+
const interrupted = jobs.filter((job) => job.state === "failed").length;
|
|
53
|
+
const partial = mediaJobs.filter((job) => job.state === "failed").length;
|
|
54
|
+
const total = jobs.length + mediaJobs.length;
|
|
35
55
|
if (running)
|
|
36
56
|
return running === 1 ? "Caricamento in corso" : `${running} caricamenti in corso`;
|
|
37
57
|
if (paused)
|
|
38
58
|
return paused === 1 ? "Caricamento in pausa" : `${paused} caricamenti in pausa`;
|
|
39
|
-
if (
|
|
40
|
-
return
|
|
41
|
-
|
|
59
|
+
if (interrupted)
|
|
60
|
+
return interrupted === 1 ? "Caricamento interrotto" : `${interrupted} caricamenti interrotti`;
|
|
61
|
+
if (partial)
|
|
62
|
+
return partial === 1 ? "Caricamento completato con errori" : `${partial} caricamenti con errori`;
|
|
63
|
+
return total === 1 ? "Caricamento completato" : "Caricamenti completati";
|
|
42
64
|
}
|
|
43
65
|
function line(job) {
|
|
44
66
|
if (job.state === "done") {
|
package/dist/app/site.d.ts
CHANGED
|
@@ -82,6 +82,10 @@ export interface SiteAdminConfig {
|
|
|
82
82
|
fromPayload?: (payload: unknown) => Record<string, unknown>;
|
|
83
83
|
toPayload?: (settings: Record<string, unknown>) => unknown;
|
|
84
84
|
};
|
|
85
|
+
/** Separate versioned D1 access control. Passwords never enter an editorial document. */
|
|
86
|
+
siteAccess?: {
|
|
87
|
+
siteId: string;
|
|
88
|
+
};
|
|
85
89
|
}
|
|
86
90
|
/** Chiamata da `mountAdmin` prima del primo render. */
|
|
87
91
|
export declare function setSiteConfig(config: SiteAdminConfig): void;
|