contentoh-components-library 21.6.36 → 21.6.38

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.
@@ -16,6 +16,10 @@ var _jsxRuntime = require("react/jsx-runtime");
16
16
  *
17
17
  * El estado "propio" es el que carga información —ese campo se separó del padre—
18
18
  * así que es el que lleva acento y el que ofrece la acción de volver a heredar.
19
+ *
20
+ * OJO: "propio" ya NO significa "a salvo de la propagación". Desde el cambio del
21
+ * 2026-09-01 la propagación sobrescribe también estos valores; el aviso de que va
22
+ * a pasar vive en el diálogo de propagación, no aquí. El tooltip lo dice así.
19
23
  * "Heredado" es el caso normal y se mantiene discreto: si gritara en cada campo,
20
24
  * la ficha entera se volvería ruido.
21
25
  *
@@ -51,9 +55,9 @@ var InheritanceTag = function InheritanceTag(_ref) {
51
55
  className: "inheritance-tag ".concat(state, " ").concat(className),
52
56
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
53
57
  className: "tag-label",
54
- title: isOverridden ? "Valor propio de este producto hijo. La propagación del padre no lo modifica." : "Valor heredado".concat(parentUpc ? " del producto ".concat(parentUpc) : " del producto padre", "."),
58
+ title: isOverridden ? "Valor capturado en este producto hijo, distinto al del padre. Si el padre propaga este atributo, se reemplaza." : "Valor heredado".concat(parentUpc ? " del producto ".concat(parentUpc) : " del producto padre", "."),
55
59
  children: isOverridden ? "Propio" : "Heredado"
56
- }), isOverridden && onRestore && /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
60
+ }), Boolean(isOverridden) && onRestore && /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
57
61
  type: "button",
58
62
  className: "tag-action",
59
63
  onClick: onRestore,
