contentoh-components-library 21.6.34 → 21.6.36

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.
@@ -18,9 +18,9 @@ var _templateObject;
18
18
  // Rosa ContentOh, el mismo del modal de ligar productos.
19
19
  var PINK = "#E13AA8";
20
20
 
21
- 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 .action-buttons{\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\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\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-radius: 4px;\n text-align: left;\n font-family: ", ";\n font-size: 12px;\n color: ", ";\n }\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) {
21
+ 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 .action-buttons{\n display: flex;\n flex-direction: column;\n gap: 1rem;\n }\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\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 /* La fila es un bot\xF3n que navega; desvincular tiene que quedar FUERA de\n \xE9l \u2014un bot\xF3n dentro de otro no es HTML v\xE1lido y el clic se confunde\u2014. */\n .family-item {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .family-item .family-row {\n flex: 1;\n min-width: 0;\n }\n\n .desvincular {\n flex-shrink: 0;\n border: none;\n background: none;\n padding: 4px 6px;\n cursor: pointer;\n font-family: ", ";\n font-size: 10px;\n color: ", ";\n text-decoration: underline;\n white-space: nowrap;\n }\n\n .desvincular:hover {\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) {
22
22
  var headerTop = _ref.headerTop;
23
23
  return headerTop;
24
- }, _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, PINK, _variables.FontFamily.Raleway_600, PINK, _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, PINK, _variables.GlobalColors.gray, _variables.GlobalColors.gray);
24
+ }, _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, PINK, _variables.FontFamily.Raleway_600, PINK, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, _variables.GlobalColors.s2, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, _variables.FontFamily.Raleway_500, _variables.GlobalColors.gray, PINK, _variables.FontFamily.Raleway_500, _variables.GlobalColors.deep_gray, _variables.GlobalColors.s2, PINK, _variables.GlobalColors.gray, _variables.GlobalColors.gray);
25
25
 
26
26
  exports.Container = Container;
@@ -145,10 +145,15 @@ var normalizeProduct = function normalizeProduct(product) {
145
145
 
146
146
  var retailersIdsInOrder = productData.statusByRetailer ? productData.statusByRetailer.map(function (status) {
147
147
  return status.retailer_id;
148
- }) : [];
149
- var categoryRetailerInOrder = categoryRetailerNormalized.filter(function (rel) {
148
+ }) : []; // Un producto sin ninguna relación de cadena+categoría activa llega con
149
+ // `categoryRetailer` en null, y filtrar sobre null tumbaba la pantalla entera.
150
+ // Se vuelve alcanzable al saltar entre productos de una familia, que es cuando
151
+ // se abren fichas que nadie eligió desde el listado. Se conserva el null para
152
+ // no cambiar lo que ya leen los consumidores.
153
+
154
+ var categoryRetailerInOrder = categoryRetailerNormalized ? categoryRetailerNormalized.filter(function (rel) {
150
155
  return retailersIdsInOrder.includes(rel.id_retailer);
151
- });
156
+ }) : null;
152
157
  return {
153
158
  id_article: productData.id_article || article.id_article || 0,
154
159
  upc: productData.upc || article.upc || null,
package/dist/index.js CHANGED
@@ -1122,7 +1122,7 @@ Object.keys(_index85).forEach(function (key) {
1122
1122
  });
1123
1123
  });
1124
1124
 
1125
- var _index86 = require("./components/organisms/ProductImageModal/index");
1125
+ var _index86 = require("./components/organisms/UnlinkVariantModal/index");
1126
1126
 
1127
1127
  Object.keys(_index86).forEach(function (key) {
1128
1128
  if (key === "default" || key === "__esModule") return;
@@ -1135,7 +1135,7 @@ Object.keys(_index86).forEach(function (key) {
1135
1135
  });
1136
1136
  });
1137
1137
 
1138
- var _index87 = require("./components/organisms/Chat/index");
1138
+ var _index87 = require("./components/organisms/ProductImageModal/index");
1139
1139
 
1140
1140
  Object.keys(_index87).forEach(function (key) {
1141
1141
  if (key === "default" || key === "__esModule") return;
@@ -1148,7 +1148,7 @@ Object.keys(_index87).forEach(function (key) {
1148
1148
  });
1149
1149
  });
1150
1150
 
1151
- var _index88 = require("./components/organisms/Modal/index");
1151
+ var _index88 = require("./components/organisms/Chat/index");
1152
1152
 
