contentoh-components-library 21.6.57 → 21.6.59

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 (49) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +90 -135
  3. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +148 -162
  4. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +16 -0
  5. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +1 -0
  6. package/dist/components/pages/CreatorsProductEdition/sections/ImageGallery.js +433 -0
  7. package/dist/components/pages/CreatorsProductEdition/sections/NoticeDialog.js +155 -0
  8. package/dist/components/pages/CreatorsProductEdition/sections/RejectDialog.js +201 -0
  9. package/dist/components/pages/CreatorsProductEdition/sections/VersionDrawer.js +615 -0
  10. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +3 -31
  11. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +55 -30
  12. package/dist/components/pages/CreatorsProductEdition/stories/products.js +923 -0
  13. package/dist/components/pages/CreatorsProductEdition/styles.js +27 -7
  14. package/dist/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +2 -1
  15. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +325 -10
  16. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -2
  17. package/dist/components/pages/ProviderProductEdition/index.js +486 -231
  18. package/dist/index.js +13 -0
  19. package/package.json +1 -1
  20. package/src/components/pages/CreatorsProductEdition/CreatorsProductEditionView.jsx +777 -0
  21. package/src/components/pages/CreatorsProductEdition/constants.js +21 -0
  22. package/src/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2238 -0
  23. package/src/components/pages/CreatorsProductEdition/index.js +19 -0
  24. package/src/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +357 -0
  25. package/src/components/pages/CreatorsProductEdition/mocks/mockApi.js +144 -0
  26. package/src/components/pages/CreatorsProductEdition/sections/CommentBox.jsx +183 -0
  27. package/src/components/pages/CreatorsProductEdition/sections/ImageGallery.jsx +269 -0
  28. package/src/components/pages/CreatorsProductEdition/sections/ImagePanel.jsx +332 -0
  29. package/src/components/pages/CreatorsProductEdition/sections/ImagesSection.jsx +385 -0
  30. package/src/components/pages/CreatorsProductEdition/sections/NoticeDialog.jsx +111 -0
  31. package/src/components/pages/CreatorsProductEdition/sections/ProductHeader.jsx +305 -0
  32. package/src/components/pages/CreatorsProductEdition/sections/ProductMedia.jsx +101 -0
  33. package/src/components/pages/CreatorsProductEdition/sections/RejectDialog.jsx +125 -0
  34. package/src/components/pages/CreatorsProductEdition/sections/RetailerMenu.jsx +129 -0
  35. package/src/components/pages/CreatorsProductEdition/sections/ServiceActions.jsx +382 -0
  36. package/src/components/pages/CreatorsProductEdition/sections/StatusChip.jsx +37 -0
  37. package/src/components/pages/CreatorsProductEdition/sections/TabsBar.jsx +187 -0
  38. package/src/components/pages/CreatorsProductEdition/sections/VariantsPanel.jsx +311 -0
  39. package/src/components/pages/CreatorsProductEdition/sections/VersionDrawer.jsx +357 -0
  40. package/src/components/pages/CreatorsProductEdition/sections/tokens.js +63 -0
  41. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +111 -0
  42. package/src/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +85 -0
  43. package/src/components/pages/CreatorsProductEdition/stories/products.js +966 -0
  44. package/src/components/pages/CreatorsProductEdition/styles.js +178 -0
  45. package/src/components/pages/ProviderProductEdition/ProviderProductEdition.stories.js +1 -0
  46. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +201 -0
  47. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.reducer.js +12 -0
  48. package/src/components/pages/ProviderProductEdition/index.js +228 -7
  49. package/src/index.js +1 -0
