@interware/content-generator 0.7.2 → 0.8.1

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.
Files changed (46) hide show
  1. package/dist/_chunks/{App-CTh6owSM.mjs → App-2eX-mcfo.mjs} +2 -2
  2. package/dist/_chunks/{App-ugsCmlsC.js → App-CvZirjX_.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-Umf3GODW.mjs → ImageStudioTabs-B6fkCK-o.mjs} +353 -21
  4. package/dist/_chunks/{ImageStudioTabs-BFEF9ggK.js → ImageStudioTabs-CNktoRax.js} +351 -19
  5. package/dist/_chunks/{Settings-Sk78JUBS.js → Settings-B-QPodCh.js} +169 -1
  6. package/dist/_chunks/{Settings-CdGW2O8h.mjs → Settings-CaGruvIm.mjs} +170 -2
  7. package/dist/_chunks/{StudioApp-DoC0z4eh.js → StudioApp-BDoB31L0.js} +2 -2
  8. package/dist/_chunks/{StudioApp-CrGn-9Gg.mjs → StudioApp-CF03znuL.mjs} +2 -2
  9. package/dist/_chunks/{index-D6UAvh53.mjs → index-DlemRDzs.mjs} +3 -3
  10. package/dist/_chunks/{index-C2s9Jhcc.js → index-DwHGS-BJ.js} +3 -3
  11. package/dist/admin/index.js +1 -1
  12. package/dist/admin/index.mjs +1 -1
  13. package/dist/admin/src/components/settings/DriveSettings.d.ts +77 -0
  14. package/dist/admin/src/features/drive/DriveTab.d.ts +42 -0
  15. package/dist/admin/src/features/image-studio/ImageStudioTabs.d.ts +3 -0
  16. package/dist/admin/src/features/library/FileCard.d.ts +5 -2
  17. package/dist/admin/src/features/library/LibraryTab.d.ts +5 -0
  18. package/dist/admin/src/features/library/driveSync.d.ts +41 -0
  19. package/dist/admin/src/features/templates/export/templateOrigin.d.ts +10 -0
  20. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  21. package/dist/server/index.js +1411 -49
  22. package/dist/server/index.mjs +1409 -47
  23. package/dist/server/src/bootstrap.d.ts +2 -0
  24. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  25. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  26. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  27. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  28. package/dist/server/src/content-types/index.d.ts +106 -0
  29. package/dist/server/src/controllers/drive.d.ts +30 -0
  30. package/dist/server/src/controllers/index.d.ts +130 -0
  31. package/dist/server/src/drive/client.d.ts +32 -0
  32. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  33. package/dist/server/src/drive/common.d.ts +6 -0
  34. package/dist/server/src/drive/fakes.d.ts +104 -0
  35. package/dist/server/src/drive/files.d.ts +136 -0
  36. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  37. package/dist/server/src/drive/links.d.ts +23 -0
  38. package/dist/server/src/drive/model.d.ts +99 -0
  39. package/dist/server/src/drive/oauth.d.ts +47 -0
  40. package/dist/server/src/drive/pull.d.ts +23 -0
  41. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  42. package/dist/server/src/drive/sync.d.ts +40 -0
  43. package/dist/server/src/index.d.ts +294 -0
  44. package/dist/server/src/services/drive.d.ts +141 -0
  45. package/dist/server/src/services/index.d.ts +55 -0
  46. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  const jsxRuntime = require("react/jsx-runtime");
3
3
  const designSystem = require("@strapi/design-system");
4
4
  const reactRouterDom = require("react-router-dom");
5
- const index = require("./index-C2s9Jhcc.js");
5
+ const index = require("./index-DwHGS-BJ.js");
6
6
  const react = require("react");
7
7
  const icons = require("@strapi/icons");
8
8
  const styledComponents = require("styled-components");
@@ -1174,7 +1174,7 @@ const closestRatio = (width, height, ratios) => {
1174
1174
  };
1175
1175
  return ratios.reduce((best, r) => Math.abs(value(r) - target) < Math.abs(value(best) - target) ? r : best);
1176
1176
  };
