contentoh-components-library 21.6.25 → 21.6.27

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,6 +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 { SingleSelect } from "../../molecules/SingleSelect";
5
6
  import { getProductImage, getRetailerPicture } from "../../../_utils/helper";
6
7
 
7
8
  import {
@@ -303,6 +304,19 @@ export const LinkVariantsModal = ({
303
304
  };
304
305
 
305
306
  const toggleChoice = (dimension, id) => {
307
+ // Marcar un elemento ES elegir cuáles, así que el radio se pone solo. Antes
308
+ // se podían marcar atributos con los dos radios en blanco: la pantalla decía
309
+ // una cosa y el estado era otra.
310
+ //
311
+ // Se hace así y no dejando "elegir cuáles" marcado de entrada porque eso
312
+ // daría las cuatro dimensiones por decididas desde el principio y habilitaría
313
+ // Ligar sin que nadie hubiera revisado nada.
314
+ setModes((current) =>
315
+ current[dimension] === null
316
+ ? { ...current, [dimension]: "some" }
317
+ : current
318
+ );
319
+
306
320
  setChosen((current) => {
307
321
  const lista = current[dimension];
308
322
  return {
@@ -360,11 +374,15 @@ export const LinkVariantsModal = ({
360
374
  categories: [],
361
375
  };
362
376
  }
377
+ // El tercer nivel no viene como {id, name} sino como
378
+ // {category_id, subcategory}: con el mapeo de los otros dos, la
379
+ // categoría salía en la lista pero sin texto. Mismo criterio que usa
380
+ // el alta de productos, incluido el separador legible.
363
381
  return {
364
382
  ...current,
365
383
  categories: lista.map((item) => ({
366
- value: item.id,
367
- label: item.name,
384
+ value: item.category_id,
385
+ label: (item.subcategory ?? "").replace("|", " > "),
368
386
  })),
369
387
  };
370
388
  });
@@ -385,6 +403,22 @@ export const LinkVariantsModal = ({
385
403
  }
386
404
  }, [step, filters.retailers.length, loadFilters]);
387
405
 
406
+ /*
407
+ Quita una cadena agregada a mano. Solo esas: las del padre se desmarcan con
408
+ su casilla, pero siguen existiendo y volverán a aparecer.
409
+ */
410
+ const removeExtraRetailer = (clave) => {
411
+ setExtraRetailers((current) =>
412
+ current.filter((item) => item.clave !== clave)
413
+ );
414
+ setChosen((current) => ({
415
+ ...current,
416
+ categoryRetailers: current.categoryRetailers.filter(
417
+ (item) => item !== clave
418
+ ),
419
+ }));
420
+ };
421
+
388
422
  const addRetailer = () => {
389
423
  const retailerId = Number(newRetailer);
390
424
  const categoryId = Number(newCategory);
@@ -555,7 +589,9 @@ export const LinkVariantsModal = ({
555
589
  const decidida = (dimension) => {
556
590
  if (modes[dimension] !== null) return true;
557
591
  if (dimension === "categoryRetailers") {
558
- return offered.categoryRetailers.length === 0 && extraRetailers.length === 0;
592
+ return (
593
+ offered.categoryRetailers.length === 0 && extraRetailers.length === 0
594
+ );
559
595
  }
560
596
  // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
561
597
  return dimension === "images"
@@ -874,63 +910,66 @@ export const LinkVariantsModal = ({
874
910
  </span>
875
911
  </span>
876
912
  </label>
913
+
914
+ {/* Solo las agregadas a mano se pueden quitar. Las del
915
+ padre se desmarcan con su casilla, pero siguen ahí. */}
916
+ {!item.delPadre && (
917
+ <button
918
+ type="button"
919
+ className="quitar"
920
+ title="Quitar esta cadena"
921
+ aria-label={`Quitar ${item.retailerName}`}
922
+ onClick={() => removeExtraRetailer(item.clave)}
923
+ >
924
+ ×
925
+ </button>
926
+ )}
877
927
  </li>
878
928
  ))}
879
929
  </List>
880
930
 
881
931
  <AddRetailer>
882
- <select
932
+ <SingleSelect
933
+ id="link-variants-retailer"
934
+ placeholder="Agregar otra cadena…"
935
+ options={filters.retailers}
883
936
  value={newRetailer}
884
- onChange={(event) => {
885
- const valor = event.target.value;
886
- setNewRetailer(valor);
937
+ loading={loadingFilters && !filters.retailers.length}
938
+ onChange={(valor) => {
939
+ setNewRetailer(valor ?? "");
887
940
  setNewDepartment("");
888
941
  setNewCategory("");
889
942
  if (valor) loadFilters(valor, null);
890
943
  }}
891
- >
892
- <option value="">Agregar otra cadena…</option>
893
- {filters.retailers.map((item) => (
894
- <option key={item.value} value={item.value}>
895
- {item.label}
896
- </option>
897
- ))}
898
- </select>
899
- <select
944
+ />
945
+ <SingleSelect
946
+ id="link-variants-department"
947
+ placeholder="Departamento…"
948
+ options={filters.departments}
900
949
  value={newDepartment}
901
- disabled={!newRetailer || !filters.departments.length}
902
- onChange={(event) => {
903
- const valor = event.target.value;
904
- setNewDepartment(valor);
950
+ disabled={!newRetailer}
951
+ loading={loadingFilters && !!newRetailer}
952
+ onChange={(valor) => {
953
+ setNewDepartment(valor ?? "");
905
954
  setNewCategory("");
906
955
  if (valor) loadFilters(newRetailer, valor);
907
956
  }}
908
- >
909
- <option value="">Departamento…</option>
910
- {filters.departments.map((item) => (
911
- <option key={item.value} value={item.value}>
912
- {item.label}
913
- </option>
914
- ))}
915
- </select>
916
- <select
957
+ />
958
+ <SingleSelect
959
+ id="link-variants-category"
960
+ placeholder="Categoría…"
961
+ options={filters.categories}
917
962
  value={newCategory}
918
- disabled={!newDepartment || !filters.categories.length}
919
- onChange={(event) => setNewCategory(event.target.value)}
920
- >
921
- <option value="">Categoría…</option>
922
- {filters.categories.map((item) => (
923
- <option key={item.value} value={item.value}>
924
- {item.label}
925
- </option>
926
- ))}
927
- </select>
963
+ disabled={!newDepartment}
964
+ loading={loadingFilters && !!newDepartment}
965
+ onChange={(valor) => setNewCategory(valor ?? "")}
966
+ />
928
967
  <button
929
968
  type="button"
930
969
  onClick={addRetailer}
931
970
  disabled={!newRetailer || !newCategory || loadingFilters}
932
971
  >
933
- {loadingFilters ? "…" : "Agregar"}
972
+ Agregar
934
973
  </button>
935
974
  </AddRetailer>
936
975
  </Block>
@@ -1111,89 +1150,85 @@ export const LinkVariantsModal = ({
1111
1150
  )}
1112
1151
  </Block>
1113
1152
 
1114
- <Block>
1115
- <h3>
1116
- <span className="paso">4</span> Imágenes
1117
- </h3>
1118
- {imagenesUtiles.length ? (
1119
- <>
1120
- <Choices>
1121
- <label>
1153
+ {/* Sin imágenes utilizables el bloque no se dibuja: un paso que
1154
+ solo sirve para decir que no hay nada es ruido, y ademas ya no
1155
+ hay decision que tomar ahi. */}
1156
+ {imagenesUtiles.length > 0 && (
1157
+ <Block>
1158
+ <h3>
1159
+ <span className="paso">4</span> Imágenes
1160
+ </h3>
1161
+ <Choices>
1162
+ <label>
1163
+ <input
1164
+ type="radio"
1165
+ name={`images-${activeChild ?? "todos"}`}
1166
+ checked={modes.images === "all"}
1167
+ onChange={() =>
1168
+ chooseMode(
1169
+ "images",
1170
+ "all",
1171
+ imagenesUtiles.map((item) => item.id)
1172
+ )
1173
+ }
1174
+ />
1175
+ <span>
1176
+ Todas
1177
+ <span className="hint">
1178
+ {imagenesUtiles.length} imagen(es) en el padre.
1179
+ </span>
1180
+ </span>
1181
+ </label>
1182
+ <label>
1183
+ <input
1184
+ type="radio"
1185
+ name={`images-${activeChild ?? "todos"}`}
1186
+ checked={modes.images === "some"}
1187
+ onChange={() => chooseMode("images", "some", [])}
1188
+ />
1189
+ <span>
1190
+ Elegir cuáles
1191
+ <span className="hint">
1192
+ Útil cuando la variante cambia de color o de empaque.
1193
+ </span>
1194
+ </span>
1195
+ </label>
1196
+ </Choices>
1197
+
1198
+ <ImageGrid>
1199
+ {imagenesUtiles.map((image) => (
1200
+ <label
1201
+ key={image.id}
1202
+ className={[
1203
+ chosen.images.includes(image.id) ? "picked" : "",
1204
+ modes.images === "all" ? "locked" : "",
1205
+ ]
1206
+ .filter(Boolean)
1207
+ .join(" ")}
1208
+ >
1122
1209
  <input
1123
- type="radio"
1124
- name={`images-${activeChild ?? "todos"}`}
1125
- checked={modes.images === "all"}
1126
- onChange={() =>
1127
- chooseMode(
1128
- "images",
1129
- "all",
1130
- imagenesUtiles.map((item) => item.id)
1210
+ type="checkbox"
1211
+ disabled={modes.images === "all"}
1212
+ checked={chosen.images.includes(image.id)}
1213
+ onChange={() => toggleChoice("images", image.id)}
1214
+ />
1215
+ <img
1216
+ src={getProductImage(image.src)}
1217
+ alt=""
1218
+ onError={() =>
1219
+ setBrokenImages((current) =>
1220
+ current.includes(image.id)
1221
+ ? current
1222
+ : [...current, image.id]
1131
1223
  )
1132
1224
  }
1133
1225
  />
1134
- <span>
1135
- Todas
1136
- <span className="hint">
1137
- {imagenesUtiles.length} imagen(es) en el padre.
1138
- </span>
1139
- </span>
1226
+ <span>{image.imageType || "Sin tipo de toma"}</span>
1140
1227
  </label>
1141
- <label>
1142
- <input
1143
- type="radio"
1144
- name={`images-${activeChild ?? "todos"}`}
1145
- checked={modes.images === "some"}
1146
- onChange={() => chooseMode("images", "some", [])}
1147
- />
1148
- <span>
1149
- Elegir cuáles
1150
- <span className="hint">
1151
- Útil cuando la variante cambia de color o de
1152
- empaque.
1153
- </span>
1154
- </span>
1155
- </label>
1156
- </Choices>
1157
-
1158
- <ImageGrid>
1159
- {imagenesUtiles.map((image) => (
1160
- <label
1161
- key={image.id}
1162
- className={[
1163
- chosen.images.includes(image.id) ? "picked" : "",
1164
- modes.images === "all" ? "locked" : "",
1165
- ]
1166
- .filter(Boolean)
1167
- .join(" ")}
1168
- >
1169
- <input
1170
- type="checkbox"
1171
- disabled={modes.images === "all"}
1172
- checked={chosen.images.includes(image.id)}
1173
- onChange={() => toggleChoice("images", image.id)}
1174
- />
1175
- <img
1176
- src={getProductImage(image.src)}
1177
- alt=""
1178
- onError={() =>
1179
- setBrokenImages((current) =>
1180
- current.includes(image.id)
1181
- ? current
1182
- : [...current, image.id]
1183
- )
1184
- }
1185
- />
1186
- <span>{image.imageType || "Sin tipo de toma"}</span>
1187
- </label>
1188
- ))}
1189
- </ImageGrid>
1190
- </>
1191
- ) : (
1192
- <Message>
1193
- El producto padre no tiene imágenes que heredar.
1194
- </Message>
1195
- )}
1196
- </Block>
1228
+ ))}
1229
+ </ImageGrid>
1230
+ </Block>
1231
+ )}
1197
1232
 
1198
1233
  {/* Es la diferencia entre link y propagate, y es la que sorprende:
1199
1234
  aquí lo elegido se escribe encima aunque la variante lo hubiera
@@ -1212,7 +1247,10 @@ export const LinkVariantsModal = ({
1212
1247
  ? selectionLabel
1213
1248
  : todoDecidido
1214
1249
  ? "Todo revisado. Puedes ligar."
1215
- : !applyToAll && mode === "children"
1250
+ : /* Contar productos solo tiene sentido cuando hay varios y cada
1251
+ uno lleva su propia selección. Con uno, lo útil es decir qué
1252
+ falta, no repetir que falta "1 producto". */
1253
+ !applyToAll && mode === "children" && selected.length > 1
1216
1254
  ? `Faltan ${
1217
1255
  selected.filter((id) => !decididoPara(id)).length
1218
1256
  } producto(s) por revisar.`
@@ -192,6 +192,32 @@ export const List = styled.ul`
192
192
  flex-shrink: 0;
193
193
  }
194
194
 
195
+ li {
196
+ display: flex;
197
+ align-items: center;
198
+ }
199
+
200
+ li > label {
201
+ flex: 1;
202
+ min-width: 0;
203
+ }
204
+
205
+ .quitar {
206
+ flex-shrink: 0;
207
+ margin-right: 10px;
208
+ border: none;
209
+ background: none;
210
+ padding: 2px 6px;
211
+ cursor: pointer;
212
+ font-size: 16px;
213
+ line-height: 1;
214
+ color: ${GlobalColors.gray};
215
+ }
216
+
217
+ .quitar:hover {
218
+ color: ${PINK};
219
+ }
220
+
195
221
  .count {
196
222
  margin-left: auto;
197
223
  font-size: 12px;
@@ -363,24 +389,12 @@ export const ImageGrid = styled.div`
363
389
  export const AddRetailer = styled.div`
364
390
  display: flex;
365
391
  flex-wrap: wrap;
392
+ align-items: flex-end;
366
393
  gap: 8px;
367
394
  margin-bottom: 16px;
368
395
 
369
- select {
370
- flex: 1 1 140px;
371
- min-width: 0;
372
- height: 32px;
373
- padding: 0 8px;
374
- border: 1px solid ${GlobalColors.s2};
375
- border-radius: 6px;
376
- font-family: ${FontFamily.Raleway};
377
- font-size: 13px;
378
- color: ${GlobalColors.deep_gray};
379
- background-color: ${GlobalColors.white};
380
- }
381
-
382
396
  button {
383
- height: 32px;
397
+ height: 38px;
384
398
  padding: 0 14px;
385
399
  border: 1px solid ${PINK};
386
400
  border-radius: 6px;
@@ -389,11 +403,7 @@ export const AddRetailer = styled.div`
389
403
  font-size: 13px;
390
404
  color: ${PINK};
391
405
  cursor: pointer;
392
- }
393
-
394
- select:disabled {
395
- opacity: 0.5;
396
- cursor: default;
406
+ flex-shrink: 0;
397
407
  }
398
408
 
399
409
  button:disabled {
package/src/index.js CHANGED
@@ -67,6 +67,7 @@ export * from "./components/molecules/TagAndInput/index";
67
67
  export * from "./components/molecules/VerificationCodeResetPasswordLogin/index";
68
68
  export * from "./components/molecules/RetailerSelector/index";
69
69
  export * from "./components/molecules/CustomSelect/index";
70
+ export * from "./components/molecules/SingleSelect/index";
70
71
  export * from "./components/molecules/ButtonDownloadFile/index";
71
72
  export * from "./components/molecules/ImageTooltip/index";
72
73
  export * from "./components/molecules/SelectV2/index";