1153
1153
  Object.keys(_index88).forEach(function (key) {
1154
1154
  if (key === "default" || key === "__esModule") return;
@@ -1161,7 +1161,7 @@ Object.keys(_index88).forEach(function (key) {
1161
1161
  });
1162
1162
  });
1163
1163
 
1164
- var _index89 = require("./components/organisms/OrderDetail/index");
1164
+ var _index89 = require("./components/organisms/Modal/index");
1165
1165
 
1166
1166
  Object.keys(_index89).forEach(function (key) {
1167
1167
  if (key === "default" || key === "__esModule") return;
@@ -1174,6 +1174,19 @@ Object.keys(_index89).forEach(function (key) {
1174
1174
  });
1175
1175
  });
1176
1176
 
1177
+ var _index90 = require("./components/organisms/OrderDetail/index");
1178
+
1179
+ Object.keys(_index90).forEach(function (key) {
1180
+ if (key === "default" || key === "__esModule") return;
1181
+ if (key in exports && exports[key] === _index90[key]) return;
1182
+ Object.defineProperty(exports, key, {
1183
+ enumerable: true,
1184
+ get: function get() {
1185
+ return _index90[key];
1186
+ }
1187
+ });
1188
+ });
1189
+
1177
1190
  var _RangeCalendar = require("./components/organisms/RangeCalendar");
1178
1191
 
1179
1192
  Object.keys(_RangeCalendar).forEach(function (key) {
@@ -1200,28 +1213,28 @@ Object.keys(_TableResizable).forEach(function (key) {
1200
1213
  });
1201
1214
  });
1202
1215
 
1203
- var _index90 = require("./components/organisms/SideModal/index");
1216
+ var _index91 = require("./components/organisms/SideModal/index");
1204
1217
 
