contentoh-components-library 21.6.22 → 21.6.23

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.
@@ -15,9 +15,9 @@ var _variables = require("../../../global-files/variables");
15
15
 
16
16
  var _templateObject;
17
17
 
18
- var Container = _styledComponents.default.div(_templateObject || (_templateObject = (0, _taggedTemplateLiteral2.default)(["\n display: flex;\n flex-direction: column;\n height: 100%;\n flex: 1;\n overflow: auto;\n\n .data-container {\n display: flex;\n flex: 0%;\n height: calc(100% - ", "px);\n .image-data-panel {\n width: 340px;\n\n & + * {\n margin-left: 10px;\n }\n }\n\n .product-information {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n\n .services-information-container {\n height: 100%;\n overflow: auto;\n }\n\n .image-services {\n aside {\n display: grid;\n grid-template-columns: repeat(auto-fill, 179px);\n column-gap: 15px;\n row-gap: 15px;\n padding: 20px;\n }\n }\n\n .commentary-box {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n padding: 10px;\n padding-left: 0;\n\n .commentary {\n display: flex;\n align-items: flex-end;\n\n .input-container {\n width: 500px;\n\n .quill {\n height: 100px;\n }\n\n & + * {\n margin-left: 5px;\n }\n }\n\n .buttons-box {\n display: flex;\n width: 210px;\n flex-wrap: wrap;\n\n .general-transparent-button {\n & + * {\n margin-top: 5px;\n }\n }\n\n .general-transparent-button,\n .general-green-button,\n .general-button-disabled {\n width: fit-content;\n min-width: 201px;\n height: 40px;\n }\n }\n }\n }\n\n .feedback-box {\n display: flex;\n }\n\n .required-inputs-message {\n font-family: ", ";\n font-size: 13px;\n color: ", ";\n padding: 10px;\n display: flex;\n align-items: center;\n\n p + * {\n margin-top: 10px;\n }\n\n button {\n min-width: fit-content;\n }\n }\n }\n }\n #modal-message-box {\n width: 400px;\n height: 100px;\n }\n .container {\n width: 100%;\n height: 90%;\n .dropzone {\n height: 100%;\n width: 100%;\n position: relative;\n transition: all 0.5s ease;\n\n &.drag-active {\n background-color: rgba(", ", 0.05);\n border: 2px dashed ", ";\n border-radius: 8px;\n\n aside {\n opacity: 0.3;\n pointer-events: none;\n }\n }\n\n .drag-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(", ", 0.1);\n border-radius: 8px;\n pointer-events: none;\n z-index: 10;\n\n p {\n font-family: ", ";\n font-size: 24px;\n font-weight: 600;\n color: ", ";\n text-align: center;\n padding: 20px;\n background-color: ", ";\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n }\n }\n }\n .variants-actions {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 8px;\n\n .link-variants {\n border: 1px solid ", ";\n border-radius: 6px;\n background: none;\n padding: 6px 12px;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n cursor: pointer;\n }\n\n .variants-count {\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n }\n }\n"])), function (_ref) {
18
+ var Container = _styledComponents.default.div(_templateObject || (_templateObject = (0, _taggedTemplateLiteral2.default)(["\n display: flex;\n flex-direction: column;\n height: 100%;\n flex: 1;\n overflow: auto;\n\n .data-container {\n display: flex;\n flex: 0%;\n height: calc(100% - ", "px);\n .image-data-panel {\n width: 340px;\n\n & + * {\n margin-left: 10px;\n }\n }\n\n .product-information {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n\n .services-information-container {\n height: 100%;\n overflow: auto;\n }\n\n .image-services {\n aside {\n display: grid;\n grid-template-columns: repeat(auto-fill, 179px);\n column-gap: 15px;\n row-gap: 15px;\n padding: 20px;\n }\n }\n\n .commentary-box {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n padding: 10px;\n padding-left: 0;\n\n .commentary {\n display: flex;\n align-items: flex-end;\n\n .input-container {\n width: 500px;\n\n .quill {\n height: 100px;\n }\n\n & + * {\n margin-left: 5px;\n }\n }\n\n .buttons-box {\n display: flex;\n width: 210px;\n flex-wrap: wrap;\n\n .general-transparent-button {\n & + * {\n margin-top: 5px;\n }\n }\n\n .general-transparent-button,\n .general-green-button,\n .general-button-disabled {\n width: fit-content;\n min-width: 201px;\n height: 40px;\n }\n }\n }\n }\n\n .feedback-box {\n display: flex;\n }\n\n .required-inputs-message {\n font-family: ", ";\n font-size: 13px;\n color: ", ";\n padding: 10px;\n display: flex;\n align-items: center;\n\n p + * {\n margin-top: 10px;\n }\n\n button {\n min-width: fit-content;\n }\n }\n }\n }\n #modal-message-box {\n width: 400px;\n height: 100px;\n }\n .container {\n width: 100%;\n height: 90%;\n .dropzone {\n height: 100%;\n width: 100%;\n position: relative;\n transition: all 0.5s ease;\n\n &.drag-active {\n background-color: rgba(", ", 0.05);\n border: 2px dashed ", ";\n border-radius: 8px;\n\n aside {\n opacity: 0.3;\n pointer-events: none;\n }\n }\n\n .drag-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(", ", 0.1);\n border-radius: 8px;\n pointer-events: none;\n z-index: 10;\n\n p {\n font-family: ", ";\n font-size: 24px;\n font-weight: 600;\n color: ", ";\n text-align: center;\n padding: 20px;\n background-color: ", ";\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n }\n }\n }\n .variants-actions {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 8px;\n\n .link-variants {\n border: 1px solid ", ";\n border-radius: 6px;\n background: none;\n padding: 6px 12px;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n cursor: pointer;\n }\n\n .variants-count {\n border: none;\n background: none;\n padding: 0;\n cursor: pointer;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n text-decoration: underline;\n }\n }\n\n .variants-family {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-bottom: 10px;\n padding: 8px;\n border: 1px solid ", ";\n border-radius: 6px;\n\n .loading {\n margin: 0;\n font-family: ", ";\n font-size: 11px;\n color: ", ";\n }\n\n .family-row {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 6px 8px;\n border: none;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n text-align: left;\n font-family: ", ";\n font-size: 12px;\n color: ", ";\n }\n\n .family-row:hover:not(:disabled) {\n background-color: ", ";\n }\n\n .family-row:disabled {\n cursor: default;\n opacity: 0.7;\n }\n\n .family-row .role {\n min-width: 96px;\n font-size: 10px;\n color: ", ";\n }\n\n .family-row.current .role {\n color: ", ";\n }\n\n .family-row .name {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .family-row .upc {\n font-size: 11px;\n color: ", ";\n }\n }\n"])), function (_ref) {
19
19
  var headerTop = _ref.headerTop;
20
20
  return headerTop;
21
- }, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_gray, _variables.GlobalColors.rgb_color_pink, _variables.GlobalColors.color_pink, _variables.GlobalColors.rgb_color_pink, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_pink, _variables.GlobalColors.white, _variables.GlobalColors.original_magenta, _variables.FontFamily.Raleway_600, _variables.GlobalColors.original_magenta, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray);
21
+ }, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_gray, _variables.GlobalColors.rgb_color_pink, _variables.GlobalColors.color_pink, _variables.GlobalColors.rgb_color_pink, _variables.FontFamily.AvenirNext, _variables.GlobalColors.color_pink, _variables.GlobalColors.white, _variables.GlobalColors.original_magenta, _variables.FontFamily.Raleway_600, _variables.GlobalColors.original_magenta, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, _variables.GlobalColors.s2, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, _variables.FontFamily.Raleway_500, _variables.GlobalColors.deep_gray, _variables.GlobalColors.s2, _variables.GlobalColors.original_magenta, _variables.GlobalColors.gray, _variables.GlobalColors.gray);
22
22
 
