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
@@ -11,6 +11,8 @@ import {
11
11
  faFileAudio as IconFileAudio,
12
12
  faFileLines as IconFileDefault,
13
13
  faFileVideo as IconFileVideo,
14
+ faSwatchbook as IconFileDesign,
15
+ faFilm as IconFileMotion,
14
16
  } from "@fortawesome/free-solid-svg-icons";
15
17
  import React, { useRef } from "react";
16
18
  import { CustomIcon } from "../CustomIcon";
@@ -41,8 +43,30 @@ export const IconFile = (props) => {
41
43
  // file csv
42
44
  csv: { icon: IconFileCsv, type: "csv" },
43
45
  // file image
44
- svg: { icon: IconFileImg, type: "image" },
45
46
  gif: { icon: IconFileImg, type: "image" },
47
+ jpg: { icon: IconFileImg, type: "image" },
48
+ jpeg: { icon: IconFileImg, type: "image" },
49
+ png: { icon: IconFileImg, type: "image" },
50
+ webp: { icon: IconFileImg, type: "image" },
51
+
52
+ // archivos de diseño editables: no se pueden previsualizar en el navegador,
53
+ // así que el icono es lo único que distingue uno de otro en la bandeja.
54
+ psd: { icon: IconFileDesign, type: "design" },
55
+ psb: { icon: IconFileDesign, type: "design" },
56
+ ai: { icon: IconFileDesign, type: "design" },
57
+ eps: { icon: IconFileDesign, type: "design" },
58
+ indd: { icon: IconFileDesign, type: "design" },
59
+ idml: { icon: IconFileDesign, type: "design" },
60
+ sketch: { icon: IconFileDesign, type: "design" },
61
+ fig: { icon: IconFileDesign, type: "design" },
62
+ xd: { icon: IconFileDesign, type: "design" },
63
+ afphoto: { icon: IconFileDesign, type: "design" },
64
+ afdesign: { icon: IconFileDesign, type: "design" },
65
+ svg: { icon: IconFileDesign, type: "design" },
66
+
67
+ // proyectos de animación y edición de video
68
+ aep: { icon: IconFileMotion, type: "motion" },
69
+ prproj: { icon: IconFileMotion, type: "motion" },
46
70
  // file pdf
47
71
  pdf: { icon: IconFilePdf, type: "pdf" },
48
72
  // file power point
@@ -60,6 +84,7 @@ export const IconFile = (props) => {
60
84
  rar: { icon: IconFileZip, type: "zip" },
61
85
  zip: { icon: IconFileZip, type: "zip" },
62
86
  tar: { icon: IconFileZip, type: "zip" },
87
+ "7z": { icon: IconFileZip, type: "zip" },
63
88
 
64
89
  // file code
65
90
  xml: { icon: IconFileCode, type: "code" },
@@ -80,6 +105,8 @@ export const IconFile = (props) => {
80
105
  // file video
81
106
  avi: { icon: IconFileVideo, type: "video" },
82
107
  mp4: { icon: IconFileVideo, type: "video" },
108
+ mov: { icon: IconFileVideo, type: "video" },
109
+ webm: { icon: IconFileVideo, type: "video" },
83
110
  mpeg: { icon: IconFileVideo, type: "video" },
84
111
  mpg: { icon: IconFileVideo, type: "video" },
85
112
  wmv: { icon: IconFileVideo, type: "video" },
@@ -52,6 +52,14 @@ export const Container = styled.div`
52
52
  color: #1c778e;
53
53
  border-color: #1c778e;
54
54
  }
55
+ &.icon-design .icon {
56
+ color: #2a5cb8;
57
+ border-color: #2a5cb8;
58
+ }
59
+ &.icon-motion .icon {
60
+ color: #6d3fc0;
61
+ border-color: #6d3fc0;
62
+ }
55
63
  &.icon-video .icon {
56
64
  color: #a15bb1;
57
65
  border-color: #a15bb1;
@@ -26,3 +26,41 @@ ProductImageDefault.args = {
26
26
  imageType: "main-image",
27
27
  altText: "default image",
28
28
  };
29
+
30
+ /*
31
+ Las escalas de video, lado a lado. Las cuatro primeras son miniaturas: sin
32
+ controles, con su duración y un botón de play, y todo el recuadro clicable.
33
+ Las dos últimas son reproductores de verdad.
34
+ */
35
+ const VIDEO =
36
+ "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4";
37
+
38
+ const Escala = ({ imageType }) => (
39
+ <figure style={{ margin: 0 }}>
40
+ <ProductImage
41
+ img={VIDEO}
42
+ ext="mp4"
43
+ imageType={imageType}
44
+ altText="video de producto"
45
+ onClick={() => console.log("clic en", imageType)}
46
+ />
47
+ <figcaption style={{ fontFamily: "Lato", fontSize: 11, marginTop: 4 }}>
48
+ {imageType}
49
+ </figcaption>
50
+ </figure>
51
+ );
52
+
53
+ export const VideoMiniaturas = () => (
54
+ <div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
55
+ <Escala imageType="carousel-image" />
56
+ <Escala imageType="catalogue-image-small-size" />
57
+ <Escala imageType="catalogue-modal-image" />
58
+ <Escala imageType="catalogue-image-big-size" />
59
+ </div>
60
+ );
61
+
62
+ export const VideoReproductor = () => (
63
+ <div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
64
+ <Escala imageType="main-image" />
65
+ </div>
66
+ );
@@ -1,29 +1,104 @@
1
- import { Container } from "./styles";
1
+ import { useState } from "react";
2
2
  import ReactImageFallback from "react-image-fallback";
3
+
4
+ import { Container } from "./styles";
5
+ import {
6
+ ASSET_PREVIEW,
7
+ formatDuration,
8
+ resolvePreviewKind,
9
+ } from "../../../global-files/digital-assets/assetTypes";
3
10
  import defaultProductImage from "../../../assets/images/defaultImages/defaultProductImage.png";
4
11
 
5
- export const ProductImage = ({ img, imageType, altText, onClick}) => {
6
- // Expresión regular para validar que los últimos tres caracteres sean ".mp4"
7
- const regex = /\.mp4$/;
8
- if (regex.test(img)){
9
- return (
10
- <Container className={imageType}>
11
- <video controls width="100%" height="auto">
12
- <source src= {img} type="video/mp4" />
13
- </video>
14
- </Container>
15
- );
16
- }
17
- else{
12
+ /*
13
+ Tamaños en los que un video se muestra como miniatura y no como reproductor.
14
+
15
+ Los controles nativos del navegador necesitan alrededor de 150px de ancho
16
+ para ser usables; en un recuadro de 60 o de 85 se pintan encimados y no se
17
+ puede ni dar play. Peor aún, un <video> con controles se traga el clic, así
18
+ que un video en el carrusel no se podía ni seleccionar: `onClick` nunca
19
+ llegaba a correr.
20
+
21
+ En estos tamaños se muestra el primer fotograma con su duración y un botón de
22
+ play, y el recuadro completo lleva al visor grande, que es donde el video sí
23
+ se ve y se controla.
24
+ */
25
+ const THUMBNAIL_TYPES = [
26
+ "carousel-image",
27
+ "catalogue-image-small-size",
28
+ "catalogue-image-big-size",
29
+ "catalogue-modal-image",
30
+ ];
31
+
32
+ const PlayBadge = () => (
33
+ <span className="play-badge" aria-hidden="true">
34
+ <svg viewBox="0 0 24 24">
35
+ <circle cx="12" cy="12" r="11" />
36
+ <polygon points="9.5,7.5 17,12 9.5,16.5" />
37
+ </svg>
38
+ </span>
39
+ );
40
+
41
+ /**
42
+ * Imagen —o video— de producto.
43
+ *
44
+ * El tipo de archivo se decide por `ext`/`mime` cuando quien llama los conoce.
45
+ * Antes se comprobaba con `/\.mp4$/` sobre la URL, y nunca acertaba: la URL
46
+ * guardada apunta al redimensionador de CloudFront y no conserva la extensión.
47
+ * El regex queda solo como respaldo para los llamadores que aún mandan `img`
48
+ * a secas.
49
+ */
50
+ export const ProductImage = ({ img, imageType, altText, onClick, ext, mime, poster }) => {
51
+ // La duración no está en la base: se toma de los metadatos, que el navegador
52
+ // ya descarga por el `preload="metadata"`. No cuesta una petición extra.
53
+ const [duration, setDuration] = useState(null);
54
+
55
+ const kind =
56
+ ext || mime
57
+ ? resolvePreviewKind({ ext, mime })
58
+ : /\.mp4($|[?#])/i.test(img || "")
59
+ ? ASSET_PREVIEW.VIDEO
60
+ : ASSET_PREVIEW.IMAGE;
61
+
62
+ if (kind === ASSET_PREVIEW.VIDEO) {
63
+ const isThumbnail = THUMBNAIL_TYPES.includes(imageType);
64
+
18
65
  return (
19
- <Container className={imageType}>
20
- <ReactImageFallback
21
- src={img}
22
- fallbackImage={defaultProductImage}
23
- alt={altText}
24
- onClick={onClick}
66
+ <Container
67
+ className={`${imageType} ${isThumbnail ? "video-thumbnail" : "video-player"}`}
68
+ onClick={isThumbnail ? onClick : undefined}
69
+ >
70
+ <video
71
+ // `#t=0.5` hace que el navegador muestre el fotograma del medio
72
+ // segundo en lugar de un cuadro negro, sin descargar el archivo
73
+ // completo.
74
+ src={poster ? img : `${img}#t=0.5`}
75
+ poster={poster || undefined}
76
+ controls={!isThumbnail}
77
+ muted={isThumbnail}
78
+ playsInline
79
+ preload="metadata"
80
+ onLoadedMetadata={(e) => setDuration(e.target.duration)}
25
81
  />
82
+ {isThumbnail && (
83
+ <>
84
+ <PlayBadge />
85
+ {duration ? (
86
+ <span className="video-duration">{formatDuration(duration)}</span>
87
+ ) : null}
88
+ </>
89
+ )}
26
90
  </Container>
27
91
  );
28
92
  }
93
+
94
+ return (
95
+ <Container className={imageType}>
96
+ <ReactImageFallback
97
+ src={img}
98
+ fallbackImage={defaultProductImage}
99
+ alt={altText}
100
+ onClick={onClick}
101
+ />
102
+ </Container>
103
+ );
29
104
  };
