contentoh-components-library 21.6.21 → 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.21",
3
+ "version": "21.6.23",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -14,11 +14,16 @@ import { Container } from "./styles";
14
14
  * que llamarlo "Heredado" es falso además de ruidoso. Sin esto la ficha de una
15
15
  * variante sale con el chip repetido en cada campo, incluidos los obligatorios en
16
16
  * rojo que están sin capturar, y la marca deja de distinguir nada.
17
+ *
18
+ * Tampoco la lleva un atributo NO heredable. El UPC y el SKU son la identidad del
19
+ * producto: el servidor los excluye de link y propagate, así que pintarlos como
20
+ * "Propio" y ofrecer "Volver a heredar" daría un botón incapaz de hacer nada.
17
21
  */
18
22
  export const InheritanceTag = ({
19
23
  isVariant = false,
20
24
  isOverridden = false,
21
25
  hasValue = true,
26
+ inheritable = true,
22
27
  parentUpc,
23
28
  onRestore,
24
29
  restoreLabel = "Volver a heredar",
@@ -26,6 +31,7 @@ export const InheritanceTag = ({
26
31
  }) => {
27
32
  if (!isVariant) return null;
28
33
  if (!hasValue) return null;
34
+ if (!inheritable) return null;
29
35
 
30
36
  const state = isOverridden ? "own" : "inherited";
31
37
 
@@ -47,6 +47,7 @@ export const TagAndInput = ({
47
47
  boxOnboardingData,
48
48
  isVariant,
49
49
  isOverridden,
50
+ inheritable,
50
51
  parentUpc,
51
52
  onRestoreInheritance,
52
53
  }) => {
@@ -332,6 +333,7 @@ export const TagAndInput = ({
332
333
  isVariant={isVariant}
333
334
  isOverridden={isOverridden}
334
335
  hasValue={`${value ?? ""}`.trim() !== ""}
336
+ inheritable={inheritable}
335
337
  parentUpc={parentUpc}
336
338
  onRestore={
337
339
  onRestoreInheritance
@@ -36,6 +36,7 @@ export const TagAndInputv2 = ({
36
36
  showMissingChars,
37
37
  isVariant,
38
38
  isOverridden,
39
+ inheritable,
39
40
  parentUpc,
40
41
  onRestoreInheritance,
41
42
  }) => {
@@ -64,6 +65,7 @@ export const TagAndInputv2 = ({
64
65
  isVariant={isVariant}
65
66
  isOverridden={isOverridden}
66
67
  hasValue={`${value ?? ""}`.trim() !== ""}
68
+ inheritable={inheritable}
67
69
  parentUpc={parentUpc}
68
70
  onRestore={
69
71
  onRestoreInheritance
@@ -234,6 +234,7 @@ export const InputGroup = ({
234
234
  showTooltip={true}
235
235
  isVariant={variant?.isVariant}
236
236
  isOverridden={dataInputs[input]?.isOverridden}
237
+ inheritable={dataInputs[input]?.inheritable}
237
238
  parentUpc={variant?.parent?.upc}
238
239
  onRestoreInheritance={onRestoreInheritance}
239
240
  auditClass={
@@ -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"}