@interware/content-generator 0.7.2 → 0.8.0

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 (45) hide show
  1. package/dist/_chunks/{App-CTh6owSM.mjs → App-DD1MzaWm.mjs} +2 -2
  2. package/dist/_chunks/{App-ugsCmlsC.js → App-DspaMfyW.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-Umf3GODW.mjs → ImageStudioTabs-DwaLxRjl.mjs} +308 -16
  4. package/dist/_chunks/{ImageStudioTabs-BFEF9ggK.js → ImageStudioTabs-uXSBkFeu.js} +306 -14
  5. package/dist/_chunks/{Settings-CdGW2O8h.mjs → Settings-C6TbUrgU.mjs} +170 -2
  6. package/dist/_chunks/{Settings-Sk78JUBS.js → Settings-ps0o7-fC.js} +169 -1
  7. package/dist/_chunks/{StudioApp-CrGn-9Gg.mjs → StudioApp-B5I1OfGw.mjs} +2 -2
  8. package/dist/_chunks/{StudioApp-DoC0z4eh.js → StudioApp-B7kGpqqo.js} +2 -2
  9. package/dist/_chunks/{index-C2s9Jhcc.js → index-BudahdfX.js} +3 -3
  10. package/dist/_chunks/{index-D6UAvh53.mjs → index-zenHkX-p.mjs} +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/driveSync.d.ts +41 -0
  18. package/dist/admin/src/features/templates/export/templateOrigin.d.ts +10 -0
  19. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  20. package/dist/server/index.js +1411 -49
  21. package/dist/server/index.mjs +1409 -47
  22. package/dist/server/src/bootstrap.d.ts +2 -0
  23. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  24. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  25. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  26. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  27. package/dist/server/src/content-types/index.d.ts +106 -0
  28. package/dist/server/src/controllers/drive.d.ts +30 -0
  29. package/dist/server/src/controllers/index.d.ts +130 -0
  30. package/dist/server/src/drive/client.d.ts +32 -0
  31. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  32. package/dist/server/src/drive/common.d.ts +6 -0
  33. package/dist/server/src/drive/fakes.d.ts +104 -0
  34. package/dist/server/src/drive/files.d.ts +136 -0
  35. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  36. package/dist/server/src/drive/links.d.ts +23 -0
  37. package/dist/server/src/drive/model.d.ts +99 -0
  38. package/dist/server/src/drive/oauth.d.ts +47 -0
  39. package/dist/server/src/drive/pull.d.ts +23 -0
  40. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  41. package/dist/server/src/drive/sync.d.ts +40 -0
  42. package/dist/server/src/index.d.ts +294 -0
  43. package/dist/server/src/services/drive.d.ts +141 -0
  44. package/dist/server/src/services/index.d.ts +55 -0
  45. 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-BudahdfX.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,255 @@ 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
+ };
1900
2146
  const status = (err) => err?.response?.status ?? err?.status;