@@ -1,14 +1,92 @@
1
1
  import styled from "styled-components";
2
+ import { FontFamily } from "../../../global-files/variables";
2
3
 
3
4
  export const Container = styled.div`
4
5
  width: fit-content;
5
6
 
6
- img {
7
+ img,
8
+ video {
7
9
  object-fit: contain;
8
10
  }
9
11
 
12
+ /*
13
+ Miniatura de video: fotograma quieto, con su duración y un botón de play.
14
+ El clic lo recibe el contenedor, no el <video>, para que lleve al visor
15
+ grande en vez de quedarse peleando con los controles.
16
+ */
17
+ &.video-thumbnail {
18
+ position: relative;
19
+ cursor: pointer;
20
+
21
+ video {
22
+ pointer-events: none;
23
+ background: #000;
24
+ }
25
+
26
+ .play-badge {
27
+ position: absolute;
28
+ top: 50%;
29
+ left: 50%;
30
+ transform: translate(-50%, -50%);
31
+ width: 32px;
32
+ height: 32px;
33
+ pointer-events: none;
34
+
35
+ svg {
36
+ width: 100%;
37
+ height: 100%;
38
+ display: block;
39
+ }
40
+
41
+ circle {
42
+ fill: rgba(0, 0, 0, 0.55);
43
+ }
44
+
45
+ polygon {
46
+ fill: #ffffff;
47
+ }
48
+ }
49
+
50
+ &:hover .play-badge circle {
51
+ fill: rgba(0, 0, 0, 0.75);
52
+ }
53
+
54
+ .video-duration {
55
+ position: absolute;
56
+ right: 4px;
57
+ bottom: 4px;
58
+ padding: 0 4px;
59
+ border-radius: 3px;
60
+ background: rgba(0, 0, 0, 0.75);
61
+ color: #ffffff;
62
+ font-family: ${FontFamily.Lato};
63
+ font-size: 10px;
64
+ line-height: 15px;
65
+ pointer-events: none;
66
+ }
67
+ }
68
+
69
+ /* En 60px un badge de 32 tapa el fotograma completo. */
70
+ &.catalogue-image-small-size.video-thumbnail .play-badge {
71
+ width: 20px;
72
+ height: 20px;
73
+ }
74
+
75
+ &.carousel-image.video-thumbnail {
76
+ .play-badge {
77
+ width: 22px;
78
+ height: 22px;
79
+ }
80
+
81
+ .video-duration {
82
+ font-size: 9px;
83
+ line-height: 13px;
84
+ }
85
+ }
86
+
10
87
  &.carousel-image {
11
- img {
88
+ img,
89
+ video {
12
90
  width: 85px;
13
91
  height: 55px;
14
92
  cursor: pointer;
@@ -20,7 +98,8 @@ export const Container = styled.div`
20
98
  }
