contentoh-components-library 21.6.55 → 21.6.56

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 (82) hide show
  1. package/.claude/launch.json +14 -0
  2. package/.env.test +1 -0
  3. package/dist/ai/prompts/attribute.prompt.js +28 -0
  4. package/dist/ai/prompts/description.prompt.js +33 -0
  5. package/dist/ai/schemas/attribute.schema.js +35 -0
  6. package/dist/ai/schemas/description.schema.js +27 -0
  7. package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +118 -0
  8. package/dist/components/atoms/AssetPreview/index.js +150 -0
  9. package/dist/components/atoms/AssetPreview/styles.js +28 -0
  10. package/dist/components/atoms/IconFile/index.js +85 -2
  11. package/dist/components/atoms/IconFile/styles.js +1 -1
  12. package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
  13. package/dist/components/atoms/ProductImage/index.js +100 -27
  14. package/dist/components/atoms/ProductImage/styles.js +3 -1
  15. package/dist/components/molecules/EditionActiveImage/index.js +4 -2
  16. package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
  17. package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
  18. package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
  19. package/dist/components/molecules/GalleryElement/index.js +13 -4
  20. package/dist/components/molecules/GalleryHeader/index.js +96 -24
  21. package/dist/components/molecules/GalleryHeader/styles.js +4 -1
  22. package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
  23. package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
  24. package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
  25. package/dist/components/molecules/ImageSelector/index.js +1 -0
  26. package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +171 -0
  27. package/dist/components/organisms/DigitalAssetsTray/index.js +211 -0
  28. package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
  29. package/dist/components/organisms/ImagePreviewer/index.js +1 -0
  30. package/dist/components/organisms/ProductImageModal/index.js +3 -2
  31. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
  32. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
  33. package/dist/components/pages/RetailerProductEdition/index.js +606 -425
  34. package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  35. package/dist/global-files/data.js +16 -3
  36. package/dist/global-files/digital-assets/assetTypes.js +480 -0
  37. package/dist/global-files/digital-assets/assets.actions.js +64 -0
  38. package/dist/global-files/digital-assets/assets.adapter.js +304 -0
  39. package/dist/global-files/digital-assets/assets.mock.js +136 -0
  40. package/dist/global-files/digital-assets/assets.reducer.js +220 -0
  41. package/dist/global-files/digital-assets/uploadAssets.js +179 -0
  42. package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
  43. package/dist/index.js +142 -90
  44. package/docs/activos-digitales.md +273 -0
  45. package/package.json +1 -1
  46. package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
  47. package/src/components/atoms/AssetPreview/index.js +119 -0
  48. package/src/components/atoms/AssetPreview/styles.js +190 -0
  49. package/src/components/atoms/IconFile/index.js +28 -1
  50. package/src/components/atoms/IconFile/styles.js +8 -0
  51. package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
  52. package/src/components/atoms/ProductImage/index.js +95 -20
  53. package/src/components/atoms/ProductImage/styles.js +92 -7
  54. package/src/components/molecules/EditionActiveImage/index.js +2 -2
  55. package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
  56. package/src/components/molecules/GalleryAssetElement/index.js +162 -0
  57. package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
  58. package/src/components/molecules/GalleryElement/index.js +9 -0
  59. package/src/components/molecules/GalleryHeader/index.js +65 -13
  60. package/src/components/molecules/GalleryHeader/styles.js +2 -1
  61. package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
  62. package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
  63. package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
  64. package/src/components/molecules/ImageSelector/index.js +1 -0
  65. package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
  66. package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
  67. package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
  68. package/src/components/organisms/ImagePreviewer/index.js +1 -0
  69. package/src/components/organisms/ProductImageModal/index.js +1 -0
  70. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
  71. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
  72. package/src/components/pages/RetailerProductEdition/index.js +356 -139
  73. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  74. package/src/global-files/data.js +17 -2
  75. package/src/global-files/digital-assets/assetTypes.js +419 -0
  76. package/src/global-files/digital-assets/assets.actions.js +50 -0
  77. package/src/global-files/digital-assets/assets.adapter.js +229 -0
  78. package/src/global-files/digital-assets/assets.mock.js +122 -0
  79. package/src/global-files/digital-assets/assets.reducer.js +197 -0
  80. package/src/global-files/digital-assets/uploadAssets.js +109 -0
  81. package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
  82. package/src/index.js +4 -0
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.toPayload = exports.revokeAssetUrl = exports.isPersisted = exports.fromFile = exports.fromBackendList = exports.fromBackend = exports.filterByRetailer = exports.buildSavePayload = exports.buildAssetUrl = exports.assetTypeLabel = exports.assetKey = exports.ASSET_STATUS = void 0;
9
+
10
+ var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/typeof"));
11
+
12
+ var _assetTypes = require("./assetTypes");
13
+
14
+ /**
15
+ * Traducción entre el activo digital como lo guarda el backend y como lo usa la
16
+ * interfaz.
17
+ *
18
+ * DOS DECISIONES DEL NEGOCIO QUE SE NOTAN EN ESTE MODELO:
19
+ *
20
+ * - **Los activos no tienen versión.** Un activo general es el mismo sin
21
+ * importar la versión del producto, así que no lleva `id_version` ni en el
22
+ * modelo ni en la clave de S3. Eso evita el problema que sí tienen las
23
+ * imágenes —copiar archivos al crear una versión— y hace que un video subido
24
+ * en la v2 se vea también al mirar la v1.
25
+ * - **Por ahora todos son generales**, o sea `id_retailer = null`. La columna
26
+ * existe y este adaptador ya la respeta para cuando se pida acotar un activo
27
+ * a una cadena; hasta entonces nada la escribe.
28
+ *
29
+ * Es el único archivo que conoce los nombres de campo del backend. Mientras el
30
+ * contrato siga sin cerrarse, todo lo demás —tarjetas, bandeja, validaciones—
31
+ * se construye contra el modelo de esta capa; cuando el contrato quede fijo se
32
+ * ajustan estas funciones y nada más.
33
+ *
34
+ * El contrato propuesto está en `rel_article_asset`: una tabla aparte de
35
+ * `rel_article_image`, porque esa última guarda una sola fila por
36
+ * (artículo, versión, tipo de toma) y pisaría un activo con el siguiente.
37
+ */
38
+
39
+ /**
40
+ * Estado de un activo en pantalla. `pending`/`uploading`/`error` solo existen
41
+ * en el navegador: un activo con id del backend siempre está `saved`.
42
+ */
43
+ var ASSET_STATUS = {
44
+ PENDING: "pending",
45
+ UPLOADING: "uploading",
46
+ SAVED: "saved",
47
+ ERROR: "error"
48
+ };
49
+ exports.ASSET_STATUS = ASSET_STATUS;
50
+
51
+ var IMAGES_BUCKET = function IMAGES_BUCKET() {
52
+ return process.env.REACT_APP_IMAGES_BUCKET;
53
+ };
54
+ /**
55
+ * URL directa al objeto en S3.
56
+ *
57
+ * A propósito NO pasa por el redimensionador de CloudFront: ese servicio
58
+ * reescribe la imagen con sharp y para un mp4, un gif animado o un pdf
59
+ * devuelve basura. Es la misma URL que ya usa la descarga de imágenes.
60
+ */
61
+
62
+
63
+ var buildAssetUrl = function buildAssetUrl(key) {
64
+ if (!key) return "";
65
+ if (/^https?:\/\//i.test(key)) return key;
66
+ return "https://".concat(IMAGES_BUCKET(), ".s3.amazonaws.com/").concat(key);
67
+ };
68
+ /**
69
+ * `Number(null)` y `Number("")` son 0, no NaN. Sin descartarlos antes, un
70
+ * `id_retailer: null` —que significa "de todas las cadenas"— se convierte en el
71
+ * retailer 0 y el activo deja de verse en ninguna; una duración nula se vuelve
72
+ * un video de cero segundos.
73
+ */
74
+
75
+
76
+ exports.buildAssetUrl = buildAssetUrl;
77
+
78
+ var toNumberOrNull = function toNumberOrNull(value) {
79
+ if (value === null || value === undefined || value === "") return null;
80
+ var parsed = Number(value);
81
+ return Number.isFinite(parsed) ? parsed : null;
82
+ };
83
+ /**
84
+ * Fila del backend -> modelo de interfaz.
85
+ *
86
+ * Acepta `url` ya resuelta si el backend la manda; si no, la arma desde la
87
+ * clave de S3. Así el front funciona igual con cualquiera de las dos formas del
88
+ * contrato y no hay que esperar a que se decida para seguir construyendo.
89
+ */
90
+
91
+
92
+ var fromBackend = function fromBackend() {
93
+ var _row$id;
94
+
95
+ var row = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
96
+ var ext = row.ext || (0, _assetTypes.getExtension)(row.name || row.src);
97
+ var assetType = row.asset_type || (0, _assetTypes.suggestAssetType)({
98
+ mime: row.mime,
99
+ name: row.name
100
+ });
101
+ return {
102
+ id: (_row$id = row.id) !== null && _row$id !== void 0 ? _row$id : null,
103
+ tempId: null,
104
+ assetType: assetType,
105
+ name: row.name || "",
106
+ ext: ext,
107
+ mime: row.mime || "",
108
+ sizeBytes: toNumberOrNull(row.size_bytes),
109
+ duration: toNumberOrNull(row.duration),
110
+ width: toNumberOrNull(row.width),
111
+ height: toNumberOrNull(row.height),
112
+ // Clave en S3. Se conserva porque es lo que el backend necesita de vuelta;
113
+ // la URL es derivada y no debe viajar en el payload.
114
+ src: row.src || "",
115
+ url: row.url || buildAssetUrl(row.src),
116
+ poster: row.poster ? buildAssetUrl(row.poster) : "",
117
+ // `id_retailer` nulo = activo general, que es lo único que se escribe hoy.
118
+ retailerId: toNumberOrNull(row.id_retailer),
119
+ createdAt: row.created_at || null,
120
+ file: null,
121
+ status: ASSET_STATUS.SAVED,
122
+ progress: 100,
123
+ error: null
124
+ };
125
+ };
126
+
127
+ exports.fromBackend = fromBackend;
128
+
129
+ var fromBackendList = function fromBackendList() {
130
+ var rows = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
131
+ return Array.isArray(rows) ? rows.map(fromBackend) : [];
132
+ };
133
+
134
+ exports.fromBackendList = fromBackendList;
135
+ var tempCounter = 0;
136
+ /**
137
+ * Archivo recién elegido -> modelo de interfaz.
138
+ *
139
+ * El `File` nativo se guarda tal cual, sin leerlo a base64. Convertirlo cuesta
140
+ * un tercio más de memoria y es lo que hoy vuelve imposible subir un video
141
+ * largo o un manual grande; con el `File` intacto da igual si termina subiendo
142
+ * por URL prefirmada o por el cliente de S3.
143
+ *
144
+ * El `tempId` permite seguirle la pista a un activo que todavía no tiene id del
145
+ * backend: sin él no hay forma de actualizar su progreso ni de quitarlo de la
146
+ * lista si falla.
147
+ */
148
+
149
+ var fromFile = function fromFile(file) {
150
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
151
+ assetType = _ref.assetType,
152
+ objectUrl = _ref.objectUrl;
153
+
154
+ tempCounter += 1;
155
+ var ext = (0, _assetTypes.getExtension)(file === null || file === void 0 ? void 0 : file.name);
156
+ return {
157
+ id: null,
158
+ tempId: "tmp-".concat(Date.now(), "-").concat(tempCounter),
159
+ assetType: assetType || (0, _assetTypes.suggestAssetType)({
160
+ mime: file === null || file === void 0 ? void 0 : file.type,
161
+ name: file === null || file === void 0 ? void 0 : file.name
162
+ }),
163
+ name: (file === null || file === void 0 ? void 0 : file.name) || "",
164
+ ext: ext,
165
+ mime: (file === null || file === void 0 ? void 0 : file.type) || "",
166
+ sizeBytes: toNumberOrNull(file === null || file === void 0 ? void 0 : file.size),
167
+ duration: null,
168
+ width: null,
169
+ height: null,
170
+ src: "",
171
+ // `URL.createObjectURL` deja ver el archivo sin subirlo ni codificarlo.
172
+ // Quien lo crea es responsable de liberarlo con `revokeAssetUrl`.
173
+ url: objectUrl || "",
174
+ poster: "",
175
+ retailerId: null,
176
+ createdAt: null,
177
+ file: file,
178
+ status: ASSET_STATUS.PENDING,
179
+ progress: 0,
180
+ error: null
181
+ };
182
+ };
183
+ /** Identificador estable de un activo, tenga o no id del backend. */
184
+
185
+
186
+ exports.fromFile = fromFile;
187
+
188
+ var assetKey = function assetKey() {
189
+ var _ref2, _ref3, _asset$id;
190
+
191
+ var asset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
192
+ return (_ref2 = (_ref3 = (_asset$id = asset.id) !== null && _asset$id !== void 0 ? _asset$id : asset.tempId) !== null && _ref3 !== void 0 ? _ref3 : asset.src) !== null && _ref2 !== void 0 ? _ref2 : asset.name;
193
+ };
194
+
195
+ exports.assetKey = assetKey;
196
+
197
+ var isPersisted = function isPersisted() {
198
+ var asset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
199
+ return asset.id !== null && asset.id !== undefined;
200
+ };
201
+ /**
202
+ * Modelo de interfaz -> payload del backend.
203
+ *
204
+ * Nunca incluye `file` ni `url`: el archivo ya está en S3 cuando esto se envía
205
+ * y la URL es derivada. Es la misma razón por la que el guardado de imágenes
206
+ * borra `src`/`video_src` antes del PUT — el binario en el cuerpo devuelve 413.
207
+ */
208
+
209
+
210
+ exports.isPersisted = isPersisted;
211
+
212
+ var toPayload = function toPayload() {
213
+ var _asset$sizeBytes, _asset$duration, _asset$width, _asset$height;
214
+
215
+ var asset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
216
+ var payload = {
217
+ asset_type: asset.assetType,
218
+ name: asset.name,
219
+ ext: asset.ext,
220
+ mime: asset.mime,
221
+ size_bytes: (_asset$sizeBytes = asset.sizeBytes) !== null && _asset$sizeBytes !== void 0 ? _asset$sizeBytes : null,
222
+ duration: (_asset$duration = asset.duration) !== null && _asset$duration !== void 0 ? _asset$duration : null,
223
+ width: (_asset$width = asset.width) !== null && _asset$width !== void 0 ? _asset$width : null,
224
+ height: (_asset$height = asset.height) !== null && _asset$height !== void 0 ? _asset$height : null,
225
+ key: asset.src || null
226
+ };
227
+ if (isPersisted(asset)) payload.id = asset.id;
228
+
229
+ if (asset.retailerId !== null && asset.retailerId !== undefined) {
230
+ payload.id_retailer = asset.retailerId;
231
+ }
232
+
233
+ return payload;
234
+ };
235
+ /**
236
+ * Separa la lista en lo que el backend espera recibir: altas, cambios y bajas.
237
+ * `deleted` son ids, no objetos, porque para borrar no hace falta nada más.
238
+ */
239
+
240
+
241
+ exports.toPayload = toPayload;
242
+
243
+ var buildSavePayload = function buildSavePayload() {
244
+ var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
245
+ _ref4$assets = _ref4.assets,
246
+ assets = _ref4$assets === void 0 ? [] : _ref4$assets,
247
+ _ref4$updated = _ref4.updated,
248
+ updated = _ref4$updated === void 0 ? [] : _ref4$updated,
249
+ _ref4$deleted = _ref4.deleted,
250
+ deleted = _ref4$deleted === void 0 ? [] : _ref4$deleted;
251
+
252
+ return {
253
+ newAssets: assets.filter(function (asset) {
254
+ return !isPersisted(asset);
255
+ }).map(toPayload),
256
+ updateAssets: updated.filter(isPersisted).map(toPayload),
257
+ deleteAssets: deleted.map(function (asset) {
258
+ return (0, _typeof2.default)(asset) === "object" ? asset.id : asset;
259
+ }).filter(Boolean)
260
+ };
261
+ };
262
+ /**
263
+ * Qué activos ve la cadena activa: los generales (`retailerId` nulo) más los
264
+ * suyos.
265
+ *
266
+ * Hoy todos son generales, así que en la práctica no filtra nada. Se deja
267
+ * escrito y probado para el día que se pida acotar un activo a una cadena: ese
268
+ * día basta con que el backend mande el `id_retailer`, sin tocar la pantalla.
269
+ */
270
+
271
+
272
+ exports.buildSavePayload = buildSavePayload;
273
+
274
+ var filterByRetailer = function filterByRetailer() {
275
+ var assets = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
276
+ var retailerId = arguments.length > 1 ? arguments[1] : undefined;
277
+ if (!retailerId) return assets;
278
+ return assets.filter(function (asset) {
279
+ return asset.retailerId === null || asset.retailerId === undefined || Number(asset.retailerId) === Number(retailerId);
280
+ });
281
+ };
282
+ /** Libera el object URL de un activo local. Sin esto el archivo se queda en memoria. */
283
+
284
+
285
+ exports.filterByRetailer = filterByRetailer;
286
+
287
+ var revokeAssetUrl = function revokeAssetUrl() {
288
+ var asset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
289
+
290
+ if (asset.file && asset.url && asset.url.startsWith("blob:")) {
291
+ URL.revokeObjectURL(asset.url);
292
+ }
293
+ };
294
+ /** Etiqueta legible del tipo, para badges y tablas. */
295
+
296
+
297
+ exports.revokeAssetUrl = revokeAssetUrl;
298
+
299
+ var assetTypeLabel = function assetTypeLabel() {
300
+ var asset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
301
+ return (0, _assetTypes.getAssetType)(asset.assetType).label;
302
+ };
303
+
304
+ exports.assetTypeLabel = assetTypeLabel;
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.partialBackendAssetMock = exports.makeFileMock = exports.backendAssetsMock = void 0;
7
+
8
+ /**
9
+ * Activos de ejemplo con la forma EXACTA que devolvería el backend, para
10
+ * Storybook y pruebas.
11
+ *
12
+ * Están crudos a propósito, sin pasar por `fromBackend`: así, si el contrato
13
+ * cambia, estos objetos se editan y el adaptador se prueba de verdad en vez de
14
+ * comparar el modelo de interfaz contra sí mismo.
15
+ */
16
+ var backendAssetsMock = [{
17
+ id: 91,
18
+ asset_type: "360",
19
+ name: "giro_producto.mp4",
20
+ ext: "mp4",
21
+ mime: "video/mp4",
22
+ size_bytes: 8412160,
23
+ duration: 12,
24
+ width: 1080,
25
+ height: 1080,
26
+ src: "id-136038/assets/91-6d0f9a1e-0f0c-4a2b-9f1d-2a7c1b3e5d90.mp4",
27
+ poster: "id-136038/assets/91-6d0f9a1e-0f0c-4a2b-9f1d-2a7c1b3e5d90.jpg",
28
+ id_retailer: null,
29
+ created_at: "2026-09-10T16:22:04.000Z"
30
+ }, {
31
+ id: 92,
32
+ asset_type: "360",
33
+ name: "giro_empaque.gif",
34
+ ext: "gif",
35
+ mime: "image/gif",
36
+ size_bytes: 3145728,
37
+ duration: null,
38
+ width: 800,
39
+ height: 800,
40
+ src: "id-136038/assets/92-b1c2d3e4-1111-2222-3333-444455556666.gif",
41
+ poster: null,
42
+ id_retailer: null,
43
+ created_at: "2026-09-10T16:24:41.000Z"
44
+ }, {
45
+ id: 93,
46
+ asset_type: "video",
47
+ name: "demostracion_uso.mp4",
48
+ ext: "mp4",
49
+ mime: "video/mp4",
50
+ size_bytes: 54525952,
51
+ duration: 96,
52
+ width: 1920,
53
+ height: 1080,
54
+ src: "id-136038/assets/93-77aa88bb-99cc-4dd0-8ee1-ff0011223344.mp4",
55
+ poster: "id-136038/assets/93-77aa88bb-99cc-4dd0-8ee1-ff0011223344.jpg",
56
+ // Activo de una sola cadena: sirve para probar `filterByRetailer`.
57
+ id_retailer: 1017,
58
+ created_at: "2026-09-11T09:02:17.000Z"
59
+ }, {
60
+ id: 94,
61
+ asset_type: "documento",
62
+ name: "manual_de_usuario.pdf",
63
+ ext: "pdf",
64
+ mime: "application/pdf",
65
+ size_bytes: 2418192,
66
+ duration: null,
67
+ width: null,
68
+ height: null,
69
+ src: "id-136038/assets/94-cc55dd66-7788-99aa-bbcc-ddeeff001122.pdf",
70
+ poster: null,
71
+ id_retailer: null,
72
+ created_at: "2026-09-12T11:48:00.000Z"
73
+ }, {
74
+ id: 96,
75
+ asset_type: "editable",
76
+ name: "arte_empaque.psd",
77
+ ext: "psd",
78
+ // El navegador no le asigna mime a un .psd: llega vacío. Es el caso que
79
+ // obliga a resolver el tipo por extensión.
80
+ mime: "",
81
+ size_bytes: 268435456,
82
+ duration: null,
83
+ width: null,
84
+ height: null,
85
+ src: "id-136038/assets/96-11223344-5566-7788-99aa-bbccddeeff00.psd",
86
+ poster: null,
87
+ id_retailer: null,
88
+ created_at: "2026-09-15T10:12:33.000Z"
89
+ }, {
90
+ id: 97,
91
+ asset_type: "hoja_calculo",
92
+ name: "tabla_de_medidas.xlsx",
93
+ ext: "xlsx",
94
+ mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
95
+ size_bytes: 48128,
96
+ duration: null,
97
+ width: null,
98
+ height: null,
99
+ src: "id-136038/assets/97-99887766-5544-3322-1100-ffeeddccbbaa.xlsx",
100
+ poster: null,
101
+ id_retailer: null,
102
+ created_at: "2026-09-15T10:14:02.000Z"
103
+ }];
104
+ /**
105
+ * Caso incompleto a propósito: sin `mime`, sin `ext` y sin `url`. El backend
106
+ * todavía no existe y es muy probable que las primeras respuestas lleguen así;
107
+ * el adaptador tiene que poder deducirlos del nombre y de la clave de S3.
108
+ */
109
+
110
+ exports.backendAssetsMock = backendAssetsMock;
111
+ var partialBackendAssetMock = {
112
+ id: 95,
113
+ name: "ficha_tecnica.pdf",
114
+ src: "id-136038/assets/95-aa11bb22-cc33-dd44-ee55-ff6677889900.pdf"
115
+ };
116
+ /** `File` falso para probar validaciones sin tocar disco ni red. */
117
+
118
+ exports.partialBackendAssetMock = partialBackendAssetMock;
119
+
120
+ var makeFileMock = function makeFileMock() {
121
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
122
+ _ref$name = _ref.name,
123
+ name = _ref$name === void 0 ? "giro_producto.mp4" : _ref$name,
124
+ _ref$type = _ref.type,
125
+ type = _ref$type === void 0 ? "video/mp4" : _ref$type,
126
+ _ref$size = _ref.size,
127
+ size = _ref$size === void 0 ? 8412160 : _ref$size;
128
+
129
+ return {
130
+ name: name,
131
+ type: type,
132
+ size: size
133
+ };
134
+ };
135
+
136
+ exports.makeFileMock = makeFileMock;
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ Object.defineProperty(exports, "assetsInitialState", {
9
+ enumerable: true,
10
+ get: function get() {
11
+ return _assets.assetsInitialState;
12
+ }
13
+ });
14
+ exports.selectAssetsToSave = exports.hasUnsavedAssets = exports.countUnsavedAssets = exports.assetsReducer = void 0;
15
+
16
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/toConsumableArray"));
17
+
18
+ var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectSpread2"));
19
+
20
+ var _assets = require("./assets.actions");
21
+
22
+ var _assets2 = require("./assets.adapter");
23
+
24
+ /*
25
+ Estado de los activos digitales de la ficha.
26
+
27
+ Separado de `active-state` a propósito: ese reducer ya lleva 344 líneas y toda
28
+ la lógica de imágenes, que no comparte nada con esto —los activos no tienen
29
+ tipo de toma, ni obligatoriedad, ni cupo por cadena—.
30
+
31
+ Vive aquí y no en `context/reducers/` —donde están los otros cuatro— porque no
32
+ depende de la pantalla: es una función pura sobre activos, igual que el
33
+ adaptador y el catálogo. Así el hook y las historias de Storybook lo usan sin
34
+ arrastrar la ficha completa, y `provider-product-edition.reducer.js` solo lo
35
+ enchufa con una línea, igual que a los demás.
36
+
37
+ El reducer es puro. Liberar los object URL de los archivos locales es un efecto
38
+ y se hace en el hook antes de despachar; meterlo aquí rompería poder probar
39
+ este archivo sin navegador.
40
+ */
41
+ var assetsReducer = function assetsReducer(state, action) {
42
+ switch (action.type) {
43
+ case _assets.ASSET_ACTIONS.SET_ASSETS_VALUES:
44
+ {
45
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
46
+ assets_values: Array.isArray(action.payload) ? action.payload : [],
47
+ // Lo que venga del backend es, por definición, lo ya guardado: no hay
48
+ // nada pendiente de enviar.
49
+ updated_assets: [],
50
+ deleted_assets: [],
51
+ selected_assets: [],
52
+ all_assets_checked: false
53
+ });
54
+ }
55
+
56
+ case _assets.ASSET_ACTIONS.ADD_ASSETS:
57
+ {
58
+ var incoming = Array.isArray(action.payload) ? action.payload : [action.payload];
59
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
60
+ assets_values: [].concat((0, _toConsumableArray2.default)(state.assets_values), (0, _toConsumableArray2.default)(incoming))
61
+ });
62
+ }
63
+
64
+ case _assets.ASSET_ACTIONS.UPDATE_ASSET:
65
+ {
66
+ var _action$payload = action.payload,
67
+ key = _action$payload.key,
68
+ changes = _action$payload.changes;
69
+ var touched = null;
70
+ var assets_values = state.assets_values.map(function (asset) {
71
+ if ((0, _assets2.assetKey)(asset) !== key) return asset;
72
+ touched = asset;
73
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, asset), changes);
74
+ }); // Un activo que todavía no existe en el backend no se "actualiza": se
75
+ // creará con los valores finales, así que no hay nada que rastrear.
76
+
77
+ var shouldTrack = touched && (0, _assets2.isPersisted)(touched) && !state.updated_assets.includes(key);
78
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
79
+ assets_values: assets_values,
80
+ updated_assets: shouldTrack ? [].concat((0, _toConsumableArray2.default)(state.updated_assets), [key]) : state.updated_assets
81
+ });
82
+ }
83
+
84
+ case _assets.ASSET_ACTIONS.PATCH_ASSET:
85
+ {
86
+ var _action$payload2 = action.payload,
87
+ _key = _action$payload2.key,
88
+ _changes = _action$payload2.changes;
89
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
90
+ assets_values: state.assets_values.map(function (asset) {
91
+ return (0, _assets2.assetKey)(asset) === _key ? (0, _objectSpread2.default)((0, _objectSpread2.default)({}, asset), _changes) : asset;
92
+ })
93
+ });
94
+ }
95
+
96
+ case _assets.ASSET_ACTIONS.REMOVE_ASSET:
97
+ {
98
+ var _key2 = action.payload.key;
99
+ var target = state.assets_values.find(function (asset) {
100
+ return (0, _assets2.assetKey)(asset) === _key2;
101
+ });
102
+ if (!target) return state;
103
+
104
+ var _assets_values = state.assets_values.filter(function (asset) {
105
+ return (0, _assets2.assetKey)(asset) !== _key2;
106
+ });
107
+
108
+ var selected_assets = state.selected_assets.filter(function (asset) {
109
+ return (0, _assets2.assetKey)(asset) !== _key2;
110
+ });
111
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
112
+ assets_values: _assets_values,
113
+ // Solo se le pide al backend que borre lo que el backend tiene. Un
114
+ // activo local que nunca se subió simplemente desaparece.
115
+ deleted_assets: (0, _assets2.isPersisted)(target) ? [].concat((0, _toConsumableArray2.default)(state.deleted_assets), [target.id]) : state.deleted_assets,
116
+ // Sin esto, un activo borrado seguiría contando como seleccionado y como
117
+ // modificado: el guardado intentaría enviar un activo que ya no existe.
118
+ updated_assets: state.updated_assets.filter(function (item) {
119
+ return item !== _key2;
120
+ }),
121
+ selected_assets: selected_assets,
122
+ // Se recalcula en vez de apagarse: si estaban todos marcados y se borra
123
+ // uno, los que quedan siguen marcados y la casilla de "seleccionar todo"
124
+ // debe seguir reflejándolo.
125
+ all_assets_checked: _assets_values.length > 0 && selected_assets.length === _assets_values.length
126
+ });
127
+ }
128
+
129
+ case _assets.ASSET_ACTIONS.SET_SELECTED_ASSETS:
130
+ {
131
+ var selected = Array.isArray(action.payload) ? action.payload : [];
132
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
133
+ selected_assets: selected,
134
+ all_assets_checked: state.assets_values.length > 0 && selected.length === state.assets_values.length
135
+ });
136
+ }
137
+
138
+ case _assets.ASSET_ACTIONS.TOGGLE_CHECK_ALL_ASSETS:
139
+ {
140
+ // Con la lista vacía no hay nada que marcar: dejarlo en `true` pintaría la
141
+ // casilla de "seleccionar todo" activa sobre una bandeja sin activos.
142
+ var isChecked = Boolean(action.payload) && state.assets_values.length > 0;
143
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
144
+ all_assets_checked: isChecked,
145
+ selected_assets: isChecked ? (0, _toConsumableArray2.default)(state.assets_values) : []
146
+ });
147
+ }
148
+
149
+ case _assets.ASSET_ACTIONS.CLEAR_ASSETS_TRACKING:
150
+ {
151
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, state), {}, {
152
+ updated_assets: [],
153
+ deleted_assets: []
154
+ });
155
+ }
156
+
157
+ default:
158
+ {
159
+ return null;
160
+ }
161
+ }
162
+ };
163
+ /**
164
+ * Los tres grupos que espera el backend, listos para `buildSavePayload`.
165
+ *
166
+ * Se deriva de `assets_values` en el momento de guardar, no se acumula: las
167
+ * claves rastreadas dicen QUÉ cambió y el objeto vigente dice CÓMO quedó. Es lo
168
+ * que evita enviar el valor anterior cuando el usuario edita dos veces el mismo
169
+ * activo antes de guardar.
170
+ */
171
+
172
+
173
+ exports.assetsReducer = assetsReducer;
174
+
175
+ var selectAssetsToSave = function selectAssetsToSave(state) {
176
+ return {
177
+ assets: state.assets_values,
178
+ updated: state.assets_values.filter(function (asset) {
179
+ return state.updated_assets.includes((0, _assets2.assetKey)(asset));
180
+ }),
181
+ deleted: state.deleted_assets
182
+ };
183
+ };
184
+ /** ¿Hay algo sin guardar? Sirve para avisar antes de salir y para habilitar el botón. */
185
+
186
+ /**
187
+ * Desglose de lo que falta por guardar.
188
+ *
189
+ * Se cuenta por separado —altas, cambios, bajas— y no como un número suelto
190
+ * porque "3 cambios sin guardar" no le dice al usuario qué perdería si se va:
191
+ * no es lo mismo dejar sin guardar un borrado que un archivo recién subido.
192
+ *
193
+ * Un activo nuevo solo cuenta si ya está en S3. Uno que todavía se está
194
+ * subiendo, o que falló, no es un cambio pendiente de guardar: es un archivo que
195
+ * aún no existe, y anunciarlo como tal haría que el contador subiera y bajara
196
+ * solo mientras la barra de progreso avanza.
197
+ */
198
+
199
+
200
+ exports.selectAssetsToSave = selectAssetsToSave;
201
+
202
+ var countUnsavedAssets = function countUnsavedAssets(state) {
203
+ var nuevos = state.assets_values.filter(function (asset) {
204
+ return !(0, _assets2.isPersisted)(asset) && Boolean(asset.src);
205
+ }).length;
206
+ return {
207
+ nuevos: nuevos,
208
+ modificados: state.updated_assets.length,
209
+ borrados: state.deleted_assets.length,
210
+ total: nuevos + state.updated_assets.length + state.deleted_assets.length
211
+ };
212
+ };
213
+
214
+ exports.countUnsavedAssets = countUnsavedAssets;
215
+
216
+ var hasUnsavedAssets = function hasUnsavedAssets(state) {
217
+ return countUnsavedAssets(state).total > 0;
218
+ };
219
+
220
+ exports.hasUnsavedAssets = hasUnsavedAssets;