contentoh-components-library 21.6.49 → 21.6.50

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 (90) 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/organisms/PropagateChangesModal/index.js +12 -1
  32. package/dist/components/organisms/PropagateChangesModal/non-propagable.js +45 -0
  33. package/dist/components/pages/ProviderProductEdition/index.js +11 -1
  34. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
  35. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
  36. package/dist/components/pages/RetailerProductEdition/index.js +605 -414
  37. package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  38. package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +45 -145
  39. package/dist/global-files/data.js +16 -3
  40. package/dist/global-files/digital-assets/assetTypes.js +480 -0
  41. package/dist/global-files/digital-assets/assets.actions.js +64 -0
  42. package/dist/global-files/digital-assets/assets.adapter.js +304 -0
  43. package/dist/global-files/digital-assets/assets.mock.js +136 -0
  44. package/dist/global-files/digital-assets/assets.reducer.js +220 -0
  45. package/dist/global-files/digital-assets/uploadAssets.js +179 -0
  46. package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
  47. package/dist/index.js +142 -90
  48. package/docs/activos-digitales.md +273 -0
  49. package/package.json +1 -1
  50. package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
  51. package/src/components/atoms/AssetPreview/index.js +119 -0
  52. package/src/components/atoms/AssetPreview/styles.js +190 -0
  53. package/src/components/atoms/IconFile/index.js +28 -1
  54. package/src/components/atoms/IconFile/styles.js +8 -0
  55. package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
  56. package/src/components/atoms/ProductImage/index.js +95 -20
  57. package/src/components/atoms/ProductImage/styles.js +92 -7
  58. package/src/components/molecules/EditionActiveImage/index.js +2 -2
  59. package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
  60. package/src/components/molecules/GalleryAssetElement/index.js +162 -0
  61. package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
  62. package/src/components/molecules/GalleryElement/index.js +9 -0
  63. package/src/components/molecules/GalleryHeader/index.js +65 -13
  64. package/src/components/molecules/GalleryHeader/styles.js +2 -1
  65. package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
  66. package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
  67. package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
  68. package/src/components/molecules/ImageSelector/index.js +1 -0
  69. package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
  70. package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
  71. package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
  72. package/src/components/organisms/ImagePreviewer/index.js +1 -0
  73. package/src/components/organisms/ProductImageModal/index.js +1 -0
  74. package/src/components/organisms/PropagateChangesModal/index.js +11 -1
  75. package/src/components/organisms/PropagateChangesModal/non-propagable.js +29 -0
  76. package/src/components/pages/ProviderProductEdition/index.js +10 -1
  77. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
  78. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
  79. package/src/components/pages/RetailerProductEdition/index.js +366 -140
  80. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  81. package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +51 -104
  82. package/src/global-files/data.js +17 -2
  83. package/src/global-files/digital-assets/assetTypes.js +419 -0
  84. package/src/global-files/digital-assets/assets.actions.js +50 -0
  85. package/src/global-files/digital-assets/assets.adapter.js +229 -0
  86. package/src/global-files/digital-assets/assets.mock.js +122 -0
  87. package/src/global-files/digital-assets/assets.reducer.js +197 -0
  88. package/src/global-files/digital-assets/uploadAssets.js +109 -0
  89. package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
  90. package/src/index.js +4 -0
