contentoh-components-library 21.6.55 → 21.6.57

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 (112) 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/AsignationOption/index.js +1 -0
  8. package/dist/components/atoms/AsignationOption/styles.js +1 -1
  9. package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +118 -0
  10. package/dist/components/atoms/AssetPreview/index.js +150 -0
  11. package/dist/components/atoms/AssetPreview/styles.js +28 -0
  12. package/dist/components/atoms/IconFile/index.js +85 -2
  13. package/dist/components/atoms/IconFile/styles.js +1 -1
  14. package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
  15. package/dist/components/atoms/ProductImage/index.js +100 -27
  16. package/dist/components/atoms/ProductImage/styles.js +3 -1
  17. package/dist/components/molecules/EditionActiveImage/index.js +4 -2
  18. package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
  19. package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
  20. package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
  21. package/dist/components/molecules/GalleryElement/index.js +13 -4
  22. package/dist/components/molecules/GalleryHeader/index.js +96 -24
  23. package/dist/components/molecules/GalleryHeader/styles.js +4 -1
  24. package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
  25. package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
  26. package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
  27. package/dist/components/molecules/ImageSelector/index.js +1 -0
  28. package/dist/components/molecules/RetailerSelector/RetailerSelector.stories.js +23 -2
  29. package/dist/components/molecules/RetailerSelector/index.js +7 -2
  30. package/dist/components/molecules/RetailerSelectorv2/index.js +6 -2
  31. package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +235 -0
  32. package/dist/components/organisms/DigitalAssetsTray/index.js +219 -0
  33. package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
  34. package/dist/components/organisms/ImagePreviewer/index.js +1 -0
  35. package/dist/components/organisms/ProductImageModal/index.js +3 -2
  36. package/dist/components/pages/CreatorsProductEdition/CreatorsProductEditionView.js +905 -0
  37. package/dist/components/pages/CreatorsProductEdition/constants.js +19 -0
  38. package/dist/components/pages/CreatorsProductEdition/hooks/useCreatorsProductEdition.js +2967 -0
  39. package/dist/components/pages/CreatorsProductEdition/index.js +33 -0
  40. package/dist/components/pages/CreatorsProductEdition/mocks/api.fixtures.js +537 -0
  41. package/dist/components/pages/CreatorsProductEdition/mocks/mockApi.js +251 -0
  42. package/dist/components/pages/CreatorsProductEdition/sections/CommentBox.js +311 -0
  43. package/dist/components/pages/CreatorsProductEdition/sections/ImagePanel.js +421 -0
  44. package/dist/components/pages/CreatorsProductEdition/sections/ImagesSection.js +473 -0
  45. package/dist/components/pages/CreatorsProductEdition/sections/ProductHeader.js +379 -0
  46. package/dist/components/pages/CreatorsProductEdition/sections/ProductMedia.js +138 -0
  47. package/dist/components/pages/CreatorsProductEdition/sections/RetailerMenu.js +162 -0
  48. package/dist/components/pages/CreatorsProductEdition/sections/ServiceActions.js +542 -0
  49. package/dist/components/pages/CreatorsProductEdition/sections/StatusChip.js +50 -0
  50. package/dist/components/pages/CreatorsProductEdition/sections/TabsBar.js +247 -0
  51. package/dist/components/pages/CreatorsProductEdition/sections/VariantsPanel.js +389 -0
  52. package/dist/components/pages/CreatorsProductEdition/sections/tokens.js +90 -0
  53. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEdition.stories.js +168 -0
  54. package/dist/components/pages/CreatorsProductEdition/stories/CreatorsProductEditionAPI.stories.js +74 -0
  55. package/dist/components/pages/CreatorsProductEdition/styles.js +46 -0
  56. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
  57. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
  58. package/dist/components/pages/RetailerProductEdition/index.js +606 -425
  59. package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  60. package/dist/global-files/data.js +16 -3
  61. package/dist/global-files/digital-assets/assetTypes.js +517 -0
  62. package/dist/global-files/digital-assets/assets.actions.js +64 -0
  63. package/dist/global-files/digital-assets/assets.adapter.js +304 -0
  64. package/dist/global-files/digital-assets/assets.mock.js +136 -0
  65. package/dist/global-files/digital-assets/assets.reducer.js +220 -0
  66. package/dist/global-files/digital-assets/uploadAssets.js +202 -0
  67. package/dist/global-files/digital-assets/useDigitalAssets.js +535 -0
  68. package/dist/index.js +142 -90
  69. package/docs/activos-digitales.md +273 -0
  70. package/package.json +1 -1
  71. package/src/components/atoms/AsignationOption/index.js +1 -1
  72. package/src/components/atoms/AsignationOption/styles.js +9 -0
  73. package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
  74. package/src/components/atoms/AssetPreview/index.js +119 -0
  75. package/src/components/atoms/AssetPreview/styles.js +190 -0
  76. package/src/components/atoms/IconFile/index.js +28 -1
  77. package/src/components/atoms/IconFile/styles.js +8 -0
  78. package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
  79. package/src/components/atoms/ProductImage/index.js +95 -20
  80. package/src/components/atoms/ProductImage/styles.js +92 -7
  81. package/src/components/molecules/EditionActiveImage/index.js +2 -2
  82. package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
  83. package/src/components/molecules/GalleryAssetElement/index.js +162 -0
  84. package/src/components/molecules/GalleryAssetElement/styles.js +163 -0
  85. package/src/components/molecules/GalleryElement/index.js +9 -0
  86. package/src/components/molecules/GalleryHeader/index.js +65 -13
  87. package/src/components/molecules/GalleryHeader/styles.js +2 -1
  88. package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
  89. package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
  90. package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
  91. package/src/components/molecules/ImageSelector/index.js +1 -0
  92. package/src/components/molecules/RetailerSelector/RetailerSelector.stories.js +15 -1
  93. package/src/components/molecules/RetailerSelector/index.js +8 -1
  94. package/src/components/molecules/RetailerSelectorv2/index.js +7 -1
  95. package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +164 -0
  96. package/src/components/organisms/DigitalAssetsTray/index.js +193 -0
  97. package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
  98. package/src/components/organisms/ImagePreviewer/index.js +1 -0
  99. package/src/components/organisms/ProductImageModal/index.js +1 -0
  100. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
  101. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
  102. package/src/components/pages/RetailerProductEdition/index.js +356 -139
  103. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
  104. package/src/global-files/data.js +17 -2
  105. package/src/global-files/digital-assets/assetTypes.js +451 -0
  106. package/src/global-files/digital-assets/assets.actions.js +50 -0
  107. package/src/global-files/digital-assets/assets.adapter.js +229 -0
  108. package/src/global-files/digital-assets/assets.mock.js +122 -0
  109. package/src/global-files/digital-assets/assets.reducer.js +197 -0
  110. package/src/global-files/digital-assets/uploadAssets.js +124 -0
  111. package/src/global-files/digital-assets/useDigitalAssets.js +340 -0
  112. package/src/index.js +4 -0
