contentoh-components-library 21.6.24 → 21.6.25

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.
@@ -2,10 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import axios from "axios";
3
3
 
4
4
  import { readPayload, ApiError } from "./api";
5
- import {
6
- getProductImage,
7
- getRetailerPicture,
8
- } from "../../../_utils/helper";
5
+ import { getProductImage, getRetailerPicture } from "../../../_utils/helper";
9
6
 
10
7
  import {
11
8
  Overlay,
@@ -130,11 +127,27 @@ export const LinkVariantsModal = ({
130
127
  cuatro decisiones por producto. Al desmarcarlo aparecen las pestañas y cada
131
128
  producto guarda su propia selección en `perChild`.
132
129
  */
133
- const [applyToAll, setApplyToAll] = useState(true);
130
+ // Desmarcado a propósito: aplicar la misma herencia a varios productos es una
131
+ // decisión, no un valor por omisión. Marcado de entrada se ligaban todos igual
132
+ // sin que nadie lo hubiera elegido.
133
+ const [applyToAll, setApplyToAll] = useState(false);
134
134
  const [activeChild, setActiveChild] = useState(null);
135
135
  const [perChild, setPerChild] = useState({});
136
136
 
137
137
  // Cadenas que el padre NO tiene y el usuario decidió agregar a la variante.
138
+ /*
139
+ Imágenes que la base lista pero cuyo archivo no está en S3.
140
+
141
+ Pasa: hay filas de rel_article_image con un src válido que apunta a un objeto
142
+ que ya no existe —comprobado, CloudFront responde 404—. Sin esto el paso 4
143
+ muestra una tarjeta rota, con el texto alternativo donde debería ir la foto, y
144
+ deja marcar para heredar algo que no se puede heredar.
145
+
146
+ No se puede saber desde el servidor sin ir a S3 por cada imagen, así que se
147
+ detecta al fallar la carga y se descuenta de la lista y del conteo.
148
+ */
149
+ const [brokenImages, setBrokenImages] = useState([]);
150
+
138
151
  const [extraRetailers, setExtraRetailers] = useState([]);
139
152
  const [newRetailer, setNewRetailer] = useState("");
140
153
  const [newDepartment, setNewDepartment] = useState("");
@@ -277,8 +290,9 @@ export const LinkVariantsModal = ({
277
290
  images: null,
278
291
  });
279
292
  setExtraRetailers([]);
293
+ setBrokenImages([]);
280
294
  setPerChild({});
281
- setApplyToAll(true);
295
+ setApplyToAll(false);
282
296
  setActiveChild(mode === "children" ? selected[0] : null);
283
297
  setStep("inherit");
284
298
  } catch (err) {
@@ -315,20 +329,25 @@ export const LinkVariantsModal = ({
315
329
  setLoadingFilters(true);
316
330
  try {
317
331
  const res = await axios.get(
318
- `${filtersEndpoint}?retailerSelected=${retailer ?? ""}&departmentSelected=${
319
- department ?? ""
320
- }`,
332
+ `${filtersEndpoint}?retailerSelected=${
333
+ retailer ?? ""
334
+ }&departmentSelected=${department ?? ""}`,
321
335
  { headers: { Authorization: token } }
322
336
  );
323
337
 
324
- const { body } = readPayload(res);
325
- const lista = (body ? JSON.parse(body) : res.data)?.data ?? [];
338
+ // readPayload YA devuelve el cuerpo parseado. Volver a buscar `body`
339
+ // dentro dejaba la lista siempre vacía, y por eso el selector de cadenas
340
+ // no mostraba nada.
341
+ const lista = readPayload(res)?.data ?? [];
326
342
 
327
343
  setFilters((current) => {
328
344
  if (!retailer) {
329
345
  return {
330
346
  ...current,
331
- retailers: lista.map((item) => ({ value: item.id, label: item.name })),
347
+ retailers: lista.map((item) => ({
348
+ value: item.id,
349
+ label: item.name,
350
+ })),
332
351
  };
333
352
  }
334
353
  if (!department) {
@@ -343,7 +362,10 @@ export const LinkVariantsModal = ({
343
362
  }
344
363
  return {
345
364
  ...current,
346
- categories: lista.map((item) => ({ value: item.id, label: item.name })),
365
+ categories: lista.map((item) => ({
366
+ value: item.id,
367
+ label: item.name,
368
+ })),
347
369
  };
348
370
  });
349
371
  } catch (error) {
@@ -412,6 +434,12 @@ export const LinkVariantsModal = ({
412
434
  Las parejas cadena+categoría viajan como objetos, no como ids, porque pueden
413
435
  ser cadenas que el padre no tiene: ahí no hay nada que heredar, se asigna.
414
436
  */
437
+ // Las que de verdad se pueden heredar: la base puede listar filas cuyo archivo
438
+ // ya no existe, y esas no cuentan ni para la lista ni para el "todas".
439
+ const imagenesUtiles = offered.images.filter(
440
+ (image) => !brokenImages.includes(image.id)
441
+ );
442
+
415
443
  const buildInherit = (seleccion) => {
416
444
  const parejas = [
417
445
  ...offered.categoryRetailers.map((item) => ({
@@ -431,7 +459,8 @@ export const LinkVariantsModal = ({
431
459
  seleccion.modes.descriptions === "all"
432
460
  ? "all"
433
461
  : seleccion.chosen.descriptions,
434
- images: seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
462
+ images:
463
+ seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
435
464
  categoryRetailers: parejas
436
465
  .filter((par) => seleccion.chosen.categoryRetailers.includes(par.clave))
437
466
  .map(({ retailerId, categoryId }) => ({ retailerId, categoryId })),
@@ -484,7 +513,9 @@ export const LinkVariantsModal = ({
484
513
  !applyToAll && mode === "children"
485
514
  ? selected.reduce((acc, id) => {
486
515
  const guardado =
487
- id === activeChild ? { modes, chosen, extraRetailers } : perChild[id];
516
+ id === activeChild
517
+ ? { modes, chosen, extraRetailers }
518
+ : perChild[id];
488
519
  if (guardado) acc[id] = buildInherit(guardado);
489
520
  return acc;
490
521
  }, {})
@@ -521,11 +552,16 @@ export const LinkVariantsModal = ({
521
552
 
522
553
  // Una dimensión está decidida si el usuario eligió un modo, o si el padre no
523
554
  // ofrece nada en ella —no se puede pedir que decida sobre una lista vacía—.
524
- const decidida = (dimension) =>
525
- modes[dimension] !== null ||
526
- (dimension === "categoryRetailers"
527
- ? offered.categoryRetailers.length === 0 && extraRetailers.length === 0
528
- : offered[dimension].length === 0);
555
+ const decidida = (dimension) => {
556
+ if (modes[dimension] !== null) return true;
557
+ if (dimension === "categoryRetailers") {
558
+ return offered.categoryRetailers.length === 0 && extraRetailers.length === 0;
559
+ }
560
+ // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
561
+ return dimension === "images"
562
+ ? imagenesUtiles.length === 0
563
+ : offered[dimension].length === 0;
564
+ };
529
565
 
530
566
  const DIMENSIONES = [
531
567
  "categoryRetailers",
@@ -544,9 +580,14 @@ export const LinkVariantsModal = ({
544
580
 
545
581
  return DIMENSIONES.every((dimension) => {
546
582
  if (guardado.modes[dimension] !== null) return true;
547
- return dimension === "categoryRetailers"
548
- ? offered.categoryRetailers.length === 0 &&
549
- (guardado.extraRetailers ?? []).length === 0
583
+ if (dimension === "categoryRetailers") {
584
+ return (
585
+ offered.categoryRetailers.length === 0 &&
586
+ (guardado.extraRetailers ?? []).length === 0
587
+ );
588
+ }
589
+ return dimension === "images"
590
+ ? imagenesUtiles.length === 0
550
591
  : offered[dimension].length === 0;
551
592
  });
552
593
  };
@@ -582,7 +623,12 @@ export const LinkVariantsModal = ({
582
623
  {articleUpc ? ` · UPC ${articleUpc}` : ""}
583
624
  </p>
584
625
  </div>
585
- <button type="button" className="close" onClick={onClose} aria-label="Cerrar">
626
+ <button
627
+ type="button"
628
+ className="close"
629
+ onClick={onClose}
630
+ aria-label="Cerrar"
631
+ >
586
632
  ×
587
633
  </button>
588
634
  </Header>
@@ -654,7 +700,9 @@ export const LinkVariantsModal = ({
654
700
  {candidate.name || `Producto ${candidate.articleId}`}
655
701
  <span className="meta">
656
702
  UPC {candidate.upc || "—"}
657
- {candidate.categoryName ? ` · ${candidate.categoryName}` : ""}
703
+ {candidate.categoryName
704
+ ? ` · ${candidate.categoryName}`
705
+ : ""}
658
706
  </span>
659
707
  </span>
660
708
  {candidate.variantsCount > 0 && (
@@ -677,7 +725,11 @@ export const LinkVariantsModal = ({
677
725
  Mostrando {candidates.length} de {total}
678
726
  </span>
679
727
  {hasMore && (
680
- <button type="button" onClick={loadMore} disabled={loadingMore}>
728
+ <button
729
+ type="button"
730
+ onClick={loadMore}
731
+ disabled={loadingMore}
732
+ >
681
733
  {loadingMore ? "Cargando…" : "Cargar más"}
682
734
  </button>
683
735
  )}
@@ -748,6 +800,7 @@ export const LinkVariantsModal = ({
748
800
  <label>
749
801
  <input
750
802
  type="radio"
803
+ name={`categoryRetailers-${activeChild ?? "todos"}`}
751
804
  checked={modes.categoryRetailers === "all"}
752
805
  onChange={() =>
753
806
  chooseMode(
@@ -769,8 +822,11 @@ export const LinkVariantsModal = ({
769
822
  <label>
770
823
  <input
771
824
  type="radio"
825
+ name={`categoryRetailers-${activeChild ?? "todos"}`}
772
826
  checked={modes.categoryRetailers === "some"}
773
- onChange={() => chooseMode("categoryRetailers", "some", [])}
827
+ onChange={() =>
828
+ chooseMode("categoryRetailers", "some", [])
829
+ }
774
830
  />
775
831
  <span>
776
832
  Elegir cuáles
@@ -788,14 +844,19 @@ export const LinkVariantsModal = ({
788
844
  clave: `${item.retailerId}-${item.categoryId}`,
789
845
  delPadre: true,
790
846
  })),
791
- ...extraRetailers.map((item) => ({ ...item, delPadre: false })),
847
+ ...extraRetailers.map((item) => ({
848
+ ...item,
849
+ delPadre: false,
850
+ })),
792
851
  ].map((item) => (
793
852
  <li key={item.clave}>
794
853
  <label>
795
854
  <input
796
855
  type="checkbox"
797
856
  disabled={modes.categoryRetailers === "all"}
798
- checked={chosen.categoryRetailers.includes(item.clave)}
857
+ checked={chosen.categoryRetailers.includes(
858
+ item.clave
859
+ )}
799
860
  onChange={() =>
800
861
  toggleChoice("categoryRetailers", item.clave)
801
862
  }
@@ -884,6 +945,7 @@ export const LinkVariantsModal = ({
884
945
  <label>
885
946
  <input
886
947
  type="radio"
948
+ name={`descriptions-${activeChild ?? "todos"}`}
887
949
  checked={modes.descriptions === "all"}
888
950
  onChange={() =>
889
951
  chooseMode(
@@ -904,8 +966,11 @@ export const LinkVariantsModal = ({
904
966
  <label>
905
967
  <input
906
968
  type="radio"
969
+ name={`descriptions-${activeChild ?? "todos"}`}
907
970
  checked={modes.descriptions === "some"}
908
- onChange={() => chooseMode("descriptions", "some", [])}
971
+ onChange={() =>
972
+ chooseMode("descriptions", "some", [])
973
+ }
909
974
  />
910
975
  <span>
911
976
  Elegir cuáles
@@ -925,7 +990,9 @@ export const LinkVariantsModal = ({
925
990
  type="checkbox"
926
991
  disabled={modes.descriptions === "all"}
927
992
  checked={chosen.descriptions.includes(item.id)}
928
- onChange={() => toggleChoice("descriptions", item.id)}
993
+ onChange={() =>
994
+ toggleChoice("descriptions", item.id)
995
+ }
929
996
  />
930
997
  <span>
931
998
  {item.name}
@@ -961,6 +1028,7 @@ export const LinkVariantsModal = ({
961
1028
  <label>
962
1029
  <input
963
1030
  type="radio"
1031
+ name={`attributes-${activeChild ?? "todos"}`}
964
1032
  checked={modes.attributes === "all"}
965
1033
  onChange={() =>
966
1034
  chooseMode(
@@ -973,14 +1041,15 @@ export const LinkVariantsModal = ({
973
1041
  <span>
974
1042
  Todos los que tienen valor
975
1043
  <span className="hint">
976
- {offered.attributes.length} atributo(s) con información
977
- en el padre.
1044
+ {offered.attributes.length} atributo(s) con
1045
+ información en el padre.
978
1046
  </span>
979
1047
  </span>
980
1048
  </label>
981
1049
  <label>
982
1050
  <input
983
1051
  type="radio"
1052
+ name={`attributes-${activeChild ?? "todos"}`}
984
1053
  checked={modes.attributes === "some"}
985
1054
  onChange={() => chooseMode("attributes", "some", [])}
986
1055
  />
@@ -996,8 +1065,9 @@ export const LinkVariantsModal = ({
996
1065
 
997
1066
  {activeRetailerId && !nameScope && (
998
1067
  <Message>
999
- El producto padre no comparte la cadena que tienes abierta,
1000
- así que los nombres de abajo son los de otra de sus cadenas.
1068
+ El producto padre no comparte la cadena que tienes
1069
+ abierta, así que los nombres de abajo son los de otra de
1070
+ sus cadenas.
1001
1071
  </Message>
1002
1072
  )}
1003
1073
 
@@ -1009,7 +1079,9 @@ export const LinkVariantsModal = ({
1009
1079
  type="checkbox"
1010
1080
  disabled={modes.attributes === "all"}
1011
1081
  checked={chosen.attributes.includes(attribute.id)}
1012
- onChange={() => toggleChoice("attributes", attribute.id)}
1082
+ onChange={() =>
1083
+ toggleChoice("attributes", attribute.id)
1084
+ }
1013
1085
  />
1014
1086
  <span>
1015
1087
  {attribute.name}
@@ -1043,45 +1115,48 @@ export const LinkVariantsModal = ({
1043
1115
  <h3>
1044
1116
  <span className="paso">4</span> Imágenes
1045
1117
  </h3>
1046
- {offered.images.length ? (
1118
+ {imagenesUtiles.length ? (
1047
1119
  <>
1048
1120
  <Choices>
1049
1121
  <label>
1050
1122
  <input
1051
1123
  type="radio"
1124
+ name={`images-${activeChild ?? "todos"}`}
1052
1125
  checked={modes.images === "all"}
1053
1126
  onChange={() =>
1054
1127
  chooseMode(
1055
1128
  "images",
1056
1129
  "all",
1057
- offered.images.map((item) => item.id)
1130
+ imagenesUtiles.map((item) => item.id)
1058
1131
  )
1059
1132
  }
1060
1133
  />
1061
1134
  <span>
1062
1135
  Todas
1063
1136
  <span className="hint">
1064
- {offered.images.length} imagen(es) en el padre.
1137
+ {imagenesUtiles.length} imagen(es) en el padre.
1065
1138
  </span>
1066
1139
  </span>
1067
1140
  </label>
1068
1141
  <label>
1069
1142
  <input
1070
1143
  type="radio"
1144
+ name={`images-${activeChild ?? "todos"}`}
1071
1145
  checked={modes.images === "some"}
1072
1146
  onChange={() => chooseMode("images", "some", [])}
1073
1147
  />
1074
1148
  <span>
1075
1149
  Elegir cuáles
1076
1150
  <span className="hint">
1077
- Útil cuando la variante cambia de color o de empaque.
1151
+ Útil cuando la variante cambia de color o de
1152
+ empaque.
1078
1153
  </span>
1079
1154
  </span>
1080
1155
  </label>
1081
1156
  </Choices>
1082
1157
 
1083
1158
  <ImageGrid>
1084
- {offered.images.map((image) => (
1159
+ {imagenesUtiles.map((image) => (
1085
1160
  <label
1086
1161
  key={image.id}
1087
1162
  className={[
@@ -1099,7 +1174,14 @@ export const LinkVariantsModal = ({
1099
1174
  />
1100
1175
  <img
1101
1176
  src={getProductImage(image.src)}
1102
- alt={image.imageType || "Imagen del producto"}
1177
+ alt=""
1178
+ onError={() =>
1179
+ setBrokenImages((current) =>
1180
+ current.includes(image.id)
1181
+ ? current
1182
+ : [...current, image.id]
1183
+ )
1184
+ }
1103
1185
  />
1104
1186
  <span>{image.imageType || "Sin tipo de toma"}</span>
1105
1187
  </label>
@@ -1131,7 +1213,9 @@ export const LinkVariantsModal = ({
1131
1213
  : todoDecidido
1132
1214
  ? "Todo revisado. Puedes ligar."
1133
1215
  : !applyToAll && mode === "children"
1134
- ? `Faltan ${selected.filter((id) => !decididoPara(id)).length} producto(s) por revisar.`
1216
+ ? `Faltan ${
1217
+ selected.filter((id) => !decididoPara(id)).length
1218
+ } producto(s) por revisar.`
1135
1219
  : `Falta decidir: ${faltantes.join(", ")}.`}
1136
1220
  </span>
1137
1221
  <div className="actions">
@@ -1145,7 +1229,12 @@ export const LinkVariantsModal = ({
1145
1229
  Atrás
1146
1230
  </button>
1147
1231
  )}
1148
- <button type="button" className="secondary" onClick={onClose} disabled={loading}>
1232
+ <button
1233
+ type="button"
1234
+ className="secondary"
1235
+ onClick={onClose}
1236
+ disabled={loading}
1237
+ >
1149
1238
  Cancelar
1150
1239
  </button>
1151
1240
  {step === "search" ? (