@@ -0,0 +1,178 @@
1
+ import styled, { createGlobalStyle } from "styled-components";
2
+ import { Footer as PropagateFooter } from "../../organisms/PropagateChangesModal/styles";
3
+ import { Footer as GroupFooter } from "../../organisms/VariantGroupModal/styles";
4
+ import { Footer as LinkFooter } from "../../organisms/LinkVariantsModal/styles";
5
+ import { Footer as UnlinkFooter } from "../../organisms/UnlinkVariantModal/styles";
6
+ import {
7
+ StyledDialog as ChangeStatusDialog,
8
+ ScopeButtonGroup,
9
+ FooterActions as ChangeStatusFooter,
10
+ } from "../../organisms/ChangeStatusModal/styles";
11
+
12
+ /*
13
+ Estilos de la edición de producto de Creators.
14
+
15
+ Parte de `RetailerProductEdition/styles.js` (mismo diseño) y agrega lo que la
16
+ hace responsiva. Tres tamaños, los mismos que usa MUI por omisión:
17
+
18
+ - celular (< 600px): una columna, un solo scroll para toda la pantalla.
19
+ - tablet (600-899px): igual, con más aire.
20
+ - desktop (>= 900px): las dos columnas de siempre, cada una con su scroll.
21
+
22
+ Varios hijos (ImagePreviewer, ImageDataTable, InputGroup...) traen anchos fijos
23
+ y la regla es no tocarlos. Donde estorban se ajustan DESDE AQUÍ, con selectores
24
+ acotados a esta página. Las fases siguientes los van reemplazando por MUI y
25
+ estos ajustes desaparecen con ellos.
26
+ */
27
+
28
+ export const BREAKPOINTS = {
29
+ tablet: 600,
30
+ desktop: 900,
31
+ };
32
+
33
+ const mobile = `@media (max-width: ${BREAKPOINTS.tablet - 1}px)`;
34
+ const belowDesktop = `@media (max-width: ${BREAKPOINTS.desktop - 1}px)`;
35
+
36
+ export const Container = styled.div`
37
+ display: flex;
38
+ flex-direction: column;
39
+ height: 100%;
40
+ flex: 1;
41
+ min-width: 0;
42
+ overflow: auto;
43
+ box-sizing: border-box;
44
+
45
+ /* El scroll lo lleva SIEMPRE este contenedor, como en la vista original: en
46
+ la app la pantalla vive dentro de un layout de altura fija, y si se dejara
47
+ al documento, en celular no habría nada que desplazar. */
48
+ ${belowDesktop} {
49
+ overflow-x: hidden;
50
+ overflow-y: auto;
51
+ -webkit-overflow-scrolling: touch;
52
+ /* Sin overscroll-behavior: contain. Al llegar al final, el scroll tiene
53
+ que poder pasar al contenedor de afuera (el lienzo de Storybook, o la
54
+ página si la app la mete en algo que se desplaza); con contain se queda
55
+ atorado y no hay forma de ver lo que está debajo. */
56
+ }
57
+
58
+ .data-container {
59
+ display: flex;
60
+ flex: 0%;
61
+ gap: 16px;
62
+ min-width: 0;
63
+ height: calc(100% - ${({ headerTop }) => headerTop}px);
64
+ padding: 0 12px 12px;
65
+ box-sizing: border-box;
66
+
67
+ ${belowDesktop} {
68
+ flex-direction: column;
69
+ flex: none;
70
+ height: auto;
71
+ }
72
+
73
+ ${mobile} {
74
+ padding: 0 8px 12px;
75
+ gap: 12px;
76
+ }
77
+
78
+ .image-data-panel {
79
+ flex: 0 0 clamp(280px, 30%, 360px);
80
+ min-width: 0;
81
+
82
+ ${belowDesktop} {
83
+ flex: 0 0 auto;
84
+ width: 100%;
85
+ }
86
+
87
+ ${mobile} {
88
+ max-width: 480px;
89
+ align-self: center;
90
+ }
91
+ }
92
+
93
+ .product-information {
94
+ flex: 1;
95
+ min-width: 0;
96
+ height: 100%;
97
+ display: flex;
98
+ flex-direction: column;
99
+
100
+ ${belowDesktop} {
101
+ height: auto;
102
+ }
103
+
104
+ /* Cabecera, pestañas y grupos de inputs: que puedan encogerse en vez de
105
+ empujar la página hacia los lados. */
106
+ & > * {
107
+ min-width: 0;
108
+ max-width: 100%;
109
+ }
110
+
111
+ .services-information-container {
112
+ height: 100%;
113
+ min-width: 0;
114
+ overflow: auto;
115
+
116
+ ${belowDesktop} {
117
+ height: auto;
118
+ overflow: visible;
119
+ }
120
+ }
121
+
122
+ }
123
+ }
124
+
125
+ /* Modales de variantes (se reutilizan tal cual). Su pie pone el resumen y
126
+ tres botones en una sola fila; en celular no cabe y el botón principal se
127
+ sale del panel. Aquí el resumen va en su propia fila y los botones se
128
+ reparten el ancho. */
129
+ ${mobile} {
130
+ ${PropagateFooter}, ${GroupFooter}, ${LinkFooter}, ${UnlinkFooter} {
131
+ flex-wrap: wrap;
132
+ padding: 12px 16px;
133
+
134
+ .resumen,
135
+ .summary {
136
+ flex: 1 1 100%;
137
+ }
138
+
139
+ .actions {
140
+ flex: 1 1 100%;
141
+ margin-left: 0;
142
+ }
143
+
144
+ button {
145
+ flex: 1 1 0;
146
+ min-width: 0;
147
+ height: auto;
148
+ min-height: 38px;
149
+ padding: 6px 10px;
150
+ }
151
+ }
152
+ }
153
+ `;
154
+
155
+ /*
156
+ Ajustes en celular para `ChangeStatusModal`, que se reutiliza tal cual. Es un
157
+ Dialog de MUI y se pinta en un portal, fuera de `Container`, así que no lo
158
+ alcanzan los selectores de arriba: va como estilo global, que solo existe
159
+ mientras esta pantalla está montada. Los tres botones de «Alcance del cambio»
160
+ no caben en una fila y se encimaban.
161
+ */
162
+ export const ReusedModalsStyles = createGlobalStyle`
163
+ ${mobile} {
164
+ ${ChangeStatusDialog} .MuiDialog-paper {
165
+ margin: 16px;
166
+ max-height: calc(100% - 32px);
167
+ }
168
+
169
+ ${ChangeStatusDialog} ${ScopeButtonGroup} {
170
+ grid-template-columns: 1fr;
171
+ gap: 8px;
172
+ }
173
+
174
+ ${ChangeStatusDialog} ${ChangeStatusFooter} {
175
+ padding: 12px 16px;
176
+ }
177
+ }
178
+ `;
@@ -268,6 +268,7 @@ ProviderProductEditionFromProducts.args = {
268
268
  retailers: [],
269
269
  },
