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.
- package/.claude/launch.json +14 -0
- package/.env.test +1 -0
- package/dist/ai/prompts/attribute.prompt.js +28 -0
- package/dist/ai/prompts/description.prompt.js +33 -0
- package/dist/ai/schemas/attribute.schema.js +35 -0
- package/dist/ai/schemas/description.schema.js +27 -0
- package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +118 -0
- package/dist/components/atoms/AssetPreview/index.js +150 -0
- package/dist/components/atoms/AssetPreview/styles.js +28 -0
- package/dist/components/atoms/IconFile/index.js +85 -2
- package/dist/components/atoms/IconFile/styles.js +1 -1
- package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
- package/dist/components/atoms/ProductImage/index.js +100 -27
- package/dist/components/atoms/ProductImage/styles.js +3 -1
- package/dist/components/molecules/EditionActiveImage/index.js +4 -2
- package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
- package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
- package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
- package/dist/components/molecules/GalleryElement/index.js +13 -4
- package/dist/components/molecules/GalleryHeader/index.js +96 -24
- package/dist/components/molecules/GalleryHeader/styles.js +4 -1
- package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
- package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
- package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
- package/dist/components/molecules/ImageSelector/index.js +1 -0
- package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +171 -0
- package/dist/components/organisms/DigitalAssetsTray/index.js +211 -0
- package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
- package/dist/components/organisms/ImagePreviewer/index.js +1 -0
- package/dist/components/organisms/ProductImageModal/index.js +3 -2
- package/dist/components/organisms/PropagateChangesModal/index.js +12 -1
- package/dist/components/organisms/PropagateChangesModal/non-propagable.js +45 -0
- package/dist/components/pages/ProviderProductEdition/index.js +11 -1
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
- package/dist/components/pages/RetailerProductEdition/index.js +605 -414
- package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +45 -145
- package/dist/global-files/data.js +16 -3
- package/dist/global-files/digital-assets/assetTypes.js +480 -0
- package/dist/global-files/digital-assets/assets.actions.js +64 -0
- package/dist/global-files/digital-assets/assets.adapter.js +304 -0
- package/dist/global-files/digital-assets/assets.mock.js +136 -0
- package/dist/global-files/digital-assets/assets.reducer.js +220 -0
- package/dist/global-files/digital-assets/uploadAssets.js +179 -0
- package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
- package/dist/index.js +142 -90
- package/docs/activos-digitales.md +273 -0
- package/package.json +1 -1
- package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
- package/src/components/atoms/AssetPreview/index.js +119 -0
- package/src/components/atoms/AssetPreview/styles.js +190 -0
- package/src/components/atoms/IconFile/index.js +28 -1
- package/src/components/atoms/IconFile/styles.js +8 -0
- package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
- package/src/components/atoms/ProductImage/index.js +95 -20
- package/src/components/atoms/ProductImage/styles.js +92 -7
- package/src/components/molecules/EditionActiveImage/index.js +2 -2
- package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
- package/src/components/molecules/GalleryAssetElement/index.js +162 -0
- package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
- package/src/components/molecules/GalleryElement/index.js +9 -0
- package/src/components/molecules/GalleryHeader/index.js +65 -13
- package/src/components/molecules/GalleryHeader/styles.js +2 -1
- package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
- package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
- package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
- package/src/components/molecules/ImageSelector/index.js +1 -0
- package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
- package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
- package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
- package/src/components/organisms/ImagePreviewer/index.js +1 -0
- package/src/components/organisms/ProductImageModal/index.js +1 -0
- package/src/components/organisms/PropagateChangesModal/index.js +11 -1
- package/src/components/organisms/PropagateChangesModal/non-propagable.js +29 -0
- package/src/components/pages/ProviderProductEdition/index.js +10 -1
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
- package/src/components/pages/RetailerProductEdition/index.js +366 -140
- package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +51 -104
- package/src/global-files/data.js +17 -2
- package/src/global-files/digital-assets/assetTypes.js +419 -0
- package/src/global-files/digital-assets/assets.actions.js +50 -0
- package/src/global-files/digital-assets/assets.adapter.js +229 -0
- package/src/global-files/digital-assets/assets.mock.js +122 -0
- package/src/global-files/digital-assets/assets.reducer.js +197 -0
- package/src/global-files/digital-assets/uploadAssets.js +109 -0
- package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
- package/src/index.js +4 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Activos digitales — estado del trabajo
|
|
2
|
+
|
|
3
|
+
> Documento de continuidad. Última actualización: 2026-09-23.
|
|
4
|
+
> **El circuito ya funciona de punta a punta en dev**, borrado en S3 incluido.
|
|
5
|
+
> El exportable está escrito y probado en local, sin commitear ni desplegar.
|
|
6
|
+
> Falta el póster.
|
|
7
|
+
|
|
8
|
+
## La tarea
|
|
9
|
+
|
|
10
|
+
1. Permitir el almacenamiento de activos digitales dentro de las bandejas de imágenes.
|
|
11
|
+
2. Añadir en el exportable de los productos las URL de los activos almacenados,
|
|
12
|
+
para que el usuario los descargue por su cuenta.
|
|
13
|
+
|
|
14
|
+
El punto 1 está hecho y desplegado en dev. El punto 2 está escrito, **sin
|
|
15
|
+
commitear ni desplegar** (ver "El exportable").
|
|
16
|
+
|
|
17
|
+
Pantalla: `RetailerProductEdition`, pestaña Imágenes, bandeja *Activos digitales*.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Cómo funciona hoy
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
NAVEGADOR LAMBDA S3
|
|
25
|
+
────────────────────────────────────────────────────────────────────────
|
|
26
|
+
1. Sueltas un archivo
|
|
27
|
+
2. Valida en local
|
|
28
|
+
extensión, peso,
|
|
29
|
+
duración, cupo
|
|
30
|
+
│ (nada ha salido todavía)
|
|
31
|
+
3. POST /articles-assets/presign ──────►
|
|
32
|
+
4. Comprueba sesión y acceso
|
|
33
|
+
Valida extensión → tipo
|
|
34
|
+
Genera la clave, firma
|
|
35
|
+
◄──────────────────────── url + fields + key
|
|
36
|
+
5. POST multipart ──────────────────────────────────────────►
|
|
37
|
+
con barra de progreso 6. S3 aplica las
|
|
38
|
+
condiciones firmadas
|
|
39
|
+
◄────────────────────────────────────────── 204
|
|
40
|
+
7. Al terminar, se registra SOLO (no espera al botón de guardar)
|
|
41
|
+
PUT /articles-data?asset=true ──►
|
|
42
|
+
8. Comprueba que la clave sea
|
|
43
|
+
de ese artículo; INSERT;
|
|
44
|
+
devuelve la lista completa
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Al reabrir la ficha, `read-data-images` devuelve el arreglo `assets` junto a las
|
|
48
|
+
imágenes, y la bandeja se llena sola.
|
|
49
|
+
|
|
50
|
+
### Decisiones que dan forma a esto
|
|
51
|
+
|
|
52
|
+
- **Se valida dos veces.** En el navegador para no hacer esperar por un archivo
|
|
53
|
+
que se va a rechazar; en el servidor porque el front se puede saltar y quien
|
|
54
|
+
firma decide qué entra al bucket. La del servidor es la que manda.
|
|
55
|
+
- **El archivo nunca toca un Lambda.** Por API Gateway no cabe —10 MB y 29
|
|
56
|
+
segundos— y convertirlo a base64 en el navegador tumba la pestaña con un .psd
|
|
57
|
+
grande.
|
|
58
|
+
- **S3 aplica los límites, no el navegador.** Por eso es un POST multipart
|
|
59
|
+
firmado con `createPresignedPost` y no un PUT: solo el POST admite condiciones
|
|
60
|
+
que S3 verifica al recibir (tamaño, Content-Type, clave).
|
|
61
|
+
- **El tipo se decide por EXTENSIÓN, no por mime.** El navegador no sabe qué es
|
|
62
|
+
un .psd —manda mime vacío— y a un .ai le pone `application/pdf`. Por lo mismo
|
|
63
|
+
el `accept` del dropzone lleva extensiones además de mimes: con solo mimes,
|
|
64
|
+
react-dropzone descartaría esos archivos antes de que la validación llegue a
|
|
65
|
+
verlos, y al soltarlos no pasaría nada.
|
|
66
|
+
- **Sube al soltar y se registra solo al terminar.** Con la barra llena nadie
|
|
67
|
+
supone que falta un paso. El precio es que un archivo subido y luego quitado
|
|
68
|
+
antes de registrarse deja un huérfano en S3.
|
|
69
|
+
- **Solo mp4, webm y ogg cuentan como reproducibles.** Un .mov se acepta, se
|
|
70
|
+
guarda y se descarga, pero se muestra con icono y avisa al soltarlo: Chrome y
|
|
71
|
+
Firefox no decodifican QuickTime.
|
|
72
|
+
- **Agregar es inmediato; borrar espera al guardar.** Registrar completa algo que
|
|
73
|
+
ya pasó —el archivo está en S3—; borrar es destructivo y conviene poder
|
|
74
|
+
deshacerlo cerrando sin guardar.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 2. Decisiones del manager (2026-09-22)
|
|
79
|
+
|
|
80
|
+
| # | Pregunta | Respuesta |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| 0 | ¿Renombrar la pestaña? | **Se queda como "Imágenes"** |
|
|
83
|
+
| 1 | ¿Qué tipos entran? | 360 (mp4/gif), video, gif, pdf, hojas de cálculo y archivos editables de diseño |
|
|
84
|
+
| 2 | ¿Del producto o de una cadena? | **General**, con `id_retailer = null`. La columna queda preparada |
|
|
85
|
+
| 3 | ¿ZIP del exportable o solo URL? | **Solo URL** |
|
|
86
|
+
| 4 | ¿Cuentan para el % y para aprobar/rechazar? | **No** |
|
|
87
|
+
| 5 | ¿Quién sube y borra? | Los mismos que suben imágenes |
|
|
88
|
+
| 6 | ¿Versionado? | **No hay versión.** Un activo general es el mismo para todas |
|
|
89
|
+
| 7 | Peso y cantidad máxima | **SIN RESPONDER** — provisionales en `assetTypes.js` |
|
|
90
|
+
| 8 | Duración de video | Sin responder; se conserva 10 s – 10 min |
|
|
91
|
+
| 9 | ¿URLs permanentes o firmadas? | **Permanentes, indefinidas** |
|
|
92
|
+
| 10 | ¿Se autoriza `rel_article_asset`? | **Sí** |
|
|
93
|
+
|
|
94
|
+
### Lo que sigue abierto
|
|
95
|
+
|
|
96
|
+
**Pesos y cantidad (pregunta 7).** Provisionales, en un solo archivo por lado:
|
|
97
|
+
`assetTypes.js` (front) y `asset-types.js` (back). **Deben moverse juntos.**
|
|
98
|
+
|
|
99
|
+
| Tipo | Tope | Máximo por producto |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| 360 / video | 200 MB | **20 activos** |
|
|
102
|
+
| GIF | 20 MB | |
|
|
103
|
+
| Documento (pdf, doc, docx) | 50 MB | |
|
|
104
|
+
| Hoja de cálculo (xlsx, xls, csv) | 20 MB | |
|
|
105
|
+
| Editable (psd, ai, indd, aep…) | 500 MB | |
|
|
106
|
+
| Comprimido (zip, rar, 7z) | 500 MB | |
|
|
107
|
+
|
|
108
|
+
**Confirmar la lista de extensiones editables** que se propuso para el "etc":
|
|
109
|
+
`psd psb ai eps indd idml aep prproj sketch fig xd afphoto afdesign svg`.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 3. Qué está hecho y dónde
|
|
114
|
+
|
|
115
|
+
### Front — `components-library`, rama `feature/digital-assets`
|
|
116
|
+
|
|
117
|
+
**Seis commits, SIN empujar** (la rama no tiene remoto todavía). Sale de
|
|
118
|
+
`feature/multiple-retailer-category`, que aún no está en master.
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
65523ae feat: avisar cuántos cambios quedan sin guardar
|
|
122
|
+
b607eea feat: poder abrir un activo, y avisar cuando no se podrá ver
|
|
123
|
+
82e1a7d feat: registrar el activo en cuanto termina de subir
|
|
124
|
+
26986f1 feat: subida directa a S3 con URL prefirmada
|
|
125
|
+
16bc7da fix(ficha): evitar que la edición reviente o se quede cargando
|
|
126
|
+
003c80a feat: bandeja de 360, video, gif, documentos y editables
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Archivo | Qué es |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `global-files/digital-assets/assetTypes.js` | Catálogo de 7 tipos y 27 formatos, límites, validaciones y avisos |
|
|
132
|
+
| `global-files/digital-assets/assets.adapter.js` | Traducción backend ↔ interfaz. **Único archivo que conoce los nombres de campo del backend** |
|
|
133
|
+
| `global-files/digital-assets/assets.actions.js` | Vocabulario compartido entre el reducer y el hook |
|
|
134
|
+
| `global-files/digital-assets/assets.reducer.js` | Reducer + selectores `selectAssetsToSave`, `hasUnsavedAssets`, `countUnsavedAssets` |
|
|
135
|
+
| `global-files/digital-assets/useDigitalAssets.js` | Validación, duración, ciclo de vida de la subida y orquestación |
|
|
136
|
+
| `global-files/digital-assets/uploadAssets.js` | Presign + POST multipart a S3 con progreso |
|
|
137
|
+
| `global-files/digital-assets/assets.mock.js` | Activos con la forma cruda del backend |
|
|
138
|
+
| `atoms/AssetPreview/` | Previsualización: video, imagen o icono |
|
|
139
|
+
| `molecules/GalleryAssetElement/` | Fila de activo (aparte de `GalleryElement` a propósito) |
|
|
140
|
+
| `molecules/GalleryTrayTabs/` | Toggle `Imágenes (n) │ Activos digitales (n)` |
|
|
141
|
+
| `organisms/DigitalAssetsTray/` | Bandeja: dropzone propio, avisos, aviso de cambios sin guardar |
|
|
142
|
+
|
|
143
|
+
Modificados: `global-files/data.js`, `atoms/ProductImage`, `atoms/IconFile`,
|
|
144
|
+
`molecules/GalleryElement`, `molecules/GalleryHeader`, `ImageSelector`,
|
|
145
|
+
`EditionActiveImage`, `ImagePreviewer`, `ProductImageModal`,
|
|
146
|
+
`RetailerProductEdition/index.js`, su contexto y su reducer raíz, `src/index.js`
|
|
147
|
+
y los tres `.env`.
|
|
148
|
+
|
|
149
|
+
**El flag `enableDigitalAssets` está apagado por omisión.** La historia
|
|
150
|
+
`ImageEditor` lo enciende para poder ver la bandeja en Storybook.
|
|
151
|
+
|
|
152
|
+
### Backend — `content-management-backend`, rama `feature/digital-assets`
|
|
153
|
+
|
|
154
|
+
**Seis commits, empujados.** Sin PR: no se mergea todavía.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
0aafcdec fix: que el borrado en S3 sea posible y visible
|
|
158
|
+
ab38d16e fix: declarar el charset de rel_article_asset
|
|
159
|
+
a543ba30 fix: devolver el ref del cliente en el presign
|
|
160
|
+
080d2481 feat: lectura y escritura de activos
|
|
161
|
+
9c7b8176 feat: endpoint para firmar la subida de activos
|
|
162
|
+
2c1d5f1f feat: tabla rel_article_asset
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| Archivo | Qué es |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `docs/create-rel-article-asset.sql` | DDL. Sin `id_version`; `asset_type` es VARCHAR y no ENUM; charset utf8mb4 explícito |
|
|
168
|
+
| `CRUD/articles/articles-assets/asset-types.js` | Allowlist del servidor. Los editables se sirven como `octet-stream`, el .svg incluido |
|
|
169
|
+
| `CRUD/articles/articles-assets/presign-asset-upload.js` | `POST /articles-assets/presign` |
|
|
170
|
+
| `CRUD/articles/articles-data/utils/assets-actions.js` | Altas, cambios, bajas y lectura |
|
|
171
|
+
| `read-data-images.js`, `update-articles-data.js` | Devuelven/aceptan activos |
|
|
172
|
+
| `serverless.yml` | La función nueva y los permisos de S3 |
|
|
173
|
+
|
|
174
|
+
### Estado del despliegue (dev)
|
|
175
|
+
|
|
176
|
+
| Pieza | Estado |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Tabla `rel_article_asset` | Creada. **Necesita el `ALTER` de charset** (ver el .sql) |
|
|
179
|
+
| `presign-asset-upload` | Desplegada |
|
|
180
|
+
| `read-data-images` con `assets` | Desplegada |
|
|
181
|
+
| `update-articles-data?asset=true` | Desplegada |
|
|
182
|
+
| `ref` en la respuesta del presign (`a543ba30`) | Desplegado (deploy completo, 2026-09-23) |
|
|
183
|
+
| `s3:DeleteObject` en el rol (`0aafcdec`) | Desplegado y comprobado: al borrar un activo desaparece del bucket |
|
|
184
|
+
| Activos en el exportable (servicio `exports`) | **Sin commitear ni desplegar** |
|
|
185
|
+
|
|
186
|
+
Nada desplegado en test ni en prod.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 4. Lo que falta
|
|
191
|
+
|
|
192
|
+
1. **Commitear y desplegar el exportable.** El servicio es `exports`, no
|
|
193
|
+
`functions`. Hay que desplegar el orquestador (Lambda) y además
|
|
194
|
+
reconstruir la imagen de Docker del procesador (Fargate).
|
|
195
|
+
2. **El póster con ffmpeg** tras la subida, para que los videos tengan miniatura
|
|
196
|
+
sin que el navegador la genere.
|
|
197
|
+
3. **Huérfanos en S3**: un archivo subido que se quita antes de registrarse no se
|
|
198
|
+
borra nunca. O una regla de ciclo de vida sobre `assets/`, o un endpoint de
|
|
199
|
+
borrado por clave para ese caso.
|
|
200
|
+
4. **El front no lee `orphaned`**: si S3 rechaza un borrado, la bandeja dice
|
|
201
|
+
"guardados con éxito" igual.
|
|
202
|
+
|
|
203
|
+
### El exportable
|
|
204
|
+
|
|
205
|
+
Los activos salen como filas de *"Archivo Urls.xlsx"*, el mismo archivo que se
|
|
206
|
+
genera con el servicio `urls`. Se agrega una columna `Tipo`, que en las imágenes
|
|
207
|
+
dice `Imagen` y en los activos `Video`, `Editable`, etc.
|
|
208
|
+
|
|
209
|
+
| Archivo (`exports/nuevo-exportable/`) | Qué hace |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `asset-urls.js` | `attachArticleAssets()` lee `rel_article_asset` + `article.upc`; `collectAssetUrlRows()` arma las filas |
|
|
212
|
+
| `handler-orchestrator.js` | Llama a `attachArticleAssets` junto a las demás cargas |
|
|
213
|
+
| `handler-processor.js` | Suma las filas de activos a las de imágenes cuando se pide `urls` |
|
|
214
|
+
| `xlsx-url-writer.js` | Columna `Tipo`; los activos van después de las imágenes de su producto |
|
|
215
|
+
| `asset-urls.test.js` | 9 pruebas. `node --test exports/nuevo-exportable/` |
|
|
216
|
+
|
|
217
|
+
Decisiones:
|
|
218
|
+
- **No se copian al bucket público**, a diferencia de lo que planteaba el plan
|
|
219
|
+
original. Las imágenes se copian porque cada cadena las recibe transformadas y
|
|
220
|
+
renombradas. Un activo se entrega tal cual, y ya está en S3 con
|
|
221
|
+
`public-read` y su Content-Type real. Se usa su URL de siempre: es permanente
|
|
222
|
+
(decisión 9) y es la misma que usa la ficha. Copiarlo duplicaría archivos de
|
|
223
|
+
hasta 500 MB en cada exportación, y ya no hace falta tocar `getContentType`.
|
|
224
|
+
- **Renombre = `<UPC>_<nombre original sin extensión>`**, y si no hay UPC, el
|
|
225
|
+
id del artículo. El UPC sale de `article.upc`.
|
|
226
|
+
- **Aparece aunque no se pidan imágenes**: basta con el servicio `urls`.
|
|
227
|
+
- **Un activo sale una vez por cadena**, aunque el producto venga en varias
|
|
228
|
+
categorías o versiones.
|
|
229
|
+
- **Si la tabla no existe** (`ER_NO_SUCH_TABLE`), el exportable sale sin
|
|
230
|
+
activos en vez de fallar. Así se puede desplegar antes de crear la tabla en
|
|
231
|
+
prod.
|
|
232
|
+
|
|
233
|
+
Limitación conocida: si una cadena nombra sus imágenes con otro atributo (SKU,
|
|
234
|
+
por ejemplo), sus activos no quedan junto a esas imágenes en el Excel, porque
|
|
235
|
+
van por UPC.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 5. Hallazgos ajenos a este trabajo
|
|
240
|
+
|
|
241
|
+
- **El rol `general-dev-us-east-1-lambdaRole` no tiene permisos de S3 sobre las
|
|
242
|
+
imágenes de producto**, solo sobre el prefijo `assets/`. Pero `images-actions.js`
|
|
243
|
+
hace `copyObject` al bucket de respaldo al guardar y `deleteObject` en los dos
|
|
244
|
+
buckets al eliminar. Los errores mueren dentro de un `Promise.all` con try/catch
|
|
245
|
+
arriba. Si es lo que parece: el respaldo de imágenes no se está haciendo y las
|
|
246
|
+
imágenes borradas siguen ocupando el bucket. **Comprobar el bucket
|
|
247
|
+
`content-management-raw-images`**; si está congelado en una fecha vieja, lleva
|
|
248
|
+
tiempo roto.
|
|
249
|
+
- **La política del rol es en línea y la genera Serverless** (`general-dev-lambda`).
|
|
250
|
+
No tiene políticas administradas adjuntas: cualquier permiso agregado a mano se
|
|
251
|
+
borra en el siguiente despliegue.
|
|
252
|
+
- **`AddGroup.stories.js` está roto** (`bags is not defined`) y ensucia la consola
|
|
253
|
+
de Storybook.
|
|
254
|
+
- **`version-manager` y `articles-observations/read-observation` no permiten CORS**
|
|
255
|
+
desde `localhost:6006`. No impiden que la ficha cargue.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 6. Trampas del repo que conviene recordar
|
|
260
|
+
|
|
261
|
+
- **No correr prettier sobre archivos existentes** de `components-library`. No hay
|
|
262
|
+
`.prettierrc` y la versión instalada es la 2.3.0, pero el código está formateado
|
|
263
|
+
con estilo de la 3. Formatear un archivo existente genera ~100 líneas de ruido.
|
|
264
|
+
- **Hay directorios versionados que solo difieren en mayúsculas**:
|
|
265
|
+
`editionActiveImage`/`EditionActiveImage`, `imageSelector`/`ImageSelector`,
|
|
266
|
+
`productImage`/`ProductImage`. En macOS son el mismo directorio, así que
|
|
267
|
+
aparecen siempre como modificados y `git add -A` falla. Se preparan a mano las
|
|
268
|
+
rutas en mayúscula; las otras son fantasmas que nadie importa.
|
|
269
|
+
- **`functions/CRUD/database-structure.txt` está desactualizado** para
|
|
270
|
+
`rel_article_image`: dice `article_id`/`image_id` pero los lambdas usan
|
|
271
|
+
`id_article`/`id_image`.
|
|
272
|
+
- **El stage `dev` apunta al bucket de imágenes de PRODUCCIÓN**
|
|
273
|
+
(`content-management-images-prod`). Cualquier archivo de prueba queda ahí.
|
package/package.json
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AssetPreview } from "./index";
|
|
2
|
+
import { fromBackendList } from "../../../global-files/digital-assets/assets.adapter";
|
|
3
|
+
import { backendAssetsMock } from "../../../global-files/digital-assets/assets.mock";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Los activos del mock apuntan a claves de S3 que no existen todavía. Para poder
|
|
7
|
+
ver el componente antes de que haya backend, se sustituyen las URLs por
|
|
8
|
+
archivos públicos de ejemplo; el resto del objeto (tipo, nombre, peso,
|
|
9
|
+
duración) es el que devolvería el adaptador.
|
|
10
|
+
*/
|
|
11
|
+
const SAMPLES = {
|
|
12
|
+
mp4: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
|
|
13
|
+
gif: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const [giro360, gif360, video, pdf] = fromBackendList(backendAssetsMock).map(
|
|
17
|
+
(asset) => ({
|
|
18
|
+
...asset,
|
|
19
|
+
url:
|
|
20
|
+
asset.ext === "mp4"
|
|
21
|
+
? SAMPLES.mp4
|
|
22
|
+
: asset.ext === "gif"
|
|
23
|
+
? SAMPLES.gif
|
|
24
|
+
: "",
|
|
25
|
+
poster: "",
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
title: "Components/atoms/AssetPreview",
|
|
31
|
+
component: AssetPreview,
|
|
32
|
+
argTypes: {
|
|
33
|
+
size: {
|
|
34
|
+
options: [
|
|
35
|
+
"catalogue-image-small-size",
|
|
36
|
+
"catalogue-image-big-size",
|
|
37
|
+
"carousel-image",
|
|
38
|
+
"catalogue-modal-image",
|
|
39
|
+
"main-image",
|
|
40
|
+
],
|
|
41
|
+
control: { type: "select" },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const Template = (args) => <AssetPreview {...args} />;
|
|
47
|
+
|
|
48
|
+
export const Video360 = Template.bind({});
|
|
49
|
+
Video360.args = { asset: giro360, size: "catalogue-image-big-size" };
|
|
50
|
+
|
|
51
|
+
export const Gif360 = Template.bind({});
|
|
52
|
+
Gif360.args = { asset: gif360, size: "catalogue-image-big-size" };
|
|
53
|
+
|
|
54
|
+
export const VideoConControles = Template.bind({});
|
|
55
|
+
VideoConControles.args = { asset: video, size: "main-image", controls: true };
|
|
56
|
+
|
|
57
|
+
export const Pdf = Template.bind({});
|
|
58
|
+
Pdf.args = { asset: pdf, size: "catalogue-image-big-size" };
|
|
59
|
+
|
|
60
|
+
/** Un activo sin URL todavía —recién soltado en la bandeja— no debe romper. */
|
|
61
|
+
export const SinFuente = Template.bind({});
|
|
62
|
+
SinFuente.args = {
|
|
63
|
+
asset: { name: "manual_sin_subir.pdf", ext: "pdf", assetType: "pdf" },
|
|
64
|
+
size: "catalogue-image-big-size",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Cómo se ve la bandeja completa, que es el uso real. */
|
|
68
|
+
export const Bandeja = () => (
|
|
69
|
+
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
|
|
70
|
+
{[giro360, gif360, video, pdf].map((asset) => (
|
|
71
|
+
<AssetPreview
|
|
72
|
+
key={asset.id}
|
|
73
|
+
asset={asset}
|
|
74
|
+
size="catalogue-image-big-size"
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import ReactImageFallback from "react-image-fallback";
|
|
2
|
+
|
|
3
|
+
import { Container } from "./styles";
|
|
4
|
+
import { IconFile } from "../IconFile";
|
|
5
|
+
import {
|
|
6
|
+
ASSET_PREVIEW,
|
|
7
|
+
formatDuration,
|
|
8
|
+
getAssetType,
|
|
9
|
+
getExtension,
|
|
10
|
+
resolvePreviewKind,
|
|
11
|
+
} from "../../../global-files/digital-assets/assetTypes";
|
|
12
|
+
import defaultProductImage from "../../../assets/images/defaultImages/defaultProductImage.png";
|
|
13
|
+
|
|
14
|
+
const PlayBadge = () => (
|
|
15
|
+
<span className="play-badge" aria-hidden="true">
|
|
16
|
+
<svg viewBox="0 0 24 24">
|
|
17
|
+
<circle cx="12" cy="12" r="11" />
|
|
18
|
+
<polygon points="9.5,7.5 17,12 9.5,16.5" />
|
|
19
|
+
</svg>
|
|
20
|
+
</span>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Previsualización de un activo digital o de una imagen de producto.
|
|
25
|
+
*
|
|
26
|
+
* Acepta el modelo de `assets.adapter` y también un objeto suelto
|
|
27
|
+
* (`{ url, mime, ext, name }`), para poder usarse donde todavía no hay activos
|
|
28
|
+
* adaptados.
|
|
29
|
+
*
|
|
30
|
+
* `controls` distingue los dos usos: en la bandeja se quieren miniaturas
|
|
31
|
+
* quietas; en el visor grande, un reproductor.
|
|
32
|
+
*/
|
|
33
|
+
export const AssetPreview = ({
|
|
34
|
+
asset = {},
|
|
35
|
+
size = "catalogue-image-small-size",
|
|
36
|
+
controls = false,
|
|
37
|
+
showBadge = true,
|
|
38
|
+
showDuration = true,
|
|
39
|
+
onClick,
|
|
40
|
+
className = "",
|
|
41
|
+
}) => {
|
|
42
|
+
const { url, src, poster, mime, ext, name, assetType, duration } = asset;
|
|
43
|
+
|
|
44
|
+
const source = url || src || "";
|
|
45
|
+
const kind = resolvePreviewKind({ mime, ext, name });
|
|
46
|
+
const extension = (ext || getExtension(name) || "").toLowerCase();
|
|
47
|
+
const label = assetType
|
|
48
|
+
? getAssetType(assetType).label
|
|
49
|
+
: extension.toUpperCase();
|
|
50
|
+
|
|
51
|
+
const badge =
|
|
52
|
+
showBadge && label ? <span className="asset-badge">{label}</span> : null;
|
|
53
|
+
const durationTag =
|
|
54
|
+
showDuration && duration ? (
|
|
55
|
+
<span className="asset-duration">{formatDuration(duration)}</span>
|
|
56
|
+
) : null;
|
|
57
|
+
|
|
58
|
+
const body = () => {
|
|
59
|
+
if (!source) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="asset-fallback">
|
|
62
|
+
<IconFile ext={extension} size="34" />
|
|
63
|
+
<p title={name}>{name}</p>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (kind === ASSET_PREVIEW.VIDEO) {
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<video
|
|
72
|
+
// `#t=0.5` pide al navegador el fotograma del medio segundo en vez
|
|
73
|
+
// de una pantalla negra. Resuelve la miniatura sin descargar el
|
|
74
|
+
// archivo entero ni convertirlo a base64, que es lo que hacía el
|
|
75
|
+
// parche que este componente reemplaza.
|
|
76
|
+
src={poster ? source : `${source}#t=0.5`}
|
|
77
|
+
poster={poster || undefined}
|
|
78
|
+
controls={controls}
|
|
79
|
+
muted={!controls}
|
|
80
|
+
playsInline
|
|
81
|
+
preload="metadata"
|
|
82
|
+
/>
|
|
83
|
+
{/* Sin controles, nada indica que el recuadro es reproducible. */}
|
|
84
|
+
{!controls && <PlayBadge />}
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (kind === ASSET_PREVIEW.IMAGE) {
|
|
90
|
+
return (
|
|
91
|
+
<ReactImageFallback
|
|
92
|
+
src={source}
|
|
93
|
+
fallbackImage={defaultProductImage}
|
|
94
|
+
alt={name}
|
|
95
|
+
onClick={onClick}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="asset-fallback" onClick={onClick}>
|
|
102
|
+
<IconFile ext={extension} size="34" />
|
|
103
|
+
<p title={name}>{name}</p>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Container
|
|
110
|
+
className={`${size} ${className}`.trim()}
|
|
111
|
+
onClick={controls ? undefined : onClick}
|
|
112
|
+
$clickable={Boolean(onClick) && !controls}
|
|
113
|
+
>
|
|
114
|
+
{body()}
|
|
115
|
+
{badge}
|
|
116
|
+
{durationTag}
|
|
117
|
+
</Container>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
import { GlobalColors, FontFamily } from "../../../global-files/variables";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Los tamaños replican los de `ProductImage` con los mismos nombres de clase,
|
|
6
|
+
para que el componente entre donde ya había una imagen sin tocar el layout
|
|
7
|
+
de la bandeja.
|
|
8
|
+
*/
|
|
9
|
+
export const Container = styled.div`
|
|
10
|
+
position: relative;
|
|
11
|
+
width: fit-content;
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
cursor: ${({ $clickable }) => ($clickable ? "pointer" : "default")};
|
|
17
|
+
|
|
18
|
+
img,
|
|
19
|
+
video {
|
|
20
|
+
object-fit: contain;
|
|
21
|
+
background: ${GlobalColors.gray_light};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.asset-fallback {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 6px;
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: 100%;
|
|
32
|
+
padding: 8px;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
background: ${GlobalColors.gray_light};
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
|
|
37
|
+
p {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-family: ${FontFamily.Raleway};
|
|
40
|
+
font-size: 10px;
|
|
41
|
+
color: ${GlobalColors.gray};
|
|
42
|
+
text-align: center;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
text-overflow: ellipsis;
|
|
45
|
+
display: -webkit-box;
|
|
46
|
+
-webkit-line-clamp: 2;
|
|
47
|
+
-webkit-box-orient: vertical;
|
|
48
|
+
word-break: break-all;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Solo aparece cuando el video se muestra sin controles. */
|
|
53
|
+
.play-badge {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 50%;
|
|
56
|
+
left: 50%;
|
|
57
|
+
transform: translate(-50%, -50%);
|
|
58
|
+
width: 32px;
|
|
59
|
+
height: 32px;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
|
|
62
|
+
svg {
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
display: block;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
circle {
|
|
69
|
+
fill: rgba(0, 0, 0, 0.55);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
polygon {
|
|
73
|
+
fill: ${GlobalColors.white};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&:hover .play-badge circle {
|
|
78
|
+
fill: rgba(0, 0, 0, 0.75);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.asset-badge {
|
|
82
|
+
position: absolute;
|
|
83
|
+
top: 4px;
|
|
84
|
+
left: 4px;
|
|
85
|
+
padding: 1px 6px;
|
|
86
|
+
border-radius: 10px;
|
|
87
|
+
background: ${GlobalColors.color_purple};
|
|
88
|
+
color: ${GlobalColors.white};
|
|
89
|
+
font-family: ${FontFamily.Raleway_600};
|
|
90
|
+
font-size: 9px;
|
|
91
|
+
line-height: 14px;
|
|
92
|
+
letter-spacing: 0.02em;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.asset-duration {
|
|
97
|
+
position: absolute;
|
|
98
|
+
bottom: 4px;
|
|
99
|
+
right: 4px;
|
|
100
|
+
padding: 1px 5px;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
background: rgba(38, 38, 38, 0.75);
|
|
103
|
+
color: ${GlobalColors.white};
|
|
104
|
+
font-family: ${FontFamily.Raleway};
|
|
105
|
+
font-size: 9px;
|
|
106
|
+
line-height: 14px;
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&.carousel-image {
|
|
111
|
+
.play-badge {
|
|
112
|
+
width: 22px;
|
|
113
|
+
height: 22px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
img,
|
|
117
|
+
video,
|
|
118
|
+
.asset-fallback {
|
|
119
|
+
width: 85px;
|
|
120
|
+
height: 55px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
& + * {
|
|
125
|
+
margin-left: 1px;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&.main-image {
|
|
130
|
+
img,
|
|
131
|
+
video,
|
|
132
|
+
.asset-fallback {
|
|
133
|
+
width: 340px;
|
|
134
|
+
height: 295px;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&.catalogue-image-big-size {
|
|
139
|
+
img,
|
|
140
|
+
video,
|
|
141
|
+
.asset-fallback {
|
|
142
|
+
width: 179px;
|
|
143
|
+
height: 179px;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&.catalogue-image-small-size {
|
|
148
|
+
img,
|
|
149
|
+
video,
|
|
150
|
+
.asset-fallback {
|
|
151
|
+
width: 60px;
|
|
152
|
+
height: 60px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.asset-badge {
|
|
156
|
+
font-size: 8px;
|
|
157
|
+
padding: 0 4px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* En 60px un badge de 32 tapa el fotograma completo. */
|
|
161
|
+
.play-badge {
|
|
162
|
+
width: 20px;
|
|
163
|
+
height: 20px;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&.catalogue-modal-image {
|
|
168
|
+
flex: 1 0 50%;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
|
|
171
|
+
img,
|
|
172
|
+
video,
|
|
173
|
+
.asset-fallback {
|
|
174
|
+
width: 168px;
|
|
175
|
+
height: 108px;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&.expanded-modal-image {
|
|
180
|
+
width: 710px;
|
|
181
|
+
height: 710px;
|
|
182
|
+
|
|
183
|
+
img,
|
|
184
|
+
video,
|
|
185
|
+
.asset-fallback {
|
|
186
|
+
width: 710px;
|
|
187
|
+
height: 710px;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`;
|