23
23
  exports.Container = Container;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.6.22",
3
+ "version": "21.6.23",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -13,6 +13,9 @@ import {
13
13
  List,
14
14
  AttributeList,
15
15
  Section,
16
+ SectionHead,
17
+ ImageGrid,
18
+ AddRetailer,
16
19
  Message,
17
20
  Footer,
18
21
  } from "./styles";
@@ -66,6 +69,8 @@ export const LinkVariantsModal = ({
66
69
  articleName,
67
70
  articleUpc,
68
71
  activeRetailerId,
72
+ retailersAvailable,
73
+ categoriesAvailable,
69
74
  token,
70
75
  endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
71
76
  onClose,
@@ -80,13 +85,29 @@ export const LinkVariantsModal = ({
80
85
  const [searched, setSearched] = useState(false);
81
86
  const [selected, setSelected] = useState([]);
82
87
 
83
- const [attributes, setAttributes] = useState([]);
88
+ // Lo que el padre ofrece en cada dimensión, y lo que el usuario eligió de
89
+ // cada una. Se guardan por separado porque se eligen por separado: heredar
90
+ // todos los atributos no dice nada sobre qué fotos quiere la variante.
91
+ const [offered, setOffered] = useState({
92
+ attributes: [],
93
+ descriptions: [],
94
+ images: [],
95
+ categoryRetailers: [],
96
+ });
97
+ const [chosen, setChosen] = useState({
98
+ attributes: [],
99
+ descriptions: [],
100
+ images: [],
101
+ categoryRetailers: [],
102
+ });
84
103
  const [nameScope, setNameScope] = useState(null);
85
104
  const [inheritMode, setInheritMode] = useState("all-filled");
86
- const [chosenAttributes, setChosenAttributes] = useState([]);
87
- const [inheritImages, setInheritImages] = useState(true);
88
- const [inheritDescriptions, setInheritDescriptions] = useState(true);
89
- const [inheritCategoryRetailers, setInheritCategoryRetailers] = useState(true);
105
+ const [openSection, setOpenSection] = useState("attributes");
106
+
107
+ // Cadenas que el padre NO tiene y el usuario decidió agregar a la variante.
108
+ const [extraRetailers, setExtraRetailers] = useState([]);
109
+ const [newRetailer, setNewRetailer] = useState("");
110
+ const [newCategory, setNewCategory] = useState("");
90
111
 
91
112
  const [loading, setLoading] = useState(false);
92
113
  const [error, setError] = useState(null);
@@ -160,9 +181,27 @@ export const LinkVariantsModal = ({
160
181
  parentArticleId,
161
182
  retailerId: activeRetailerId,
162
183
  });
163
- setAttributes(data.attributes || []);
184
+
185
+ const listas = {
186
+ attributes: data.attributes || [],
187
+ descriptions: data.descriptions || [],
188
+ images: data.images || [],
189
+ categoryRetailers: data.categoryRetailers || [],
190
+ };
191
+
192
+ setOffered(listas);
164
193
  setNameScope(data.nameScope ?? null);
165
- setChosenAttributes((data.attributes || []).map((item) => item.id));
194
+ // Todo preseleccionado: heredar es el caso normal, y quitar lo que sobra
195
+ // es menos trabajo que marcar de cero lo que casi siempre se quiere.
196
+ setChosen({
197
+ attributes: listas.attributes.map((item) => item.id),
198
+ descriptions: listas.descriptions.map((item) => item.id),
199
+ images: listas.images.map((item) => item.id),
200
+ categoryRetailers: listas.categoryRetailers.map(
201
+ (item) => `${item.retailerId}-${item.categoryId}`
202
+ ),
203
+ });
204
+ setExtraRetailers([]);
166
205
  setStep("inherit");
167
206
  } catch (err) {
168
207
  setError(readError(err));
@@ -171,23 +210,86 @@ export const LinkVariantsModal = ({
171
210
  }
172
211
  };
173
212
 
174
- const toggleAttribute = (id) => {
175
- setChosenAttributes((current) =>
176
- current.includes(id)
177
- ? current.filter((item) => item !== id)
178
- : [...current, id]
213
+ const toggleChoice = (dimension, id) => {
214
+ setChosen((current) => {
215
+ const lista = current[dimension];
216
+ return {
217
+ ...current,
218
+ [dimension]: lista.includes(id)
219
+ ? lista.filter((item) => item !== id)
220
+ : [...lista, id],
221
+ };
222
+ });
223
+ };
224
+
225
+ const toggleAll = (dimension, ids) => {
226
+ setChosen((current) => ({
227
+ ...current,
228
+ [dimension]: current[dimension].length === ids.length ? [] : ids,
229
+ }));
230
+ };
231
+
232
+ const addRetailer = () => {
233
+ const retailerId = Number(newRetailer);
234
+ const categoryId = Number(newCategory);
235
+ if (!retailerId || !categoryId) return;
236
+
237
+ const clave = `${retailerId}-${categoryId}`;
238
+ const yaEsta =
239
+ offered.categoryRetailers.some(
240
+ (item) => `${item.retailerId}-${item.categoryId}` === clave
241
+ ) || extraRetailers.some((item) => item.clave === clave);
242
+
243
+ if (yaEsta) return;
244
+
245
+ const retailer = (retailersAvailable || []).find(
246
+ (item) => Number(item.id ?? item.value) === retailerId
247
+ );
248
+ const category = (categoriesAvailable || []).find(
249
+ (item) => Number(item.id ?? item.value) === categoryId
179
250
  );
251
+
252
+ setExtraRetailers((current) => [
253
+ ...current,
254
+ {
255
+ clave,
256
+ retailerId,
257
+ categoryId,
258
+ retailerName: retailer?.name ?? retailer?.label ?? `Cadena ${retailerId}`,
259
+ categoryName: category?.name ?? category?.label ?? `Categoría ${categoryId}`,
260
+ },
261
+ ]);
262
+ setChosen((current) => ({
263
+ ...current,
264
+ categoryRetailers: [...current.categoryRetailers, clave],
265
+ }));
266
+ setNewRetailer("");
267
+ setNewCategory("");
180
268
  };
181
269
 
182
270
  const submit = async () => {
183
271
  setLoading(true);
184
272
  setError(null);
185
273
 
274
+ // Las parejas cadena+categoría viajan como objetos, no como ids: pueden ser
275
+ // cadenas que el padre no tiene, y ahí no hay nada que "heredar" — se asigna.
276
+ const todasLasParejas = [
277
+ ...offered.categoryRetailers.map((item) => ({
278
+ clave: `${item.retailerId}-${item.categoryId}`,
279
+ retailerId: item.retailerId,
280
+ categoryId: item.categoryId,
281
+ })),
282
+ ...extraRetailers,
283
+ ];
284
+
186
285
  const inherit = {
187
- attributes: inheritMode === "all-filled" ? "all-filled" : chosenAttributes,
188
- images: inheritImages,
189
- descriptions: inheritDescriptions,
190
- categoryRetailers: inheritCategoryRetailers,
286
+ attributes:
287
+ inheritMode === "all-filled" ? "all-filled" : chosen.attributes,
288
+ descriptions: chosen.descriptions,
289
+ images: chosen.images,
290
+ categoryRetailers: todasLasParejas
291
+ .filter((par) => chosen.categoryRetailers.includes(par.clave))
292
+ .map(({ retailerId, categoryId }) => ({ retailerId, categoryId })),
191
293
  };
192
294
 
193
295
  const body =
@@ -197,7 +299,14 @@ export const LinkVariantsModal = ({
197
299
 
198
300
  try {
199
301
  const data = await request("link", body);
200
- onLinked?.(data);
302
+ // El modo importa para quien escucha: en `children` este producto GANA
303
+ // variantes; en `parent` pasa a SER una. Son efectos opuestos sobre la
304
+ // misma ficha, así que quien refresque el estado necesita distinguirlos.
305
+ onLinked?.({
306
+ ...data,
307
+ mode,
308
+ parentArticleId: mode === "parent" ? selected[0] : articleId,
309
+ });
201
310
  onClose?.();
202
311
  } catch (err) {
203
312
  setError(readError(err));
@@ -314,7 +423,7 @@ export const LinkVariantsModal = ({
314
423
  {step === "inherit" && (
315
424
  <>
316
425
  <Section>
317
- <h3>¿Qué se hereda del producto padre?</h3>
426
+ <h3>Atributos de la ficha técnica</h3>
318
427
  <label className="choice">
319
428
  <input
320
429
  type="radio"
@@ -322,9 +431,10 @@ export const LinkVariantsModal = ({
322
431
  onChange={() => setInheritMode("all-filled")}
323
432
  />
324
433
  <span>
325
- Todos los atributos con valor
434
+ Todos los que tienen valor
326
435
  <span className="hint">
327
- {attributes.length} atributo(s) con información en el padre.
436
+ {offered.attributes.length} atributo(s) con información en
437
+ el padre.
328
438
  </span>
329
439
  </span>
330
440
  </label>
@@ -352,13 +462,13 @@ export const LinkVariantsModal = ({
352
462
 
353
463
  {inheritMode === "selected" && (
354
464
  <AttributeList>
355
- {attributes.map((attribute) => (
465
+ {offered.attributes.map((attribute) => (
356
466
  <li key={attribute.id}>
357
467
  <label>
358
468
  <input
359
469
  type="checkbox"
360
- checked={chosenAttributes.includes(attribute.id)}
361
- onChange={() => toggleAttribute(attribute.id)}
470
+ checked={chosen.attributes.includes(attribute.id)}
471
+ onChange={() => toggleChoice("attributes", attribute.id)}
362
472
  />
363
473
  <span>
364
474
  {attribute.name}
@@ -383,36 +493,220 @@ export const LinkVariantsModal = ({
383
493
  )}
384
494
 
385
495
  <Section>
386
- <h3>También se hereda</h3>
387
- <label className="choice">
388
- <input
389
- type="checkbox"
390
- checked={inheritImages}
391
- onChange={() => setInheritImages((value) => !value)}
392
- />
393
- <span>Imágenes</span>
394
- </label>
395
- <label className="choice">
396
- <input
397
- type="checkbox"
398
- checked={inheritDescriptions}
399
- onChange={() => setInheritDescriptions((value) => !value)}
400
- />
401
- <span>Descripciones</span>
402
- </label>
403
- <label className="choice">
404
- <input
405
- type="checkbox"
406
- checked={inheritCategoryRetailers}
407
- onChange={() => setInheritCategoryRetailers((value) => !value)}
408
- />
409
- <span>
496
+ <SectionHead>
497
+ <button
498
+ type="button"
499
+ className="toggle"
500
+ aria-expanded={openSection === "descriptions"}
501
+ onClick={() =>
502
+ setOpenSection(
503
+ openSection === "descriptions" ? null : "descriptions"
504
+ )
505
+ }
506
+ >
507
+ Descripciones
508
+ <span className="count">
509
+ {chosen.descriptions.length} de{" "}
510
+ {offered.descriptions.length}
511
+ </span>
512
+ </button>
513
+ {offered.descriptions.length > 0 && (
514
+ <button
515
+ type="button"
516
+ className="all"
517
+ onClick={() =>
518
+ toggleAll(
519
+ "descriptions",
520
+ offered.descriptions.map((item) => item.id)
521
+ )
522
+ }
523
+ >
524
+ {chosen.descriptions.length === offered.descriptions.length
525
+ ? "Ninguna"
526
+ : "Todas"}
527
+ </button>
528
+ )}
529
+ </SectionHead>
530
+
531
+ {openSection === "descriptions" &&
532
+ (offered.descriptions.length ? (
533
+ <AttributeList>
534
+ {offered.descriptions.map((item) => (
535
+ <li key={item.id}>
536
+ <label>
537
+ <input
538
+ type="checkbox"
539
+ checked={chosen.descriptions.includes(item.id)}
540
+ onChange={() => toggleChoice("descriptions", item.id)}
541
+ />
542
+ <span>{item.name}</span>
543
+ <span className="value" title={item.value}>
544
+ {item.value}
545
+ </span>
546
+ </label>
547
+ </li>
548
+ ))}
549
+ </AttributeList>
550
+ ) : (
551
+ <Message>El producto padre no tiene descripciones.</Message>
552
+ ))}
553
+ </Section>
554
+
555
+ <Section>
556
+ <SectionHead>
557
+ <button
558
+ type="button"
559
+ className="toggle"
560
+ aria-expanded={openSection === "images"}
561
+ onClick={() =>
562
+ setOpenSection(openSection === "images" ? null : "images")
563
+ }
564
+ >
565
+ Imágenes
566
+ <span className="count">
567
+ {chosen.images.length} de {offered.images.length}
568
+ </span>
569
+ </button>
570
+ {offered.images.length > 0 && (
571
+ <button
572
+ type="button"
573
+ className="all"
574
+ onClick={() =>
575
+ toggleAll(
576
+ "images",
577
+ offered.images.map((item) => item.id)
578
+ )
579
+ }
580
+ >
581
+ {chosen.images.length === offered.images.length
582
+ ? "Ninguna"
583
+ : "Todas"}
584
+ </button>
585
+ )}
586
+ </SectionHead>
587
+
588
+ {openSection === "images" &&
589
+ (offered.images.length ? (
590
+ <ImageGrid>
591
+ {offered.images.map((image) => (
592
+ <label
593
+ key={image.id}
594
+ className={
595
+ chosen.images.includes(image.id) ? "picked" : ""
596
+ }
597
+ >
598
+ <input
599
+ type="checkbox"
600
+ checked={chosen.images.includes(image.id)}
601
+ onChange={() => toggleChoice("images", image.id)}
602
+ />
603
+ <img src={image.src} alt={image.imageType || ""} />
604
+ <span>{image.imageType || "Sin tipo"}</span>
605
+ </label>
606
+ ))}
607
+ </ImageGrid>
608
+ ) : (
609
+ <Message>El producto padre no tiene imágenes.</Message>
610
+ ))}
611
+ </Section>
612
+
613
+ <Section>
614
+ <SectionHead>
615
+ <button
616
+ type="button"
617
+ className="toggle"
618
+ aria-expanded={openSection === "retailers"}
619
+ onClick={() =>
620
+ setOpenSection(
621
+ openSection === "retailers" ? null : "retailers"
622
+ )
623
+ }
624
+ >
410
625
  Cadenas y categorías
411
- <span className="hint">
412
- Las parejas cadena-categoría del padre se copian a la variante.
626
+ <span className="count">
627
+ {chosen.categoryRetailers.length} seleccionada(s)
413
628
  </span>
414
- </span>
415
- </label>
629
+ </button>
630
+ </SectionHead>
631
+
632
+ {openSection === "retailers" && (
633
+ <>
634
+ <List>
635
+ {[
636
+ ...offered.categoryRetailers.map((item) => ({
637
+ ...item,
638
+ clave: `${item.retailerId}-${item.categoryId}`,
639
+ delPadre: true,
640
+ })),
641
+ ...extraRetailers.map((item) => ({
642
+ ...item,
643
+ delPadre: false,
644
+ })),
645
+ ].map((item) => (
646
+ <li key={item.clave}>
647
+ <label>
648
+ <input
649
+ type="checkbox"
650
+ checked={chosen.categoryRetailers.includes(
651
+ item.clave
652
+ )}
653
+ onChange={() =>
654
+ toggleChoice("categoryRetailers", item.clave)
655
+ }
656
+ />
657
+ <span>
658
+ {item.retailerName}
659
+ <span className="meta">
660
+ {item.categoryName || "Sin categoría"}
661
+ {!item.delPadre && " · no la tiene el padre"}
662
+ </span>
663
+ </span>
664
+ </label>
665
+ </li>
666
+ ))}
667
+ </List>
668
+
669
+ {/* Agregar una cadena que el padre no tiene. No es herencia:
670
+ la variante entra a esa cadena por su cuenta. */}
671
+ <AddRetailer>
672
+ <select
673
+ value={newRetailer}
674
+ onChange={(event) => setNewRetailer(event.target.value)}
675
+ >
676
+ <option value="">Agregar otra cadena…</option>
677
+ {(retailersAvailable || []).map((item) => (
678
+ <option
679
+ key={item.id ?? item.value}
680
+ value={item.id ?? item.value}
681
+ >
682
+ {item.name ?? item.label}
683
+ </option>
684
+ ))}
685
+ </select>
686
+ <select
687
+ value={newCategory}
688
+ onChange={(event) => setNewCategory(event.target.value)}
689
+ >
690
+ <option value="">Categoría…</option>
691
+ {(categoriesAvailable || []).map((item) => (
692
+ <option
693
+ key={item.id ?? item.value}
694
+ value={item.id ?? item.value}
695
+ >
696
+ {item.name ?? item.label}
697
+ </option>
698
+ ))}
699
+ </select>
700
+ <button
701
+ type="button"
702
+ onClick={addRetailer}
703
+ disabled={!newRetailer || !newCategory}
704
+ >
705
+ Agregar
706
+ </button>
707
+ </AddRetailer>
708
+ </>
709
+ )}
416
710
  </Section>
417
711
 
418
712
  {/* Es la diferencia entre link y propagate, y es la que sorprende:
@@ -464,8 +758,8 @@ export const LinkVariantsModal = ({
464
758
  // Con el padre sin atributos heredables, ligar sigue siendo
465
759
  // válido: se heredan imágenes, descripciones y cadenas.
466
760
  (inheritMode === "selected" &&
467
- attributes.length > 0 &&
468
- !chosenAttributes.length)
761
+ offered.attributes.length > 0 &&
762
+ !chosen.attributes.length)
469
763
  }
470
764
  >
471
765
  {loading ? "Ligando…" : "Ligar"}
@@ -262,3 +262,123 @@ export const Footer = styled.div`
262
262
  cursor: default;
263
263
  }
264
264
  `;
265
+
266
+ export const SectionHead = styled.div`
267
+ display: flex;
268
+ align-items: center;
269
+ gap: 8px;
270
+ margin-bottom: 6px;
271
+
272
+ .toggle {
273
+ flex: 1;
274
+ display: flex;
275
+ align-items: center;
276
+ gap: 8px;
277
+ border: none;
278
+ background: none;
279
+ padding: 0;
280
+ cursor: pointer;
281
+ font-family: ${FontFamily.Raleway_600};
282
+ font-size: 13px;
283
+ color: ${GlobalColors.deep_gray};
284
+ text-align: left;
285
+ }
286
+
287
+ .toggle .count {
288
+ font-family: ${FontFamily.Raleway_500};
289
+ font-size: 11px;
290
+ color: ${GlobalColors.gray};
291
+ }
292
+
293
+ .all {
294
+ border: none;
295
+ background: none;
296
+ padding: 0;
297
+ cursor: pointer;
298
+ font-family: ${FontFamily.Raleway_500};
299
+ font-size: 11px;
300
+ color: ${GlobalColors.original_magenta};
301
+ text-decoration: underline;
302
+ }
303
+ `;
304
+
305
+ export const ImageGrid = styled.div`
306
+ display: grid;
307
+ grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
308
+ gap: 8px;
309
+ margin-bottom: 16px;
310
+
311
+ label {
312
+ position: relative;
313
+ display: flex;
314
+ flex-direction: column;
315
+ gap: 4px;
316
+ padding: 6px;
317
+ border: 1px solid ${GlobalColors.s2};
318
+ border-radius: 6px;
319
+ cursor: pointer;
320
+ }
321
+
322
+ label.picked {
323
+ border-color: ${GlobalColors.original_magenta};
324
+ background-color: rgba(${GlobalColors.rgb_color_pink}, 0.06);
325
+ }
326
+
327
+ label input {
328
+ position: absolute;
329
+ top: 8px;
330
+ left: 8px;
331
+ }
332
+
333
+ label img {
334
+ width: 100%;
335
+ height: 72px;
336
+ object-fit: contain;
337
+ background-color: ${GlobalColors.s2};
338
+ border-radius: 4px;
339
+ }
340
+
341
+ label span {
342
+ font-size: 10px;
343
+ color: ${GlobalColors.gray};
344
+ text-align: center;
345
+ overflow: hidden;
346
+ text-overflow: ellipsis;
347
+ white-space: nowrap;
348
+ }
349
+ `;
350
+
351
+ export const AddRetailer = styled.div`
352
+ display: flex;
353
+ gap: 8px;
354
+ margin-bottom: 16px;
355
+
356
+ select {
357
+ flex: 1;
358
+ height: 32px;
359
+ padding: 0 8px;
360
+ border: 1px solid ${GlobalColors.s2};
361
+ border-radius: 6px;
362
+ font-family: ${FontFamily.Raleway};
363
+ font-size: 12px;
364
+ color: ${GlobalColors.deep_gray};
365
+ background-color: ${GlobalColors.white};
366
+ }
367
+
368
+ button {
369
+ height: 32px;
370
+ padding: 0 14px;
371
+ border: 1px solid ${GlobalColors.original_magenta};
372
+ border-radius: 6px;
373
+ background: none;
374
+ font-family: ${FontFamily.Raleway_600};
375
+ font-size: 12px;
376
+ color: ${GlobalColors.original_magenta};
377
+ cursor: pointer;
378
+ }
379
+
380
+ button:disabled {
381
+ opacity: 0.4;
382
+ cursor: default;
383
+ }
384
+ `;
@@ -159,9 +159,22 @@ export const ProviderProductEditionProvider = ({ children }) => {
159
159
  type: "SET_UPDATED_DATASHEETS_INPUTS",
160
160
  payload: [],
161
161
  });
162
+ // En una variante, todo atributo que el hijo toca deja de ser heredado:
163
+ // update-articles-data le pone is_overridden = 1 en la base. Si aquí solo
164
+ // se refresca el valor, el chip sigue diciendo "Heredado" hasta la
165
+ // siguiente carga de la ficha, y el usuario ve una marca que ya es falsa.
166
+ const esVariante =
167
+ state.services?.datasheets?.variant?.isVariant ??
168
+ !!state.product?.parentArticleId;
169
+
162
170
  dispatch({
163
171
  type: "UPDATE_DATASHEETS_INPUTS",
164
- payload: state.updated_datasheets_inputs,
172
+ payload: esVariante
173
+ ? state.updated_datasheets_inputs.map((item) => ({
174
+ ...item,
175
+ isOverridden: 1,
176
+ }))
177
+ : state.updated_datasheets_inputs,
165
178
  });
166
179
 
167
180
  // Mostrar modal de éxito