@@ -0,0 +1,329 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ import { ASSET_ACTIONS } from "./assets.actions";
4
+ import { requestPresign, uploadToS3 } from "./uploadAssets";
5
+ import {
6
+ MAX_ASSETS_PER_PRODUCT,
7
+ validateAssetDuration,
8
+ validateAssetFile,
9
+ } from "./assetTypes";
10
+ import {
11
+ ASSET_STATUS,
12
+ assetKey,
13
+ fromBackendList,
14
+ fromFile,
15
+ revokeAssetUrl,
16
+ } from "./assets.adapter";
17
+
18
+ /**
19
+ * Duración de un video sin leerlo a base64.
20
+ *
21
+ * `URL.createObjectURL` le da al navegador una referencia al archivo del disco;
22
+ * `preload="metadata"` hace que solo baje la cabecera. La alternativa que usa
23
+ * hoy la ficha —`FileReader.readAsDataURL`— carga el archivo entero en memoria
24
+ * y lo infla un tercio, que es justo lo que impide subir videos largos.
25
+ */
26
+ export const readVideoDuration = (objectUrl) => new Promise((resolve) => {
27
+ const video = document.createElement("video");
28
+ video.preload = "metadata";
29
+ video.onloadedmetadata = () => resolve(video.duration);
30
+ video.onerror = () => resolve(null);
31
+ video.src = objectUrl;
32
+ });
33
+
34
+ /**
35
+ * La bandeja de activos digitales, sobre el reducer de la ficha.
36
+ *
37
+ * El hook NO guarda la lista: la fuente de verdad es `state.assets_values`, para
38
+ * que el guardado del contexto pueda leerla igual que lee `images_values`. Aquí
39
+ * vive lo que un reducer no puede tener: la validación, la lectura asíncrona de
40
+ * la duración, y la liberación de los object URL (efectos, no estado).
41
+ *
42
+ * La subida arranca sola al soltar los archivos, no al guardar. Un .psd de 300
43
+ * MB tarda; hacerlo al guardar dejaría al usuario mirando un botón bloqueado
44
+ * durante minutos, mientras que así sube en segundo plano con su barra de
45
+ * progreso y guardar es instantáneo. El precio es que un archivo que se sube y
46
+ * luego se quita deja un objeto huérfano en S3, que recoge una regla de ciclo
47
+ * de vida.
48
+ *
49
+ * Sin `articleId` o sin `token` no se sube nada: es lo que permite usar la
50
+ * bandeja en Storybook, donde no hay sesión ni producto.
51
+ *
52
+ * `onUploaded` recibe los activos que terminaron de subir, para registrarlos en
53
+ * la base sin esperar al botón de guardar. Al ver la barra llegar al final
54
+ * cualquiera da por hecho que el activo ya quedó ligado al producto; si no lo
55
+ * estuviera y cerrara la ficha, el archivo se quedaría en S3 sin fila.
56
+ *
57
+ * `notices` es lo único que sí vive aquí: mensajes efímeros sobre los archivos
58
+ * que se acaban de soltar —uno rechazado, otro aceptado pero que el navegador no
59
+ * podrá reproducir—. No son parte del producto y no tienen por qué ensuciar el
60
+ * estado de la ficha. Cada uno lleva su `level` para que la bandeja distinga lo
61
+ * que no entró de lo que entró con advertencia.
62
+ */
63
+ export const useDigitalAssets = ({
64
+ assets = [],
65
+ dispatch,
66
+ notices = [],
67
+ setNotices = () => {},
68
+ articleId = null,
69
+ token = null,
70
+ onUploaded = null,
71
+ } = {}) => {
72
+ /*
73
+ Copia sincrónica de la lista, solo para contar el cupo.
74
+
75
+ `assets` viene del render y `addFiles` es asíncrono: si el usuario suelta una
76
+ segunda tanda mientras la primera todavía está midiendo la duración de un
77
+ video, esa segunda llamada vería el conteo viejo y dejaría pasar de más.
78
+ Escribir la ref al despachar la mantiene exacta.
79
+ */
80
+ const countRef = useRef(assets.length);
81
+ useEffect(() => {
82
+ countRef.current = assets.length;
83
+ }, [assets.length]);
84
+
85
+ const assetsRef = useRef(assets);
86
+ useEffect(() => {
87
+ assetsRef.current = assets;
88
+ }, [assets]);
89
+
90
+ // Los object URL se liberan al desmontar. Sin esto, cada archivo que el
91
+ // usuario mira se queda en memoria hasta que recargue la pestaña.
92
+ useEffect(() => () => assetsRef.current.forEach(revokeAssetUrl), []);
93
+
94
+ const replaceAll = useCallback(
95
+ (rows) => {
96
+ const list = fromBackendList(rows);
97
+ countRef.current = list.length;
98
+ dispatch({ type: ASSET_ACTIONS.SET_ASSETS_VALUES, payload: list });
99
+ setNotices([]);
100
+ },
101
+ [dispatch, setNotices],
102
+ );
103
+
104
+ /*
105
+ Ciclo de vida de una subida. Va antes de `uploadAssets` y de `addFiles`
106
+ porque los dos lo usan: leer el archivo de arriba abajo cuenta la historia
107
+ en orden —qué puede pasarle a un activo, quién lo provoca— en vez de
108
+ obligar a saltar al final para entender la mitad de arriba.
109
+ */
110
+ const patch = useCallback(
111
+ (key, changes) => dispatch({ type: ASSET_ACTIONS.PATCH_ASSET, payload: { key, changes } }),
112
+ [dispatch],
113
+ );
114
+
115
+ const startUpload = useCallback(
116
+ (key) => patch(key, { status: ASSET_STATUS.UPLOADING, progress: 0, error: null }),
117
+ [patch],
118
+ );
119
+
120
+ const setProgress = useCallback((key, progress) => patch(key, { progress }), [patch]);
121
+
122
+ const completeUpload = useCallback(
123
+ (key, changes = {}) => patch(key, {
124
+ status: ASSET_STATUS.SAVED,
125
+ progress: 100,
126
+ error: null,
127
+ ...changes,
128
+ }),
129
+ [patch],
130
+ );
131
+
132
+ const failUpload = useCallback(
133
+ (key, message) => patch(key, { status: ASSET_STATUS.ERROR, error: message }),
134
+ [patch],
135
+ );
136
+
137
+ /**
138
+ * Firma y sube los archivos recién aceptados, en segundo plano.
139
+ *
140
+ * Las subidas van en paralelo a propósito: son transferencias a S3, no
141
+ * consultas a una base, y el cuello de botella es el ancho de banda del
142
+ * usuario. Ponerlas en fila haría que el último archivo de diez empezara
143
+ * cuando los otros nueve ya terminaron.
144
+ *
145
+ * Un fallo se marca en SU activo y no tumba a los demás: si un archivo se
146
+ * corta, los otros nueve siguen y el usuario reintenta solo ese.
147
+ */
148
+ const uploadAssets = useCallback(
149
+ async (pendingAssets) => {
150
+ if (!articleId || !token || pendingAssets.length === 0) return;
151
+
152
+ let presigned;
153
+ try {
154
+ presigned = await requestPresign({ articleId, assets: pendingAssets, token });
155
+ } catch (error) {
156
+ // Si la firma falla, ninguno se puede subir: se marcan todos, porque
157
+ // dejarlos en "pendiente" haría creer que siguen en camino.
158
+ pendingAssets.forEach((asset) => failUpload(assetKey(asset), error.message));
159
+ return;
160
+ }
161
+
162
+ /*
163
+ Emparejar cada resultado con su activo.
164
+
165
+ Por `ref` cuando el servidor lo devuelve, que es lo fiable. Mientras no
166
+ se despliegue esa versión, `ref` llega vacío y se cae al nombre —con la
167
+ lista de candidatos menguando, para que dos archivos que se llamen igual
168
+ no acaben los dos apuntando al mismo—.
169
+ */
170
+ const sinEmparejar = [...pendingAssets];
171
+ const emparejar = (item) => {
172
+ const index = sinEmparejar.findIndex((asset) => (
173
+ item.ref ? asset.tempId === item.ref : asset.name === item.name
174
+ ));
175
+ if (index === -1) return null;
176
+ return sinEmparejar.splice(index, 1)[0];
177
+ };
178
+
179
+ // El servidor valida por su cuenta: lo que rechace se marca con su motivo.
180
+ presigned.rejected.forEach((item) => {
181
+ const target = emparejar(item);
182
+ if (target) failUpload(assetKey(target), item.reason);
183
+ });
184
+
185
+ const subidos = await Promise.all(
186
+ presigned.uploads.map(async (upload) => {
187
+ const asset = emparejar(upload);
188
+ if (!asset?.file) return null;
189
+
190
+ const key = assetKey(asset);
191
+ startUpload(key);
192
+
193
+ try {
194
+ await uploadToS3({
195
+ upload,
196
+ file: asset.file,
197
+ onProgress: (percent) => setProgress(key, percent),
198
+ });
199
+ // `src` es lo que convierte un activo local en guardable: el
200
+ // guardado se niega a registrar un activo sin clave en S3.
201
+ completeUpload(key, { src: upload.key });
202
+ return { ...asset, src: upload.key };
203
+ } catch (error) {
204
+ failUpload(key, error.message);
205
+ return null;
206
+ }
207
+ }),
208
+ );
209
+
210
+ /*
211
+ Se avisa una sola vez, con todo lo que llegó completo, en lugar de una
212
+ llamada por archivo: soltar diez archivos haría diez escrituras a la
213
+ base para el mismo producto.
214
+
215
+ Solo lo que subió bien. Registrar un activo cuyo archivo no llegó a S3
216
+ crearía una fila apuntando a nada.
217
+ */
218
+ const completados = subidos.filter(Boolean);
219
+ if (completados.length > 0) await onUploaded?.(completados);
220
+ },
221
+ [articleId, token, onUploaded, startUpload, setProgress, completeUpload, failUpload],
222
+ );
223
+
224
+ /**
225
+ * Valida y agrega archivos. Devuelve lo aceptado y lo rechazado para que quien
226
+ * llama decida cómo avisar.
227
+ *
228
+ * El cupo se cuenta contra lo que ya hay MÁS lo aceptado en esta misma tanda:
229
+ * si no, soltar quince archivos de golpe los dejaría pasar todos, porque cada
230
+ * uno vería el mismo conteo inicial.
231
+ */
232
+ const addFiles = useCallback(
233
+ async (files = []) => {
234
+ const accepted = [];
235
+ const avisos = [];
236
+ let count = countRef.current;
237
+
238
+ for (const file of Array.from(files)) {
239
+ const check = validateAssetFile(file, { currentCount: count });
240
+ if (!check.valid) {
241
+ avisos.push({
242
+ level: "error", name: file.name, code: check.code, message: check.message,
243
+ });
244
+ continue;
245
+ }
246
+ // Se acepta, pero hay algo que decirle al usuario sobre este archivo.
247
+ if (check.warning) {
248
+ avisos.push({ level: "warning", name: file.name, message: check.warning });
249
+ }
250
+
251
+ const objectUrl = URL.createObjectURL(file);
252
+ const asset = fromFile(file, { assetType: check.assetType, objectUrl });
253
+
254
+ // La duración solo se mide en lo que la valida, y después del peso: no
255
+ // tiene sentido abrir un archivo que ya se rechazó.
256
+ if (check.assetType === "video" || check.assetType === "360") {
257
+ const duration = await readVideoDuration(objectUrl);
258
+ const durationCheck = validateAssetDuration(duration, check.assetType);
259
+ if (!durationCheck.valid) {
260
+ URL.revokeObjectURL(objectUrl);
261
+ avisos.push({
262
+ level: "error",
263
+ name: file.name,
264
+ code: durationCheck.code,
265
+ message: durationCheck.message,
266
+ });
267
+ continue;
268
+ }
269
+ asset.duration = duration;
270
+ }
271
+
272
+ accepted.push(asset);
273
+ count += 1;
274
+ }
275
+
276
+ if (accepted.length) {
277
+ countRef.current = count;
278
+ dispatch({ type: ASSET_ACTIONS.ADD_ASSETS, payload: accepted });
279
+ // Sin `await`: la subida corre en segundo plano y la bandeja ya muestra
280
+ // los archivos con su barra de progreso.
281
+ uploadAssets(accepted);
282
+ }
283
+ setNotices(avisos);
284
+
285
+ return { accepted, notices: avisos };
286
+ },
287
+ [dispatch, setNotices, uploadAssets],
288
+ );
289
+
290
+ const removeAsset = useCallback(
291
+ (asset) => {
292
+ // El reducer es puro, así que el object URL se libera aquí.
293
+ revokeAssetUrl(asset);
294
+ countRef.current = Math.max(0, countRef.current - 1);
295
+ dispatch({ type: ASSET_ACTIONS.REMOVE_ASSET, payload: { key: assetKey(asset) } });
296
+ },
297
+ [dispatch],
298
+ );
299
+
300
+ const changeAssetType = useCallback(
301
+ (index, assetType) => {
302
+ const target = assetsRef.current[index];
303
+ if (!target) return;
304
+ dispatch({
305
+ type: ASSET_ACTIONS.UPDATE_ASSET,
306
+ payload: { key: assetKey(target), changes: { assetType } },
307
+ });
308
+ },
309
+ [dispatch],
310
+ );
311
+
312
+ const clearNotices = useCallback(() => setNotices([]), [setNotices]);
313
+
314
+ return {
315
+ assets,
316
+ notices,
317
+ remaining: Math.max(0, MAX_ASSETS_PER_PRODUCT - assets.length),
318
+ pending: assets.filter((asset) => asset.status === ASSET_STATUS.PENDING),
319
+ addFiles,
320
+ removeAsset,
321
+ changeAssetType,
322
+ replaceAll,
323
+ startUpload,
324
+ setProgress,
325
+ completeUpload,
326
+ failUpload,
327
+ clearNotices,
328
+ };
329
+ };
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./components/atoms/InheritanceTag/index";
15
15
  export * from "./components/atoms/Loading/index";