1177
- const BASE = "/strapi-content-generator-gemini/image-studio";
1177
+ const BASE$2 = "/strapi-content-generator-gemini/image-studio";
1178
1178
  const toResult = ({ image, mimeType, model, grounding }) => ({
1179
1179
  src: `data:${mimeType};base64,${image}`,
1180
1180
  model,
@@ -1186,7 +1186,7 @@ const errorMessage = (err, fallback) => {
1186
1186
  };
1187
1187
  const createGeneratorApi = (client) => ({
1188
1188
  async models() {
1189
- const { data } = await client.get(`${BASE}/models`);
1189
+ const { data } = await client.get(`${BASE$2}/models`);
1190
1190
  return data;
1191
1191
  },
1192
1192
  async generate(p) {
@@ -1199,7 +1199,7 @@ const createGeneratorApi = (client) => ({
1199
1199
  form.append("allowPeople", String(p.allowPeople));
1200
1200
  form.append("grounding", String(p.grounding));
1201
1201
  p.references.forEach((f) => form.append("references", f, f.name));
1202
- const { data } = await client.post(`${BASE}/generate`, form);
1202
+ const { data } = await client.post(`${BASE$2}/generate`, form);
1203
1203
  return toResult(data);
1204
1204
  },
1205
1205
  async edit(p) {
@@ -1212,11 +1212,11 @@ const createGeneratorApi = (client) => ({
1212
1212
  p.references.forEach((f) => form.append("references", f, f.name));
1213
1213
  if (p.aspectRatio) form.append("aspectRatio", p.aspectRatio);
1214
1214
  form.append("image", new File([blob], `base.${blob.type.split("/")[1] ?? "png"}`, { type: blob.type }));
1215
- const { data } = await client.post(`${BASE}/edit`, form);
1215
+ const { data } = await client.post(`${BASE$2}/edit`, form);
1216
1216
  return toResult(data);
1217
1217
  },
1218
1218
  async expand(prompt) {
1219
- const { data } = await client.post(`${BASE}/expand`, { prompt });
1219
+ const { data } = await client.post(`${BASE$2}/expand`, { prompt });
1220
1220
  return data.prompt;
1221
1221
  }
1222
1222
  });
@@ -1838,7 +1838,7 @@ const isImage = (f) => (f.mime ?? "").startsWith("image/");
1838
1838
  const canRemoveBackground = (f) => isImage(f) && f.mime !== "image/svg+xml";
1839
1839
  const isGenerated = (f) => (f.caption ?? "").startsWith("AI generated content");
1840
1840
  const formatSize = (kb = 0) => kb >= 1024 ? `${(kb / 1024).toFixed(1)} MB` : `${Math.round(kb)} KB`;
1841
- const FileCard = ({ file, onAction, busy }) => {
1841
+ const FileCard = ({ file, onAction, busy, drive }) => {
1842
1842
  const c = useThemeColors();
1843
1843
  const thumb2 = file.formats?.thumbnail?.url ?? file.formats?.small?.url ?? file.url;
1844
1844
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -1877,7 +1877,8 @@ const FileCard = ({ file, onAction, busy }) => {
1877
1877
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
1878
1878
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: formatSize(file.size) }),
1879
1879
  isGenerated(file) && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { children: "Generada" }),
1880
- busy && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: true, children: busy })
1880
+ busy && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: true, children: busy }),
1881
+ drive?.label && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: drive.tone, title: drive.title, children: drive.label })
1881
1882
  ] })
1882
1883
  ] }),
1883
1884
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Menu.Root, { children: [
@@ -1889,6 +1890,8 @@ const FileCard = ({ file, onAction, busy }) => {
1889
1890
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Menu.Item, { onSelect: () => onAction("replace", file), children: "Reemplazar" }),
1890
1891
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Menu.Item, { onSelect: () => onAction("move", file), children: "Mover a…" }),
1891
1892
  canRemoveBackground(file) && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Menu.Item, { onSelect: () => onAction("removeBackground", file), disabled: !!busy, children: "Quitar fondo" }),
1893
+ drive?.action === "mark" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Menu.Item, { onSelect: () => onAction("syncDrive", file), children: "Sincronizar con Drive" }),
1894
+ drive?.action === "unmark" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Menu.Item, { onSelect: () => onAction("unsyncDrive", file), children: "Dejar de sincronizar" }),
1892
1895
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Menu.Item, { onSelect: () => onAction("delete", file), variant: "danger", children: "Eliminar" })
1893
1896
  ] })
1894
1897
  ] })
@@ -1897,10 +1900,260 @@ const FileCard = ({ file, onAction, busy }) => {
1897
1900
  }
1898
1901
  );
1899
1902
  };