@@ -420,7 +420,7 @@ var TagAndInput = function TagAndInput(_ref) {
420
420
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Container, {
421
421
  inputType: inputType,
422
422
  className: "input-container",
423
- children: [(label === null || label === void 0 ? void 0 : label.length) && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
423
+ children: [Boolean(label === null || label === void 0 ? void 0 : label.length) && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
424
424
  className: "title-container",
425
425
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
426
426
  className: "titles",
@@ -54,7 +54,7 @@ var TagAndInputv2 = function TagAndInputv2(_ref) {
54
54
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Container, {
55
55
  inputType: inputType,
56
56
  className: "input-container",
57
- children: [(label === null || label === void 0 ? void 0 : label.length) && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
57
+ children: [Boolean(label === null || label === void 0 ? void 0 : label.length) && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
58
58
  className: "title-container",
59
59
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ScreenHeaderv.ScreenHeaderv2, {
60
60
  text: label,
@@ -150,8 +150,7 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
150
150
  var _useState21 = (0, _react.useState)({
151
151
  attributes: [],
152
152
  descriptions: [],
153
- images: [],
154
- categoryRetailers: []
153
+ images: []
155
154
  }),
156
155
  _useState22 = (0, _slicedToArray2.default)(_useState21, 2),
157
156
  offered = _useState22[0],
@@ -160,8 +159,7 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
160
159
  var _useState23 = (0, _react.useState)({
161
160
  attributes: [],
162
161
  descriptions: [],
163
- images: [],
164
- categoryRetailers: []
162
+ images: []
165
163
  }),
166
164
  _useState24 = (0, _slicedToArray2.default)(_useState23, 2),
167
165
  chosen = _useState24[0],
@@ -177,7 +175,6 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
177
175
 
178
176
 
179
177
  var _useState27 = (0, _react.useState)({
180
- categoryRetailers: null,
181
178
  descriptions: null,
182
179
  attributes: null,
183
180
  images: null
@@ -451,19 +448,16 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
451
448
  listas = {
452
449
  attributes: data.attributes || [],
453
450
  descriptions: data.descriptions || [],
454
- images: data.images || [],
455
- categoryRetailers: data.categoryRetailers || []
451
+ images: data.images || []
456
452
  };
457
453
  setOffered(listas);
458
454
  setNameScope((_data$nameScope = data.nameScope) !== null && _data$nameScope !== void 0 ? _data$nameScope : null);
459
455
  setChosen({
460
456
  attributes: [],
461
457
  descriptions: [],
462
- images: [],
463
- categoryRetailers: []
458
+ images: []
464
459
  });
465
460
  setModes({
466
- categoryRetailers: null,
467
461
  descriptions: null,
468
462
  attributes: null,
469
463
  images: null
@@ -544,34 +538,28 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
544
538
  });
545
539
 
546
540
  var buildInherit = function buildInherit(seleccion) {
547
- var parejas = offered.categoryRetailers.map(function (item) {
548
- return {
549
- clave: "".concat(item.retailerId, "-").concat(item.categoryId),
550
- retailerId: item.retailerId,
551
- categoryId: item.categoryId
552
- };
553
- });
554
541
  return {
555
542
  attributes: seleccion.modes.attributes === "all" ? "all-filled" : seleccion.chosen.attributes,
556
543
  descriptions: seleccion.modes.descriptions === "all" ? "all" : seleccion.chosen.descriptions,
557
544
  images: seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
558
- categoryRetailers: parejas.filter(function (par) {
559
- return seleccion.chosen.categoryRetailers.includes(par.clave);
560
- }).map(function (_ref6) {
561
- var retailerId = _ref6.retailerId,
562
- categoryId = _ref6.categoryId;
563
- return {
564
- retailerId: retailerId,
565
- categoryId: categoryId
566
- };
567
- })
545
+
546
+ /*
547
+ Siempre "all", nunca una lista.
548
+ El hijo se queda con las cadenas del padre, sin preguntar. No es una
549
+ preferencia: es lo que hace que la copia de atributos SE VEA. La ficha
550
+ arma su lista desde `attribute_manager` acotado por las cadenas del
551
+ propio producto, así que un valor copiado a un atributo que la cadena del
552
+ hijo no configura queda escrito y invisible.
553
+ Se manda la palabra y no la lista para que signifique "todas" también si
554
+ alguien agregó una cadena al padre entre abrir el modal y guardar.
555
+ */
556
+ categoryRetailers: "all"
568
557
  };
569
558
  };
570
559
 
571
560
  var seleccionVacia = function seleccionVacia() {
572
561
  return {
573
562
  modes: {
574
- categoryRetailers: null,
575
563
  descriptions: null,
576
564
  attributes: null,
577
565
  images: null
@@ -579,8 +567,7 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
579
567
  chosen: {
580
568
  attributes: [],
581
569
  descriptions: [],
582
- images: [],
583
- categoryRetailers: []
570
+ images: []
584
571
  }
585
572
  };
586
573
  };
@@ -606,7 +593,7 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
606
593
  };
607
594
 
608
595
  var submit = /*#__PURE__*/function () {
609
- var _ref7 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5() {
596
+ var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5() {
610
597
  var inherit, inheritByChild, body, data;
611
598
  return _regenerator.default.wrap(function _callee5$(_context5) {
612
599
  while (1) {
@@ -678,24 +665,19 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
678
665
  }));
679
666
 
680
667
  return function submit() {
681
- return _ref7.apply(this, arguments);
668
+ return _ref6.apply(this, arguments);
682
669
  };
683
670
  }(); // Una dimensión está decidida si el usuario eligió un modo, o si el padre no
684
671
  // ofrece nada en ella —no se puede pedir que decida sobre una lista vacía—.
685
672
 
686
673
 
687
674
  var decidida = function decidida(dimension) {
688
- if (modes[dimension] !== null) return true;
689
-
690
- if (dimension === "categoryRetailers") {
691
- return offered.categoryRetailers.length === 0;
692
- } // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
693
-
675
+ if (modes[dimension] !== null) return true; // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
694
676
 
695
677
  return dimension === "images" ? imagenesUtiles.length === 0 : offered[dimension].length === 0;
696
678
  };
697
679
 
698
- var DIMENSIONES = ["categoryRetailers", "descriptions", "attributes", "images"]; // Con pestañas, que la que estoy viendo esté completa no basta: ligar escribe
680
+ var DIMENSIONES = ["descriptions", "attributes", "images"]; // Con pestañas, que la que estoy viendo esté completa no basta: ligar escribe
699
681
  // en todos, así que todos tienen que estar decididos. Si no, un producto se
700
682
  // ligaría sin heredar nada solo porque nadie abrió su pestaña.
701
683
 
@@ -707,24 +689,19 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
707
689
  if (!guardado) return false;
708
690
  return DIMENSIONES.every(function (dimension) {
709
691
  if (guardado.modes[dimension] !== null) return true;
710
-
711
- if (dimension === "categoryRetailers") {
712
- return offered.categoryRetailers.length === 0;
713
- }
714
-
715
692
  return dimension === "images" ? imagenesUtiles.length === 0 : offered[dimension].length === 0;
716
693
  });
717
694
  };
718
695
 
719
696
  var todoDecidido = step === "inherit" && (mode === "parent" ? pushToParent !== null : applyToAll ? DIMENSIONES.every(decidida) : selected.every(decididoPara));
720
- var faltantes = [["categoryRetailers", "cadenas"], ["descriptions", "descripciones"], ["attributes", "atributos"], ["images", "imágenes"]].filter(function (_ref8) {
721
- var _ref9 = (0, _slicedToArray2.default)(_ref8, 1),
722
- clave = _ref9[0];
697
+ var faltantes = [["descriptions", "descripciones"], ["attributes", "atributos"], ["images", "imágenes"]].filter(function (_ref7) {
698
+ var _ref8 = (0, _slicedToArray2.default)(_ref7, 1),
699
+ clave = _ref8[0];
723
700
 
724
701
  return !decidida(clave);
725
- }).map(function (_ref10) {
726
- var _ref11 = (0, _slicedToArray2.default)(_ref10, 2),
727
- nombre = _ref11[1];
702
+ }).map(function (_ref9) {
703
+ var _ref10 = (0, _slicedToArray2.default)(_ref9, 2),
704
+ nombre = _ref10[1];
728
705
 
729
706
  return nombre;
730
707
  });
@@ -1003,73 +980,6 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
1003
980
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
1004
981
  className: "paso",
1005
982
  children: "1"
1006
- }), " Cadenas y categor\xEDas"]
1007
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Choices, {
1008
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("label", {
1009
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("input", {
1010
- type: "radio",
1011
- name: "categoryRetailers-".concat(activeChild !== null && activeChild !== void 0 ? activeChild : "todos"),
1012
- checked: modes.categoryRetailers === "all",
1013
- onChange: function onChange() {
1014
- return chooseMode("categoryRetailers", "all", offered.categoryRetailers.map(function (item) {
1015
- return "".concat(item.retailerId, "-").concat(item.categoryId);
1016
- }));
1017
- }
1018
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
1019
- children: ["Todas las del producto padre", /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
1020
- className: "hint",
1021
- children: [offered.categoryRetailers.length, " cadena(s)."]
1022
- })]
1023
- })]
1024
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("label", {
1025
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("input", {
1026
- type: "radio",
1027
- name: "categoryRetailers-".concat(activeChild !== null && activeChild !== void 0 ? activeChild : "todos"),
1028
- checked: modes.categoryRetailers === "some",
1029
- onChange: function onChange() {
1030
- return chooseMode("categoryRetailers", "some", []);
1031
- }
1032
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
1033
- children: ["Elegir cu\xE1les", /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
1034
- className: "hint",
1035
- children: "Solo se puede heredar de las que ya tiene el padre."
1036
- })]
1037
- })]
1038
- })]
1039
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_styles.List, {
1040
- children: offered.categoryRetailers.map(function (item) {
1041
- return (0, _objectSpread7.default)((0, _objectSpread7.default)({}, item), {}, {
1042
- clave: "".concat(item.retailerId, "-").concat(item.categoryId)
1043
- });
1044
- }).map(function (item) {
1045
- return /*#__PURE__*/(0, _jsxRuntime.jsx)("li", {
1046
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("label", {
1047
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("input", {
1048
- type: "checkbox",
1049
- disabled: modes.categoryRetailers === "all",
1050
- checked: chosen.categoryRetailers.includes(item.clave),
1051
- onChange: function onChange() {
1052
- return toggleChoice("categoryRetailers", item.clave);
1053
- }
1054
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)("img", {
1055
- className: "logo",
1056
- src: (0, _helper.getRetailerPicture)(item.retailerId, 24, 24),
1057
- alt: ""
1058
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
1059
- children: [item.retailerName, /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
1060
- className: "meta",
1061
- children: item.categoryName || "Sin categoría"
1062
- })]
1063
- })]
1064
- })
1065
- }, item.clave);
1066
- })
1067
- })]
1068
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Block, {
1069
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("h3", {
1070
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
1071
- className: "paso",
1072
- children: "2"
1073
983
  }), " Descripciones"]
1074
984
  }), offered.descriptions.length ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
1075
985
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Choices, {
@@ -1136,7 +1046,7 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
1136
1046
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("h3", {
1137
1047
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
1138
1048
  className: "paso",
1139
- children: "3"
1049
+ children: "2"
1140
1050
  }), " Atributos de la ficha t\xE9cnica"]
1141
1051
  }), offered.attributes.length ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