1901
2147
  const LibraryTab = () => {
1902
2148
  const c = useThemeColors();
1903
2149
  const { api, notify } = useLibraryApi();
2150
+ const [drivePending, setDrivePending] = react.useState(false);
2151
+ const drive = useDriveSync(drivePending);
1904
2152
  const [tree, setTree] = react.useState([]);
1905
2153
  const [counts, setCounts] = react.useState({});
1906
2154
  const [current, setCurrent] = react.useState(null);
@@ -1944,7 +2192,20 @@ const LibraryTab = () => {
1944
2192
  react.useEffect(() => {
1945
2193
  loadFiles();
1946
2194
  }, [loadFiles]);
1947
- const refresh = () => Promise.all([loadTree(), loadFiles()]);
2195
+ const refresh = () => Promise.all([loadTree(), loadFiles(), drive.refresh()]);
2196
+ const toggleDrive = async (mode, cmsType, cmsId) => {
2197
+ try {
2198
+ if (mode === "mark") {
2199
+ await drive.mark(cmsType, cmsId);
2200
+ notify("success", "Se sincronizará con Drive en unos segundos.");
2201
+ } else {
2202
+ await drive.unmark(cmsType, cmsId);
2203
+ notify("success", "Dejó de sincronizarse. No se borró nada en Drive ni aquí.");
2204
+ }
2205
+ } catch (err) {
2206
+ notify("danger", err instanceof Error ? err.message : "No se pudo completar la operación con Drive.");
2207
+ }
2208
+ };
1948
2209
  const run = async (action, ok) => {
1949
2210
  try {
1950
2211
  await action();
@@ -2001,12 +2262,24 @@ const LibraryTab = () => {
2001
2262
  if (action === "rename") return setDialog({ kind: "renameFile", file });
2002
2263
  if (action === "move") return setDialog({ kind: "moveFile", file });
2003
2264
  if (action === "delete") return setDialog({ kind: "deleteFile", file });
2265
+ if (action === "syncDrive") return toggleDrive("mark", "file", file.id);
2266
+ if (action === "unsyncDrive") return toggleDrive("unmark", "file", file.id);
2004
2267
  };
2005
2268
  const validateFolder = (parent, except) => (name) => cleanFolderName(name) !== except && nameTaken(tree, parent, name) ? "Ya existe una carpeta con ese nombre aquí." : null;
2269
+ const pendingShown = (() => {
2270
+ const inside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2271
+ const isPending = (key) => driveView(drive.links, key, inside)?.label === "Drive: pendiente";
2272
+ return files.some((f) => isPending(`file:${f.id}`)) || childrenOf(tree, current).some((f) => isPending(`folder:${f.id}`));
2273
+ })();
2274
+ react.useEffect(() => {
2275
+ setDrivePending(pendingShown);
2276
+ }, [pendingShown]);
2006
2277
  if (forbidden) {
2007
2278
  return /* @__PURE__ */ jsxRuntime.jsx(EmptyNote, { children: "Para usar la biblioteca necesitas permiso de la Media Library de Strapi. Pídelo a un administrador." });
2008
2279
  }
2009
2280
  const subfolders = childrenOf(tree, current);
2281
+ const currentInside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2282
+ 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
2283
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, alignItems: "stretch", children: [
2011
2284
  /* @__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
2285
  FolderTree,
@@ -2053,6 +2326,9 @@ const LibraryTab = () => {
2053
2326
  ] }, f.id))
2054
2327
  ] }),
2055
2328
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
2329
+ folder && folderDrive?.label && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { variant: folderDrive.tone, title: folderDrive.title, children: folderDrive.label }),
2330
+ folder && folderDrive?.action === "mark" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", onClick: () => toggleDrive("mark", "folder", folder.id), children: "Sincronizar con Drive" }),
2331
+ folder && folderDrive?.action === "unmark" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: () => toggleDrive("unmark", "folder", folder.id), children: "Dejar de sincronizar" }),
2056
2332
  folder && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2057
2333
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {}), onClick: () => setDialog({ kind: "renameFolder" }), children: "Renombrar carpeta" }),
2058
2334
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "danger-light", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}), onClick: () => setDialog({ kind: "deleteFolder" }), children: "Eliminar carpeta" })
@@ -2077,11 +2353,15 @@ const LibraryTab = () => {
2077
2353
  children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
2078
2354
  /* @__PURE__ */ jsxRuntime.jsx(icons.Folder, { width: 16, height: 16, "aria-hidden": true }),
2079
2355
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { ellipsis: true, style: { flex: 1 }, children: f.name }),
2356
+ (() => {
2357
+ const view = driveView(drive.links, `folder:${f.id}`, currentInside);
2358
+ return view?.label ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { size: "S", variant: view.tone, title: view.title, children: view.label }) : null;
2359
+ })(),
2080
2360
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: counts[f.id]?.files ?? 0 })
2081
2361
  ] })
2082
2362
  }
2083
2363
  ) }, 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)) }),
2364
+ 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
2365
  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
2366
  "Suelta para subir a «",
2087
2367
  folder?.name ?? ROOT_LABEL,
@@ -2763,6 +3043,15 @@ const editorReducer = (state, action) => {
2763
3043
  }
2764
3044
  }
2765
3045
  };
3046
+ const ORIGIN_URL = "/strapi-content-generator-gemini/drive/origin";
3047
+ const recordTemplateOrigin = async (post, files, template) => {
3048
+ for (const file of files) {
3049
+ try {
3050
+ await post(ORIGIN_URL, { fileId: file.id, templateDocumentId: template.documentId ?? template.id, templateName: template.name });
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ };
2766
3055
  const useEditorIO = () => {
2767
3056
  const { post } = admin.useFetchClient();
2768
3057
  const { toggleNotification } = admin.useNotification();
@@ -2795,7 +3084,8 @@ const useEditorIO = () => {
2795
3084
  if (!node) return false;
2796
3085
  try {
2797
3086
  const blob = await renderPiece(node, template, "png");
2798
- await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3087
+ const saved = await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3088
+ await recordTemplateOrigin((u, body) => post(u, body), saved, template);
2799
3089
  toggleNotification({ type: "success", message: "Pieza guardada en la biblioteca." });
2800
3090
  return true;
2801
3091
  } catch {
@@ -2803,7 +3093,7 @@ const useEditorIO = () => {
2803
3093
  return false;
2804
3094
  }
2805
3095
  },
2806
- [api, toggleNotification]
3096
+ [api, post, toggleNotification]
2807
3097
  );
2808
3098
  return { loadImage: loadImage2, exportAs, saveToLibrary };
2809
3099
  };
@@ -5248,7 +5538,8 @@ const TemplatesTab = () => {
5248
5538
  const STUDIO_TABS = [
5249
5539
  { value: "generator", label: "Generador" },
5250
5540
  { value: "templates", label: "Plantillas" },
5251
- { value: "library", label: "Imágenes y documentos" }
5541
+ { value: "library", label: "Imágenes y documentos" },
5542
+ { value: "drive", label: "Google Drive" }
5252
5543
  ];
5253
5544
  const PANEL = { height: "100%", overflowY: "auto" };
5254
5545
  const isStudioTab = (v) => STUDIO_TABS.some((t) => t.value === v);
@@ -5276,7 +5567,8 @@ const ImageStudioTabs = () => {
5276
5567
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { flex: "1", style: { minHeight: 0 }, children: [
5277
5568
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "generator", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(GeneratorTab, {}) }) }),
5278
5569
  /* @__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, {}) }) })
5570
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(LibraryTab, {}) }) }),
5571
+ /* @__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
5572
  ] })
5281
5573
  ]
5282
5574
  }