1903
+ const BASE$1 = "/strapi-content-generator-gemini/drive";
1904
+ const FOLDER = "application/vnd.google-apps.folder";
1905
+ const NATIVE_TYPES = {
1906
+ "application/vnd.google-apps.document": "Documento de Google",
1907
+ "application/vnd.google-apps.spreadsheet": "Hoja de cálculo de Google",
1908
+ "application/vnd.google-apps.presentation": "Presentación de Google",
1909
+ "application/vnd.google-apps.form": "Formulario de Google",
1910
+ "application/vnd.google-apps.drawing": "Dibujo de Google",
1911
+ "application/vnd.google-apps.shortcut": "Acceso directo"
1912
+ };
1913
+ const typeLabel = (mime) => {
1914
+ if (mime === FOLDER) return "Carpeta";
1915
+ if (NATIVE_TYPES[mime]) return NATIVE_TYPES[mime];
1916
+ if (mime.startsWith("application/vnd.google-apps.")) return "Archivo de Google";
1917
+ if (mime === "application/pdf") return "PDF";
1918
+ if (mime.startsWith("image/")) return "Imagen";
1919
+ if (mime.startsWith("video/")) return "Video";
1920
+ if (mime.startsWith("audio/")) return "Audio";
1921
+ return "Archivo";
1922
+ };
1923
+ const TypeIcon = ({ mime }) => mime === FOLDER ? /* @__PURE__ */ jsxRuntime.jsx(icons.Folder, { width: 16, height: 16, "aria-hidden": true }) : mime === "application/pdf" ? /* @__PURE__ */ jsxRuntime.jsx(icons.FilePdf, { width: 16, height: 16, "aria-hidden": true }) : /* @__PURE__ */ jsxRuntime.jsx(icons.File, { width: 16, height: 16, "aria-hidden": true });
1924
+ const formatDate = (iso) => iso ? new Date(iso).toLocaleString("es-MX", { dateStyle: "medium", timeStyle: "short", timeZone: "America/Mexico_City" }) : "—";
1925
+ const confirmText = (item, count) => {
1926
+ const parts = [`«${item.name}» irá a la papelera de Drive, donde se puede recuperar durante 30 días.`];
1927
+ if (item.mimeType === FOLDER && count) {
1928
+ const total = count.files + count.folders;
1929
+ parts.push(
1930
+ total === 0 ? "La carpeta está vacía." : `Contiene ${count.truncated ? "más de " : ""}${total.toLocaleString("es-MX")} ${total === 1 ? "elemento" : "elementos"} (${count.files} archivos y ${count.folders} carpetas), que también se van.`
1931
+ );
1932
+ }
1933
+ if (item.synced || count?.synced) parts.push("Está sincronizado: también se borrará de la Biblioteca del CMS.");
1934
+ return parts.join(" ");
1935
+ };
1936
+ const serverMessage$1 = (error, fallback) => error?.response?.data?.error?.message ?? (error instanceof Error && error.message ? error.message : fallback);
1937
+ const useDriveBrowserApi = () => {
1938
+ const { get, del } = admin.useFetchClient();
1939
+ return react.useMemo(
1940
+ () => ({
1941
+ browse: async (folderId, pageToken) => {
1942
+ try {
1943
+ return (await get(`${BASE$1}/folders/${encodeURIComponent(folderId)}`, { params: pageToken ? { pageToken } : {} })).data;
1944
+ } catch (error) {
1945
+ throw new Error(serverMessage$1(error, "No se pudo leer Google Drive."));
1946
+ }
1947
+ },
1948
+ count: async (id) => (await get(`${BASE$1}/items/${encodeURIComponent(id)}/count`)).data,
1949
+ trash: async (id) => {
1950
+ try {
1951
+ await del(`${BASE$1}/items/${encodeURIComponent(id)}`);
1952
+ } catch (error) {
1953
+ throw new Error(serverMessage$1(error, "No se pudo mandar a la papelera."));
1954
+ }
1955
+ }
1956
+ }),
1957
+ [get, del]
1958
+ );
1959
+ };
1960
+ const DriveTab = ({ api: injected, notify }) => {
1961
+ const fallbackApi = useDriveBrowserApi();
1962
+ const api = injected ?? fallbackApi;
1963
+ const { toggleNotification } = admin.useNotification();
1964
+ const say = notify ?? ((type, message) => toggleNotification({ type, message }));
1965
+ const [folderId, setFolderId] = react.useState("root");
1966
+ const [page, setPage] = react.useState(null);
1967
+ const [error, setError] = react.useState(null);
1968
+ const [loading2, setLoading] = react.useState(true);
1969
+ const [loadingMore, setLoadingMore] = react.useState(false);
1970
+ const [doomed, setDoomed] = react.useState(null);
1971
+ const load = react.useCallback(async () => {
1972
+ setLoading(true);
1973
+ setError(null);
1974
+ try {
1975
+ setPage(await api.browse(folderId));
1976
+ } catch (err) {
1977
+ setError(err instanceof Error ? err.message : "No se pudo leer Google Drive.");
1978
+ } finally {
1979
+ setLoading(false);
1980
+ }
1981
+ }, [api, folderId]);
1982
+ react.useEffect(() => {
1983
+ load();
1984
+ }, [load]);
1985
+ const more = async () => {
1986
+ if (!page?.nextPageToken) return;
1987
+ setLoadingMore(true);
1988
+ try {
1989
+ const next = await api.browse(folderId, page.nextPageToken);
1990
+ setPage({ ...next, items: [...page.items, ...next.items] });
1991
+ } finally {
1992
+ setLoadingMore(false);
1993
+ }
1994
+ };
1995
+ const askDelete = async (item) => {
1996
+ setDoomed({ item, count: null });
1997
+ if (item.mimeType === FOLDER) {
1998
+ try {
1999
+ const count = await api.count(item.id);
2000
+ setDoomed((d) => d?.item.id === item.id ? { item, count } : d);
2001
+ } catch {
2002
+ }
2003
+ }
2004
+ };
2005
+ const confirmDelete = async () => {
2006
+ if (!doomed) return;
2007
+ const { item } = doomed;
2008
+ setDoomed(null);
2009
+ try {
2010
+ await api.trash(item.id);
2011
+ say("success", `«${item.name}» se mandó a la papelera de Drive.`);
2012
+ await load();
2013
+ } catch (err) {
2014
+ say("danger", err instanceof Error ? err.message : "No se pudo mandar a la papelera.");
2015
+ }
2016
+ };
2017
+ if (error) return /* @__PURE__ */ jsxRuntime.jsx(EmptyNote, { children: error });
2018
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
2019
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { "data-testid": "drive-breadcrumb", gap: 1, alignItems: "center", wrap: "wrap", paddingBottom: 4, children: [
2020
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => setFolderId("root"), children: "Mi unidad" }),
2021
+ (page?.path ?? []).map((p, i, all) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, alignItems: "center", children: [
2022
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ChevronRight, { width: 10, height: 10, "aria-hidden": true }),
2023
+ i === all.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "bold", children: p.name }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => setFolderId(p.id), children: p.name })
2024
+ ] }, p.id))
2025
+ ] }),
2026
+ loading2 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Cargando Google Drive…" }) }) : !page?.items.length ? /* @__PURE__ */ jsxRuntime.jsx(EmptyNote, { children: "Esta carpeta de Drive está vacía." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2027
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount: 6, rowCount: page.items.length + 1, children: [
2028
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Thead, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
2029
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Nombre" }) }),
2030
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Tipo" }) }),
2031
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Modificado" }) }),
2032
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Tamaño" }) }),
2033
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Estado" }) }),
2034
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.VisuallyHidden, { children: "Acciones" }) })
2035
+ ] }) }),
2036
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: page.items.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
2037
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { style: { maxWidth: 420, whiteSpace: "normal" }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
2038
+ /* @__PURE__ */ jsxRuntime.jsx(TypeIcon, { mime: item.mimeType }),
2039
+ item.mimeType === FOLDER ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.TextButton, { onClick: () => setFolderId(item.id), style: { textAlign: "left", height: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "semiBold", textColor: "neutral800", style: { overflowWrap: "anywhere" }, children: item.name }) }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral800", style: { overflowWrap: "anywhere" }, children: item.name })
2040
+ ] }) }),
2041
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral700", children: typeLabel(item.mimeType) }) }),
2042
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral700", style: { whiteSpace: "nowrap" }, children: formatDate(item.modifiedTime) }) }),
2043
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral700", children: item.size === null ? "—" : formatSize(item.size / 1e3) }) }),
2044
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: item.synced && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: "success", children: "Sincronizado" }) }),
2045
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "end", children: [
2046
+ item.webViewLink && /* @__PURE__ */ jsxRuntime.jsx(
2047
+ designSystem.LinkButton,
2048
+ {
2049
+ variant: "tertiary",
2050
+ size: "S",
2051
+ href: item.webViewLink,
2052
+ target: "_blank",
2053
+ rel: "noreferrer noopener",
2054
+ endIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ExternalLink, {}),
2055
+ children: "Abrir en Drive"
2056
+ }
2057
+ ),
2058
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.IconButton, { label: `Eliminar ${item.name}`, variant: "ghost", onClick: () => askDelete(item), children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}) })
2059
+ ] }) })
2060
+ ] }, item.id)) })
2061
+ ] }),
2062
+ page.nextPageToken && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: more, loading: loadingMore, children: "Mostrar más" }) })
2063
+ ] }),
2064
+ /* @__PURE__ */ jsxRuntime.jsx(
2065
+ ConfirmDialog,
2066
+ {
2067
+ open: !!doomed,
2068
+ title: "¿Mandar a la papelera de Drive?",
2069
+ confirmLabel: "Mandar a la papelera",
2070
+ onClose: () => setDoomed(null),
2071
+ onConfirm: confirmDelete,
2072
+ children: doomed ? confirmText(doomed.item, doomed.count) : ""
2073
+ }
2074
+ )
2075
+ ] });
2076
+ };
2077
+ const BASE = "/strapi-content-generator-gemini/drive";
2078
+ const POLL_MS = 1e4;
2079
+ const LABELS = {
2080
+ synced: { label: "Drive", tone: "success" },
2081
+ pending: { label: "Drive: pendiente", tone: "neutral" },
2082
+ error: { label: "Drive: error", tone: "danger" },
2083
+ skipped: { label: "Drive: omitido", tone: "warning" }
2084
+ };
2085
+ const driveView = (links, key, parentInside) => {
2086
+ if (!links?.connected) return null;
2087
+ const item = links.items[key];
2088
+ if (item?.inside) {
2089
+ return { ...LABELS[item.status], title: item.error ?? void 0, action: item.root ? "unmark" : null };
2090
+ }
2091
+ if (parentInside) return { ...LABELS.pending, action: null };
2092
+ return { label: null, tone: "neutral", action: "mark" };
2093
+ };
2094
+ const serverMessage = (error, fallback) => error?.response?.data?.error?.message ?? fallback;
2095
+ const useDriveSync = (watching = false) => {
2096
+ const { get, post, del } = admin.useFetchClient();
2097
+ const [links, setLinks] = react.useState(null);
2098
+ const refresh = react.useCallback(async () => {
2099
+ try {
2100
+ const { data } = await get(`${BASE}/links`);
2101
+ setLinks(data);
2102
+ } catch {
2103
+ setLinks(null);
2104
+ }
2105
+ }, [get]);
2106
+ react.useEffect(() => {
2107
+ refresh();
2108
+ }, [refresh]);
2109
+ const busy = !!links?.connected && (watching || links.pending > 0 || Object.values(links.items).some((i) => i.status === "pending"));
2110
+ react.useEffect(() => {
2111
+ if (!busy) return;
2112
+ const timer = setInterval(refresh, POLL_MS);
2113
+ return () => clearInterval(timer);
2114
+ }, [busy, refresh]);
2115
+ react.useEffect(() => {
2116
+ const onVisible = () => document.visibilityState === "visible" && refresh();
2117
+ document.addEventListener("visibilitychange", onVisible);
2118
+ return () => document.removeEventListener("visibilitychange", onVisible);
2119
+ }, [refresh]);
2120
+ return react.useMemo(
2121
+ () => ({
2122
+ links,
2123
+ refresh,
2124
+ async mark(cmsType, cmsId) {
2125
+ try {
2126
+ await post(`${BASE}/links`, { cmsType, cmsId });
2127
+ } catch (error) {
2128
+ throw new Error(serverMessage(error, "No se pudo sincronizar con Drive."));
2129
+ } finally {
2130
+ await refresh();
2131
+ }
2132
+ },
2133
+ async unmark(cmsType, cmsId) {
2134
+ try {
2135
+ await del(`${BASE}/links/${cmsType}/${cmsId}`);
2136
+ } catch (error) {
2137
+ throw new Error(serverMessage(error, "No se pudo dejar de sincronizar."));
2138
+ } finally {
2139
+ await refresh();
2140
+ }
2141
+ }
2142
+ }),
2143
+ [links, refresh, post, del]
2144
+ );
2145
+ };
2146
+ const uploadSummary = (total, failed) => {
2147
+ if (failed.length === 0) return { type: "success", message: total === 1 ? "Archivo subido" : `${total} archivos subidos` };
2148
+ if (failed.length === total) return { type: "danger", message: `No se pudo subir: ${failed.join(", ")}.` };
2149
+ return { type: "warning", message: `${total - failed.length} de ${total} archivos subidos. No se pudo subir: ${failed.join(", ")}.` };
2150
+ };
1900
2151
  const status = (err) => err?.response?.status ?? err?.status;