21
99
 
22
100
  &.main-image {
23
- img {
101
+ img,
102
+ video {
24
103
  width: 340px;
25
104
  height: 295px;
26
105
  }
@@ -30,14 +109,16 @@ export const Container = styled.div`
30
109
  display: flex;
31
110
  justify-content: center;
32
111
  align-items: center;
33
- img {
112
+ img,
113
+ video {
34
114
  width: 179px;
35
115
  height: 179px;
36
116
  }
37
117
  }
38
118
 
39
119
  &.catalogue-image-small-size {
40
- img {
120
+ img,
121
+ video {
41
122
  width: 60px;
42
123
  height: 60px;
43
124
  }
@@ -47,7 +128,9 @@ export const Container = styled.div`
47
128
  flex: 1 0 50%;
48
129
  cursor: pointer;
49
130
 
50
- img {
131
+ img,
132
+
133
+ video {
51
134
  width: 168px;
52
135
  height: 108px;
53
136
  }
@@ -60,7 +143,9 @@ export const Container = styled.div`
60
143
  justify-content: center;
61
144
  align-items: center;
62
145
 
63
- img {
146
+ img,
147
+
148
+ video {
64
149
  width: 710px;
65
150
  height: 710px;
66
151
  }
@@ -2,10 +2,10 @@ import { Container } from "./styles";
2
2
  import { ProductImage } from "../../atoms/ProductImage/index";
3
3
  import { Button } from "../../atoms/GeneralButton/index";
4
4
 
5
- export const EditionActiveImage = ({ img, altText, setShowModal }) => {
5
+ export const EditionActiveImage = ({ img, altText, setShowModal, ext }) => {
6
6
  return (
7
7
  <Container>
8
- <ProductImage img={img} imageType="main-image" altText={altText} />
8
+ <ProductImage img={img} imageType="main-image" altText={altText} ext={ext} />
9
9
  <Button
10
10
  buttonType="open-modal-button"
11
11
  onClick={() => setShowModal(true)}
@@ -0,0 +1,102 @@
1
+ import { useState } from "react";
2
+
3
+ import { GalleryAssetElement } from "./index";
4
+ import { GalleryHeader } from "../GalleryHeader";
5
+ import {
6
+ fromBackendList,
7
+ ASSET_STATUS,
8
+ } from "../../../global-files/digital-assets/assets.adapter";
9
+ import { backendAssetsMock } from "../../../global-files/digital-assets/assets.mock";
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
+ /* Las claves del mock apuntan a S3, que todavía no tiene nada: se sustituyen
17
+ por archivos públicos para poder ver el componente. */
18
+ const assets = fromBackendList(backendAssetsMock).map((asset) => ({
19
+ ...asset,
20
+ url:
21
+ asset.ext === "mp4" ? SAMPLES.mp4 : asset.ext === "gif" ? SAMPLES.gif : "",
22
+ poster: "",
23
+ }));
24
+
25
+ export default {
26
+ title: "Components/molecules/GalleryAssetElement",
27
+ component: GalleryAssetElement,
28
+ };
29
+
30
+ const Template = (args) => <GalleryAssetElement {...args} />;
31
+
32
+ export const Fila = Template.bind({});
33
+ Fila.args = { asset: assets[0], index: 0 };
34
+
35
+ export const Cuadricula = Template.bind({});
36
+ Cuadricula.args = { asset: assets[0], index: 0, gridLayout: true };
37
+
38
+ export const Subiendo = Template.bind({});
39
+ Subiendo.args = {
40
+ asset: { ...assets[2], status: ASSET_STATUS.UPLOADING, progress: 42 },
41
+ index: 0,
42
+ };
43
+
44
+ export const ConError = Template.bind({});
45
+ ConError.args = {
46
+ asset: {
47
+ ...assets[3],
48
+ status: ASSET_STATUS.ERROR,
49
+ error: "No se pudo subir el archivo. Intenta de nuevo.",
50
+ },
51
+ index: 0,
52
+ };
53
+
54
+ /** La bandeja completa con su encabezado: es el uso real del componente. */
55
+ export const BandejaCompleta = () => {
56
+ const [selected, setSelected] = useState([]);
57
+
58
+ return (
59
+ <div style={{ width: 900 }}>
60
+ <GalleryHeader
61
+ variant="asset"
62
+ checkAllId="check-all-assets"
63
+ itemSelector="[id^='gallery-asset-']"
64
+ setCheckAll={() => {}}
65
+ setSelectedImages={setSelected}
66
+ />
67
+ {assets.map((asset, index) => (
68
+ <GalleryAssetElement
69
+ key={asset.id}
70
+ asset={asset}
71
+ index={index}
72
+ selectedAssets={selected}
73
+ setSelectedAssets={setSelected}
74
+ setCheckAll={() => {}}
75
+ changeAsset={({ value }) => console.log("nuevo tipo:", value)}
76
+ onRemove={(item) => console.log("quitar:", item.name)}
77
+ />
78
+ ))}
79
+ <p style={{ fontFamily: "Lato", fontSize: 12, marginTop: 12 }}>
80
+ Seleccionados: {selected.length}
81
+ </p>
82
+ </div>
83
+ );
84
+ };
85
+
86
+ /** Comparación de los dos encabezados, para verificar que no se movió el de imágenes. */
87
+ export const EncabezadosComparados = () => (
88
+ <div style={{ width: 900 }}>
89
+ <GalleryHeader
90
+ variant="image"
91
+ setCheckAll={() => {}}
92
+ setSelectedImages={() => {}}
93
+ />
94
+ <GalleryHeader
95
+ variant="asset"
96
+ checkAllId="check-all-assets-demo"
97
+ itemSelector="[id^='gallery-asset-demo-']"
98
+ setCheckAll={() => {}}
99
+ setSelectedImages={() => {}}
100
+ />
101
+ </div>
102
+ );
@@ -0,0 +1,162 @@
1
+ import { Container } from "./styles";
2
+ import { AssetPreview } from "../../atoms/AssetPreview";
3
+ import { CheckBox } from "../../atoms/CheckBox";
4
+ import Select from "../../atoms/Select";
5
+ import {
6
+ ASSET_PREVIEW,
7
+ assetTypeOptions,
8
+ formatBytes,
9
+ resolvePreviewKind,
10
+ } from "../../../global-files/digital-assets/assetTypes";
11
+ import {
12
+ ASSET_STATUS,
13
+ assetKey,
14
+ } from "../../../global-files/digital-assets/assets.adapter";
15
+
16
+ /*
17
+ Fila de un activo digital en la bandeja.
18
+
19
+ Es un componente aparte de `GalleryElement` a propósito, no una bifurcación
20
+ dentro de él. Un activo no comparte casi nada con una imagen de producto: no
21
+ tiene tipo de toma, ni leyenda, ni panel de validación, ni comparación contra
22
+ la versión auditable. Meterlo en el mismo archivo habría partido su render en
23
+ dos mitades y puesto en riesgo la bandeja de imágenes, que ya funciona.
24
+
25
+ Lo que sí comparte —rejilla, tamaños, colores— vive en los estilos, calcados
26
+ de `GalleryElement` para que las dos listas se vean como una sola tabla.
27
+ */
28
+ export const GalleryAssetElement = ({
29
+ asset = {},
30
+ index = 0,
31
+ gridLayout = false,
32
+ id,
33
+ label,
34
+ selectedAssets = [],
35
+ setSelectedAssets,
36
+ setCheckAll,
37
+ changeAsset,
38
+ onRemove,
39
+ readOnly = false,
40
+ checkAllId = "check-all-assets",
41
+ itemSelector = "[id^='gallery-asset-']",
42
+ }) => {
43
+ const elementId = id || `gallery-asset-${index}`;
44
+ const key = assetKey(asset);
45
+ const isUploading = asset.status === ASSET_STATUS.UPLOADING;
46
+ const hasError = asset.status === ASSET_STATUS.ERROR;
47
+
48
+ const checkOnChange = (e) => {
49
+ const chkGlobal = document.getElementById(checkAllId);
50
+ const checkboxList = document.querySelectorAll(itemSelector);
51
+ let temp = selectedAssets.slice();
52
+
53
+ // Se compara por `assetKey` y no por `id`: un activo recién soltado todavía
54
+ // no tiene id del backend, y comparando por id todos los pendientes serían
55
+ // "el mismo" y se quitarían juntos de la selección.
56
+ temp = e.target.checked
57
+ ? [...temp, asset]
58
+ : temp.filter((item) => assetKey(item) !== key);
59
+
60
+ let counter = 0;
61
+ checkboxList?.forEach((chk) => chk.checked && counter++);
62
+ if (chkGlobal) chkGlobal.checked = checkboxList.length === counter;
63
+ setCheckAll?.(checkboxList.length === counter);
64
+ setSelectedAssets?.(temp);
65
+ };
66
+
67
+ /*
68
+ Abrir el activo en otra pestaña.
69
+
70
+ Sirve para TODOS los tipos y por eso se prefiere a un visor propio: un video
71
+ se reproduce con el reproductor del navegador, un PDF se abre, y un .psd se
72
+ descarga. Sin esto, la bandeja mostraba el archivo pero no había forma de
73
+ verlo — un video quedaba como un fotograma fijo sin botón de reproducir.
74
+
75
+ Solo cuando ya está en S3: un activo recién soltado apunta al archivo del
76
+ disco del usuario y abrirlo no le diría nada nuevo.
77
+ */
78
+ const abrir = () => {
79
+ if (!asset.id || !asset.url) return;
80
+ window.open(asset.url, "_blank", "noopener,noreferrer");
81
+ };
82
+
83
+ const onChangeType = (e) => {
84
+ changeAsset?.({
85
+ action: "changeAssetInfo",
86
+ attribute: "assetType",
87
+ value: e.target.value,
88
+ index,
89
+ });
90
+ };
91
+
92
+ return (
93
+ <Container
94
+ className={`${gridLayout ? "grid-gallery" : "row-gallery"} ${
95
+ hasError ? "asset-error" : ""
96
+ }`.trim()}
97
+ >
98
+ <div className="buttons-container">
99
+ <CheckBox id={elementId} label={label} onChange={checkOnChange} />
100
+ </div>
101
+ <div className="asset-container">
102
+ <AssetPreview
103
+ asset={asset}
104
+ size={`catalogue-image-${gridLayout ? "big" : "small"}-size`}
105
+ /*
106
+ En cuadrícula hay sitio para el reproductor; en la fila, de 60px,
107
+ los controles no caben y estorban al clic.
108
+ */
109
+ controls={gridLayout && resolvePreviewKind(asset) === ASSET_PREVIEW.VIDEO}
110
+ onClick={abrir}
111
+ />
112
+ <div className="information-container">
113
+ <p
114
+ className={asset.id ? "asset-name asset-link" : "asset-name"}
115
+ title={asset.id ? `Abrir ${asset.name}` : asset.name}
116
+ onClick={abrir}
117
+ >
118
+ {asset.name}
119
+ </p>
120
+ {!gridLayout && (
121
+ <>
122
+ <p className="asset-weight">{formatBytes(asset.sizeBytes)}</p>
123
+ <p className="asset-format">{(asset.ext || "").toUpperCase()}</p>
124
+ <Select
125
+ width="100%"
126
+ placeholder="Tipo de activo"
127
+ options={assetTypeOptions()}
128
+ valueSelected={asset.assetType}
129
+ onChange={onChangeType}
130
+ className={readOnly ? "disabled" : ""}
131
+ />
132
+ </>
133
+ )}
134
+ </div>
135
+ </div>
136
+
137
+ {isUploading && (
138
+ <div className="asset-progress" aria-label="Subiendo activo">
139
+ <div
140
+ className="asset-progress-fill"
141
+ style={{
142
+ width: `${Math.min(100, Math.max(0, asset.progress ?? 0))}%`,
143
+ }}
144
+ />
145
+ </div>
146
+ )}
147
+
148
+ {hasError && <p className="asset-error-message">{asset.error}</p>}
149
+
150
+ {onRemove && !readOnly && (
151
+ <button
152
+ type="button"
153
+ className="asset-remove"
154
+ onClick={() => onRemove(asset)}
155
+ title="Quitar activo"
156
+ >
157
+ ×
158
+ </button>
159
+ )}
160
+ </Container>
161
+ );
162
+ };