contentoh-components-library 21.6.24 → 21.6.26

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,8 @@ 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 { SingleSelect } from "../../molecules/SingleSelect";
6
+ import { getProductImage, getRetailerPicture } from "../../../_utils/helper";
9
7
 
10
8
  import {
11
9
  Overlay,
@@ -130,11 +128,27 @@ export const LinkVariantsModal = ({
130
128
  cuatro decisiones por producto. Al desmarcarlo aparecen las pestañas y cada
131
129
  producto guarda su propia selección en `perChild`.
132
130
  */
133
- const [applyToAll, setApplyToAll] = useState(true);
131
+ // Desmarcado a propósito: aplicar la misma herencia a varios productos es una
132
+ // decisión, no un valor por omisión. Marcado de entrada se ligaban todos igual
133
+ // sin que nadie lo hubiera elegido.
134
+ const [applyToAll, setApplyToAll] = useState(false);
134
135
  const [activeChild, setActiveChild] = useState(null);
135
136
  const [perChild, setPerChild] = useState({});
136
137
 
137
138
  // Cadenas que el padre NO tiene y el usuario decidió agregar a la variante.
139
+ /*
140
+ Imágenes que la base lista pero cuyo archivo no está en S3.
141
+
142
+ Pasa: hay filas de rel_article_image con un src válido que apunta a un objeto
143
+ que ya no existe —comprobado, CloudFront responde 404—. Sin esto el paso 4
144
+ muestra una tarjeta rota, con el texto alternativo donde debería ir la foto, y
145
+ deja marcar para heredar algo que no se puede heredar.
146
+
147
+ No se puede saber desde el servidor sin ir a S3 por cada imagen, así que se
148
+ detecta al fallar la carga y se descuenta de la lista y del conteo.
149
+ */
150
+ const [brokenImages, setBrokenImages] = useState([]);
151
+
138
152
  const [extraRetailers, setExtraRetailers] = useState([]);
139
153
  const [newRetailer, setNewRetailer] = useState("");
140
154
  const [newDepartment, setNewDepartment] = useState("");
@@ -277,8 +291,9 @@ export const LinkVariantsModal = ({
277
291
  images: null,
278
292
  });
279
293
  setExtraRetailers([]);
294
+ setBrokenImages([]);
280
295
  setPerChild({});
281
- setApplyToAll(true);
296
+ setApplyToAll(false);
282
297
  setActiveChild(mode === "children" ? selected[0] : null);
283
298
  setStep("inherit");
284
299
  } catch (err) {
@@ -315,20 +330,25 @@ export const LinkVariantsModal = ({
315
330
  setLoadingFilters(true);
316
331
  try {
317
332
  const res = await axios.get(
318
- `${filtersEndpoint}?retailerSelected=${retailer ?? ""}&departmentSelected=${
319
- department ?? ""
320
- }`,
333
+ `${filtersEndpoint}?retailerSelected=${
334
+ retailer ?? ""
335
+ }&departmentSelected=${department ?? ""}`,
321
336
  { headers: { Authorization: token } }
322
337
  );
323
338
 
324
- const { body } = readPayload(res);
325
- const lista = (body ? JSON.parse(body) : res.data)?.data ?? [];
339
+ // readPayload YA devuelve el cuerpo parseado. Volver a buscar `body`
340
+ // dentro dejaba la lista siempre vacía, y por eso el selector de cadenas
341
+ // no mostraba nada.
342
+ const lista = readPayload(res)?.data ?? [];
326
343
 
327
344
  setFilters((current) => {
328
345
  if (!retailer) {
329
346
  return {
330
347
  ...current,
331
- retailers: lista.map((item) => ({ value: item.id, label: item.name })),
348
+ retailers: lista.map((item) => ({
349
+ value: item.id,
350
+ label: item.name,
351
+ })),
332
352
  };
333
353
  }
334
354
  if (!department) {
@@ -341,9 +361,16 @@ export const LinkVariantsModal = ({
341
361
  categories: [],
342
362
  };
343
363
  }
364
+ // El tercer nivel no viene como {id, name} sino como
365
+ // {category_id, subcategory}: con el mapeo de los otros dos, la
366
+ // categoría salía en la lista pero sin texto. Mismo criterio que usa
367
+ // el alta de productos, incluido el separador legible.
344
368
  return {
345
369
  ...current,
346
- categories: lista.map((item) => ({ value: item.id, label: item.name })),
370
+ categories: lista.map((item) => ({
371
+ value: item.category_id,
372
+ label: (item.subcategory ?? "").replace("|", " > "),
373
+ })),
347
374
  };
348
375
  });
349
376
  } catch (error) {
@@ -412,6 +439,12 @@ export const LinkVariantsModal = ({
412
439
  Las parejas cadena+categoría viajan como objetos, no como ids, porque pueden
413
440
  ser cadenas que el padre no tiene: ahí no hay nada que heredar, se asigna.
414
441
  */
442
+ // Las que de verdad se pueden heredar: la base puede listar filas cuyo archivo
443
+ // ya no existe, y esas no cuentan ni para la lista ni para el "todas".
444
+ const imagenesUtiles = offered.images.filter(
445
+ (image) => !brokenImages.includes(image.id)
446
+ );
447
+
415
448
  const buildInherit = (seleccion) => {
416
449
  const parejas = [
417
450
  ...offered.categoryRetailers.map((item) => ({
@@ -431,7 +464,8 @@ export const LinkVariantsModal = ({
431
464
  seleccion.modes.descriptions === "all"
432
465
  ? "all"
433
466
  : seleccion.chosen.descriptions,
434
- images: seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
467
+ images:
468
+ seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
435
469
  categoryRetailers: parejas
436
470
  .filter((par) => seleccion.chosen.categoryRetailers.includes(par.clave))
437
471
  .map(({ retailerId, categoryId }) => ({ retailerId, categoryId })),
@@ -484,7 +518,9 @@ export const LinkVariantsModal = ({
484
518
  !applyToAll && mode === "children"
485
519
  ? selected.reduce((acc, id) => {
486
520
  const guardado =
487
- id === activeChild ? { modes, chosen, extraRetailers } : perChild[id];
521
+ id === activeChild
522
+ ? { modes, chosen, extraRetailers }
523
+ : perChild[id];
488
524
  if (guardado) acc[id] = buildInherit(guardado);
489
525
  return acc;
490
526
  }, {})
@@ -521,11 +557,18 @@ export const LinkVariantsModal = ({
521
557
 
522
558
  // Una dimensión está decidida si el usuario eligió un modo, o si el padre no
523
559
  // 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);
560
+ const decidida = (dimension) => {
561
+ if (modes[dimension] !== null) return true;
562
+ if (dimension === "categoryRetailers") {
563
+ return (
564
+ offered.categoryRetailers.length === 0 && extraRetailers.length === 0
565
+ );
566
+ }
567
+ // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
568
+ return dimension === "images"
569
+ ? imagenesUtiles.length === 0
570
+ : offered[dimension].length === 0;
571
+ };
529
572
 
530
573
  const DIMENSIONES = [
531
574
  "categoryRetailers",
@@ -544,9 +587,14 @@ export const LinkVariantsModal = ({
544
587
 
545
588
  return DIMENSIONES.every((dimension) => {
546
589
  if (guardado.modes[dimension] !== null) return true;
547
- return dimension === "categoryRetailers"
548
- ? offered.categoryRetailers.length === 0 &&
549
- (guardado.extraRetailers ?? []).length === 0
590
+ if (dimension === "categoryRetailers") {
591
+ return (
592
+ offered.categoryRetailers.length === 0 &&
593
+ (guardado.extraRetailers ?? []).length === 0
594
+ );
595
+ }
596
+ return dimension === "images"
597
+ ? imagenesUtiles.length === 0
550
598
  : offered[dimension].length === 0;
551
599
  });
552
600
  };
@@ -582,7 +630,12 @@ export const LinkVariantsModal = ({
582
630
  {articleUpc ? ` · UPC ${articleUpc}` : ""}
583
631
  </p>
584
632
  </div>
585
- <button type="button" className="close" onClick={onClose} aria-label="Cerrar">
633
+ <button
634
+ type="button"
635
+ className="close"
636
+ onClick={onClose}
637
+ aria-label="Cerrar"
638
+ >
586
639
  ×
587
640
  </button>
588
641
  </Header>
@@ -654,7 +707,9 @@ export const LinkVariantsModal = ({
654
707
  {candidate.name || `Producto ${candidate.articleId}`}
655
708
  <span className="meta">
656
709
  UPC {candidate.upc || "—"}
657
- {candidate.categoryName ? ` · ${candidate.categoryName}` : ""}
710
+ {candidate.categoryName
711
+ ? ` · ${candidate.categoryName}`
712
+ : ""}
658
713
  </span>
659
714
  </span>
660
715
  {candidate.variantsCount > 0 && (
@@ -677,7 +732,11 @@ export const LinkVariantsModal = ({
677
732
  Mostrando {candidates.length} de {total}
678
733
  </span>
679
734
  {hasMore && (
680
- <button type="button" onClick={loadMore} disabled={loadingMore}>
735
+ <button
736
+ type="button"
737
+ onClick={loadMore}
738
+ disabled={loadingMore}
739
+ >
681
740
  {loadingMore ? "Cargando…" : "Cargar más"}
682
741
  </button>
683
742
  )}
@@ -748,6 +807,7 @@ export const LinkVariantsModal = ({
748
807
  <label>
749
808
  <input
750
809
  type="radio"
810
+ name={`categoryRetailers-${activeChild ?? "todos"}`}
751
811
  checked={modes.categoryRetailers === "all"}
752
812
  onChange={() =>
753
813
  chooseMode(
@@ -769,8 +829,11 @@ export const LinkVariantsModal = ({
769
829
  <label>
770
830
  <input
771
831
  type="radio"
832
+ name={`categoryRetailers-${activeChild ?? "todos"}`}
772
833
  checked={modes.categoryRetailers === "some"}
773
- onChange={() => chooseMode("categoryRetailers", "some", [])}
834
+ onChange={() =>
835
+ chooseMode("categoryRetailers", "some", [])
836
+ }
774
837
  />
775
838
  <span>
776
839
  Elegir cuáles
@@ -788,14 +851,19 @@ export const LinkVariantsModal = ({
788
851
  clave: `${item.retailerId}-${item.categoryId}`,
789
852
  delPadre: true,
790
853
  })),
791
- ...extraRetailers.map((item) => ({ ...item, delPadre: false })),
854
+ ...extraRetailers.map((item) => ({
855
+ ...item,
856
+ delPadre: false,
857
+ })),
792
858
  ].map((item) => (
793
859
  <li key={item.clave}>
794
860
  <label>
795
861
  <input
796
862
  type="checkbox"
797
863
  disabled={modes.categoryRetailers === "all"}
798
- checked={chosen.categoryRetailers.includes(item.clave)}
864
+ checked={chosen.categoryRetailers.includes(
865
+ item.clave
866
+ )}
799
867
  onChange={() =>
800
868
  toggleChoice("categoryRetailers", item.clave)
801
869
  }
@@ -818,58 +886,47 @@ export const LinkVariantsModal = ({
818
886
  </List>
819
887
 
820
888
  <AddRetailer>
821
- <select
889
+ <SingleSelect
890
+ id="link-variants-retailer"
891
+ placeholder="Agregar otra cadena…"
892
+ options={filters.retailers}
822
893
  value={newRetailer}
823
- onChange={(event) => {
824
- const valor = event.target.value;
825
- setNewRetailer(valor);
894
+ loading={loadingFilters && !filters.retailers.length}
895
+ onChange={(valor) => {
896
+ setNewRetailer(valor ?? "");
826
897
  setNewDepartment("");
827
898
  setNewCategory("");
828
899
  if (valor) loadFilters(valor, null);
829
900
  }}
830
- >
831
- <option value="">Agregar otra cadena…</option>
832
- {filters.retailers.map((item) => (
833
- <option key={item.value} value={item.value}>
834
- {item.label}
835
- </option>
836
- ))}
837
- </select>
838
- <select
901
+ />
902
+ <SingleSelect
903
+ id="link-variants-department"
904
+ placeholder="Departamento…"
905
+ options={filters.departments}
839
906
  value={newDepartment}
840
- disabled={!newRetailer || !filters.departments.length}
841
- onChange={(event) => {
842
- const valor = event.target.value;
843
- setNewDepartment(valor);
907
+ disabled={!newRetailer}
908
+ loading={loadingFilters && !!newRetailer}
909
+ onChange={(valor) => {
910
+ setNewDepartment(valor ?? "");
844
911
  setNewCategory("");
845
912
  if (valor) loadFilters(newRetailer, valor);
846
913
  }}
847
- >
848
- <option value="">Departamento…</option>
849
- {filters.departments.map((item) => (
850
- <option key={item.value} value={item.value}>
851
- {item.label}
852
- </option>
853
- ))}
854
- </select>
855
- <select
914
+ />
915
+ <SingleSelect
916
+ id="link-variants-category"
917
+ placeholder="Categoría…"
918
+ options={filters.categories}
856
919
  value={newCategory}
857
- disabled={!newDepartment || !filters.categories.length}
858
- onChange={(event) => setNewCategory(event.target.value)}
859
- >
860
- <option value="">Categoría…</option>
861
- {filters.categories.map((item) => (
862
- <option key={item.value} value={item.value}>
863
- {item.label}
864
- </option>
865
- ))}
866
- </select>
920
+ disabled={!newDepartment}
921
+ loading={loadingFilters && !!newDepartment}
922
+ onChange={(valor) => setNewCategory(valor ?? "")}
923
+ />
867
924
  <button
868
925
  type="button"
869
926
  onClick={addRetailer}
870
927
  disabled={!newRetailer || !newCategory || loadingFilters}
871
928
  >
872
- {loadingFilters ? "…" : "Agregar"}
929
+ Agregar
873
930
  </button>
874
931
  </AddRetailer>
875
932
  </Block>
@@ -884,6 +941,7 @@ export const LinkVariantsModal = ({
884
941
  <label>
885
942
  <input
886
943
  type="radio"
944
+ name={`descriptions-${activeChild ?? "todos"}`}
887
945
  checked={modes.descriptions === "all"}
888
946
  onChange={() =>
889
947
  chooseMode(
@@ -904,8 +962,11 @@ export const LinkVariantsModal = ({
904
962
  <label>
905
963
  <input
906
964
  type="radio"
965
+ name={`descriptions-${activeChild ?? "todos"}`}
907
966
  checked={modes.descriptions === "some"}
908
- onChange={() => chooseMode("descriptions", "some", [])}
967
+ onChange={() =>
968
+ chooseMode("descriptions", "some", [])
969
+ }
909
970
  />
910
971
  <span>
911
972
  Elegir cuáles
@@ -925,7 +986,9 @@ export const LinkVariantsModal = ({
925
986
  type="checkbox"
926
987
  disabled={modes.descriptions === "all"}
927
988
  checked={chosen.descriptions.includes(item.id)}
928
- onChange={() => toggleChoice("descriptions", item.id)}
989
+ onChange={() =>
990
+ toggleChoice("descriptions", item.id)
991
+ }
929
992
  />
930
993
  <span>
931
994
  {item.name}
@@ -961,6 +1024,7 @@ export const LinkVariantsModal = ({
961
1024
  <label>
962
1025
  <input
963
1026
  type="radio"
1027
+ name={`attributes-${activeChild ?? "todos"}`}
964
1028
  checked={modes.attributes === "all"}
965
1029
  onChange={() =>
966
1030
  chooseMode(
@@ -973,14 +1037,15 @@ export const LinkVariantsModal = ({
973
1037
  <span>
974
1038
  Todos los que tienen valor
975
1039
  <span className="hint">
976
- {offered.attributes.length} atributo(s) con información
977
- en el padre.
1040
+ {offered.attributes.length} atributo(s) con
1041
+ información en el padre.
978
1042
  </span>
979
1043
  </span>
980
1044
  </label>
981
1045
  <label>
982
1046
  <input
983
1047
  type="radio"
1048
+ name={`attributes-${activeChild ?? "todos"}`}
984
1049
  checked={modes.attributes === "some"}
985
1050
  onChange={() => chooseMode("attributes", "some", [])}
986
1051
  />
@@ -996,8 +1061,9 @@ export const LinkVariantsModal = ({
996
1061
 
997
1062
  {activeRetailerId && !nameScope && (
998
1063
  <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.
1064
+ El producto padre no comparte la cadena que tienes
1065
+ abierta, así que los nombres de abajo son los de otra de
1066
+ sus cadenas.
1001
1067
  </Message>
1002
1068
  )}
1003
1069
 
@@ -1009,7 +1075,9 @@ export const LinkVariantsModal = ({
1009
1075
  type="checkbox"
1010
1076
  disabled={modes.attributes === "all"}
1011
1077
  checked={chosen.attributes.includes(attribute.id)}
1012
- onChange={() => toggleChoice("attributes", attribute.id)}
1078
+ onChange={() =>
1079
+ toggleChoice("attributes", attribute.id)
1080
+ }
1013
1081
  />
1014
1082
  <span>
1015
1083
  {attribute.name}
@@ -1039,79 +1107,85 @@ export const LinkVariantsModal = ({
1039
1107
  )}
1040
1108
  </Block>
1041
1109
 
1042
- <Block>
1043
- <h3>
1044
- <span className="paso">4</span> Imágenes
1045
- </h3>
1046
- {offered.images.length ? (
1047
- <>
1048
- <Choices>
1049
- <label>
1110
+ {/* Sin imágenes utilizables el bloque no se dibuja: un paso que
1111
+ solo sirve para decir que no hay nada es ruido, y ademas ya no
1112
+ hay decision que tomar ahi. */}
1113
+ {imagenesUtiles.length > 0 && (
1114
+ <Block>
1115
+ <h3>
1116
+ <span className="paso">4</span> Imágenes
1117
+ </h3>
1118
+ <Choices>
1119
+ <label>
1120
+ <input
1121
+ type="radio"
1122
+ name={`images-${activeChild ?? "todos"}`}
1123
+ checked={modes.images === "all"}
1124
+ onChange={() =>
1125
+ chooseMode(
1126
+ "images",
1127
+ "all",
1128
+ imagenesUtiles.map((item) => item.id)
1129
+ )
1130
+ }
1131
+ />
1132
+ <span>
1133
+ Todas
1134
+ <span className="hint">
1135
+ {imagenesUtiles.length} imagen(es) en el padre.
1136
+ </span>
1137
+ </span>
1138
+ </label>
1139
+ <label>
1140
+ <input
1141
+ type="radio"
1142
+ name={`images-${activeChild ?? "todos"}`}
1143
+ checked={modes.images === "some"}
1144
+ onChange={() => chooseMode("images", "some", [])}
1145
+ />
1146
+ <span>
1147
+ Elegir cuáles
1148
+ <span className="hint">
1149
+ Útil cuando la variante cambia de color o de empaque.
1150
+ </span>
1151
+ </span>
1152
+ </label>
1153
+ </Choices>
1154
+
1155
+ <ImageGrid>
1156
+ {imagenesUtiles.map((image) => (
1157
+ <label
1158
+ key={image.id}
1159
+ className={[
1160
+ chosen.images.includes(image.id) ? "picked" : "",
1161
+ modes.images === "all" ? "locked" : "",
1162
+ ]
1163
+ .filter(Boolean)
1164
+ .join(" ")}
1165
+ >
1050
1166
  <input
1051
- type="radio"
1052
- checked={modes.images === "all"}
1053
- onChange={() =>
1054
- chooseMode(
1055
- "images",
1056
- "all",
1057
- offered.images.map((item) => item.id)
1167
+ type="checkbox"
1168
+ disabled={modes.images === "all"}
1169
+ checked={chosen.images.includes(image.id)}
1170
+ onChange={() => toggleChoice("images", image.id)}
1171
+ />
1172
+ <img
1173
+ src={getProductImage(image.src)}
1174
+ alt=""
1175
+ onError={() =>
1176
+ setBrokenImages((current) =>
1177
+ current.includes(image.id)
1178
+ ? current
1179
+ : [...current, image.id]
1058
1180
  )
1059
1181
  }
1060
1182
  />
1061
- <span>
1062
- Todas
1063
- <span className="hint">
1064
- {offered.images.length} imagen(es) en el padre.
1065
- </span>
1066
- </span>
1067
- </label>
1068
- <label>
1069
- <input
1070
- type="radio"
1071
- checked={modes.images === "some"}
1072
- onChange={() => chooseMode("images", "some", [])}
1073
- />
1074
- <span>
1075
- Elegir cuáles
1076
- <span className="hint">
1077
- Útil cuando la variante cambia de color o de empaque.
1078
- </span>
1079
- </span>
1183
+ <span>{image.imageType || "Sin tipo de toma"}</span>
1080
1184
  </label>
1081
- </Choices>
1082
-
1083
- <ImageGrid>
1084
- {offered.images.map((image) => (
1085
- <label
1086
- key={image.id}
1087
- className={[
1088
- chosen.images.includes(image.id) ? "picked" : "",
1089
- modes.images === "all" ? "locked" : "",
1090
- ]
1091
- .filter(Boolean)
1092
- .join(" ")}
1093
- >
1094
- <input
1095
- type="checkbox"
1096
- disabled={modes.images === "all"}
1097
- checked={chosen.images.includes(image.id)}
1098
- onChange={() => toggleChoice("images", image.id)}
1099
- />
1100
- <img
1101
- src={getProductImage(image.src)}
1102
- alt={image.imageType || "Imagen del producto"}
1103
- />
1104
- <span>{image.imageType || "Sin tipo de toma"}</span>
1105
- </label>
1106
- ))}
1107
- </ImageGrid>
1108
- </>
1109
- ) : (
1110
- <Message>
1111
- El producto padre no tiene imágenes que heredar.
1112
- </Message>
1113
- )}
1114
- </Block>
1185
+ ))}
1186
+ </ImageGrid>
1187
+ </Block>
1188
+ )}
1115
1189
 
1116
1190
  {/* Es la diferencia entre link y propagate, y es la que sorprende:
1117
1191
  aquí lo elegido se escribe encima aunque la variante lo hubiera
@@ -1131,7 +1205,9 @@ export const LinkVariantsModal = ({
1131
1205
  : todoDecidido
1132
1206
  ? "Todo revisado. Puedes ligar."
1133
1207
  : !applyToAll && mode === "children"
1134
- ? `Faltan ${selected.filter((id) => !decididoPara(id)).length} producto(s) por revisar.`
1208
+ ? `Faltan ${
1209
+ selected.filter((id) => !decididoPara(id)).length
1210
+ } producto(s) por revisar.`
1135
1211
  : `Falta decidir: ${faltantes.join(", ")}.`}
1136
1212
  </span>
1137
1213
  <div className="actions">
@@ -1145,7 +1221,12 @@ export const LinkVariantsModal = ({
1145
1221
  Atrás
1146
1222
  </button>
1147
1223
  )}
1148
- <button type="button" className="secondary" onClick={onClose} disabled={loading}>
1224
+ <button
1225
+ type="button"
1226
+ className="secondary"
1227
+ onClick={onClose}
1228
+ disabled={loading}
1229
+ >
1149
1230
  Cancelar
1150
1231
  </button>
1151
1232
  {step === "search" ? (