1901
2152
  const LibraryTab = () => {
1902
2153
  const c = useThemeColors();
1903
2154
  const { api, notify } = useLibraryApi();
2155
+ const [drivePending, setDrivePending] = react.useState(false);
2156
+ const drive = useDriveSync(drivePending);
1904
2157
  const [tree, setTree] = react.useState([]);
1905
2158
  const [counts, setCounts] = react.useState({});
1906
2159
  const [current, setCurrent] = react.useState(null);
@@ -1911,7 +2164,10 @@ const LibraryTab = () => {
1911
2164
  const [dragOver, setDragOver] = react.useState(false);
1912
2165
  const [bgBusy, setBgBusy] = react.useState({});
1913
2166
  const [bgTarget, setBgTarget] = react.useState(null);
2167
+ const [uploading, setUploading] = react.useState(null);
1914
2168
  const uploadInput = react.useRef(null);
2169
+ const currentRef = react.useRef(current);
2170
+ currentRef.current = current;
1915
2171
  const replaceInput = react.useRef(null);
1916
2172
  const replaceTarget = react.useRef(null);
1917
2173
  const path = pathTo(tree, current);
@@ -1944,7 +2200,20 @@ const LibraryTab = () => {
1944
2200
  react.useEffect(() => {
1945
2201
  loadFiles();
1946
2202
  }, [loadFiles]);
1947
- const refresh = () => Promise.all([loadTree(), loadFiles()]);
2203
+ const refresh = () => Promise.all([loadTree(), loadFiles(), drive.refresh()]);
2204
+ const toggleDrive = async (mode, cmsType, cmsId) => {
2205
+ try {
2206
+ if (mode === "mark") {
2207
+ await drive.mark(cmsType, cmsId);
2208
+ notify("success", "Se sincronizará con Drive en unos segundos.");
2209
+ } else {
2210
+ await drive.unmark(cmsType, cmsId);
2211
+ notify("success", "Dejó de sincronizarse. No se borró nada en Drive ni aquí.");
2212
+ }
2213
+ } catch (err) {
2214
+ notify("danger", err instanceof Error ? err.message : "No se pudo completar la operación con Drive.");
2215
+ }
2216
+ };
1948
2217
  const run = async (action, ok) => {
1949
2218
  try {
1950
2219
  await action();
@@ -1956,9 +2225,30 @@ const LibraryTab = () => {
1956
2225
  await refresh();
1957
2226
  }
1958
2227
  };
1959
- const upload = (list) => {
2228
+ react.useEffect(() => {
2229
+ if (!uploading) return;
2230
+ const warn = (e) => e.preventDefault();
2231
+ window.addEventListener("beforeunload", warn);
2232
+ return () => window.removeEventListener("beforeunload", warn);
2233
+ }, [uploading]);
2234
+ const upload = async (list) => {
1960
2235
  const arr = [...list ?? []];
1961
- if (arr.length) run(() => api.uploadFiles(arr, current), arr.length === 1 ? "Archivo subido" : `${arr.length} archivos subidos`);
2236
+ if (!arr.length || uploading) return;
2237
+ const folderId = current;
2238
+ const failed = [];
2239
+ for (const [i, file] of arr.entries()) {
2240
+ setUploading({ done: i, total: arr.length, name: file.name });
2241
+ try {
2242
+ const uploaded = await api.uploadFiles([file], folderId);
2243
+ if (currentRef.current === folderId) setFiles((prev) => [...uploaded, ...prev]);
2244
+ } catch {
2245
+ failed.push(file.name);
2246
+ }
2247
+ }
2248
+ setUploading(null);
2249
+ const { type, message } = uploadSummary(arr.length, failed);
2250
+ notify(type, message);
2251
+ await refresh();
1962
2252
  };
1963
2253
  const setBgStatus = (file, s) => setBgBusy((b) => {
1964
2254
  const next = { ...b };
@@ -2001,12 +2291,24 @@ const LibraryTab = () => {
2001
2291
  if (action === "rename") return setDialog({ kind: "renameFile", file });
2002
2292
  if (action === "move") return setDialog({ kind: "moveFile", file });
2003
2293
  if (action === "delete") return setDialog({ kind: "deleteFile", file });
2294
+ if (action === "syncDrive") return toggleDrive("mark", "file", file.id);
2295
+ if (action === "unsyncDrive") return toggleDrive("unmark", "file", file.id);
2004
2296
  };
2005
2297
  const validateFolder = (parent, except) => (name) => cleanFolderName(name) !== except && nameTaken(tree, parent, name) ? "Ya existe una carpeta con ese nombre aquí." : null;
2298
+ const pendingShown = (() => {
2299
+ const inside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2300
+ const isPending = (key) => driveView(drive.links, key, inside)?.label === "Drive: pendiente";
2301
+ return files.some((f) => isPending(`file:${f.id}`)) || childrenOf(tree, current).some((f) => isPending(`folder:${f.id}`));
2302
+ })();
2303
+ react.useEffect(() => {
2304
+ setDrivePending(pendingShown);
2305
+ }, [pendingShown]);
2006
2306
  if (forbidden) {
2007
2307
  return /* @__PURE__ */ jsxRuntime.jsx(EmptyNote, { children: "Para usar la biblioteca necesitas permiso de la Media Library de Strapi. Pídelo a un administrador." });
2008
2308
  }
2009
2309
  const subfolders = childrenOf(tree, current);
2310
+ const currentInside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2311
+ const folderDrive = current !== null ? driveView(drive.links, `folder:${current}`, !!(folder && path.length > 1 && drive.links?.items[`folder:${path[path.length - 2].id}`]?.inside)) : null;
2010
2312
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, alignItems: "stretch", children: [
2011
2313
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { width: "280px", shrink: 0, background: "neutral0", padding: 3, hasRadius: true, style: { maxHeight: "calc(100vh - 220px)", overflowY: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2012
2314
  FolderTree,
@@ -2028,7 +2330,7 @@ const LibraryTab = () => {
2028
2330
  hasRadius: true,
2029
2331
  position: "relative",
2030
2332
  onDragOver: (e) => {
2031
- if (e.dataTransfer.types.includes("Files")) {
2333
+ if (e.dataTransfer.types.includes("Files") && !uploading) {
2032
2334
  e.preventDefault();
2033
2335
  setDragOver(true);
2034
2336
  }
@@ -2040,7 +2342,7 @@ const LibraryTab = () => {
2040
2342
  if (e.dataTransfer.files.length) {
2041
2343
  e.preventDefault();
2042
2344
  setDragOver(false);
2043
- upload(e.dataTransfer.files);
2345
+ if (!uploading) upload(e.dataTransfer.files);
2044
2346
  }
2045
2347
  },
2046
2348
  children: [
@@ -2053,12 +2355,15 @@ const LibraryTab = () => {
2053
2355
  ] }, f.id))
2054
2356
  ] }),
2055
2357
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
2358
+ folder && folderDrive?.label && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: folderDrive.tone, title: folderDrive.title, children: folderDrive.label }),
2359
+ folder && folderDrive?.action === "mark" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", onClick: () => toggleDrive("mark", "folder", folder.id), children: "Sincronizar con Drive" }),
2360
+ folder && folderDrive?.action === "unmark" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: () => toggleDrive("unmark", "folder", folder.id), children: "Dejar de sincronizar" }),
2056
2361
  folder && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2057
2362
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {}), onClick: () => setDialog({ kind: "renameFolder" }), children: "Renombrar carpeta" }),
2058
2363
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "danger-light", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}), onClick: () => setDialog({ kind: "deleteFolder" }), children: "Eliminar carpeta" })
2059
2364
  ] }),