1205
- Object.keys(_index90).forEach(function (key) {
1218
+ Object.keys(_index91).forEach(function (key) {
1206
1219
  if (key === "default" || key === "__esModule") return;
1207
- if (key in exports && exports[key] === _index90[key]) return;
1220
+ if (key in exports && exports[key] === _index91[key]) return;
1208
1221
  Object.defineProperty(exports, key, {
1209
1222
  enumerable: true,
1210
1223
  get: function get() {
1211
- return _index90[key];
1224
+ return _index91[key];
1212
1225
  }
1213
1226
  });
1214
1227
  });
1215
1228
 
1216
- var _index91 = require("./components/organisms/GridProducts/index");
1229
+ var _index92 = require("./components/organisms/GridProducts/index");
1217
1230
 
1218
- Object.keys(_index91).forEach(function (key) {
1231
+ Object.keys(_index92).forEach(function (key) {
1219
1232
  if (key === "default" || key === "__esModule") return;
1220
- if (key in exports && exports[key] === _index91[key]) return;
1233
+ if (key in exports && exports[key] === _index92[key]) return;
1221
1234
  Object.defineProperty(exports, key, {
1222
1235
  enumerable: true,
1223
1236
  get: function get() {
1224
- return _index91[key];
1237
+ return _index92[key];
1225
1238
  }
1226
1239
  });
1227
1240
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.6.34",
3
+ "version": "21.6.36",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -0,0 +1,141 @@
1
+ import { useState } from "react";
2
+ import axios from "axios";
3
+
4
+ import { readPayload, ApiError } from "../LinkVariantsModal/api";
5
+ import { Overlay, Panel, Header, Body, Message, Footer } from "./styles";
6
+
7
+ /*
8
+ Confirmación para romper un vínculo padre/hijo.
9
+
10
+ El servidor ya sabía hacerlo desde el principio —POST /variants/unlink—, pero
11
+ no había forma de pedirlo desde ninguna pantalla: la única manera de deshacer
12
+ una liga era DESACTIVAR al padre, que suelta a todos los hijos de golpe y no
13
+ se puede revertir. Esto es la versión de a uno.
14
+
15
+ La acción se pide siempre con el id del HIJO, que es quien tiene el vínculo en
16
+ `article.parent_article_id`. Por eso las dos direcciones —un hijo que se suelta
17
+ y un padre que suelta a un hijo— son la misma llamada; lo único que cambia es
18
+ el texto, porque no es lo mismo lo que el usuario cree estar haciendo.
19
+
20
+ props (* requerido):
21
+ - *childArticleId: el hijo que pierde el vínculo
22
+ - childName / childUpc: para nombrarlo en el diálogo
23
+ - parentName / parentUpc: de quién se desprende
24
+ - mode: "self" (esta ficha deja de ser hija) | "child" (esta ficha suelta a un hijo)
25
+ - *token, endpoint
26
+ - *onClose, onUnlinked
27
+ */
28
+ export const UnlinkVariantModal = ({
29
+ childArticleId,
30
+ childName,
31
+ childUpc,
32
+ parentName,
33
+ parentUpc,
34
+ mode = "self",
35
+ token,
36
+ endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
37
+ onClose,
38
+ onUnlinked,
39
+ }) => {
40
+ const [loading, setLoading] = useState(false);
41
+ const [error, setError] = useState(null);
42
+
43
+ const nombreHijo = childName || `Producto ${childArticleId}`;
44
+ const nombrePadre = parentName || "el producto padre";
45
+
46
+ const submit = async () => {
47
+ setLoading(true);
48
+ setError(null);
49
+ try {
50
+ const res = await axios.post(
51
+ `${endpoint}/unlink`,
52
+ { articleIds: [childArticleId] },
53
+ { headers: { Authorization: token } }
54
+ );
55
+
56
+ const data = readPayload(res);
57
+ onUnlinked?.({ ...data, mode, childArticleId });
58
+ onClose?.();
59
+ } catch (err) {
60
+ // El transporte siempre responde 200; el error de negocio viaja dentro.
61
+ setError(
62
+ err instanceof ApiError || err?.name === "ApiError"
63
+ ? err.message
64
+ : "No se pudo desvincular. Intenta de nuevo."
65
+ );
66
+ setLoading(false);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <Overlay onClick={loading ? undefined : onClose}>
72
+ <Panel onClick={(event) => event.stopPropagation()}>
73
+ <Header>
74
+ <h2>
75
+ {mode === "self"
76
+ ? "Dejar de ser hijo de otro producto"
77
+ : "Desvincular este hijo"}
78
+ </h2>
79
+ <p>
80
+ {mode === "self" ? (
81
+ <>
82
+ «{nombreHijo}» dejará de colgar de «{nombrePadre}»
83
+ {parentUpc ? ` (UPC ${parentUpc})` : ""}.
84
+ </>
85
+ ) : (
86
+ <>
87
+ «{nombreHijo}»{childUpc ? ` (UPC ${childUpc})` : ""} dejará de
88
+ colgar de este producto.
89
+ </>
90
+ )}
91
+ </p>
92
+ </Header>
93
+
94
+ <Body>
95
+ <ul>
96
+ {/* Lo primero que la gente teme, y no pasa. */}
97
+ <li>
98
+ <strong>No se borra nada.</strong> Todo lo que heredó —atributos,
99
+ descripciones, imágenes y cadenas— se queda como información
100
+ propia del producto.
101
+ </li>
102
+ <li>
103
+ Desaparecen las marcas de «Heredado» y «Propio»: sin padre dejan de
104
+ querer decir algo.
105
+ </li>
106
+ <li>
107
+ Deja de recibir la propagación
108
+ {mode === "self" ? " de su padre" : " de este producto"}: lo que se
109
+ edite de un lado ya no baja al otro.
110
+ </li>
111
+ <li className="aviso">
112
+ Volver a ligarlos es a mano, y hay que elegir otra vez qué hereda.
113
+ Desvincular no se deshace solo.
114
+ </li>
115
+ </ul>
116
+ </Body>
117
+
118
+ {error && <Message>{error}</Message>}
119
+
120
+ <Footer>
121
+ <button
122
+ type="button"
123
+ className="secondary"
124
+ onClick={onClose}
125
+ disabled={loading}
126
+ >
127
+ Cancelar
128
+ </button>
129
+ <button
130
+ type="button"
131
+ className="primary"
132
+ onClick={submit}
133
+ disabled={loading}
134
+ >
135
+ {loading ? "Desvinculando…" : "Desvincular"}
136
+ </button>
137
+ </Footer>
138
+ </Panel>
139
+ </Overlay>
140
+ );
141
+ };
@@ -0,0 +1,106 @@
1
+ import styled from "styled-components";
2
+
3
+ import { GlobalColors, FontFamily } from "../../../global-files/variables";
4
+
5
+ const PINK = "#E13AA8";
6
+
7
+ export const Overlay = styled.div`
8
+ position: fixed;
9
+ inset: 0;
10
+ z-index: 1000;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ background-color: rgba(40, 31, 51, 0.4);
15
+ `;
16
+
17
+ export const Panel = styled.div`
18
+ width: min(460px, 92vw);
19
+ border-radius: 10px;
20
+ background-color: ${GlobalColors.white};
21
+ font-family: ${FontFamily.Raleway};
22
+ overflow: hidden;
23
+ `;
24
+
25
+ export const Header = styled.div`
26
+ padding: 18px 22px 10px;
27
+
28
+ h2 {
29
+ margin: 0;
30
+ font-family: ${FontFamily.Raleway_600};
31
+ font-size: 16px;
32
+ color: ${GlobalColors.deep_gray};
33
+ }
34
+
35
+ p {
36
+ margin: 6px 0 0;
37
+ font-size: 13px;
38
+ line-height: 1.4;
39
+ color: ${GlobalColors.gray};
40
+ }
41
+ `;
42
+
43
+ export const Body = styled.div`
44
+ padding: 4px 22px 8px;
45
+
46
+ ul {
47
+ margin: 0;
48
+ padding-left: 18px;
49
+ }
50
+
51
+ li {
52
+ margin-bottom: 6px;
53
+ font-size: 13px;
54
+ line-height: 1.4;
55
+ color: ${GlobalColors.deep_gray};
56
+ }
57
+
58
+ /* Lo único que no se puede deshacer solo va marcado: el resto son
59
+ consecuencias, esta es una advertencia. */
60
+ li.aviso {
61
+ color: ${PINK};
62
+ }
63
+ `;
64
+
65
+ export const Message = styled.p`
66
+ margin: 8px 22px 0;
67
+ padding: 10px 12px;
68
+ border-radius: 6px;
69
+ background-color: rgba(225, 58, 168, 0.1);
70
+ font-size: 13px;
71
+ line-height: 1.4;
72
+ color: ${PINK};
73
+ `;
74
+
75
+ export const Footer = styled.div`
76
+ display: flex;
77
+ justify-content: flex-end;
78
+ gap: 10px;
79
+ padding: 16px 22px 20px;
80
+
81
+ button {
82
+ height: 38px;
83
+ padding: 0 16px;
84
+ border-radius: 6px;
85
+ cursor: pointer;
86
+ font-family: ${FontFamily.Raleway_600};
87
+ font-size: 13px;
88
+ }
89
+
90
+ button:disabled {
91
+ opacity: 0.5;
92
+ cursor: default;
93
+ }
94
+
95
+ .secondary {
96
+ border: 1px solid ${GlobalColors.s2};
97
+ background: none;
98
+ color: ${GlobalColors.deep_gray};
99
+ }
100
+
101
+ .primary {
102
+ border: 1px solid ${PINK};
103
+ background-color: ${PINK};
104
+ color: ${GlobalColors.white};
105
+ }
106
+ `;
@@ -39,6 +39,7 @@ import { ImageDataTable } from "../../organisms/ImageDataTable";
39
39
  import { ImagePreviewer } from "../../organisms/ImagePreviewer";
40
40
  import { InputGroup } from "../../organisms/InputGroup";
41
41
  import { LinkVariantsModal } from "../../organisms/LinkVariantsModal";
42
+ import { UnlinkVariantModal } from "../../organisms/UnlinkVariantModal";
42
43
  import { PropagateChangesModal } from "../../organisms/PropagateChangesModal";
43
44
  import { readPayload } from "../../organisms/LinkVariantsModal/api";
44
45
  import { ProductImageModal } from "../../organisms/ProductImageModal";
@@ -781,8 +782,8 @@ const ProviderProductEditionView = ({
781
782
  productos no son familia, y una llamada por ficha para no mostrar nada es
782
783
  peso que paga todo el mundo por el caso de unos pocos.
783
784
  */
784
- const loadFamily = async () => {
785
- if (family) return family;
785
+ const loadFamily = async (forzar = false) => {
786
+ if (family && !forzar) return family;
786
787
  try {
787
788
  const res = await axios.post(
788
789
  `${process.env.REACT_APP_VARIANTS_ENDPOINT}/family`,
@@ -875,6 +876,71 @@ const ProviderProductEditionView = ({
875
876
  }
876
877
  };
877
878
 
879
+ /*
880
+ Qué vínculo se está por romper, o null si no hay diálogo abierto.
881
+
882
+ Guarda al HIJO, porque la relación vive en él (`article.parent_article_id`)
883
+ y es lo que pide la acción `unlink`. `mode` solo cambia el texto: no es lo
884
+ mismo soltarse de un padre que soltar a un hijo, aunque por dentro sea la
885
+ misma llamada.
886
+ */
887
+ const [porDesvincular, setPorDesvincular] = useState(null);
888
+
889
+ /*
890
+ Después de desvincular, esta ficha cambia de forma.
891
+
892
+ Si el producto se soltó de su padre, deja de ser hijo y vuelve a poder ligar
893
+ —el botón reaparece—. Si soltó a un hijo, pierde uno de la cuenta. En los dos
894
+ casos hay que repreguntar la familia: lo que se ve en la lista ya no es lo
895
+ que hay en el servidor.
896
+ */
897
+ const handleOnUnlinked = (result) => {
898
+ if (!result) return;
899
+
900
+ const updatedProduct =
901
+ result.mode === "self"
902
+ ? { ...state.product, parentArticleId: null, variantsCount: 0 }
903
+ : {
904
+ ...state.product,
905
+ variantsCount: Math.max((state.product?.variantsCount ?? 1) - 1, 0),
906
+ };
907
+
908
+ dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
909
+
910
+ try {
911
+ sessionStorage.setItem("productSelected", JSON.stringify(updatedProduct));
912
+
913
+ const productEdit = JSON.parse(sessionStorage.getItem("productEdit"));
914
+ if (productEdit) {
915
+ sessionStorage.setItem(
916
+ "productEdit",
917
+ JSON.stringify({ ...productEdit, product: updatedProduct })
918
+ );
919
+ }
920
+ } catch (error) {
921
+ console.error("unlinkVariant/sessionStorage: ", error);
922
+ }
923
+
924
+ /*
925
+ Si ya no queda familia, se cierra el desplegable: su botón desaparece con
926
+ el vínculo, y dejarlo abierto pinta una caja que dice «no tiene hijos» y
927
+ que nadie puede cerrar.
928
+ */
929
+ if (
930
+ !updatedProduct.parentArticleId &&
931
+ (updatedProduct.variantsCount ?? 0) === 0
932
+ ) {
933
+ setShowFamily(false);
934
+ }
935
+
936
+ loadFamily(true);
937
+
938
+ // Y los servicios: al desvincular, el servidor pone `is_overridden = 0` en
939
+ // todas las versiones, así que los chips de herencia tienen que dejar de
940
+ // dibujarse. Sin esto la ficha sigue mostrándolos hasta que se recarga.
941
+ setLinkNonce((n) => n + 1);
942
+ };
943
+
878
944
  const openVariant = async (articleId) => {
879
945
  setLoadingVariant(articleId);
880
946
  try {
@@ -950,8 +1016,14 @@ const ProviderProductEditionView = ({
950
1016
  };
951
1017
 
952
1018
  dispatch({ type: "SET_PRODUCT", payload: updatedProduct });
953
- // La lista cacheada quedó vieja: se descarta para que vuelva a pedirse.
954
- setFamily(null);
1019
+ /*
1020
+ Se vuelve a pedir, no se vacía.
1021
+
1022
+ `setFamily(null)` invalidaba la caché, pero la lista solo se pide al pulsar
1023
+ el desplegable: si estaba abierto al ligar, se quedaba en «Cargando
1024
+ familia…» para siempre, porque ya nadie iba a volver a pedirla.
1025
+ */
1026
+ loadFamily(true);
955
1027
  // Y los servicios, para que los chips reflejen la herencia recién creada.
956
1028
  setLinkNonce((n) => n + 1);
957
1029
 
@@ -1513,6 +1585,14 @@ const ProviderProductEditionView = ({
1513
1585
  onLinked={handleOnLinkedVariants}
1514
1586
  />
1515
1587
  )}
1588
+ {porDesvincular && (
1589
+ <UnlinkVariantModal
1590
+ {...porDesvincular}
1591
+ token={token}
1592
+ onClose={() => setPorDesvincular(null)}
1593
+ onUnlinked={handleOnUnlinked}
1594
+ />
1595
+ )}
1516
1596
  {showModal && (
1517
1597
  <ProductImageModal
1518
1598
  images={state.images_values}
@@ -1759,43 +1839,92 @@ const ProviderProductEditionView = ({
1759
1839
  </p>
1760
1840
  )}
1761
1841
 
1842
+ {/* Desvincular se ofrece donde se ve el vínculo, y solo en
1843
+ la dirección que corresponde a esta ficha: desde un hijo,
1844
+ sobre su padre —«me suelto»—; desde un padre, sobre cada
1845
+ hijo. A un hermano no: para eso se abre su ficha, donde la
1846
+ acción dice lo que de verdad va a pasar. */}
1762
1847
  {family?.parent && (
1763
- <button
1764
- type="button"
1765
- className="family-row parent"
1766
- disabled={loadingVariant !== null}
1767
- onClick={() => openVariant(family.parent.articleId)}
1768
- >
1769
- <span className="role">
1770
- {loadingVariant === family.parent.articleId
1771
- ? "Abriendo…"
1772
- : "Producto padre"}
1773
- </span>
1774
- <span className="name">{family.parent.name}</span>
1775
- <span className="upc">{family.parent.upc}</span>
1776
- </button>
1848
+ <div className="family-item">
1849
+ <button
1850
+ type="button"
1851
+ className="family-row parent"
1852
+ disabled={loadingVariant !== null}
1853
+ onClick={() => openVariant(family.parent.articleId)}
1854
+ >
1855
+ <span className="role">
1856
+ {loadingVariant === family.parent.articleId
1857
+ ? "Abriendo…"
1858
+ : "Producto padre"}
1859
+ </span>
1860
+ <span className="name">{family.parent.name}</span>
1861
+ <span className="upc">{family.parent.upc}</span>
1862
+ </button>
1863
+
1864
+ <button
1865
+ type="button"
1866
+ className="desvincular"
1867
+ title="Dejar de ser hijo de este producto"
1868
+ onClick={() =>
1869
+ setPorDesvincular({
1870
+ mode: "self",
1871
+ childArticleId: state.product?.id_article,
1872
+ childName: state.product?.name,
1873
+ childUpc: state.product?.upc,
1874
+ parentName: family.parent.name,
1875
+ parentUpc: family.parent.upc,
1876
+ })
1877
+ }
1878
+ >
1879
+ Desvincular
1880
+ </button>
1881
+ </div>
1777
1882
  )}
1778
1883
 
1779
1884
  {family?.variants?.map((variant) => (
1780
- <button
1781
- key={variant.articleId}
1782
- type="button"
1783
- className={`family-row${
1784
- variant.isCurrent ? " current" : ""
1785
- }`}
1786
- disabled={variant.isCurrent || loadingVariant !== null}
1787
- onClick={() => openVariant(variant.articleId)}
1788
- >
1789
- <span className="role">
1790
- {loadingVariant === variant.articleId
1791
- ? "Abriendo…"
1792
- : variant.isCurrent
1793
- ? "Estás aquí"
1794
- : "Hijo"}
1795
- </span>
1796
- <span className="name">{variant.name}</span>
1797
- <span className="upc">{variant.upc}</span>
1798
- </button>
1885
+ <div className="family-item" key={variant.articleId}>
1886
+ <button
1887
+ type="button"
1888
+ className={`family-row${
1889
+ variant.isCurrent ? " current" : ""
1890
+ }`}
1891
+ disabled={variant.isCurrent || loadingVariant !== null}
1892
+ onClick={() => openVariant(variant.articleId)}
1893
+ >
1894
+ <span className="role">
1895
+ {loadingVariant === variant.articleId
1896
+ ? "Abriendo…"
1897
+ : variant.isCurrent
1898
+ ? "Estás aquí"
1899
+ : "Hijo"}
1900
+ </span>
1901
+ <span className="name">{variant.name}</span>
1902
+ <span className="upc">{variant.upc}</span>
1903
+ </button>
1904
+
1905
+ {/* Solo cuando esta ficha ES el padre: si hay `parent`, lo
1906
+ que se lista son hermanos y este producto no manda
1907
+ sobre su vínculo. */}
1908
+ {!family.parent && !variant.isCurrent && (
1909
+ <button
1910
+ type="button"
1911
+ className="desvincular"
1912
+ title="Desvincular este hijo"
1913
+ onClick={() =>
1914
+ setPorDesvincular({
1915
+ mode: "child",
1916
+ childArticleId: variant.articleId,
1917
+ childName: variant.name,
1918
+ childUpc: variant.upc,
1919
+ parentName: state.product?.name,
1920
+ parentUpc: state.product?.upc,
1921
+ })
1922
+ }
1923
+ >
1924
+ Desvincular
1925
+ </button>
1926
+ )}
1927
+ </div>
1799
1928
  ))}
1800
1929
 
1801
1930
  {family && !family.error && !family.variants?.length && (