@cardinali-studio/adminkit 2.2.15 → 2.2.16

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/app/App.js CHANGED
@@ -244,7 +244,7 @@ export function App() {
244
244
  : deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
245
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 })
246
246
  : !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
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"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_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", disabled: media.busy || media.mutating, 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"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_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", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
248
248
  /* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
249
249
  rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
250
250
  il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
@@ -12,9 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  * stesso giro. Il bersaglio si illumina mentre ci passi sopra: senza, non si capisce se il drop
13
13
  * sta per funzionare, e uno spostamento andato a vuoto sembra un bug.
14
14
  *
15
- * «Tutte» accetta solo CARTELLE: è così che una sottocartella torna al primo livello. Una foto
16
- * no — la Function rifiuta una destinazione vuota per un file, e chi ce la lascia cadere se lo
17
- * sente dire invece di veder sparire il gesto nel nulla.
15
+ * «Senza cartella» accetta un file esterno come destinazione ESPLICITA di upload; per gli
16
+ * spostamenti interni continua ad accettare solo CARTELLE, perché la Function rifiuta una
17
+ * destinazione vuota per un file esistente.
18
18
  */
19
19
  import { useEffect, useState } from "react";
20
20
  export const DRAG_TYPE = "application/x-adminkit-keys";
@@ -70,7 +70,8 @@ const IcoSettings = () => (_jsxs("svg", { className: "mf-ico", viewBox: "0 0 16
70
70
  export function MediaFolders({ lib, trash, settings }) {
71
71
  const [over, setOver] = useState(null);
72
72
  const [draggingFolder, setDraggingFolder] = useState(false);
73
- const canDrop = lib.mode === "manage" && !lib.busy && !lib.mutating;
73
+ const canMove = lib.mode === "manage" && !lib.busy && !lib.mutating;
74
+ const canUpload = !lib.busy && !lib.mutating;
74
75
  /* Nel cestino non si è in nessuna cartella: senza questo, «Tutte» resterebbe accesa insieme a
75
76
  «Cestino» e la colonna indicherebbe due posti nello stesso momento. */
76
77
  const here = (p) => !trash?.active && (p === "" ? lib.prefix === "" : lib.prefix.startsWith(p));
@@ -80,19 +81,34 @@ export function MediaFolders({ lib, trash, settings }) {
80
81
  trash.leave(); lib.go(p); };
81
82
  const drop = (dest) => (e) => {
82
83
  setOver(null);
83
- if (!canDrop)
84
+ if (canUpload && e.dataTransfer.files.length) {
85
+ e.preventDefault();
86
+ if (lib.mode === "pick")
87
+ lib.openUpload(e.dataTransfer.files, dest);
88
+ else
89
+ void lib.upload(e.dataTransfer.files, dest);
90
+ return;
91
+ }
92
+ if (!canMove)
84
93
  return;
85
94
  e.preventDefault();
86
95
  const ids = readDragged(e.dataTransfer);
87
96
  if (ids.length)
88
97
  void lib.move(ids, dest);
89
98
  };
99
+ const dragOver = (dest) => (e) => {
100
+ const files = e.dataTransfer.types.includes("Files");
101
+ if ((files && canUpload) || (!files && canMove)) {
102
+ e.preventDefault();
103
+ setOver(dest);
104
+ }
105
+ };
90
106
  /* Vero mentre si trascina una cartella, ovunque nella pagina. Serve a far comparire il
91
107
  bersaglio del primo livello solo in quel momento: una riga fissa «porta qui» sarebbe rumore
92
108
  per il 99% del tempo, e prima quel bersaglio non esisteva proprio — la sola via era «Tutte»,
93
109
  che nessuno poteva indovinare. */
94
110
  useEffect(() => {
95
- if (!canDrop)
111
+ if (!canMove)
96
112
  return;
97
113
  const start = (e) => setDraggingFolder(!!e.dataTransfer?.types.includes(DRAG_FOLDERS));
98
114
  const end = () => setDraggingFolder(false);
@@ -104,14 +120,11 @@ export function MediaFolders({ lib, trash, settings }) {
104
120
  removeEventListener("dragend", end);
105
121
  removeEventListener("drop", end);
106
122
  };
107
- }, [canDrop]);
108
- return (_jsxs("div", { className: "mf", children: [_jsx("ul", { className: "mf-list", children: _jsx("li", { className: "mf-item mf-loose" + (here("") ? " is-on" : ""), children: _jsx("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
123
+ }, [canMove]);
124
+ return (_jsxs("div", { className: "mf", children: [_jsx("ul", { className: "mf-list", children: _jsx("li", { className: "mf-item mf-loose" + (here("") ? " is-on" : "") + (over === "" ? " is-over" : ""), onDragOver: dragOver(""), onDragLeave: () => setOver((p) => (p === "" ? null : p)), onDrop: drop(""), children: _jsx("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(""), children: _jsx("span", { className: "mf-name", children: "Senza cartella" }) }) }) }), _jsx("ul", { className: "mf-list", children: lib.folders.map((f) => (_jsxs("li", {
109
125
  /* Accesa anche quando si è DENTRO una sua sottocartella: la colonna risponde a
110
126
  «dove sono», e stando in food/piatti-2024/ la risposta è ancora food. */
111
127
  className: "mf-item"
112
128
  + (here(f.prefix) ? " is-on" : "")
113
- + (over === f.prefix ? " is-over" : ""), onDragOver: (e) => { if (canDrop) {
114
- e.preventDefault();
115
- setOver(f.prefix);
116
- } }, onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(f.prefix), children: [_jsx("span", { className: "mf-name", children: f.label || f.name }), f.count != null && _jsx("span", { className: "mf-count", children: f.count })] }), canDrop && _jsx(FolderMenu, { lib: lib, prefix: f.prefix, label: f.label || f.name })] }, f.prefix))) }), draggingFolder && (_jsx("div", { className: "mf-root-drop" + (over === "__root" ? " is-over" : ""), onDragOver: (e) => { e.preventDefault(); setOver("__root"); }, onDragLeave: () => setOver((p) => (p === "__root" ? null : p)), onDrop: drop(""), children: "Porta al primo livello" })), canDrop && (_jsx("button", { className: "ak-btn ak-btn-quiet mf-new", disabled: lib.busy || lib.mutating, onClick: () => void lib.newFolder(""), children: "+ Cartella" })), trash && (_jsx("ul", { className: "mf-list mf-tail", children: _jsx("li", { className: "mf-item" + (trash.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) })), settings && (_jsx("ul", { className: "mf-list mf-settings-tail", children: _jsx("li", { className: "mf-item" + (settings.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: settings.open, children: [_jsx(IcoSettings, {}), _jsx("span", { className: "mf-name", children: "Impostazioni" })] }) }) }))] }));
129
+ + (over === f.prefix ? " is-over" : ""), onDragOver: dragOver(f.prefix), onDragLeave: () => setOver((p) => (p === f.prefix ? null : p)), onDrop: drop(f.prefix), children: [_jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: () => goTo(f.prefix), children: [_jsx("span", { className: "mf-name", children: f.label || f.name }), f.count != null && _jsx("span", { className: "mf-count", children: f.count })] }), canMove && _jsx(FolderMenu, { lib: lib, prefix: f.prefix, label: f.label || f.name })] }, f.prefix))) }), draggingFolder && (_jsx("div", { className: "mf-root-drop" + (over === "__root" ? " is-over" : ""), onDragOver: (e) => { e.preventDefault(); setOver("__root"); }, onDragLeave: () => setOver((p) => (p === "__root" ? null : p)), onDrop: drop(""), children: "Porta al primo livello" })), canMove && (_jsx("button", { className: "ak-btn ak-btn-quiet mf-new", disabled: lib.busy || lib.mutating, onClick: () => void lib.newFolder(""), children: "+ Cartella" })), trash && (_jsx("ul", { className: "mf-list mf-tail", children: _jsx("li", { className: "mf-item" + (trash.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: trash.open, children: [_jsx(IcoTrash, {}), _jsx("span", { className: "mf-name", children: "Cestino" })] }) }) })), settings && (_jsx("ul", { className: "mf-list mf-settings-tail", children: _jsx("li", { className: "mf-item" + (settings.active ? " is-on" : ""), children: _jsxs("button", { className: "mf-open", disabled: lib.mutating, onClick: settings.open, children: [_jsx(IcoSettings, {}), _jsx("span", { className: "mf-name", children: "Impostazioni" })] }) }) }))] }));
117
130
  }