2060
2365
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}), onClick: () => setDialog({ kind: "newFolder" }), children: "Nueva carpeta" }),
2061
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Upload, {}), onClick: () => uploadInput.current?.click(), children: "Subir archivos" })
2366
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Upload, {}), onClick: () => uploadInput.current?.click(), disabled: !!uploading, loading: !!uploading, children: "Subir archivos" })
2062
2367
  ] })
2063
2368
  ] }),
2064
2369
  subfolders.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Root, { gap: 2, paddingBottom: 4, children: subfolders.map((f) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 3, s: 6, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -2077,11 +2382,26 @@ const LibraryTab = () => {
2077
2382
  children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
2078
2383
  /* @__PURE__ */ jsxRuntime.jsx(icons.Folder, { width: 16, height: 16, "aria-hidden": true }),
2079
2384
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { ellipsis: true, style: { flex: 1 }, children: f.name }),
2385
+ (() => {
2386
+ const view = driveView(drive.links, `folder:${f.id}`, currentInside);
2387
+ return view?.label ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { size: "S", variant: view.tone, title: view.title, children: view.label }) : null;
2388
+ })(),
2080
2389
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: counts[f.id]?.files ?? 0 })
2081
2390
  ] })
2082
2391
  }
2083
2392
  ) }, f.id)) }),