270
270
  showSurvey: (v) => v && alert("se muestra"),
271
+ enableDigitalAssets: true,
271
272
  };
272
273
 
273
274
  export const ProviderProductEditionFromTasks = Template.bind({});
@@ -12,6 +12,13 @@ import errorIcon from "../../../../assets/images/genericModal/errorModal.svg";
12
12
  import warningIcon from "../../../../assets/images/genericModal/genericModalWarning.svg";
13
13
  import AWS from "aws-sdk";
14
14
  import { v4 as uuidv4 } from "uuid";
15
+ import {
16
+ assetKey,
17
+ buildSavePayload,
18
+ isPersisted,
19
+ toPayload,
20
+ } from "../../../../global-files/digital-assets/assets.adapter";
21
+ import { selectAssetsToSave } from "../../../../global-files/digital-assets/assets.reducer";
15
22
 
16
23
  const S3_BUCKET = process.env.REACT_APP_IMAGES_BUCKET;
17
24
  const REGION = "us-east-1";
@@ -404,6 +411,198 @@ export const ProviderProductEditionProvider = ({ children }) => {
404
411
  dispatch({ type: "SET_SAVING", payload: false });
405
412
  };
406
413
 
414
+ /*
415
+ Registra en la base los activos que ACABAN de subirse, sin esperar a que el
416
+ usuario guarde.
417
+
418
+ Es una corrección de expectativa, no una comodidad: al ver la barra de
419
+ progreso llegar al final, cualquiera da por hecho que el activo ya quedó
420
+ ligado al producto. Si entonces cierra la ficha, el archivo se queda en S3
421
+ sin fila en la base —invisible y sin forma de recuperarlo desde la
422
+ pantalla—.
423
+
424
+ Manda SOLO las altas recién subidas, con `updateAssets` y `deleteAssets`
425
+ vacíos. Un reetiquetado o un borrado que el usuario todavía no confirmó no
426
+ se aplica por haber soltado otro archivo: el botón de guardar sigue siendo
427
+ quien decide sobre esos.
428
+ */
429
+ const registerUploadedAssets = async (token, uploaded = []) => {
430
+ const listos = uploaded.filter((asset) => asset.src && !isPersisted(asset));
431
+ if (listos.length === 0) return;
432
+
433
+ const data = {
434
+ articleId: state.product?.id_article,
435
+ newAssets: listos.map(toPayload),
436
+ updateAssets: [],
437
+ deleteAssets: [],
438
+ };
439
+
440
+ if (state.product?.orderId || state.product?.id_order) {
441
+ data.orderId = state.product?.orderId || state.product?.id_order;
442
+ }
443
+
444
+ try {
445
+ const res = await axios.put(
446
+ process.env.REACT_APP_ARTICLE_DATA_ENDPOINT,
447
+ data,
448
+ { params: { asset: true }, headers: { Authorization: token } },
449
+ );
450
+
451
+ if (res.data.statusCode !== 200) throw new Error("Respuesta inesperada");
452
+
453
+ const body = JSON.parse(res.data.body);
454
+ const guardados = body.assets ?? [];
455
+
456
+ /*
457
+ Se le pone a cada activo el id que le tocó, emparejando por la clave de
458
+ S3, que es única.
459
+
460
+ A propósito NO se reemplaza la lista entera con lo que devolvió el
461
+ backend: eso borraría el rastro de lo que el usuario cambió o borró y
462
+ todavía no ha guardado. Aquí solo se completa lo que faltaba.
463
+ */
464
+ listos.forEach((asset) => {
465
+ const fila = guardados.find((row) => row.src === asset.src);
466
+ if (!fila) return;
467
+ dispatch({
468
+ type: "PATCH_ASSET",
469
+ payload: { key: assetKey(asset), changes: { id: fila.id, url: fila.url } },
470
+ });
471
+ });
472
+ } catch (error) {
473
+ console.log("Error registering assets:", error);
474
+ /*
475
+ El archivo SÍ está en S3; lo que falló es la fila. Se marca en el activo
476
+ y no en un modal porque el usuario puede estar soltando más archivos:
477
+ interrumpirlo con un diálogo por cada fallo sería peor que dejarlo
478
+ señalado en su renglón, donde además se ve cuál falló.
479
+ */
480
+ listos.forEach((asset) => dispatch({
481
+ type: "PATCH_ASSET",
482
+ payload: {
483
+ key: assetKey(asset),
484
+ changes: {
485
+ status: "error",
486
+ error: "Se subió el archivo pero no se pudo guardar. Usa el botón de guardar.",
487
+ },
488
+ },
489
+ }));
490
+ }
491
+ };
492
+
493
+ /*
494
+ Guarda los activos digitales: registra los nuevos, manda los cambios y pide
495
+ las bajas.
496
+
497
+ Va aparte de `updateImages` y no dentro, aunque las dos bandejas vivan en la
498
+ misma pestaña. Un activo no pasa por ninguna de las reglas de una imagen —ni
499
+ tipo de toma, ni obligatoriedad, ni cupo por cadena— y el backend los escribe
500
+ en otra tabla. Mezclarlos obligaría a partir en dos cada validación de
501
+ `updateImages`, que ya es la función más delicada de este archivo.
502
+
503
+ Igual que con las imágenes, aquí NO viaja ningún binario: el archivo ya está
504
+ en S3 y esto solo registra metadatos. Mandar el archivo en el cuerpo devuelve
505
+ 413.
506
+ */
507
+ const saveAssets = async (token) => {
508
+ const payload = buildSavePayload(selectAssetsToSave(state));
509
+ const { newAssets, updateAssets, deleteAssets } = payload;
510
+
511
+ if (!newAssets.length && !updateAssets.length && !deleteAssets.length) {
512
+ return false;
513
+ }
514
+
515
+ /*
516
+ Un activo nuevo sin `key` es un archivo que todavía no llegó a S3.
517
+ Registrarlo crearía una fila apuntando a un objeto que no existe: la
518
+ bandeja lo mostraría y al descargarlo daría 404, sin forma de saber por qué.
519
+ Mejor no guardar nada y decirlo.
520
+ */
521
+ const sinSubir = newAssets.filter((asset) => !asset.key);
522
+ if (sinSubir.length > 0) {
523
+ dispatch({
524
+ type: "SET_MODAL",
525
+ payload: {
526
+ show: true,
527
+ title: "Faltan archivos por subir",
528
+ message: `${sinSubir.length} activo(s) todavía no se han subido. Espera a que termine la carga e intenta de nuevo.`,
529
+ image: warningIcon,
530
+ },
531
+ });
532
+ return false;
533
+ }
534
+
535
+ dispatch({ type: "SET_SAVING", payload: true });
536
+
537
+ // Sin `version`: un activo general es el mismo para todas las versiones del
538
+ // producto, así que ni la fila ni la clave de S3 la llevan.
539
+ const data = {
540
+ articleId: state.product?.id_article,
541
+ newAssets,
542
+ updateAssets,
543
+ deleteAssets,
544
+ };
545
+
546
+ if (state.product?.orderId || state.product?.id_order) {
547
+ data.orderId = state.product?.orderId || state.product?.id_order;
548
+ }
549
+
550
+ try {
551
+ const res = await axios.put(
552
+ process.env.REACT_APP_ARTICLE_DATA_ENDPOINT,
553
+ data,
554
+ {
555
+ params: { asset: true },
556
+ headers: { Authorization: token },
557
+ },
558
+ );
559
+
560
+ if (res.data.statusCode === 200) {
561
+ // Se olvida qué cambió y qué se borró: lo que hay en pantalla ya es lo
562
+ // que hay en la base. Sin esto, el siguiente guardado reenviaría todo.
563
+ dispatch({ type: "CLEAR_ASSETS_TRACKING" });
564
+ dispatch({
565
+ type: "SET_MODAL",
566
+ payload: {
567
+ show: true,
568
+ title: "",
569
+ message: "Activos digitales guardados con éxito",
570
+ image: successIcon,
571
+ },
572
+ });
573
+ /*
574
+ Devuelve `true` para que quien llama recargue.
575
+
576
+ Es imprescindible, no cortesía: los activos recién creados siguen en
577
+ memoria con su id temporal y sin el id de la base. Sin recargar, el
578
+ siguiente guardado los volvería a mandar como nuevos y se duplicarían.
579
+ Es exactamente el problema que ya tuvieron las imágenes.
580
+ */
581
+ return true;
582
+ }
583
+
584
+ return false;
585
+ } catch (error) {
586
+ console.log("Error saving assets:", error);
587
+ dispatch({
588
+ type: "SET_ERRORS",
589
+ payload: [...state.errors, error],
590
+ });
591
+ dispatch({
592
+ type: "SET_MODAL",
593
+ payload: {
594
+ show: true,
595
+ title: "Error",
596
+ message: "Hubo un error al guardar los activos digitales",
597
+ image: errorIcon,
598
+ },
599
+ });
600
+ return false;
601
+ } finally {
602
+ dispatch({ type: "SET_SAVING", payload: false });
603
+ }
604
+ };
605
+
407
606
  const createComment = async (body, token) => {
408
607
  const concept = getConceptByTab(state.active_tab);
409
608
  const data = {
@@ -427,6 +626,8 @@ export const ProviderProductEditionProvider = ({ children }) => {
427
626
  saveDescriptions,
428
627
  updateImages,
429
628
  deleteImages,
629
+ saveAssets,
630
+ registerUploadedAssets,
430
631
  handleOnDownloadImages,
431
632
  createComment,
432
633
  };
@@ -18,6 +18,11 @@ import {
18
18
  activeStateInitialState,
19
19
  activeStateReducer,
20
20
  } from "./reducers/active-state";
21
+ import { ASSET_ACTIONS } from "../../../../global-files/digital-assets/assets.actions";
22
+ import {
23
+ assetsInitialState,
24
+ assetsReducer,
25
+ } from "../../../../global-files/digital-assets/assets.reducer";
21
26
 
22
27
  // Initial state
23
28
  export const initialState = {
@@ -25,6 +30,7 @@ export const initialState = {
25
30
  ...productInitialState,
26
31
  ...inputsInitialState,
27
32
  ...activeStateInitialState,
33
+ ...assetsInitialState,
28
34
  };
29
35
 
30
36
  // Action types
@@ -33,6 +39,7 @@ export const ACTIONS = {
33
39
  ...PRODUCT_ACTIONS,
34
40
  ...INPUTS_ACTIONS,
35
41
  ...ACTIVE_STATE_ACTIONS,
42
+ ...ASSET_ACTIONS,
36
43
  };
37
44
 
38
45
  // Reducer function
@@ -58,5 +65,10 @@ export const productEditionReducer = (state, action) => {
58
65
  return activeStateResult;
59
66
  }
60
67
 
68
+ const assetsResult = assetsReducer(state, action);
69
+ if (assetsResult !== null) {
70
+ return assetsResult;
71
+ }
72
+
61
73
  return state;
62
74
  };