1142
1052
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Choices, {
@@ -1208,7 +1118,7 @@ var LinkVariantsModal = function LinkVariantsModal(_ref) {
1208
1118
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("h3", {
1209
1119
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
1210
1120
  className: "paso",
1211
- children: "4"
1121
+ children: "3"
1212
1122
  }), " Im\xE1genes"]
1213
1123
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Choices, {
1214
1124
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("label", {
@@ -51,7 +51,7 @@ var SearchRow = _styledComponents.default.form(_templateObject6 || (_templateObj
51
51
 
52
52
  exports.SearchRow = SearchRow;
53
53
 
54
- var List = _styledComponents.default.ul(_templateObject7 || (_templateObject7 = (0, _taggedTemplateLiteral2.default)(["\n list-style: none;\n margin: 0 0 16px;\n padding: 0;\n border: 1px solid ", ";\n border-radius: 6px;\n\n li + li {\n border-top: 1px solid ", ";\n }\n\n label {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n cursor: pointer;\n font-size: 13px;\n color: ", ";\n }\n\n .meta {\n display: block;\n font-size: 12px;\n color: ", ";\n }\n\n .logo {\n width: 24px;\n height: 24px;\n object-fit: contain;\n border-radius: 3px;\n flex-shrink: 0;\n }\n\n li {\n display: flex;\n align-items: center;\n }\n\n li > label {\n flex: 1;\n min-width: 0;\n }\n\n .count {\n margin-left: auto;\n font-size: 12px;\n color: ", ";\n white-space: nowrap;\n }\n"])), _variables.GlobalColors.s2, _variables.GlobalColors.s2, _variables.GlobalColors.deep_gray, _variables.GlobalColors.gray, PINK);
54
+ var List = _styledComponents.default.ul(_templateObject7 || (_templateObject7 = (0, _taggedTemplateLiteral2.default)(["\n list-style: none;\n margin: 0 0 16px;\n padding: 0;\n border: 1px solid ", ";\n border-radius: 6px;\n\n li + li {\n border-top: 1px solid ", ";\n }\n\n label {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n cursor: pointer;\n font-size: 13px;\n color: ", ";\n }\n\n .meta {\n display: block;\n font-size: 12px;\n color: ", ";\n }\n\n li {\n display: flex;\n align-items: center;\n }\n\n li > label {\n flex: 1;\n min-width: 0;\n }\n\n .count {\n margin-left: auto;\n font-size: 12px;\n color: ", ";\n white-space: nowrap;\n }\n"])), _variables.GlobalColors.s2, _variables.GlobalColors.s2, _variables.GlobalColors.deep_gray, _variables.GlobalColors.gray, PINK);
55
55
 
56
56
  exports.List = List;
57
57
  var AttributeList = (0, _styledComponents.default)(List)(_templateObject8 || (_templateObject8 = (0, _taggedTemplateLiteral2.default)(["\n max-height: 220px;\n overflow-y: auto;\n\n .note {\n display: block;\n font-size: 11px;\n font-style: italic;\n color: ", ";\n }\n\n .value {\n margin-left: auto;\n max-width: 45%;\n font-size: 12px;\n color: ", ";\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n"])), _variables.GlobalColors.gray, _variables.GlobalColors.gray);
@@ -9,6 +9,8 @@ exports.PropagateChangesModal = void 0;
9
9
 
10
10
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/toConsumableArray"));
11
11
 
12
+ var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectSpread2"));
13
+
12
14
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/slicedToArray"));
13
15
 
14
16
  var _react = require("react");
@@ -27,7 +29,10 @@ var _jsxRuntime = require("react/jsx-runtime");
27
29
 
28
30
  props (* requerido):
29
31
  - *changedAttributes: [{ id, name, value }] lo que se acaba de editar
30
- - *variants: [{ articleId, name, upc }] los hijos activos del padre
32
+ - *variants: [{ articleId, name, upc, overriddenAttributes }] los hijos
33
+ activos del padre. `overriddenAttributes` son los atributos que ese hijo
34
+ capturó a mano; la propagación los sobrescribe, y este diálogo es el
35
+ único lugar donde el usuario puede enterarse antes de que pase.
31
36
  - *onCancel: cerrar sin guardar nada
32
37
  - *onConfirm: (propagation | null) => void
33
38
  null → guardar solo el padre
@@ -67,6 +72,31 @@ var PropagateChangesModal = function PropagateChangesModal(_ref) {
67
72
  var destinatarios = scope === "all" ? variants.length : chosenChildren.length;
68
73
  var puedePropagar = chosenAttributes.length > 0 && destinatarios > 0;
69
74
  var enSegundoPlano = destinatarios > UMBRAL_SEGUNDO_PLANO;
75
+ /*
76
+ A quién se le va a pisar un valor capturado a mano.
77
+ La propagación sobrescribe: eso se decidió a propósito, porque el usuario ya
78
+ eligió atributo por atributo y hijo por hijo. Pero sobrescribir en silencio
79
+ es otra cosa, y desde que se quitaron las etiquetas de «Heredado» y «Propio»
80
+ este diálogo es el único momento en que alguien puede saberlo.
81
+ Se recalcula con lo que está marcado ahora, no al abrir: si el usuario
82
+ desmarca el atributo conflictivo, el aviso desaparece — que es justo la
83
+ salida que el aviso debería estar ofreciendo.
84
+ */
85
+
86
+ var destinos = scope === "all" ? variants : variants.filter(function (item) {
87
+ return chosenChildren.includes(item.articleId);
88
+ });
89
+ var conValorPropio = destinos.map(function (hijo) {
90
+ var _hijo$overriddenAttri;
91
+
92
+ return (0, _objectSpread2.default)((0, _objectSpread2.default)({}, hijo), {}, {
93
+ pisados: ((_hijo$overriddenAttri = hijo.overriddenAttributes) !== null && _hijo$overriddenAttri !== void 0 ? _hijo$overriddenAttri : []).filter(function (id) {
94
+ return chosenAttributes.includes(Number(id));
95
+ })
96
+ });
97
+ }).filter(function (hijo) {
98
+ return hijo.pisados.length > 0;
99
+ });
70
100
 
71
101
  var toggleAttribute = function toggleAttribute(id) {
72
102
  setChosenAttributes(function (current) {
@@ -173,8 +203,11 @@ var PropagateChangesModal = function PropagateChangesModal(_ref) {
173
203
  })
174
204
  }, variant.articleId);
175
205
  })
176
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_styles.Note, {
177
- children: "Un hijo que haya editado alguno de estos atributos por su cuenta conserva su valor. Para devolverlo al del padre est\xE1 la acci\xF3n \xABVolver a heredar\xBB en la ficha de ese hijo."
206
+ }), conValorPropio.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Note, {
207
+ className: "accent",
208
+ children: [conValorPropio.length === 1 ? "Un hijo tiene un valor propio en lo que elegiste y se va a reemplazar: " : "".concat(conValorPropio.length, " hijos tienen valores propios en lo que elegiste y se van a reemplazar: "), conValorPropio.map(function (hijo) {
209
+ return "".concat(hijo.name || "Producto ".concat(hijo.articleId), " (").concat(hijo.pisados.length, ")");
210
+ }).join(", "), ". Si no quieres pisarlos, desmarca ese atributo o ese hijo."]
178
211
  }), enSegundoPlano && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_styles.Note, {
179
212
  className: "accent",
180
213
  children: ["Son m\xE1s de ", UMBRAL_SEGUNDO_PLANO, " hijos, as\xED que la propagaci\xF3n corre en segundo plano: el guardado termina antes de que todas est\xE9n actualizadas."]
@@ -1597,7 +1597,8 @@ var RetailerProductEditionView = function RetailerProductEditionView(_ref) {
1597
1597
 
1598
1598
  var handleOnClickSave = /*#__PURE__*/function () {
1599
1599
  var _ref12 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee8() {
1600
- var esPadre, cambios;
1600
+ var _familia$variants, cambios, familia, hijos;
1601
+
1601
1602
  return _regenerator.default.wrap(function _callee8$(_context8) {
1602
1603
  while (1) {
1603
1604
  switch (_context8.prev = _context8.next) {
@@ -1611,7 +1612,7 @@ var RetailerProductEditionView = function RetailerProductEditionView(_ref) {
1611
1612
 
1612
1613
  case 2:
1613
1614
  _context8.t0 = state.active_tab;
1614
- _context8.next = _context8.t0 === "Descripción" ? 5 : _context8.t0 === "Ficha técnica" ? 8 : _context8.t0 === "Imágenes" ? 17 : 20;
1615
+ _context8.next = _context8.t0 === "Descripción" ? 5 : _context8.t0 === "Ficha técnica" ? 8 : _context8.t0 === "Imágenes" ? 32 : 35;
1615
1616
  break;
1616
1617
 
1617
1618
  case 5:
@@ -1619,57 +1620,134 @@ var RetailerProductEditionView = function RetailerProductEditionView(_ref) {
1619
1620
  return saveDescriptions(token);
1620
1621
 
1621
1622
  case 7:
1622
- return _context8.abrupt("break", 21);
1623
+ return _context8.abrupt("break", 36);
1623
1624
 
1624
1625
  case 8:
1625
- // Guardar un padre con hijos abre la decisión de propagar. Sin este
1626
- // paso el cambio se queda solo en el padre y los hijos se van
1627
- // separando de él sin que nadie lo note.
1628
- esPadre = hijosDelProducto > 0;
1629
1626
  cambios = cambiosDeFichaTecnica();
1630
1627
 
1631
- if (!(esPadre && cambios.length > 0)) {
1632
- _context8.next = 14;
1628
+ if (!(cambios.length === 0)) {
1629
+ _context8.next = 13;
1633
1630
  break;
1634
1631
  }
1635
1632
 
1633
+ _context8.next = 12;
1634
+ return saveDatasheets(token);
1635
+
1636
+ case 12:
1637
+ return _context8.abrupt("break", 36);
1638
+
1639
+ case 13:
1640
+ /*
1641
+ Guardar un padre con hijos abre la decisión de propagar. Sin este paso
1642
+ el cambio se queda solo en el padre y los hijos se van separando de él
1643
+ sin que nadie lo note.
1644
+ Si tiene hijos se resuelve AQUÍ, esperando la respuesta, en vez de
1645
+ mirar la copia en memoria. `family` es una consulta asíncrona: si
1646
+ todavía no llegó, o si falló, `hijosDelProducto` vale 0 — o sea que
1647
+ «todavía no sé» se leía igual que «no tiene hijos», y el padre se
1648
+ guardaba sin ofrecer la propagación. En silencio, que es lo peor: el
1649
+ usuario creía haber bajado el cambio a los hijos.
1650
+ La ficha de la aplicación de clientes no tiene el problema porque ahí
1651
+ `state.product.variantsCount` viene del listado; aquí el producto sale
1652
+ del endpoint de reportes, que no trae el parentesco.
1653
+ */
1636
1654
  setPreparandoPropagacion(true);
1637
- loadFamily().then(function () {
1638
- return setPendingPropagation({
1639
- cambios: cambios
1640
- });
1641
- }).finally(function () {
1642
- return setPreparandoPropagacion(false);
1655
+ familia = null;
1656
+ _context8.prev = 15;
1657
+ _context8.next = 18;
1658
+ return loadFamily(true);
1659
+
1660
+ case 18:
1661
+ familia = _context8.sent;
1662
+
1663
+ case 19:
1664
+ _context8.prev = 19;
1665
+ setPreparandoPropagacion(false);
1666
+ return _context8.finish(19);
1667
+
1668
+ case 22:
1669
+ if (!(!familia || familia.error)) {
1670
+ _context8.next = 25;
1671
+ break;
1672
+ }
1673
+
1674
+ dispatch({
1675
+ type: "SET_MODAL",
1676
+ payload: {
1677
+ show: true,
1678
+ title: "No se pudo consultar la familia",
1679
+ message: "No pudimos saber si este producto tiene hijos, así que no sabemos si hay que bajarles el cambio. Vuelve a intentar el guardado; si sigue fallando, guarda y revisa los hijos a mano.",
1680
+ image: _genericModalWarning.default,
1681
+ buttons: [{
1682
+ text: "Entendido",
1683
+ buttonType: "general-white-button",
1684
+ action: function action() {
1685
+ return dispatch({
1686
+ type: "SET_MODAL",
1687
+ payload: {
1688
+ show: false
1689
+ }
1690
+ });
1691
+ }
1692
+ }, {
1693
+ text: "Guardar solo este producto",
1694
+ buttonType: "general-button-default",
1695
+ action: function action() {
1696
+ dispatch({
1697
+ type: "SET_MODAL",
1698
+ payload: {
1699
+ show: false
1700
+ }
1701
+ });
1702
+ saveDatasheets(token);
1703
+ }
1704
+ }]
1705
+ }
1643
1706
  });
1644
1707
  return _context8.abrupt("return");
1645
1708
 
1646
- case 14:
1647
- _context8.next = 16;
1709
+ case 25:
1710
+ hijos = ((_familia$variants = familia.variants) !== null && _familia$variants !== void 0 ? _familia$variants : []).filter(function (v) {
1711
+ return !v.isCurrent;
1712
+ });
1713
+
1714
+ if (!(hijos.length > 0)) {
1715
+ _context8.next = 29;
1716
+ break;
1717
+ }
1718
+
1719
+ setPendingPropagation({
1720
+ cambios: cambios
1721
+ });
1722
+ return _context8.abrupt("return");
1723
+
1724
+ case 29:
1725
+ _context8.next = 31;
1648
1726
  return saveDatasheets(token);
1649
1727
 
1650
- case 16:
1651
- return _context8.abrupt("break", 21);
1728
+ case 31:
1729
+ return _context8.abrupt("break", 36);
1652
1730
 
1653
- case 17:
1654
- _context8.next = 19;
1731
+ case 32:
1732
+ _context8.next = 34;
1655
1733
  return updateImages(token);
1656
1734
 
1657
- case 19:
1658
- return _context8.abrupt("break", 21);
1735
+ case 34:
1736
+ return _context8.abrupt("break", 36);
1659
1737
 
1660
- case 20:
1661
- return _context8.abrupt("break", 21);
1738
+ case 35:
1739
+ return _context8.abrupt("break", 36);
1662
1740
 
1663
- case 21:
1664
- _context8.next = 23;
1741
+ case 36:
1742
+ _context8.next = 38;
1665
1743
  return loadData(false);
1666
1744
 
1667
- case 23:
1745
+ case 38:
1668
1746
  case "end":
1669
1747
  return _context8.stop();
1670
1748
  }
1671
1749
  }
1672
- }, _callee8);
1750
+ }, _callee8, null, [[15,, 19, 22]]);
1673
1751
  }));
1674
1752
 
1675
1753
  return function handleOnClickSave() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.6.36",
3
+ "version": "21.6.38",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -7,6 +7,10 @@ import { Container } from "./styles";
7
7
  *
8
8
  * El estado "propio" es el que carga información —ese campo se separó del padre—
9
9
  * así que es el que lleva acento y el que ofrece la acción de volver a heredar.
10
+ *
11
+ * OJO: "propio" ya NO significa "a salvo de la propagación". Desde el cambio del
12
+ * 2026-09-01 la propagación sobrescribe también estos valores; el aviso de que va
13
+ * a pasar vive en el diálogo de propagación, no aquí. El tooltip lo dice así.
10
14
  * "Heredado" es el caso normal y se mantiene discreto: si gritara en cada campo,
11
15
  * la ficha entera se volvería ruido.
12
16
  *
@@ -41,14 +45,20 @@ export const InheritanceTag = ({
41
45
  className="tag-label"
42
46
  title={
43
47
  isOverridden
44
- ? "Valor propio de este producto hijo. La propagación del padre no lo modifica."
48
+ ? "Valor capturado en este producto hijo, distinto al del padre. Si el padre propaga este atributo, se reemplaza."
45
49
  : `Valor heredado${parentUpc ? ` del producto ${parentUpc}` : " del producto padre"}.`
46
50
  }
47
51
  >
48
52
  {isOverridden ? "Propio" : "Heredado"}
49
53
  </span>
50
54
 
51
- {isOverridden && onRestore && (
55
+ {/* `Boolean`, no `isOverridden` a secas.
56
+
57
+ `is_overridden` llega como NÚMERO (0/1) desde la API y el reducer. Con
58
+ `{isOverridden && ...}`, un 0 se evalúa a 0 y React IMPRIME el texto
59
+ «0» — ese era el cero que aparecía en el hueco de la acción justo
60
+ después de pulsar «Volver a heredar», que es cuando el valor pasa a 0. */}
61
+ {Boolean(isOverridden) && onRestore && (
52
62
  <button type="button" className="tag-action" onClick={onRestore}>
53
63
  {restoreLabel}
54
64
  </button>
@@ -321,7 +321,9 @@ export const TagAndInput = ({
321
321
  className={"input-container"}
322
322
  key={`generalTagInput-${inputType}`}
323
323
  >
324
- {label?.length && (
324
+ {/* `Boolean`, no `label?.length`: con la etiqueta en cadena vacía la
325
+ expresión valía 0 y React imprimía un «0» en la ficha. */}
326
+ {Boolean(label?.length) && (
325
327
  <div className="title-container">
326
328
  <div className="titles">
327
329
  <ScreenHeader
@@ -46,7 +46,9 @@ export const TagAndInputv2 = ({
46
46
  className={"input-container"}
47
47
  key={`generalTagInput-${inputType}`}
48
48
  >
49
- {label?.length && (
49
+ {/* `Boolean`, no `label?.length`: con la etiqueta en cadena vacía la
50
+ expresión valía 0 y React imprimía un «0» en la ficha. */}
51
+ {Boolean(label?.length) && (
50
52
  <div className="title-container">
51
53
  <ScreenHeaderv2
52
54
  text={label}
@@ -2,7 +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 { getProductImage, getRetailerPicture } from "../../../_utils/helper";
5
+ import { getProductImage } from "../../../_utils/helper";
6
6
 
7
7
  import {
8
8
  Overlay,
@@ -106,13 +106,11 @@ export const LinkVariantsModal = ({
106
106
  attributes: [],
107
107
  descriptions: [],
108
108
  images: [],
109
- categoryRetailers: [],
110
109
  });
111
110
  const [chosen, setChosen] = useState({
112
111
  attributes: [],
113
112
  descriptions: [],
114
113
  images: [],
115
- categoryRetailers: [],
116
114
  });
117
115
  const [nameScope, setNameScope] = useState(null);
118
116
 
@@ -121,7 +119,6 @@ export const LinkVariantsModal = ({
121
119
  // se pudiera ligar sin haber mirado nada, que es justo lo que hay que evitar
122
120
  // en una operación que sobrescribe valores capturados.
123
121
  const [modes, setModes] = useState({
124
- categoryRetailers: null,
125
122
  descriptions: null,
126
123
  attributes: null,
127
124
  images: null,
@@ -285,7 +282,6 @@ export const LinkVariantsModal = ({
285
282
  attributes: data.attributes || [],
286
283
  descriptions: data.descriptions || [],
287
284
  images: data.images || [],
288
- categoryRetailers: data.categoryRetailers || [],
289
285
  };
290
286
 
291
287
  setOffered(listas);
@@ -294,10 +290,8 @@ export const LinkVariantsModal = ({
294
290
  attributes: [],
295
291
  descriptions: [],
296
292
  images: [],
297
- categoryRetailers: [],
298
293
  });
299
294
  setModes({
300
- categoryRetailers: null,
301
295
  descriptions: null,
302
296
  attributes: null,
303
297
  images: null,
@@ -367,12 +361,6 @@ export const LinkVariantsModal = ({
367
361
  );
368
362
 
369
363
  const buildInherit = (seleccion) => {
370
- const parejas = offered.categoryRetailers.map((item) => ({
371
- clave: `${item.retailerId}-${item.categoryId}`,
372
- retailerId: item.retailerId,
373
- categoryId: item.categoryId,
374
- }));
375
-
376
364
  return {
377
365
  attributes:
378
366
  seleccion.modes.attributes === "all"
@@ -384,15 +372,24 @@ export const LinkVariantsModal = ({
384
372
  : seleccion.chosen.descriptions,
385
373
  images:
386
374
  seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
387
- categoryRetailers: parejas
388
- .filter((par) => seleccion.chosen.categoryRetailers.includes(par.clave))
389
- .map(({ retailerId, categoryId }) => ({ retailerId, categoryId })),
375
+ /*
376
+ Siempre "all", nunca una lista.
377
+
378
+ El hijo se queda con las cadenas del padre, sin preguntar. No es una
379
+ preferencia: es lo que hace que la copia de atributos SE VEA. La ficha
380
+ arma su lista desde `attribute_manager` acotado por las cadenas del
381
+ propio producto, así que un valor copiado a un atributo que la cadena del
382
+ hijo no configura queda escrito y invisible.
383
+
384
+ Se manda la palabra y no la lista para que signifique "todas" también si
385
+ alguien agregó una cadena al padre entre abrir el modal y guardar.
386
+ */
387
+ categoryRetailers: "all",
390
388
  };
391
389
  };
392
390
 
393
391
  const seleccionVacia = () => ({
394
392
  modes: {
395
- categoryRetailers: null,
396
393
  descriptions: null,
397
394
  attributes: null,
398
395
  images: null,
@@ -401,7 +398,6 @@ export const LinkVariantsModal = ({
401
398
  attributes: [],
402
399
  descriptions: [],
403
400
  images: [],
404
- categoryRetailers: [],
405
401
  },
406
402
  });
407
403
 
@@ -478,21 +474,13 @@ export const LinkVariantsModal = ({
478
474
  // ofrece nada en ella —no se puede pedir que decida sobre una lista vacía—.
479
475
  const decidida = (dimension) => {
480
476
  if (modes[dimension] !== null) return true;
481
- if (dimension === "categoryRetailers") {
482
- return offered.categoryRetailers.length === 0;
483
- }
484
477
  // Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
485
478
  return dimension === "images"
486
479
  ? imagenesUtiles.length === 0
487
480
  : offered[dimension].length === 0;
488
481
  };
489
482
 
490
- const DIMENSIONES = [
491
- "categoryRetailers",
492
- "descriptions",
493
- "attributes",
494
- "images",
495
- ];
483
+ const DIMENSIONES = ["descriptions", "attributes", "images"];
496
484
 
497
485
  // Con pestañas, que la que estoy viendo esté completa no basta: ligar escribe
498
486
  // en todos, así que todos tienen que estar decididos. Si no, un producto se
@@ -503,9 +491,6 @@ export const LinkVariantsModal = ({
503
491
 
504
492
  return DIMENSIONES.every((dimension) => {
505
493
  if (guardado.modes[dimension] !== null) return true;
506
- if (dimension === "categoryRetailers") {
507
- return offered.categoryRetailers.length === 0;
508
- }
509
494
  return dimension === "images"
510
495
  ? imagenesUtiles.length === 0
511
496
  : offered[dimension].length === 0;
@@ -521,7 +506,6 @@ export const LinkVariantsModal = ({
521
506
  : selected.every(decididoPara));
522
507
 
523
508
  const faltantes = [
524
- ["categoryRetailers", "cadenas"],
525
509
  ["descriptions", "descripciones"],
526
510
  ["attributes", "atributos"],
527
511
  ["images", "imágenes"],
@@ -858,96 +842,32 @@ export const LinkVariantsModal = ({
858
842
  </>
859
843
  )}
860
844
 
861
- {/* El orden no es casual: primero se decide en qué cadenas vive el
862
- hijo, porque de eso depende qué atributos le aplican. Las
863
- imágenes van al final: son la decisión más visual y la que menos
864
- arrastra a las demás. */}
845
+ {/* Ya no se pregunta por cadenas ni categorías.
865
846
 
866
- <Block>
867
- <h3>
868
- <span className="paso">1</span> Cadenas y categorías
869
- </h3>
870
- <Choices>
871
- <label>
872
- <input
873
- type="radio"
874
- name={`categoryRetailers-${activeChild ?? "todos"}`}
875
- checked={modes.categoryRetailers === "all"}
876
- onChange={() =>
877
- chooseMode(
878
- "categoryRetailers",
879
- "all",
880
- offered.categoryRetailers.map(
881
- (item) => `${item.retailerId}-${item.categoryId}`
882
- )
883
- )
884
- }
885
- />
886
- <span>
887
- Todas las del producto padre
888
- <span className="hint">
889
- {offered.categoryRetailers.length} cadena(s).
890
- </span>
891
- </span>
892
- </label>
893
- <label>
894
- <input
895
- type="radio"
896
- name={`categoryRetailers-${activeChild ?? "todos"}`}
897
- checked={modes.categoryRetailers === "some"}
898
- onChange={() =>
899
- chooseMode("categoryRetailers", "some", [])
900
- }
901
- />
902
- <span>
903
- Elegir cuáles
904
- <span className="hint">
905
- Solo se puede heredar de las que ya tiene el padre.
906
- </span>
907
- </span>
908
- </label>
909
- </Choices>
847
+ El valor de un atributo vive en `article_data` por (artículo,
848
+ versión, atributo): no está partido por cadena, así que copiar
849
+ el atributo 27 del padre al hijo es lo mismo sin importar en
850
+ qué cadenas esté cada uno. Elegir cadenas aquí no cambiaba nada
851
+ de eso; solo servía para decidir en qué cadenas vive el hijo, y
852
+ la respuesta correcta resultó ser siempre «las del padre».
910
853
 
911
- <List>
912
- {offered.categoryRetailers
913
- .map((item) => ({
914
- ...item,
915
- clave: `${item.retailerId}-${item.categoryId}`,
916
- }))
917
- .map((item) => (
918
- <li key={item.clave}>
919
- <label>
920
- <input
921
- type="checkbox"
922
- disabled={modes.categoryRetailers === "all"}
923
- checked={chosen.categoryRetailers.includes(
924
- item.clave
925
- )}
926
- onChange={() =>
927
- toggleChoice("categoryRetailers", item.clave)
928
- }
929
- />
930
- <img
931
- className="logo"
932
- src={getRetailerPicture(item.retailerId, 24, 24)}
933
- alt=""
934
- />
935
- <span>
936
- {item.retailerName}
937
- <span className="meta">
938
- {item.categoryName || "Sin categoría"}
939
- </span>
940
- </span>
941
- </label>
942
- </li>
943
- ))}
944
- </List>
854
+ Medido en dev sobre las 24 familias reales: cuando el hijo
855
+ comparte al menos una cadena con el padre, 0 atributos quedan
856
+ sin mostrarse; cuando no comparte ninguna, quedan entre 3 y 31.
857
+ 91 de 394 en total (23%). La razón es que la ficha arma su lista
858
+ desde `attribute_manager` acotado por las cadenas DEL PROPIO
859
+ producto, así que un valor copiado a un atributo que su cadena
860
+ no configura se escribe pero no se ve. Heredar todas las del
861
+ padre es lo que hace visible la copia.
945
862
 
946
- </Block>
863
+ Por eso `buildInherit` manda "all" y esto ya no se pregunta.
864
+
865
+ El orden de lo que queda no es casual: las imágenes van al
866
+ final, son la decisión más visual y la que menos arrastra. */}
947
867
 
948
868
  <Block>
949
869
  <h3>
950
- <span className="paso">2</span> Descripciones
870
+ <span className="paso">1</span> Descripciones
951
871
  </h3>
952
872
  {offered.descriptions.length ? (
953
873
  <>
@@ -1030,7 +950,7 @@ export const LinkVariantsModal = ({
1030
950
 
1031
951
  <Block>
1032
952
  <h3>
1033
- <span className="paso">3</span> Atributos de la ficha técnica
953
+ <span className="paso">2</span> Atributos de la ficha técnica
1034
954
  </h3>
1035
955
  {offered.attributes.length ? (
1036
956
  <>
@@ -1127,7 +1047,7 @@ export const LinkVariantsModal = ({
1127
1047
  {imagenesUtiles.length > 0 && (
1128
1048
  <Block>
1129
1049
  <h3>
1130
- <span className="paso">4</span> Imágenes
1050
+ <span className="paso">3</span> Imágenes
1131
1051
  </h3>
1132
1052
  <Choices>
1133
1053
  <label>
@@ -184,14 +184,6 @@ export const List = styled.ul`
184
184
  color: ${GlobalColors.gray};
185
185
  }
186
186
 
187
- .logo {
188
- width: 24px;
189
- height: 24px;
190
- object-fit: contain;
191
- border-radius: 3px;
192
- flex-shrink: 0;
193
- }
194
-
195
187
  li {
196
188
  display: flex;
197
189
  align-items: center;
@@ -21,7 +21,10 @@ import {
21
21
 
22
22
  props (* requerido):
23
23
  - *changedAttributes: [{ id, name, value }] lo que se acaba de editar
24
- - *variants: [{ articleId, name, upc }] los hijos activos del padre
24
+ - *variants: [{ articleId, name, upc, overriddenAttributes }] los hijos
25
+ activos del padre. `overriddenAttributes` son los atributos que ese hijo
26
+ capturó a mano; la propagación los sobrescribe, y este diálogo es el
27
+ único lugar donde el usuario puede enterarse antes de que pase.
25
28
  - *onCancel: cerrar sin guardar nada
26
29
  - *onConfirm: (propagation | null) => void
27
30
  null → guardar solo el padre
@@ -49,6 +52,31 @@ export const PropagateChangesModal = ({
49
52
  const puedePropagar = chosenAttributes.length > 0 && destinatarios > 0;
50
53
  const enSegundoPlano = destinatarios > UMBRAL_SEGUNDO_PLANO;
51
54
 
55
+ /*
56
+ A quién se le va a pisar un valor capturado a mano.
57
+
58
+ La propagación sobrescribe: eso se decidió a propósito, porque el usuario ya
59
+ eligió atributo por atributo y hijo por hijo. Pero sobrescribir en silencio
60
+ es otra cosa, y desde que se quitaron las etiquetas de «Heredado» y «Propio»
61
+ este diálogo es el único momento en que alguien puede saberlo.
62
+
63
+ Se recalcula con lo que está marcado ahora, no al abrir: si el usuario
64
+ desmarca el atributo conflictivo, el aviso desaparece — que es justo la
65
+ salida que el aviso debería estar ofreciendo.
66
+ */
67
+ const destinos = scope === "all"
68
+ ? variants
69
+ : variants.filter((item) => chosenChildren.includes(item.articleId));
70
+
71
+ const conValorPropio = destinos
72
+ .map((hijo) => ({
73
+ ...hijo,
74
+ pisados: (hijo.overriddenAttributes ?? []).filter((id) =>
75
+ chosenAttributes.includes(Number(id))
76
+ ),
77
+ }))
78
+ .filter((hijo) => hijo.pisados.length > 0);
79
+
52
80
  const toggleAttribute = (id) => {
53
81
  setChosenAttributes((current) =>
54
82
  current.includes(id)
@@ -155,13 +183,20 @@ export const PropagateChangesModal = ({
155
183
  </List>
156
184
  )}
157
185
 
158
- {/* La diferencia con "ligar", y la que sorprende si no se dice: aquí
159
- lo que el hijo editó a mano NO se pisa. */}
160
- <Note>
161
- Un hijo que haya editado alguno de estos atributos por su cuenta
162
- conserva su valor. Para devolverlo al del padre está la acción
163
- «Volver a heredar» en la ficha de ese hijo.
164
- </Note>
186
+ {/* Lo que sorprende si no se dice: aquí se pisa lo que el hijo
187
+ hubiera capturado a mano. Se nombra a quién, no solo cuántos: con
188
+ «2 hijos» nadie sabe si le importa, con los nombres sí. */}
189
+ {conValorPropio.length > 0 && (
190
+ <Note className="accent">
191
+ {conValorPropio.length === 1
192
+ ? "Un hijo tiene un valor propio en lo que elegiste y se va a reemplazar: "
193
+ : `${conValorPropio.length} hijos tienen valores propios en lo que elegiste y se van a reemplazar: `}
194
+ {conValorPropio
195
+ .map((hijo) => `${hijo.name || `Producto ${hijo.articleId}`} (${hijo.pisados.length})`)
196
+ .join(", ")}
197
+ . Si no quieres pisarlos, desmarca ese atributo o ese hijo.
198
+ </Note>
199
+ )}
165
200
 
166
201
  {enSegundoPlano && (
167
202
  <Note className="accent">
@@ -1131,17 +1131,73 @@ const RetailerProductEditionView = ({
1131
1131
  await saveDescriptions(token);
1132
1132
  break;
1133
1133
  case "Ficha técnica": {
1134
- // Guardar un padre con hijos abre la decisión de propagar. Sin este
1135
- // paso el cambio se queda solo en el padre y los hijos se van
1136
- // separando de él sin que nadie lo note.
1137
- const esPadre = hijosDelProducto > 0;
1138
1134
  const cambios = cambiosDeFichaTecnica();
1139
1135
 
1140
- if (esPadre && cambios.length > 0) {
1141
- setPreparandoPropagacion(true);
1142
- loadFamily()
1143
- .then(() => setPendingPropagation({ cambios }))
1144
- .finally(() => setPreparandoPropagacion(false));
1136
+ if (cambios.length === 0) {
1137
+ await saveDatasheets(token);
1138
+ break;
1139
+ }
1140
+
1141
+ /*
1142
+ Guardar un padre con hijos abre la decisión de propagar. Sin este paso
1143
+ el cambio se queda solo en el padre y los hijos se van separando de él
1144
+ sin que nadie lo note.
1145
+
1146
+ Si tiene hijos se resuelve AQUÍ, esperando la respuesta, en vez de
1147
+ mirar la copia en memoria. `family` es una consulta asíncrona: si
1148
+ todavía no llegó, o si falló, `hijosDelProducto` vale 0 — o sea que
1149
+ «todavía no sé» se leía igual que «no tiene hijos», y el padre se
1150
+ guardaba sin ofrecer la propagación. En silencio, que es lo peor: el
1151
+ usuario creía haber bajado el cambio a los hijos.
1152
+
1153
+ La ficha de la aplicación de clientes no tiene el problema porque ahí
1154
+ `state.product.variantsCount` viene del listado; aquí el producto sale
1155
+ del endpoint de reportes, que no trae el parentesco.
1156
+ */
1157
+ setPreparandoPropagacion(true);
1158
+ let familia = null;
1159
+ try {
1160
+ familia = await loadFamily(true);
1161
+ } finally {
1162
+ setPreparandoPropagacion(false);
1163
+ }
1164
+
1165
+ // No se pudo saber si tiene hijos. Guardar de todas formas repetiría el
1166
+ // defecto que se está corrigiendo, así que se dice y se deja la decisión.
1167
+ if (!familia || familia.error) {
1168
+ dispatch({
1169
+ type: "SET_MODAL",
1170
+ payload: {
1171
+ show: true,
1172
+ title: "No se pudo consultar la familia",
1173
+ message:
1174
+ "No pudimos saber si este producto tiene hijos, así que no sabemos si hay que bajarles el cambio. Vuelve a intentar el guardado; si sigue fallando, guarda y revisa los hijos a mano.",
1175
+ image: warningIcon,
1176
+ buttons: [
1177
+ {
1178
+ text: "Entendido",
1179
+ buttonType: "general-white-button",
1180
+ action: () =>
1181
+ dispatch({ type: "SET_MODAL", payload: { show: false } }),
1182
+ },
1183
+ {
1184
+ text: "Guardar solo este producto",
1185
+ buttonType: "general-button-default",
1186
+ action: () => {
1187
+ dispatch({ type: "SET_MODAL", payload: { show: false } });
1188
+ saveDatasheets(token);
1189
+ },
1190
+ },
1191
+ ],
1192
+ },
1193
+ });
1194
+ return;
1195
+ }
1196
+
1197
+ const hijos = (familia.variants ?? []).filter((v) => !v.isCurrent);
1198
+
1199
+ if (hijos.length > 0) {
1200
+ setPendingPropagation({ cambios });
1145
1201
  return;
1146
1202
  }
1147
1203