@@ -58,7 +58,7 @@ function MoveMenu({ lib, keys }) {
58
58
  * seleziona insieme alle foto, e si trascina — dentro un'altra cartella qui, o sulla colonna di
59
59
  * sinistra per riportarla al primo livello. Ed è anche un BERSAGLIO: ci si lasciano cadere foto
60
60
  * o altre cartelle. */
61
- function FolderCell({ lib, folder, manage }) {
61
+ function FolderCell({ lib, folder, manage, onExternalDrop }) {
62
62
  const [over, setOver] = useState(false);
63
63
  const on = lib.selected.has(folder.prefix);
64
64
  const name = folder.label || folder.name;
@@ -84,6 +84,11 @@ function FolderCell({ lib, folder, manage }) {
84
84
  "è roba nostra": il tipo dei nostri dati non è leggibile in ogni passaggio del trascinamento,
85
85
  e un bersaglio che non si accende sembra un bersaglio che non funziona. */
86
86
  onDragOver: (e) => {
87
+ if (!manage && !locked && e.dataTransfer.types.includes("Files")) {
88
+ e.preventDefault();
89
+ setOver(true);
90
+ return;
91
+ }
87
92
  if (!manage || locked || e.dataTransfer.types.includes("Files"))
88
93
  return;
89
94
  /* Si accende anche quando la cartella trascinata è questa stessa: durante il
@@ -93,6 +98,13 @@ function FolderCell({ lib, folder, manage }) {
93
98
  setOver(true);
94
99
  }, onDragLeave: () => setOver(false), onDrop: (e) => {
95
100
  setOver(false);
101
+ if (!manage && !locked && e.dataTransfer.files.length) {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ onExternalDrop();
105
+ lib.openUpload(e.dataTransfer.files, folder.prefix);
106
+ return;
107
+ }
96
108
  if (!manage || locked)
97
109
  return;
98
110
  const ids = readDragged(e.dataTransfer);
@@ -116,7 +128,7 @@ export function MediaGrid({ lib, onPick, onCancel }) {
116
128
  zona di caricamento non c'entra. Il controllo è "NON è roba nostra" invece di "è un file":
117
129
  `types` non contiene "Files" allo stesso modo in ogni browser, e con quel controllo il
118
130
  caricamento non partiva affatto dove il nome era diverso. */
119
- const external = (e) => manage && !locked && !e.dataTransfer.types.includes(DRAG_TYPE) && !e.dataTransfer.types.includes(DRAG_FOLDERS);
131
+ const external = (e) => !locked && !e.dataTransfer.types.includes(DRAG_TYPE) && !e.dataTransfer.types.includes(DRAG_FOLDERS);
120
132
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mg-wrap" + (dragDepth > 0 ? " is-dropping" : ""), "aria-busy": lib.mutating ? "true" : "false",
121
133
  /* Trascinare file dal Finder sulla griglia carica, senza passare dal dialogo: è il gesto
122
134
  più corto e va lasciato funzionare. Il dialogo serve a chi cerca un bottone.
@@ -130,13 +142,18 @@ export function MediaGrid({ lib, onPick, onCancel }) {
130
142
  e.preventDefault(); }, onDragLeave: (e) => { if (external(e))
131
143
  setDragDepth((n) => Math.max(0, n - 1)); }, onDrop: (e) => {
132
144
  setDragDepth(0);
133
- if (!manage || locked || !e.dataTransfer.files.length)
145
+ if (locked || !e.dataTransfer.files.length)
134
146
  return;
135
147
  e.preventDefault();
136
- void lib.upload(e.dataTransfer.files);
137
- }, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] }) })), _jsx("header", { className: "mg-bar", children: sel.size === 0 ? (_jsxs(_Fragment, { children: [(manage || lib.multiple) && (_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: false, disabled: locked || !lib.selectableIds.length, ref: (el) => { if (el)
138
- el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), _jsx("span", { className: "ak-spacer" }), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, disabled: locked, onChange: (e) => lib.setSort(e.target.value), children: Object.entries(SORT_LABELS).map(([k, l]) => _jsx("option", { value: k, children: l }, k)) }), _jsx("button", { className: "ak-icon", disabled: locked, onClick: lib.flipDir, title: "Inverti l'ordine", children: lib.sortDir === "asc" ? "↑" : "↓" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: allOn, disabled: locked, ref: (el) => { if (el)
139
- el.indeterminate = lib.someSelected; }, onChange: () => lib.setAll(!allOn) }), _jsx("span", { children: selection.label })] }), !allOn && lib.selectableIds.length > 0 && (_jsxs("button", { className: "mg-act is-link", disabled: locked, onClick: () => lib.setAll(true), children: ["Seleziona tutti gli elementi (", lib.selectableIds.length, ")"] })), manage && (_jsxs(_Fragment, { children: [lib.selectedFiles.length > 0 && (_jsxs("button", { className: "mg-act", disabled: locked, onClick: () => download(lib.shown.filter((f) => sel.has(f.key)).map((f) => ({ url: f.url, name: f.name }))), children: ["Scarica ", lib.selectedFiles.length, " file"] })), _jsx(MoveMenu, { lib: lib, keys: [...sel] }), _jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.remove([...sel]), children: "Elimina" })] })), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "mg-act", disabled: locked, onClick: () => lib.setAll(false), children: "Deseleziona" })] })) }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error }), manage && lib.mediaOperations.length > 0 && (_jsx("section", { className: "mg-journal", "aria-live": "polite", "aria-label": "Stato operazioni Media", children: lib.mediaOperations.map((operation) => (_jsxs("div", { className: `mg-journal-row is-${operation.state}`, children: [_jsx("span", { className: "mg-journal-badge", children: operation.type === "move" ? "Spostamento" : "Eliminazione" }), _jsx("span", { children: JOURNAL_LABELS[operation.state] ?? "Operazione Media in attesa." }), operation.failedEntries > 0 && _jsxs("span", { className: "ak-hint", children: [operation.failedEntries, " file da riprovare"] }), _jsx("span", { className: "ak-spacer" }), operation.state === "completed_with_warning" && operation.releaseId && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.resumeMedia(operation), children: "Riprendi" })), ["preparing", "ready", "blocked_in_use", "failed"].includes(operation.state) && !operation.releaseId && (_jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.cancelMedia(operation), children: "Annulla" }))] }, operation.operationId))) })), lib.progress && (_jsxs("p", { className: "mg-progress", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026 \u2014 non chiudere la pagina"] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && (_jsxs("section", { className: "mg-section", children: [_jsxs("div", { className: "mg-section-head", children: [_jsx("h2", { className: "mg-section-title", children: "Cartelle" }), manage && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.newFolder(lib.prefix), children: "+ Cartella" }))] }), lib.subfolders.length > 0 && (_jsx("div", { className: "mg", children: lib.subfolders.map((f) => (_jsx(FolderCell, { lib: lib, folder: f, manage: manage }, f.prefix))) }))] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && lib.shown.length > 0 && (_jsx("h2", { className: "mg-section-title", children: "Foto" })), _jsx("div", { className: "mg", children: lib.shown.map((f, i) => {
148
+ if (manage)
149
+ void lib.upload(e.dataTransfer.files);
150
+ else
151
+ lib.openUpload(e.dataTransfer.files);
152
+ }, children: [dragDepth > 0 && (_jsx("div", { className: "mg-dropzone", children: manage
153
+ ? _jsxs("span", { children: ["Rilascia per caricare in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella" })] })
154
+ : _jsx("span", { children: "Rilascia le immagini \u2014 sceglierai la cartella prima del caricamento" }) })), _jsx("header", { className: "mg-bar", children: sel.size === 0 ? (_jsxs(_Fragment, { children: [(manage || lib.multiple) && (_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: false, disabled: locked || !lib.selectableIds.length, ref: (el) => { if (el)
155
+ el.indeterminate = false; }, onChange: () => lib.setAll(true) }), _jsx("span", { children: "Seleziona tutto" })] })), !manage && (_jsx("button", { className: "ak-btn ak-btn-quiet mg-upload", disabled: locked, onClick: () => lib.openUpload(), children: "Carica nuove foto" })), _jsx("span", { className: "ak-spacer" }), _jsxs("label", { className: "ak-hint", children: ["ordina per", " ", _jsx("select", { className: "mg-sort", value: lib.sortKey, disabled: locked, onChange: (e) => lib.setSort(e.target.value), children: Object.entries(SORT_LABELS).map(([k, l]) => _jsx("option", { value: k, children: l }, k)) }), _jsx("button", { className: "ak-icon", disabled: locked, onClick: lib.flipDir, title: "Inverti l'ordine", children: lib.sortDir === "asc" ? "↑" : "↓" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "mg-all", children: [_jsx("input", { type: "checkbox", checked: allOn, disabled: locked, ref: (el) => { if (el)
156
+ el.indeterminate = lib.someSelected; }, onChange: () => lib.setAll(!allOn) }), _jsx("span", { children: selection.label })] }), !allOn && lib.selectableIds.length > 0 && (_jsxs("button", { className: "mg-act is-link", disabled: locked, onClick: () => lib.setAll(true), children: ["Seleziona tutti gli elementi (", lib.selectableIds.length, ")"] })), manage && (_jsxs(_Fragment, { children: [lib.selectedFiles.length > 0 && (_jsxs("button", { className: "mg-act", disabled: locked, onClick: () => download(lib.shown.filter((f) => sel.has(f.key)).map((f) => ({ url: f.url, name: f.name }))), children: ["Scarica ", lib.selectedFiles.length, " file"] })), _jsx(MoveMenu, { lib: lib, keys: [...sel] }), _jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.remove([...sel]), children: "Elimina" })] })), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "mg-act", disabled: locked, onClick: () => lib.setAll(false), children: "Deseleziona" })] })) }), lib.error && _jsx("p", { className: "ak-error", onClick: lib.clearError, children: lib.error }), manage && lib.mediaOperations.length > 0 && (_jsx("section", { className: "mg-journal", "aria-live": "polite", "aria-label": "Stato operazioni Media", children: lib.mediaOperations.map((operation) => (_jsxs("div", { className: `mg-journal-row is-${operation.state}`, children: [_jsx("span", { className: "mg-journal-badge", children: operation.type === "move" ? "Spostamento" : "Eliminazione" }), _jsx("span", { children: JOURNAL_LABELS[operation.state] ?? "Operazione Media in attesa." }), operation.failedEntries > 0 && _jsxs("span", { className: "ak-hint", children: [operation.failedEntries, " file da riprovare"] }), _jsx("span", { className: "ak-spacer" }), operation.state === "completed_with_warning" && operation.releaseId && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.resumeMedia(operation), children: "Riprendi" })), ["preparing", "ready", "blocked_in_use", "failed"].includes(operation.state) && !operation.releaseId && (_jsx("button", { className: "mg-act is-danger", disabled: locked, onClick: () => void lib.cancelMedia(operation), children: "Annulla" }))] }, operation.operationId))) })), lib.progress && (_jsxs("p", { className: "mg-progress", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026 \u2014 non chiudere la pagina"] })), lib.pickUploadJob?.state === "running" && (_jsxs("p", { className: "mg-progress", children: ["Nuove immagini: preparo ", lib.pickUploadJob.conversionDone, "/", lib.pickUploadJob.total, ", carico ", lib.pickUploadJob.uploadDone, "/", lib.pickUploadJob.total, "\u2026"] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && (_jsxs("section", { className: "mg-section", children: [_jsxs("div", { className: "mg-section-head", children: [_jsx("h2", { className: "mg-section-title", children: "Cartelle" }), manage && (_jsx("button", { className: "mg-act", disabled: locked, onClick: () => void lib.newFolder(lib.prefix), children: "+ Cartella" }))] }), lib.subfolders.length > 0 && (_jsx("div", { className: "mg", children: lib.subfolders.map((f) => (_jsx(FolderCell, { lib: lib, folder: f, manage: manage, onExternalDrop: () => setDragDepth(0) }, f.prefix))) }))] })), lib.prefix !== "" && (manage || lib.subfolders.length > 0) && lib.shown.length > 0 && (_jsx("h2", { className: "mg-section-title", children: "Foto" })), _jsx("div", { className: "mg", children: lib.shown.map((f, i) => {
140
157
  const on = sel.has(f.key);
141
158
  return (_jsxs("figure", { className: "mg-item" + (on ? " is-on" : ""), draggable: manage && !locked, onDragStart: (e) => {
142
159
  if (locked) {
@@ -159,5 +176,5 @@ export function MediaGrid({ lib, onPick, onCancel }) {
159
176
  style: { aspectRatio: f.w && f.h ? `${f.w} / ${f.h}` : "3 / 2" } }), _jsx("button", { className: "mg-check", "aria-label": on ? "Deseleziona" : "Seleziona", disabled: locked, onClick: (e) => { e.stopPropagation(); lib.toggle(f.key); }, children: on ? "✓" : "" })] }), _jsx("figcaption", { className: "mg-cap", title: f.name, children: f.name })] }, f.key));
160
177
  }) }), !lib.busy && lib.shown.length === 0 && (_jsx("p", { className: "ak-hint", children: lib.prefix === "" && lib.folders.length
161
178
  ? "Nessuna foto fuori dalle cartelle. Scegline una qui a fianco."
162
- : "Nessun file qui." })), lib.mode === "pick" && (_jsxs("footer", { className: "mg-foot", children: [_jsxs("span", { className: "ak-hint", children: [sel.size, " selezionate"] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: locked, onClick: () => onCancel?.(), children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: locked || !sel.size, onClick: () => onPick?.(lib.shown.filter((f) => sel.has(f.key))), children: ["Usa ", sel.size > 0 ? `(${sel.size})` : ""] })] }))] }), _jsx(Lightbox, { lib: lib }), manage && _jsx(UploadDialog, { lib: lib })] }));
179
+ : "Nessun file qui." })), lib.mode === "pick" && (_jsxs("footer", { className: "mg-foot", children: [_jsxs("span", { className: "ak-hint", children: [sel.size, " selezionate"] }), _jsx("span", { className: "ak-spacer" }), _jsx("button", { className: "ak-btn ak-btn-quiet", disabled: locked, onClick: () => onCancel?.(), children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: locked || !sel.size, onClick: () => onPick?.(lib.shown.filter((f) => sel.has(f.key))), children: ["Usa ", sel.size > 0 ? `(${sel.size})` : ""] })] }))] }), _jsx(Lightbox, { lib: lib }), _jsx(UploadDialog, { lib: lib })] }));
163
180
  }
@@ -1,4 +1,11 @@
1
1
  import type { MediaLibrary } from "./useMediaLibrary.js";
2
+ /** Le destinazioni che il picker può dichiarare senza inventarsi un secondo browser di
3
+ * cartelle: primo livello, cartella aperta e sue figlie. I duplicati spariscono mantenendo
4
+ * l'ordine più comprensibile. */
5
+ export declare function uploadDestinations(lib: MediaLibrary): {
6
+ prefix: string;
7
+ label: string;
8
+ }[];
2
9
  export declare function UploadDialog({ lib }: {
3
10
  lib: MediaLibrary;
4
11
  }): import("react").JSX.Element | null;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /* Il dialogo di caricamento.
2
+ /* Il dialogo di caricamento, condiviso da Media e picker.
3
3
  *
4
4
  * Dice sempre DOVE finiranno le foto. Il caricamento va nella cartella che si sta guardando, e
5
5
  * senza scriverlo è una scommessa: si carica dalla vista «Tutte» e le foto atterrano nella
@@ -9,21 +9,54 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  * il messaggio resta sullo schermo invece di sparire con la finestra.
10
10
  */
11
11
  import { useRef, useState } from "react";
12
+ const UNSET = "__adminkit_unset_destination__";
13
+ /** Le destinazioni che il picker può dichiarare senza inventarsi un secondo browser di
14
+ * cartelle: primo livello, cartella aperta e sue figlie. I duplicati spariscono mantenendo
15
+ * l'ordine più comprensibile. */
16
+ export function uploadDestinations(lib) {
17
+ const out = [{ prefix: "", label: "Senza cartella" }];
18
+ const seen = new Set([""]);
19
+ const add = (prefix, label) => {
20
+ if (seen.has(prefix))
21
+ return;
22
+ seen.add(prefix);
23
+ out.push({ prefix, label });
24
+ };
25
+ for (const folder of lib.folders)
26
+ add(folder.prefix, folder.label || folder.name);
27
+ if (lib.prefix)
28
+ add(lib.prefix, lib.labelOf(lib.prefix));
29
+ for (const folder of lib.subfolders) {
30
+ const parent = lib.prefix ? `${lib.labelOf(lib.prefix)} › ` : "";
31
+ add(folder.prefix, parent + (folder.label || folder.name));
32
+ }
33
+ return out;
34
+ }
12
35
  export function UploadDialog({ lib }) {
13
36
  const input = useRef(null);
14
37
  const [over, setOver] = useState(false);
15
- if (!lib.uploadOpen)
38
+ const draft = lib.uploadDraft;
39
+ if (!draft)
16
40
  return null;
41
+ const picker = lib.mode === "pick";
17
42
  const send = async (list) => {
18
43
  if (lib.mutating || !list || !list.length)
19
44
  return;
20
- // `upload` dice se sono passati tutti. Guardare `lib.error` da qui non funzionerebbe: è il
21
- // valore catturato prima del render, quindi sarebbe sempre uguale a sé stesso.
22
- if (await lib.upload(list))
23
- lib.setUploadOpen(false);
45
+ if (picker)
46
+ return lib.setUploadFiles(list);
47
+ /* Media conserva il gesto storico: scegliere o lasciare i file li accoda subito nella
48
+ cartella aperta. Nel picker restano invece in attesa finché la destinazione è esplicita. */
49
+ if (await lib.upload(list, draft.destination ?? lib.prefix))
50
+ lib.closeUpload();
51
+ };
52
+ const start = async () => {
53
+ if (!picker || draft.destination === null || !draft.files.length)
54
+ return;
55
+ if (await lib.upload(draft.files, draft.destination))
56
+ lib.closeUpload();
24
57
  };
25
58
  return (_jsx("div", { className: "up", onClick: (e) => { if (!lib.mutating && e.target === e.currentTarget)
26
- lib.setUploadOpen(false); }, children: _jsxs("div", { className: "up-panel", children: [_jsx("button", { className: "up-close", disabled: lib.mutating, onClick: () => lib.setUploadOpen(false), "aria-label": "Chiudi", children: "\u00D7" }), _jsxs("div", { className: "up-drop" + (over ? " is-over" : ""),
59
+ lib.closeUpload(); }, children: _jsxs("div", { className: "up-panel", children: [_jsx("button", { className: "up-close", disabled: lib.mutating, onClick: lib.closeUpload, "aria-label": "Chiudi", children: "\u00D7" }), _jsxs("div", { className: "up-drop" + (over ? " is-over" : ""),
27
60
  /* `stopPropagation` oltre a `preventDefault`: il dialogo è una sovrapposizione, ma
28
61
  per React resta un nodo dell'albero e l'evento continuerebbe a salire. Oggi sopra
29
62
  non c'è più nessuna zona di rilascio (il dialogo è fuori da `.mg-wrap`), e questa
@@ -39,5 +72,11 @@ export function UploadDialog({ lib }) {
39
72
  e.stopPropagation();
40
73
  setOver(false);
41
74
  void send(e.dataTransfer.files);
42
- }, children: [_jsxs("svg", { className: "up-cloud", viewBox: "0 0 48 32", width: "56", height: "38", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", d: "M38 30a9 9 0 0 0 .6-18A13 13 0 0 0 13 13.6 8 8 0 0 0 10 30" }), _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M24 30V12m-6 6 6-6 6 6" })] }), _jsx("p", { className: "up-hint", children: "Trascina qui le foto da caricare" }), _jsx("input", { ref: input, type: "file", multiple: true, accept: "image/*,video/*", hidden: true, disabled: lib.mutating, onChange: (e) => void send(e.target.files) }), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: lib.mutating, onClick: () => input.current?.click(), children: "Scegli i file" })] }), _jsxs("p", { className: "up-dest", children: ["Finiranno in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella (la radice del bucket)" })] }), lib.progress && (_jsxs("p", { className: "ak-hint", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026"] })), lib.error && _jsx("p", { className: "ak-error", children: lib.error })] }) }));
75
+ }, children: [_jsxs("svg", { className: "up-cloud", viewBox: "0 0 48 32", width: "56", height: "38", "aria-hidden": "true", children: [_jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", d: "M38 30a9 9 0 0 0 .6-18A13 13 0 0 0 13 13.6 8 8 0 0 0 10 30" }), _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M24 30V12m-6 6 6-6 6 6" })] }), _jsx("p", { className: "up-hint", children: picker && draft.files.length
76
+ ? `${draft.files.length} ${draft.files.length === 1 ? "immagine pronta" : "immagini pronte"}`
77
+ : "Trascina qui le foto da caricare" }), picker && draft.files.length > 0 && (_jsxs("p", { className: "up-files", title: draft.files.map((file) => file.name).join("\n"), children: [draft.files.slice(0, 3).map((file) => file.name).join(", "), draft.files.length > 3 ? ` e altre ${draft.files.length - 3}` : ""] })), _jsx("input", { ref: input, type: "file", multiple: true, accept: picker ? "image/*" : "image/*,video/*", hidden: true, disabled: lib.mutating, onChange: (e) => {
78
+ const files = Array.from(e.target.files ?? []);
79
+ e.target.value = ""; // lo stesso file può essere scelto di nuovo dopo un errore
80
+ void send(files);
81
+ } }), _jsx("button", { className: "ak-btn ak-btn-primary", disabled: lib.mutating, onClick: () => input.current?.click(), children: picker && draft.files.length ? "Cambia i file" : "Scegli i file" })] }), picker ? (_jsxs("label", { className: "up-dest-field", children: [_jsx("span", { children: "Destinazione" }), _jsxs("select", { className: "ak-input", value: draft.destination === null ? UNSET : draft.destination, onChange: (event) => lib.setUploadDestination(event.target.value === UNSET ? null : event.target.value), children: [_jsx("option", { value: UNSET, children: "Scegli una cartella\u2026" }), uploadDestinations(lib).map((destination) => (_jsx("option", { value: destination.prefix, children: destination.label }, destination.prefix || "__root")))] }), _jsx("small", { children: "La cartella aperta o ricordata non viene usata automaticamente." })] })) : (_jsxs("p", { className: "up-dest", children: ["Finiranno in ", _jsx("strong", { children: lib.prefix ? lib.labelOf(lib.prefix) : "Senza cartella (la radice del bucket)" })] })), lib.progress && (_jsxs("p", { className: "ak-hint", children: [lib.progress.verb, " ", lib.progress.done, "/", lib.progress.total, "\u2026"] })), lib.error && _jsx("p", { className: "ak-error", children: lib.error }), picker && (_jsxs("div", { className: "up-actions", children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: lib.mutating, onClick: lib.closeUpload, children: "Annulla" }), _jsxs("button", { className: "ak-btn ak-btn-primary", disabled: lib.mutating || !draft.files.length || draft.destination === null, onClick: () => void start(), children: ["Carica ", draft.files.length > 0 ? `(${draft.files.length})` : ""] })] }))] }) }));
43
82
  }
@@ -1,3 +1,4 @@
1
+ import { type AssetFile } from "../../api.js";
1
2
  export type MediaUploadState = "running" | "done" | "failed";
2
3
  export interface MediaUploadJob {
3
4
  id: string;
@@ -12,6 +13,10 @@ export interface MediaUploadJob {
12
13
  failed: number;
13
14
  autoWebp: boolean;
14
15
  errors: string[];
16
+ /** I file come esistono DAVVERO nel bucket dopo conversione, naming e upload. Il picker li
17
+ * usa per selezionare i nuovi arrivi: non può ricostruirne l'URL dal nome originale, perché
18
+ * una conversione può cambiare estensione e `uniqueKey` può aggiungere un suffisso. */
19
+ uploaded: AssetFile[];
15
20
  }
16
21
  interface StartInput {
17
22
  prefix: string;
@@ -45,7 +45,7 @@ export function startMediaUpload(input) {
45
45
  jobs = [...jobs, {
46
46
  id, title: input.title, prefix: input.prefix, state: "running", total: input.files.length,
47
47
  conversionDone: 0, uploadDone: 0, converted: 0, originals: 0, failed: 0,
48
- autoWebp: input.autoWebp, errors: [],
48
+ autoWebp: input.autoWebp, errors: [], uploaded: [],
49
49
  }];
50
50
  emit();
51
51
  void run(id, input);
@@ -80,9 +80,9 @@ async function run(id, input) {
80
80
  if (!prepared)
81
81
  return;
82
82
  try {
83
- await uploadWithRetry(input.prefix, prepared.upload, prepared.capturedAt);
83
+ const uploaded = await uploadWithRetry(input.prefix, prepared.upload, prepared.capturedAt);
84
84
  const job = current(id);
85
- patch(id, { uploadDone: job.uploadDone + 1 });
85
+ patch(id, { uploadDone: job.uploadDone + 1, uploaded: [...job.uploaded, uploaded] });
86
86
  }
87
87
  catch (error) {
88
88
  const job = current(id);
@@ -1,5 +1,6 @@
1
1
  import { type AssetFile, type AssetFolder } from "../../api.js";
2
2
  import type { MediaOperationStatus } from "../../media.js";
3
+ import { type MediaUploadJob } from "./media-upload-queue.js";
3
4
  export type MediaOperationState = "idle" | "planning" | "awaiting_confirmation" | "moving" | "deleting" | "renaming" | "creating_folder" | "journal";
4
5
  export type SortKey = "uploaded" | "capturedAt" | "name" | "size";
5
6
  export declare const SORT_LABELS: Record<SortKey, string>;
@@ -31,7 +32,13 @@ export interface MediaLibrary {
31
32
  } | null;
32
33
  sortKey: SortKey;
33
34
  sortDir: "asc" | "desc";
34
- uploadOpen: boolean;
35
+ uploadDraft: {
36
+ files: File[];
37
+ destination: string | null;
38
+ } | null;
39
+ /** Il job nato da QUESTO picker. Gli upload sono globali, ma solo questo va riportato qui
40
+ * per mostrare il progresso e selezionare i file appena arrivati. */
41
+ pickUploadJob: MediaUploadJob | null;
35
42
  viewing: number | null;
36
43
  autoWebp: boolean;
37
44
  settingsBusy: boolean;
@@ -41,9 +48,13 @@ export interface MediaLibrary {
41
48
  setAll(on: boolean): void;
42
49
  setSort(k: SortKey): void;
43
50
  flipDir(): void;
44
- /** Vero se sono passati tutti: chi ha aperto un dialogo lo chiude solo in quel caso. */
45
- upload(files: File[] | FileList): Promise<boolean>;
46
- setUploadOpen(open: boolean): void;
51
+ /** Accoda lo STESSO lavoro usato da Media. `destination` è esplicita nel picker; in gestione
52
+ * il ripiego resta la cartella aperta, che è il comportamento storico. */
53
+ upload(files: File[] | FileList, destination?: string): Promise<string | null>;
54
+ openUpload(files?: File[] | FileList, destination?: string): void;
55
+ closeUpload(): void;
56
+ setUploadFiles(files: File[] | FileList): void;
57
+ setUploadDestination(destination: string | null): void;
47
58
  setAutoWebp(on: boolean): Promise<void>;
48
59
  view(i: number | null): void;
49
60
  step(delta: number): void;
@@ -12,7 +12,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
12
  import { api, ApiError } from "../../api.js";
13
13
  import { storageKey } from "../../config.js";
14
14
  import { contentPersistenceMode } from "../../app/site.js";
15
- import { startMediaUpload, useMediaUploadCompletions } from "./media-upload-queue.js";
15
+ import { startMediaUpload, useMediaUploadCompletions, useMediaUploads, } from "./media-upload-queue.js";
16
16
  import { invalidMediaMoveReason, isValidMediaMoveDestination, mediaFolderSlug, mediaSelectableItems, mediaSelectionState, normalizeMediaSelection, summarizeMediaSelection, tryBeginMediaMutation, } from "./media-model.js";
17
17
  import { executeMediaMovePlan, mediaMoveConfirmation, mediaMoveNeedsConfirmation, } from "./media-move.js";
18
18
  /* Quanti file per richiesta nelle operazioni lunghe (cestina, sposta). Ogni file costa al
@@ -42,12 +42,14 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
42
42
  const [error, setError] = useState(null);
43
43
  const [mediaOperations, setMediaOperations] = useState([]);
44
44
  const [progress, setProgress] = useState(null);
45
- const [uploadOpen, setUploadOpen] = useState(false);
45
+ const [uploadDraft, setUploadDraft] = useState(null);
46
+ const [pickUploadJobId, setPickUploadJobId] = useState(null);
46
47
  const [viewing, setViewing] = useState(null);
47
48
  const [autoWebp, setAutoWebpState] = useState(true);
48
49
  const [settingsBusy, setSettingsBusy] = useState(false);
49
50
  const [settingsLoaded, setSettingsLoaded] = useState(false);
50
51
  const mediaUploadCompletions = useMediaUploadCompletions();
52
+ const mediaUploads = useMediaUploads();
51
53
  const seenMediaUploadCompletions = useRef(mediaUploadCompletions);
52
54
  /* Lo stato React disabilita la UI; la ref chiude anche la finestra nello stesso batch React,
53
55
  * prima che il render successivo possa applicare i disabled. */
@@ -151,8 +153,8 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
151
153
  useEffect(() => { localStorage.setItem(storageKey("sortKey"), sortKey); localStorage.setItem(storageKey("sortDir"), sortDir); }, [sortKey, sortDir]);
152
154
  /* Questa preferenza è del SITO, non del browser: il backend la conserva nel bucket Media.
153
155
  * Finché non arriva la risposta vale il default ON, così anche i siti già esistenti hanno il
154
- * nuovo comportamento senza migrazioni. Si carica solo nella vista di gestione: il picker non
155
- * offre upload e non deve fare una richiesta che non userà. */
156
+ * nuovo comportamento senza migrazioni. In gestione si anticipa la richiesta; il picker la
157
+ * farà pigramente solo quando parte davvero un upload. */
156
158
  useEffect(() => {
157
159
  if (mode !== "manage" || !active || settingsLoaded)
158
160
  return;
@@ -169,11 +171,32 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
169
171
  if (!active || seenMediaUploadCompletions.current === mediaUploadCompletions)
170
172
  return;
171
173
  seenMediaUploadCompletions.current = mediaUploadCompletions;
174
+ /* Il job nato da questo picker ha un effetto dedicato qui sotto: deve andare nella propria
175
+ destinazione e selezionare i nuovi file. Un reload generico della cartella che si stava
176
+ guardando correrebbe in parallelo e potrebbe vincere per ultimo. */
177
+ if (mode === "pick" && pickUploadJobId)
178
+ return;
172
179
  // Se nel frattempo c'è una mutazione, il suo reload finale comprende anche l'upload appena
173
180
  // concluso. Un secondo load qui potrebbe invece cambiare busy e selezione a metà progresso.
174
181
  if (loaded && !mutationRef.current)
175
182
  void load(prefix);
176
- }, [active, mediaUploadCompletions, loaded, load, prefix]);
183
+ }, [active, mediaUploadCompletions, loaded, load, prefix, mode, pickUploadJobId]);
184
+ const pickUploadJob = pickUploadJobId
185
+ ? mediaUploads.find((job) => job.id === pickUploadJobId) ?? null
186
+ : null;
187
+ /* Quando il lavoro del picker finisce, apre la destinazione e seleziona gli oggetti restituiti
188
+ * dall'API. Sono quelli reali: magari `.webp`, magari `foto-2.webp`. In scelta singola se ne
189
+ * evidenzia uno solo, pur avendo correttamente caricato tutto il gruppo. */
190
+ useEffect(() => {
191
+ if (mode !== "pick" || !pickUploadJobId || !pickUploadJob || pickUploadJob.state === "running")
192
+ return;
193
+ const uploaded = pickUploadJob.uploaded.map((file) => file.key);
194
+ const chosen = multiple ? uploaded : uploaded.slice(0, 1);
195
+ setPickUploadJobId(null);
196
+ if (!uploaded.length)
197
+ return;
198
+ void load(pickUploadJob.prefix).then(() => setSelected(new Set(chosen)));
199
+ }, [mode, multiple, pickUploadJobId, pickUploadJob, load]);
177
200
  /* Chiudere o ricaricare mentre un'operazione è in corso la interrompe: il gruppo in volo non
178
201
  * arriva mai. Ora ogni gruppo è definitivo, quindi si perde poco — ma vale comunque la pena
179
202
  * chiedere, perché è esattamente il gesto che uno fa quando "non sta succedendo niente". */
@@ -226,12 +249,12 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
226
249
  return setSelected(new Set());
227
250
  setSelected(new Set(selectableIds));
228
251
  }, [selectableIds]);
229
- const upload = useCallback(async (list) => {
252
+ const upload = useCallback(async (list, requestedDestination) => {
230
253
  if (mutationRef.current)
231
- return false;
254
+ return null;
232
255
  const arr = Array.from(list);
233
256
  if (!arr.length)
234
- return true;
257
+ return null;
235
258
  let convert = autoWebp;
236
259
  /* Evita la piccola finestra fra apertura dei Media e arrivo dell'impostazione: se sul sito
237
260
  la conversione è OFF, un upload immediato non deve partire col default ON. */
@@ -244,20 +267,23 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
244
267
  }
245
268
  catch (e) {
246
269
  setError(e instanceof ApiError ? e.message : String(e));
247
- return false;
270
+ return null;
248
271
  }
249
272
  }
250
- const destination = prefix
251
- ? seenLabels.current[prefix] || prefix.replace(/\/$/, "").split("/").pop() || prefix
273
+ const target = requestedDestination ?? prefix;
274
+ const destination = target
275
+ ? seenLabels.current[target] || target.replace(/\/$/, "").split("/").pop() || target
252
276
  : "Senza cartella";
253
- startMediaUpload({
254
- prefix,
277
+ const id = startMediaUpload({
278
+ prefix: target,
255
279
  title: `${arr.length} file · ${destination}`,
256
280
  files: arr,
257
281
  autoWebp: convert,
258
282
  });
259
- return true;
260
- }, [prefix, autoWebp, settingsLoaded]);
283
+ if (mode === "pick")
284
+ setPickUploadJobId(id);
285
+ return id;
286
+ }, [prefix, autoWebp, settingsLoaded, mode]);
261
287
  const setAutoWebp = useCallback(async (on) => {
262
288
  if (mutationRef.current)
263
289
  return;
@@ -657,7 +683,7 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
657
683
  mode, multiple, prefix, folders, subfolders, shown, selected, selectableIds,
658
684
  selectedFiles: selectionSummary.files, selectedFolders: selectionSummary.folders,
659
685
  allSelected, someSelected, busy, mutating, operationState, error, mediaOperations, progress,
660
- sortKey, sortDir, uploadOpen, viewing, autoWebp, settingsBusy,
686
+ sortKey, sortDir, uploadDraft, pickUploadJob, viewing, autoWebp, settingsBusy,
661
687
  go: (p) => { if (!mutationRef.current)
662
688
  void load(p); },
663
689
  reload: () => { if (!mutationRef.current)
@@ -666,8 +692,18 @@ export function useMediaLibrary({ mode, multiple = true, active = true, remember
666
692
  setSort: setSortKey,
667
693
  flipDir: () => setSortDir((d) => (d === "asc" ? "desc" : "asc")),
668
694
  upload,
669
- setUploadOpen: (open) => { if (!open || !mutationRef.current)
670
- setUploadOpen(open); },
695
+ openUpload: (files = [], destination) => {
696
+ if (mutationRef.current)
697
+ return;
698
+ setUploadDraft({
699
+ files: Array.from(files),
700
+ destination: mode === "manage" ? (destination ?? prefix) : (destination === undefined ? null : destination),
701
+ });
702
+ },
703
+ closeUpload: () => { if (!mutationRef.current)
704
+ setUploadDraft(null); },
705
+ setUploadFiles: (files) => setUploadDraft((draft) => (draft ? { ...draft, files: Array.from(files) } : draft)),
706
+ setUploadDestination: (destination) => setUploadDraft((draft) => (draft ? { ...draft, destination } : draft)),
671
707
  setAutoWebp,
672
708
  view: setViewing,
673
709
  step,
package/dist/theme.css CHANGED
@@ -345,6 +345,7 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
345
345
  .mg-act:hover:not(:disabled) { text-decoration: underline; }
346
346
  .mg-act:disabled { color: var(--ak-ink-quiet); cursor: default; }
347
347
  .mg-act.is-danger { color: var(--ak-danger); }
348
+ .mg-upload { white-space: nowrap; }
348
349
  .mg-menu-wrap { position: relative; display: inline-flex; }
349
350
  .mg-menu {
350
351
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 30;
@@ -507,7 +508,16 @@ a.ak-btn { text-decoration: none; display: inline-flex; align-items: center; }
507
508
  .up-drop.is-over { border-color: var(--ak-accent); background: #fdf4f2; }
508
509
  .up-cloud { color: var(--ak-ink-quiet); }
509
510
  .up-hint { margin: 0; font: 400 13px/1.4 var(--ak-font); color: var(--ak-ink-quiet); }
511
+ .up-files {
512
+ max-width: 100%; margin: -7px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
513
+ color: var(--ak-ink); font: 500 var(--ak-fs-small)/1.4 var(--ak-font);
514
+ }
510
515
  .up-dest { margin: 0; font: 400 var(--ak-fs-small)/1.4 var(--ak-font); color: var(--ak-ink-quiet); }
516
+ .up-dest-field { display: flex; flex-direction: column; gap: 6px; }
517
+ .up-dest-field > span { font: 600 var(--ak-fs-small)/1.3 var(--ak-font); color: var(--ak-ink); }
518
+ .up-dest-field select { width: 100%; }
519
+ .up-dest-field small { color: var(--ak-ink-quiet); font: 400 11px/1.4 var(--ak-font); }
520
+ .up-actions { display: flex; justify-content: flex-end; gap: 7px; padding-top: 2px; }
511
521
 
512
522
  /* ---- Cestino ------------------------------------------------------------------------ */
513
523
  .tr { display: flex; flex-direction: column; gap: 10px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.15",
3
+ "version": "2.2.16",
4
4
  "type": "module",
5
5
  "description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
6
6
  "license": "UNLICENSED",