@@ -0,0 +1,163 @@
1
+ import styled from "styled-components";
2
+ import { FontFamily, GlobalColors } from "../../../global-files/variables";
3
+
4
+ /*
5
+ Calcado de `GalleryElement/styles.js` para que imágenes y activos se lean como
6
+ una sola tabla. Cambia solo la rejilla de la fila —cuatro columnas en vez de
7
+ cinco, porque un activo no tiene leyenda— y lo propio de un archivo que se
8
+ está subiendo: barra de progreso, mensaje de error y botón de quitar.
9
+ */
10
+ export const Container = styled.div`
11
+ position: relative;
12
+
13
+ &.grid-gallery {
14
+ width: 179px;
15
+ border-radius: 3px;
16
+ overflow: hidden;
17
+
18
+ .buttons-container {
19
+ position: absolute;
20
+ top: 5px;
21
+ left: 5px;
22
+ z-index: 1;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ }
27
+
28
+ .asset-container {
29
+ border: 1px solid #f0eef2;
30
+
31
+ .information-container {
32
+ background-color: ${GlobalColors.s2};
33
+ display: flex;
34
+ justify-content: space-between;
35
+ padding: 0 2px;
36
+
37
+ p {
38
+ font-family: ${FontFamily.Lato};
39
+ color: ${GlobalColors.s4};
40
+ font-size: 11px;
41
+ line-height: 17px;
42
+ overflow: hidden;
43
+ text-overflow: ellipsis;
44
+ white-space: nowrap;
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ &.row-gallery {
51
+ width: 100%;
52
+ display: grid;
53
+ grid-template-columns: 0.05fr 1fr;
54
+ align-items: center;
55
+ padding-right: 10px;
56
+
57
+ .buttons-container {
58
+ position: relative;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ }
63
+
64
+ .asset-container {
65
+ display: grid;
66
+ grid-template-columns: auto 1fr;
67
+ align-items: center;
68
+ flex: 1;
69
+
70
+ .information-container {
71
+ display: grid;
72
+ grid-template-columns: repeat(4, 1fr);
73
+ align-items: center;
74
+
75
+ p {
76
+ font-family: ${FontFamily.Lato};
77
+ color: ${GlobalColors.s4};
78
+ font-size: 14px;
79
+ line-height: 17px;
80
+ text-align: center;
81
+ overflow: hidden;
82
+ text-overflow: ellipsis;
83
+ white-space: nowrap;
84
+ }
85
+
86
+ & > * {
87
+ flex: 1;
88
+ padding: 5px;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ /* El nombre es el sitio donde uno intenta hacer clic para abrir un archivo. */
95
+ .asset-link {
96
+ cursor: pointer;
97
+ text-decoration: underline;
98
+ text-underline-offset: 2px;
99
+
100
+ &:hover {
101
+ color: ${GlobalColors.color_purple};
102
+ }
103
+ }
104
+
105
+ &.asset-error {
106
+ border: 1px solid ${GlobalColors.secondary_magenta};
107
+ border-radius: 3px;
108
+ }
109
+
110
+ /* Progreso de subida. Ocupa el borde inferior de la fila para no empujar el
111
+ resto del contenido mientras aparece y desaparece. */
112
+ .asset-progress {
113
+ position: absolute;
114
+ left: 0;
115
+ right: 0;
116
+ bottom: 0;
117
+ height: 3px;
118
+ background: ${GlobalColors.s3};
119
+ border-radius: 2px;
120
+ overflow: hidden;
121
+ }
122
+
123
+ .asset-progress-fill {
124
+ height: 100%;
125
+ background: ${GlobalColors.color_purple};
126
+ transition: width 0.2s ease-out;
127
+ }
128
+
129
+ /* En la fila, que es una rejilla de dos columnas, el mensaje ocupa todo el
130
+ ancho. Si no, cae en la columna del checkbox, la ensancha hasta su palabra
131
+ más larga y descuadra la fila contra las demás. */
132
+ .asset-error-message {
133
+ grid-column: 1 / -1;
134
+ margin: 2px 0 0;
135
+ padding: 0 8px 6px;
136
+ font-family: ${FontFamily.Lato};
137
+ font-size: 11px;
138
+ line-height: 15px;
139
+ color: ${GlobalColors.secondary_magenta};
140
+ }
141
+
142
+ .asset-remove {
143
+ position: absolute;
144
+ top: 4px;
145
+ right: 6px;
146
+ width: 18px;
147
+ height: 18px;
148
+ display: grid;
149
+ place-items: center;
150
+ padding: 0;
151
+ border: none;
152
+ border-radius: 50%;
153
+ background: ${GlobalColors.s2};
154
+ color: ${GlobalColors.s5};
155
+ font-size: 14px;
156
+ line-height: 1;
157
+ cursor: pointer;
158
+
159
+ &:hover {
160
+ background: ${GlobalColors.s3};
161
+ }
162
+ }
163
+ `;
@@ -25,6 +25,7 @@ export const GalleryElement = ({
25
25
  auditImages,
26
26
  compare,
27
27
  updateApprovedInputsImages,
28
+ onSelect,
28
29
  }) => {
29
30
  const [showValidationPanel, setShowValidationPanel] = useState(false);
30
31
  const [showDivStates, setShowDivStates] = useState({});
@@ -93,7 +94,15 @@ export const GalleryElement = ({
93
94
  <ProductImage
94
95
  img={compare ? auditImages?.values[number]?.src : image?.src}
95
96
  altText={image?.altText}
97
+ // Sin `ext`, un video se pinta como <img> rota: la URL guardada no
98
+ // conserva la extensión del archivo.
99
+ ext={compare ? auditImages?.values[number]?.ext : image?.ext}
96
100
  imageType={`catalogue-image-${gridLayout ? "big" : "small"}-size`}
101
+ // La miniatura de un video ya no lleva controles, así que el clic es
102
+ // la única forma de llegar al visor grande. Se aplica también a las
103
+ // imágenes: dos recuadros idénticos donde solo uno responde al clic
104
+ // sería peor que no responder ninguno.
105
+ onClick={onSelect}
97
106
  />
98
107
  <div className="information-container">
99
108
  <p>{image?.name}</p>
@@ -1,28 +1,80 @@
1
1
  import { Container } from "./styles";
2
2
  import { CheckBox } from "../../atoms/CheckBox";
3
3
 
4
- export const GalleryHeader = ({ setCheckAll, setSelectedImages, shotThd }) => {
4
+ /*
5
+ Las columnas de la bandeja dependen de qué se está listando: un PDF no tiene
6
+ "tipo de toma" ni alto por ancho, y una foto no tiene "tipo de activo".
7
+ Estaban escritas a mano en el marcado, así que la única forma de cambiarlas
8
+ era duplicar el componente.
9
+
10
+ El preset `image` reproduce exactamente las columnas y clases de antes, para
11
+ que la bandeja de imágenes se vea idéntica sin que nadie tenga que pasar nada.
12
+ */
13
+ export const GALLERY_COLUMNS = {
14
+ image: {
15
+ template: "0.1fr 1fr 1fr 1fr 1fr 1fr",
16
+ columns: [
17
+ { key: "preview", label: "Imagen", className: "image-header" },
18
+ { key: "name", label: "Nombre", className: "image-name" },
19
+ { key: "size", label: "Tamaño", className: "image-size" },
20
+ { key: "format", label: "Formato", className: "image-format" },
21
+ {
22
+ key: "legend",
23
+ label: "¿Tiene leyenda?",
24
+ className: "image-description",
25
+ },
26
+ { key: "shotType", label: "Tipo de toma", className: "shoot-type" },
27
+ ],
28
+ },
29
+ asset: {
30
+ template: "0.1fr 1fr 1fr 1fr 1fr",
31
+ columns: [
32
+ { key: "preview", label: "Activo", className: "asset-header" },
33
+ { key: "name", label: "Nombre", className: "asset-name" },
34
+ { key: "weight", label: "Peso", className: "asset-weight" },
35
+ { key: "format", label: "Formato", className: "asset-format" },
36
+ { key: "assetType", label: "Tipo de activo", className: "asset-type" },
37
+ ],
38
+ },
39
+ };
40
+
41
+ /**
42
+ * Encabezado de la bandeja, con su casilla de "seleccionar todo".
43
+ *
44
+ * `checkAllId` e `itemSelector` se pueden cambiar porque imágenes y activos
45
+ * conviven en la misma pestaña: si las dos listas compartieran el id de la
46
+ * casilla global, marcar todas las imágenes marcaría también los activos.
47
+ */
48
+ export const GalleryHeader = ({
49
+ setCheckAll,
50
+ setSelectedImages,
51
+ shotThd,
52
+ variant = "image",
53
+ columns,
54
+ checkAllId = "check-all-images",
55
+ itemSelector = "[id^='gallery-element-']",
56
+ }) => {
57
+ const preset = GALLERY_COLUMNS[variant] ?? GALLERY_COLUMNS.image;
58
+ const visibleColumns = columns ?? preset.columns;
59
+
5
60
  const checkAll = (e) => {
6
- const checkboxList = document.querySelectorAll("[id^='gallery-element-']");
61
+ const checkboxList = document.querySelectorAll(itemSelector);
7
62
  checkboxList.forEach((chk) => (chk.checked = e.target.checked));
8
63
  setCheckAll(e.target.checked);
9
64
  if (!e.target.checked) setSelectedImages([]);
10
65
  };
66
+
11
67
  return (
12
- <Container>
68
+ <Container template={preset.template}>
13
69
  <div className="checkbox">
14
- <CheckBox id="check-all-images" onChange={(e) => checkAll(e)} />
70
+ <CheckBox id={checkAllId} onChange={(e) => checkAll(e)} />
15
71
  </div>
16
72
  <div className="information">
17
- <p className="image-header">Imagen</p>
18
- <p className="image-name">Nombre</p>
19
- <p className="image-size">Tamaño</p>
20
- <p className="image-format">Formato</p>
21
- <p className="image-description">¿Tiene leyenda?</p>
22
- <p className="shoot-type">Tipo de toma</p>
23
- {/* <p className="">Tipo de imagen</p> */}
24
- {/* <p className="">Tipo de empaque</p> */}
25
- {/* <p className="">Estatus</p> */}
73
+ {visibleColumns.map((column) => (
74
+ <p key={column.key} className={column.className}>
75
+ {column.label}
76
+ </p>
77
+ ))}
26
78
  </div>
27
79
  </Container>
28
80
  );
@@ -15,7 +15,8 @@ export const Container = styled.div`
15
15
  .information{
16
16
  width: 90%;
17
17
  display: grid;
18
- grid-template-columns: 0.1fr 1fr 1fr 1fr 1fr 1fr;
18
+ /* Lo define el preset de columnas; el valor por defecto es el de imágenes. */
19
+ grid-template-columns: ${({ template }) => template || "0.1fr 1fr 1fr 1fr 1fr 1fr"};
19
20
  }
20
21
  .image-header{
21
22
 
@@ -0,0 +1,32 @@
1
+ import { useState } from "react";
2
+ import { GalleryTrayTabs, TRAYS } from "./index";
3
+
4
+ export default {
5
+ title: "Components/molecules/GalleryTrayTabs",
6
+ component: GalleryTrayTabs,
7
+ };
8
+
9
+ export const Interactivo = () => {
10
+ const [tray, setTray] = useState(TRAYS.IMAGES);
11
+ return (
12
+ <GalleryTrayTabs
13
+ activeTray={tray}
14
+ setActiveTray={setTray}
15
+ imagesCount={8}
16
+ assetsCount={3}
17
+ />
18
+ );
19
+ };
20
+
21
+ /** Estado real hasta que exista el backend: la bandeja de activos en cero. */
22
+ export const SinActivos = () => {
23
+ const [tray, setTray] = useState(TRAYS.IMAGES);
24
+ return (
25
+ <GalleryTrayTabs
26
+ activeTray={tray}
27
+ setActiveTray={setTray}
28
+ imagesCount={8}
29
+ assetsCount={0}
30
+ />
31
+ );
32
+ };
@@ -0,0 +1,41 @@
1
+ import { Container } from "./styles";
2
+
3
+ export const TRAYS = {
4
+ IMAGES: "images",
5
+ ASSETS: "assets",
6
+ };
7
+
8
+ /**
9
+ * Selector entre las dos bandejas de la pestaña Imágenes.
10
+ *
11
+ * Los activos viven aquí y no en una pestaña propia porque es lo que pide la
12
+ * tarea —"dentro de las bandejas de imágenes"— y porque así comparten
13
+ * selección, descarga y permisos con las fotos en vez de duplicarlos.
14
+ */
15
+ export const GalleryTrayTabs = ({
16
+ activeTray = TRAYS.IMAGES,
17
+ setActiveTray,
18
+ imagesCount = 0,
19
+ assetsCount = 0,
20
+ }) => {
21
+ const trays = [
22
+ { id: TRAYS.IMAGES, label: "Imágenes", count: imagesCount },
23
+ { id: TRAYS.ASSETS, label: "Activos digitales", count: assetsCount },
24
+ ];
25
+
26
+ return (
27
+ <Container>
28
+ {trays.map((tray) => (
29
+ <button
30
+ key={tray.id}
31
+ type="button"
32
+ className={tray.id === activeTray ? "tray-tab active" : "tray-tab"}
33
+ onClick={() => setActiveTray?.(tray.id)}
34
+ >
35
+ {tray.label}
36
+ <span className="tray-count">{tray.count}</span>
37
+ </button>
38
+ ))}
39
+ </Container>
40
+ );
41
+ };
@@ -0,0 +1,52 @@
1
+ import styled from "styled-components";
2
+ import { FontFamily, GlobalColors } from "../../../global-files/variables";
3
+
4
+ export const Container = styled.div`
5
+ display: flex;
6
+ align-items: center;
7
+ gap: 4px;
8
+ padding: 8px 0 0;
9
+
10
+ .tray-tab {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 6px;
14
+ padding: 6px 14px;
15
+ border: none;
16
+ border-bottom: 2px solid transparent;
17
+ background: none;
18
+ cursor: pointer;
19
+ font-family: ${FontFamily.AvenirNext};
20
+ font-size: 14px;
21
+ line-height: 21px;
22
+ letter-spacing: -0.015em;
23
+ color: ${GlobalColors.s4};
24
+
25
+ &:hover {
26
+ color: ${GlobalColors.s5};
27
+ }
28
+
29
+ &.active {
30
+ color: ${GlobalColors.s5};
31
+ font-weight: 600;
32
+ border-bottom-color: ${GlobalColors.color_purple};
33
+ }
34
+ }
35
+
36
+ .tray-count {
37
+ min-width: 20px;
38
+ padding: 0 6px;
39
+ border-radius: 10px;
40
+ background: ${GlobalColors.s2};
41
+ color: ${GlobalColors.s5};
42
+ font-family: ${FontFamily.Lato};
43
+ font-size: 11px;
44
+ line-height: 18px;
45
+ text-align: center;
46
+ }
47
+
48
+ .active .tray-count {
49
+ background: ${GlobalColors.color_purple};
50
+ color: ${GlobalColors.white};
51
+ }
52
+ `;
@@ -13,6 +13,7 @@ export const ImageSelector = ({
13
13
  key={index}
14
14
  img={image.src}
15
15
  altText={image.altText}
16
+ ext={image.ext}
16
17
  imageType={modalSelector ? "catalogue-modal-image" : "carousel-image"}
17
18
  onClick={() => setActiveImage(index)}
18
19
  />
@@ -1,3 +1,4 @@
1
+ import { useState } from "react";
1
2
  import { RetailerSelector } from "./index";
2
3
 
3
4
  export default {
@@ -5,7 +6,20 @@ export default {
5
6
  component: RetailerSelector,
6
7
  };
7
8
 
8
- const Template = (args) => <RetailerSelector {...args} />;
9
+ // El panel se abre hacia la izquierda del avatar: sin margen quedaría fuera de
10
+ // la vista. La cadena activa vive en estado para que elegir otra se vea.
11
+ const Template = (args) => {
12
+ const [activeRetailer, setActiveRetailer] = useState(args.retailers[1]);
13
+ return (
14
+ <div style={{ display: "flex", justifyContent: "flex-end", width: 420 }}>
15
+ <RetailerSelector
16
+ {...args}
17
+ activeRetailer={activeRetailer}
18
+ setActiveRetailer={setActiveRetailer}
19
+ />
20
+ </div>
21
+ );
22
+ };
9
23
 
10
24
  export const RetailerSelectorDefault = Template.bind({});
11
25
  RetailerSelectorDefault.args = {
@@ -89,7 +89,14 @@ export const RetailerSelector = ({
89
89
  );
90
90
  setRetailersPanel(false);
91
91
  }}
92
- active={retailer.id === activeRetailer.id}
92
+ // El id llega como `id` o como `id_retailer` según el origen
93
+ // (en RetailerProductEdition, `categoryRetailerInOrder` usa
94
+ // `id_retailer`); comparar solo `id` nunca marcaba la activa.
95
+ active={
96
+ (retailer?.id ?? retailer?.id_retailer) != null &&
97
+ (retailer?.id ?? retailer?.id_retailer) ===
98
+ (activeRetailer?.id ?? activeRetailer?.id_retailer)
99
+ }
93
100
  showStatus={showStatus}
94
101
  />
95
102
  ))}
@@ -69,7 +69,13 @@ export const RetailerSelectorv2 = ({
69
69
  );
70
70
  setRetailersPanel(false);
71
71
  }}
72
- active={retailer.id === activeRetailer.id}
72
+ // El id llega como `id` o como `id_retailer` según el origen;
73
+ // comparar solo `id` nunca marcaba la cadena activa.
74
+ active={
75
+ (retailer?.id ?? retailer?.id_retailer) != null &&
76
+ (retailer?.id ?? retailer?.id_retailer) ===
77
+ (activeRetailer?.id ?? activeRetailer?.id_retailer)
78
+ }
73
79
  showStatus={showStatus}
74
80
  />
75
81
  ))}
@@ -0,0 +1,164 @@
1
+ import { useReducer, useState } from "react";
2
+
3
+ import { DigitalAssetsTray } from "./index";
4
+ import { useDigitalAssets } from "../../../global-files/digital-assets/useDigitalAssets";
5
+ import {
6
+ assetsInitialState,
7
+ assetsReducer,
8
+ countUnsavedAssets,
9
+ hasUnsavedAssets,
10
+ selectAssetsToSave,
11
+ } from "../../../global-files/digital-assets/assets.reducer";
12
+ import {
13
+ ASSET_STATUS,
14
+ buildSavePayload,
15
+ fromFile,
16
+ } from "../../../global-files/digital-assets/assets.adapter";
17
+ import { ASSET_ACTIONS } from "../../../global-files/digital-assets/assets.actions";
18
+ import { backendAssetsMock } from "../../../global-files/digital-assets/assets.mock";
19
+
20
+ export default {
21
+ title: "Components/organisms/DigitalAssetsTray",
22
+ component: DigitalAssetsTray,
23
+ };
24
+
25
+ /*
26
+ Las historias usan EL MISMO reducer que la ficha, no un estado de juguete.
27
+ Así lo que se ve aquí es el comportamiento real: qué queda marcado para
28
+ guardar, qué se anota para borrar y qué payload saldría hacia el backend.
29
+ */
30
+ const Demo = ({
31
+ initialAssets = [],
32
+ localAssets = [],
33
+ gridLayout = false,
34
+ readOnly = false,
35
+ showPayload = false,
36
+ }) => {
37
+ const [state, dispatch] = useReducer(assetsReducer, assetsInitialState);
38
+ const [notices, setNotices] = useState([]);
39
+
40
+ const tray = useDigitalAssets({
41
+ assets: state.assets_values,
42
+ dispatch,
43
+ notices,
44
+ setNotices,
45
+ });
46
+
47
+ // Carga inicial simulando la respuesta del backend, una sola vez.
48
+ const [loaded, setLoaded] = useState(false);
49
+ if (!loaded && (initialAssets.length || localAssets.length)) {
50
+ if (initialAssets.length) tray.replaceAll(initialAssets);
51
+ // Archivos locales ya en un estado de subida (subiendo, con error...), para
52
+ // ver cómo se pintan sin tener que provocar el fallo contra el backend.
53
+ if (localAssets.length) {
54
+ dispatch({
55
+ type: ASSET_ACTIONS.ADD_ASSETS,
56
+ payload: localAssets.map(({ file, ...estado }) => ({ ...fromFile(file), ...estado })),
57
+ });
58
+ }
59
+ setLoaded(true);
60
+ }
61
+
62
+ const payload = buildSavePayload(selectAssetsToSave(state));
63
+
64
+ return (
65
+ <div style={{ width: 960 }}>
66
+ <DigitalAssetsTray
67
+ {...tray}
68
+ unsaved={countUnsavedAssets(state)}
69
+ gridLayout={gridLayout}
70
+ readOnly={readOnly}
71
+ selectedAssets={state.selected_assets}
72
+ setSelectedAssets={(assets) => dispatch({ type: "SET_SELECTED_ASSETS", payload: assets })}
73
+ setCheckAll={(checked) => dispatch({ type: "TOGGLE_CHECK_ALL_ASSETS", payload: checked })}
74
+ />
75
+
76
+ <p style={{ fontFamily: "Lato", fontSize: 12 }}>
77
+ Seleccionados: {state.selected_assets.length} · Sin subir: {tray.pending.length} ·
78
+ Marcados para actualizar: {state.updated_assets.length} · Para borrar:{" "}
79
+ {state.deleted_assets.length} · ¿Hay cambios sin guardar?{" "}
80
+ {hasUnsavedAssets(state) ? "sí" : "no"}
81
+ </p>
82
+
83
+ {showPayload && (
84
+ <pre
85
+ style={{
86
+ fontSize: 11,
87
+ background: "#f7f7fc",
88
+ padding: 12,
89
+ borderRadius: 6,
90
+ maxHeight: 280,
91
+ overflow: "auto",
92
+ }}
93
+ >
94
+ {JSON.stringify(payload, null, 2)}
95
+ </pre>
96
+ )}
97
+ </div>
98
+ );
99
+ };
100
+
101
+ /**
102
+ * Estado real de hoy: sin backend, la bandeja arranca vacía. Arrastrar un mp4,
103
+ * un gif, un PDF, un xlsx o un psd lo valida y lo agrega; cualquier otra cosa se
104
+ * rechaza con su motivo. Nada se sube.
105
+ */
106
+ export const Vacia = () => <Demo />;
107
+
108
+ export const ConActivos = () => <Demo initialAssets={backendAssetsMock} />;
109
+
110
+ export const Cuadricula = () => <Demo initialAssets={backendAssetsMock} gridLayout />;
111
+
112
+ export const SoloLectura = () => <Demo initialAssets={backendAssetsMock} readOnly />;
113
+
114
+ /**
115
+ * Lo que se le enviaría al backend, actualizándose en vivo. Cambia el tipo de un
116
+ * activo y aparece en `updateAssets`; bórralo y su id pasa a `deleteAssets`;
117
+ * suelta un archivo nuevo y aparece en `newAssets` sin `key`, porque todavía no
118
+ * se ha subido a S3.
119
+ */
120
+ export const PayloadDeGuardado = () => <Demo initialAssets={backendAssetsMock} showPayload />;
121
+
122
+ const archivo = (name, type) => new File(["x"], name, { type });
123
+
124
+ /**
125
+ * Cómo se ve una subida que falla: el motivo aparece debajo del archivo y el
126
+ * resto sigue su curso. Los textos son los que producen de verdad
127
+ * `uploadAssets.js` (S3 y firma) y `presign-asset-upload` (rechazo del servidor).
128
+ */
129
+ export const ErroresDeSubida = () => (
130
+ <Demo
131
+ localAssets={[
132
+ {
133
+ file: archivo("video-campana.mp4", "video/mp4"),
134
+ status: ASSET_STATUS.UPLOADING,
135
+ progress: 45,
136
+ },
137
+ {
138
+ file: archivo("manual-instalacion.pdf", "application/pdf"),
139
+ status: ASSET_STATUS.ERROR,
140
+ error: "No se pudo subir el archivo. Avisa a soporte (permiso de almacenamiento).",
141
+ },
142
+ {
143
+ file: archivo("render-final.psd", ""),
144
+ status: ASSET_STATUS.ERROR,
145
+ error: "El archivo supera el tamaño permitido.",
146
+ },
147
+ {
148
+ file: archivo("tabla-tallas.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
149
+ status: ASSET_STATUS.ERROR,
150
+ error: "No se pudo conectar para subir el archivo. Revisa tu conexión e inténtalo de nuevo.",
151
+ },
152
+ {
153
+ file: archivo("foto-lifestyle.jpg", "image/jpeg"),
154
+ status: ASSET_STATUS.ERROR,
155
+ error: "El archivo pesa más del máximo permitido para imagen",
156
+ },
157
+ {
158
+ file: archivo("catalogo.zip", "application/zip"),
159
+ status: ASSET_STATUS.ERROR,
160
+ error: "No se pudo preparar la subida. Revisa tu conexión e inténtalo de nuevo.",
161
+ },
162
+ ]}
163
+ />
164
+ );