2084
- loading2 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Cargando archivos…" }) }) : files.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyNote, { children: subfolders.length ? "No hay archivos sueltos en esta carpeta." : "Carpeta vacía. Arrastra archivos aquí o usa «Subir archivos»." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Root, { gap: 4, children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 3, m: 4, s: 6, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsx(FileCard, { file: f, onAction: onFileAction, busy: bgBusy[f.id] }) }, f.id)) }),
2393
+ uploading && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 3, marginBottom: 4, hasRadius: true, background: "primary100", borderColor: "primary200", role: "status", "aria-live": "polite", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
2394
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "primary700", children: [
2395
+ "Subiendo ",
2396
+ uploading.done + 1,
2397
+ " de ",
2398
+ uploading.total,
2399
+ " · ",
2400
+ uploading.name
2401
+ ] }),
2402
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.ProgressBar, { value: Math.round(uploading.done / uploading.total * 100), children: `${uploading.done} de ${uploading.total} archivos subidos` })
2403
+ ] }) }),
2404
+ loading2 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Cargando archivos…" }) }) : files.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyNote, { children: subfolders.length ? "No hay archivos sueltos en esta carpeta." : "Carpeta vacía. Arrastra archivos aquí o usa «Subir archivos»." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Root, { gap: 4, children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 3, m: 4, s: 6, xs: 12, children: /* @__PURE__ */ jsxRuntime.jsx(FileCard, { file: f, onAction: onFileAction, busy: bgBusy[f.id], drive: driveView(drive.links, `file:${f.id}`, currentInside) }) }, f.id)) }),
2085
2405
  dragOver && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { position: "absolute", justifyContent: "center", background: "primary100", hasRadius: true, style: { inset: 0, border: `2px dashed ${c.primary600}`, opacity: 0.95 }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "beta", textColor: "primary700", children: [
2086
2406
  "Suelta para subir a «",
2087
2407
  folder?.name ?? ROOT_LABEL,
@@ -2763,6 +3083,15 @@ const editorReducer = (state, action) => {
2763
3083
  }
2764
3084
  }
2765
3085
  };