16
16
  export * from "./components/atoms/LogoImage/index";
17
17
  export * from "./components/atoms/PriorityFlag/index";
18
+ export * from "./components/atoms/AssetPreview/index";
18
19
  export * from "./components/atoms/ProductImage/index";
19
20
  export * from "./components/atoms/ProgressBar/index";
20
21
  export * from "./components/atoms/ScreenHeader/index";
@@ -48,6 +49,8 @@ export * from "./components/molecules/CarouselImagesLogin";
48
49
  export * from "./components/molecules/EditionActiveImage/index";
49
50
  export * from "./components/molecules/EmailResetPasswordLogin/index";
50
51
  export * from "./components/molecules/FeaturesBar/index";
52
+ export * from "./components/molecules/GalleryTrayTabs/index";
53
+ export * from "./components/molecules/GalleryAssetElement/index";
51
54
  export * from "./components/molecules/GalleryElement/index";
52
55
  export * from "./components/molecules/HeaderTop/index";
53
56
  export * from "./components/molecules/ImageSelector/index";
@@ -79,6 +82,7 @@ export * from "./components/molecules/HeaderItem/index";
79
82
  export * from "./components/molecules/RowItem/index";
80
83
 
81
84
  //organisms
85
+ export * from "./components/organisms/DigitalAssetsTray/index";
82
86
  export * from "./components/organisms/ChangePassword/index";
83
87
  export * from "./components/organisms/Fullplan/index";
84
88
  export * from "./components/organisms/FullProductNameHeader/index";