3086
+ const ORIGIN_URL = "/strapi-content-generator-gemini/drive/origin";
3087
+ const recordTemplateOrigin = async (post, files, template) => {
3088
+ for (const file of files) {
3089
+ try {
3090
+ await post(ORIGIN_URL, { fileId: file.id, templateDocumentId: template.documentId ?? template.id, templateName: template.name });
3091
+ } catch {
3092
+ }
3093
+ }
3094
+ };
2766
3095
  const useEditorIO = () => {
2767
3096
  const { post } = admin.useFetchClient();
2768
3097
  const { toggleNotification } = admin.useNotification();
@@ -2795,7 +3124,8 @@ const useEditorIO = () => {
2795
3124
  if (!node) return false;
2796
3125
  try {
2797
3126
  const blob = await renderPiece(node, template, "png");
2798
- await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3127
+ const saved = await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3128
+ await recordTemplateOrigin((u, body) => post(u, body), saved, template);
2799
3129
  toggleNotification({ type: "success", message: "Pieza guardada en la biblioteca." });
2800
3130
  return true;
2801
3131
  } catch {
@@ -2803,7 +3133,7 @@ const useEditorIO = () => {
2803
3133
  return false;
2804
3134
  }
2805
3135
  },
2806
- [api, toggleNotification]
3136
+ [api, post, toggleNotification]
2807
3137
  );
2808
3138
  return { loadImage: loadImage2, exportAs, saveToLibrary };
2809
3139
  };
@@ -5248,7 +5578,8 @@ const TemplatesTab = () => {
5248
5578
  const STUDIO_TABS = [
5249
5579
  { value: "generator", label: "Generador" },
5250
5580
  { value: "templates", label: "Plantillas" },
5251
- { value: "library", label: "Imágenes y documentos" }
5581
+ { value: "library", label: "Imágenes y documentos" },
5582
+ { value: "drive", label: "Google Drive" }
5252
5583
  ];
5253
5584
  const PANEL = { height: "100%", overflowY: "auto" };
5254
5585
  const isStudioTab = (v) => STUDIO_TABS.some((t) => t.value === v);
@@ -5276,7 +5607,8 @@ const ImageStudioTabs = () => {
5276
5607
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { flex: "1", style: { minHeight: 0 }, children: [
5277
5608
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "generator", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(GeneratorTab, {}) }) }),
5278
5609
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "templates", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(TemplatesTab, {}) }),
5279
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(LibraryTab, {}) }) })
5610
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(LibraryTab, {}) }) }),
5611
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "drive", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: tab === "drive" && /* @__PURE__ */ jsxRuntime.jsx(DriveTab, {}) }) })
5280
5612
  ] })
5281
5613
  ]
5282
5614
  }