contentoh-components-library 21.6.19 → 21.6.21

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.
Files changed (28) hide show
  1. package/.env.test +2 -1
  2. package/dist/components/atoms/InheritanceTag/index.js +8 -0
  3. package/dist/components/molecules/TagAndInput/index.js +1 -0
  4. package/dist/components/molecules/TagAndInputv2/index.js +1 -0
  5. package/dist/components/organisms/LinkVariantsModal/api.js +102 -0
  6. package/dist/components/organisms/LinkVariantsModal/index.js +620 -0
  7. package/dist/components/organisms/LinkVariantsModal/styles.js +60 -0
  8. package/dist/components/pages/ProviderProductEdition/context/reducers/inputs.js +8 -2
  9. package/dist/components/pages/ProviderProductEdition/index.js +198 -89
  10. package/dist/components/pages/ProviderProductEdition/styles.js +2 -2
  11. package/dist/components/pages/ProviderProductEdition/utils.js +8 -0
  12. package/dist/index.js +25 -12
  13. package/package.json +1 -1
  14. package/src/components/atoms/InheritanceTag/index.js +7 -0
  15. package/src/components/molecules/TagAndInput/index.js +1 -0
  16. package/src/components/molecules/TagAndInputv2/index.js +1 -0
  17. package/src/components/organisms/LinkVariantsModal/api.js +61 -0
  18. package/src/components/organisms/LinkVariantsModal/index.js +479 -0
  19. package/src/components/organisms/LinkVariantsModal/styles.js +264 -0
  20. package/src/components/pages/ProviderProductEdition/context/reducers/inputs.js +21 -13
  21. package/src/components/pages/ProviderProductEdition/index.js +76 -0
  22. package/src/components/pages/ProviderProductEdition/styles.js +23 -0
  23. package/src/components/pages/ProviderProductEdition/utils.js +8 -0
  24. package/src/index.js +1 -0
  25. package/dist/ai/prompts/attribute.prompt.js +0 -28
  26. package/dist/ai/prompts/description.prompt.js +0 -33
  27. package/dist/ai/schemas/attribute.schema.js +0 -35
  28. package/dist/ai/schemas/description.schema.js +0 -27
package/dist/index.js CHANGED
@@ -1083,7 +1083,7 @@ Object.keys(_index82).forEach(function (key) {
1083
1083
  });
1084
1084
  });
1085
1085
 
1086
- var _index83 = require("./components/organisms/ProductImageModal/index");
1086
+ var _index83 = require("./components/organisms/LinkVariantsModal/index");
1087
1087
 
1088
1088
  Object.keys(_index83).forEach(function (key) {
1089
1089
  if (key === "default" || key === "__esModule") return;
@@ -1096,7 +1096,7 @@ Object.keys(_index83).forEach(function (key) {
1096
1096
  });
1097
1097
  });
1098
1098
 
1099
- var _index84 = require("./components/organisms/Chat/index");
1099
+ var _index84 = require("./components/organisms/ProductImageModal/index");
1100
1100
 
1101
1101
  Object.keys(_index84).forEach(function (key) {
1102
1102
  if (key === "default" || key === "__esModule") return;
@@ -1109,7 +1109,7 @@ Object.keys(_index84).forEach(function (key) {
1109
1109
  });
1110
1110
  });
1111
1111
 
1112
- var _index85 = require("./components/organisms/Modal/index");
1112
+ var _index85 = require("./components/organisms/Chat/index");
1113
1113
 
1114
1114
  Object.keys(_index85).forEach(function (key) {
1115
1115
  if (key === "default" || key === "__esModule") return;
@@ -1122,7 +1122,7 @@ Object.keys(_index85).forEach(function (key) {
1122
1122
  });
1123
1123
  });
1124
1124
 
1125
- var _index86 = require("./components/organisms/OrderDetail/index");
1125
+ var _index86 = require("./components/organisms/Modal/index");
1126
1126
 
1127
1127
  Object.keys(_index86).forEach(function (key) {
1128
1128
  if (key === "default" || key === "__esModule") return;
@@ -1135,6 +1135,19 @@ Object.keys(_index86).forEach(function (key) {
1135
1135
  });
1136
1136
  });
1137
1137
 
1138
+ var _index87 = require("./components/organisms/OrderDetail/index");
1139
+
1140
+ Object.keys(_index87).forEach(function (key) {
1141
+ if (key === "default" || key === "__esModule") return;
1142
+ if (key in exports && exports[key] === _index87[key]) return;
1143
+ Object.defineProperty(exports, key, {
1144
+ enumerable: true,
1145
+ get: function get() {
1146
+ return _index87[key];
1147
+ }
1148
+ });
1149
+ });
1150
+
1138
1151
  var _RangeCalendar = require("./components/organisms/RangeCalendar");
1139
1152
 
1140
1153
  Object.keys(_RangeCalendar).forEach(function (key) {
@@ -1161,28 +1174,28 @@ Object.keys(_TableResizable).forEach(function (key) {
1161
1174
  });
1162
1175
  });
1163
1176
 
1164
- var _index87 = require("./components/organisms/SideModal/index");
1177
+ var _index88 = require("./components/organisms/SideModal/index");
1165
1178
 
1166
- Object.keys(_index87).forEach(function (key) {
1179
+ Object.keys(_index88).forEach(function (key) {
1167
1180
  if (key === "default" || key === "__esModule") return;
1168
- if (key in exports && exports[key] === _index87[key]) return;
1181
+ if (key in exports && exports[key] === _index88[key]) return;
1169
1182
  Object.defineProperty(exports, key, {
1170
1183
  enumerable: true,
1171
1184
  get: function get() {
1172
- return _index87[key];
1185
+ return _index88[key];
1173
1186
  }
1174
1187
  });
1175
1188
  });
1176
1189
 
1177
- var _index88 = require("./components/organisms/GridProducts/index");
1190
+ var _index89 = require("./components/organisms/GridProducts/index");
1178
1191
 
1179
- Object.keys(_index88).forEach(function (key) {
1192
+ Object.keys(_index89).forEach(function (key) {
1180
1193
  if (key === "default" || key === "__esModule") return;
1181
- if (key in exports && exports[key] === _index88[key]) return;
1194
+ if (key in exports && exports[key] === _index89[key]) return;
1182
1195
  Object.defineProperty(exports, key, {
1183
1196
  enumerable: true,
1184
1197
  get: function get() {
1185
- return _index88[key];
1198
+ return _index89[key];
1186
1199
  }
1187
1200
  });
1188
1201
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentoh-components-library",
3
- "version": "21.6.19",
3
+ "version": "21.6.21",
4
4
  "dependencies": {
5
5
  "@aws-amplify/auth": "^4.5.3",
6
6
  "@aws-amplify/datastore": "^3.11.0",
@@ -9,16 +9,23 @@ import { Container } from "./styles";
9
9
  * así que es el que lleva acento y el que ofrece la acción de volver a heredar.
10
10
  * "Heredado" es el caso normal y se mantiene discreto: si gritara en cada campo,
11
11
  * la ficha entera se volvería ruido.
12
+ *
13
+ * Un campo VACÍO no lleva marca: no heredó nada y tampoco tiene valor propio, así
14
+ * que llamarlo "Heredado" es falso además de ruidoso. Sin esto la ficha de una
15
+ * variante sale con el chip repetido en cada campo, incluidos los obligatorios en
16
+ * rojo que están sin capturar, y la marca deja de distinguir nada.
12
17
  */
13
18
  export const InheritanceTag = ({
14
19
  isVariant = false,
15
20
  isOverridden = false,
21
+ hasValue = true,
16
22
  parentUpc,
17
23
  onRestore,
18
24
  restoreLabel = "Volver a heredar",
19
25
  className = "",
20
26
  }) => {
21
27
  if (!isVariant) return null;
28
+ if (!hasValue) return null;
22
29
 
23
30
  const state = isOverridden ? "own" : "inherited";
24
31
 
@@ -331,6 +331,7 @@ export const TagAndInput = ({
331
331
  <InheritanceTag
332
332
  isVariant={isVariant}
333
333
  isOverridden={isOverridden}
334
+ hasValue={`${value ?? ""}`.trim() !== ""}
334
335
  parentUpc={parentUpc}
335
336
  onRestore={
336
337
  onRestoreInheritance
@@ -63,6 +63,7 @@ export const TagAndInputv2 = ({
63
63
  <InheritanceTag
64
64
  isVariant={isVariant}
65
65
  isOverridden={isOverridden}
66
+ hasValue={`${value ?? ""}`.trim() !== ""}
66
67
  parentUpc={parentUpc}
67
68
  onRestore={
68
69
  onRestoreInheritance
@@ -0,0 +1,61 @@
1
+ /*
2
+ Las lambdas de este API están declaradas con `integration: lambda`, no con
3
+ lambda-proxy. Eso tiene dos consecuencias que se pagan caras si se ignoran:
4
+
5
+ 1. La respuesta llega ENVUELTA: axios entrega { statusCode, headers, body },
6
+ con `body` en texto. Leer `res.data.candidates` da undefined.
7
+ 2. El HTTP siempre es 200, incluso cuando la lambda respondió 400 o 500. Un
8
+ try/catch alrededor de axios NUNCA se dispara por un error de negocio: el
9
+ error viaja dentro del cuerpo y hay que mirar `statusCode` a mano.
10
+
11
+ El repo ya vive con esto —provider-product-edition.context.jsx hace
12
+ JSON.parse(res.data.body)— pero sin un lugar común cada llamada nueva vuelve a
13
+ tropezar. Esto lo centraliza.
14
+ */
15
+
16
+ export const unwrap = (res) => {
17
+ const payload = res?.data;
18
+
19
+ const envuelto = payload
20
+ && typeof payload === "object"
21
+ && "statusCode" in payload
22
+ && "body" in payload;
23
+
24
+ if (!envuelto) return { status: res?.status ?? 200, body: payload };
25
+
26
+ const body =
27
+ typeof payload.body === "string" ? safeParse(payload.body) : payload.body;
28
+
29
+ return { status: Number(payload.statusCode), body };
30
+ };
31
+
32
+ const safeParse = (text) => {
33
+ try {
34
+ return JSON.parse(text);
35
+ } catch (error) {
36
+ return { message: text };
37
+ }
38
+ };
39
+
40
+ /*
41
+ Error de negocio con el código estable que manda el backend
42
+ (VARIANT_CROSS_COMPANY, VARIANT_ALREADY_LINKED, ...). Se lanza para que el
43
+ llamador use un try/catch normal, aunque el transporte haya dicho 200.
44
+ */
45
+ export class ApiError extends Error {
46
+ constructor(code, message) {
47
+ super(message);
48
+ this.name = "ApiError";
49
+ this.code = code;
50
+ }
51
+ }
52
+
53
+ export const readPayload = (res) => {
54
+ const { status, body } = unwrap(res);
55
+
56
+ if (status >= 400) {
57
+ throw new ApiError(body?.code, body?.message || "La operación falló.");
58
+ }
59
+
60
+ return body ?? {};
61
+ };
@@ -0,0 +1,479 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import axios from "axios";
3
+
4
+ import { readPayload, ApiError } from "./api";
5
+
6
+ import {
7
+ Overlay,
8
+ Panel,
9
+ Header,
10
+ ModeSwitch,
11
+ Body,
12
+ SearchRow,
13
+ List,
14
+ AttributeList,
15
+ Section,
16
+ Message,
17
+ Footer,
18
+ } from "./styles";
19
+
20
+ /*
21
+ Modal para ligar productos como padre/variante.
22
+
23
+ props (* requerido):
24
+ - *articleId: producto desde el que se liga (el que está en edición)
25
+ - articleName / articleUpc: para que el usuario sepa sobre qué está trabajando
26
+ - *token: JWT que va en el header Authorization
27
+ - endpoint: base del API de variantes. Por defecto REACT_APP_VARIANTS_ENDPOINT
28
+ - *onClose: cerrar sin cambios
29
+ - onLinked: se llama con la respuesta del backend cuando la liga tuvo éxito
30
+
31
+ Los dos modos son los de POST /variants/link:
32
+ parent → este producto pasa a ser variante del que se elija
33
+ children → los elegidos pasan a ser variantes de este producto
34
+
35
+ En ambos el padre baja al hijo, nunca al revés. Lo que cambia es quién es quién.
36
+ */
37
+
38
+ // El backend devuelve códigos estables justamente para no mandar textos de UI
39
+ // desde el servidor. La traducción a lenguaje humano vive aquí.
40
+ const ERROR_TEXT = {
41
+ VARIANT_SELF_LINK: "Un producto no puede ser variante de sí mismo.",
42
+ VARIANT_PARENT_IS_CHILD:
43
+ "Ese producto ya es variante de otro. Las familias son de un solo nivel.",
44
+ VARIANT_CHILD_IS_PARENT:
45
+ "Ese producto ya tiene variantes propias, así que no puede colgar de otro.",
46
+ VARIANT_ALREADY_LINKED:
47
+ "Ese producto ya pertenece a otra familia. Desvincúlalo primero.",
48
+ VARIANT_NOT_OWNED_BY_USER: "Ese producto está fuera de tu alcance.",
49
+ VARIANT_CROSS_COMPANY: "Ese producto pertenece a otra compañía.",
50
+ VARIANT_INACTIVE_ARTICLE: "Ese producto está desactivado.",
51
+ VARIANT_NOT_FOUND: "No encontramos ese producto.",
52
+ VARIANT_PLAN_LIMIT_REACHED:
53
+ "Tu plan no tiene cupo disponible para más productos.",
54
+ };
55
+
56
+ const readError = (error) => {
57
+ if (error instanceof ApiError || error?.name === "ApiError") {
58
+ return ERROR_TEXT[error.code] || error.message;
59
+ }
60
+ // Falla de transporte de verdad: red caída, DNS, CORS.
61
+ return "No se pudo conectar con el servidor.";
62
+ };
63
+
64
+ export const LinkVariantsModal = ({
65
+ articleId,
66
+ articleName,
67
+ articleUpc,
68
+ activeRetailerId,
69
+ token,
70
+ endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
71
+ onClose,
72
+ onLinked,
73
+ }) => {
74
+ const [mode, setMode] = useState("parent");
75
+ const [step, setStep] = useState("search");
76
+
77
+ const [query, setQuery] = useState("");
78
+ const [candidates, setCandidates] = useState([]);
79
+ const [rejected, setRejected] = useState(null);
80
+ const [searched, setSearched] = useState(false);
81
+ const [selected, setSelected] = useState([]);
82
+
83
+ const [attributes, setAttributes] = useState([]);
84
+ const [nameScope, setNameScope] = useState(null);
85
+ 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);
90
+
91
+ const [loading, setLoading] = useState(false);
92
+ const [error, setError] = useState(null);
93
+
94
+ const request = useCallback(
95
+ async (action, body) => {
96
+ const res = await axios.post(`${endpoint}/${action}`, body, {
97
+ headers: { Authorization: token },
98
+ });
99
+ // El transporte siempre responde 200; el resultado real va en el cuerpo.
100
+ return readPayload(res);
101
+ },
102
+ [endpoint, token]
103
+ );
104
+
105
+ // En modo children el padre es el producto en edición; en modo parent es el
106
+ // candidato que el usuario acaba de elegir. De ahí sale qué atributos ofrecer.
107
+ const parentArticleId = useMemo(
108
+ () => (mode === "children" ? articleId : selected[0]),
109
+ [mode, articleId, selected]
110
+ );
111
+
112
+ // Cambiar de dirección invalida la selección: los candidatos elegibles no son
113
+ // los mismos, y arrastrar la anterior llevaría a ligar algo que no se buscó.
114
+ useEffect(() => {
115
+ setCandidates([]);
116
+ setSelected([]);
117
+ setRejected(null);
118
+ setSearched(false);
119
+ setError(null);
120
+ setStep("search");
121
+ }, [mode]);
122
+
123
+ const search = async (event) => {
124
+ event?.preventDefault();
125
+ setLoading(true);
126
+ setError(null);
127
+ try {
128
+ const data = await request("search", {
129
+ mode,
130
+ articleId,
131
+ query: query.trim(),
132
+ });
133
+ setCandidates(data.candidates || []);
134
+ setRejected(data.rejected || null);
135
+ setSearched(true);
136
+ } catch (err) {
137
+ setError(readError(err));
138
+ } finally {
139
+ setLoading(false);
140
+ }
141
+ };
142
+
143
+ const toggle = (id) => {
144
+ setSelected((current) => {
145
+ if (mode === "parent") return current[0] === id ? [] : [id];
146
+ return current.includes(id)
147
+ ? current.filter((item) => item !== id)
148
+ : [...current, id];
149
+ });
150
+ };
151
+
152
+ const goToInheritance = async () => {
153
+ setLoading(true);
154
+ setError(null);
155
+ try {
156
+ // La cadena activa decide con qué nombre se ve cada atributo: el equipo
157
+ // interno y los proveedores los renombran por cadena en attribute_manager,
158
+ // y mostrar el nombre de otra cadena confundiría al elegir.
159
+ const data = await request("attributes", {
160
+ parentArticleId,
161
+ retailerId: activeRetailerId,
162
+ });
163
+ setAttributes(data.attributes || []);
164
+ setNameScope(data.nameScope ?? null);
165
+ setChosenAttributes((data.attributes || []).map((item) => item.id));
166
+ setStep("inherit");
167
+ } catch (err) {
168
+ setError(readError(err));
169
+ } finally {
170
+ setLoading(false);
171
+ }
172
+ };
173
+
174
+ const toggleAttribute = (id) => {
175
+ setChosenAttributes((current) =>
176
+ current.includes(id)
177
+ ? current.filter((item) => item !== id)
178
+ : [...current, id]
179
+ );
180
+ };
181
+
182
+ const submit = async () => {
183
+ setLoading(true);
184
+ setError(null);
185
+
186
+ const inherit = {
187
+ attributes: inheritMode === "all-filled" ? "all-filled" : chosenAttributes,
188
+ images: inheritImages,
189
+ descriptions: inheritDescriptions,
190
+ categoryRetailers: inheritCategoryRetailers,
191
+ };
192
+
193
+ const body =
194
+ mode === "parent"
195
+ ? { mode, articleId, parentArticleId: selected[0], inherit }
196
+ : { mode, parentArticleId: articleId, childrenArticleIds: selected, inherit };
197
+
198
+ try {
199
+ const data = await request("link", body);
200
+ onLinked?.(data);
201
+ onClose?.();
202
+ } catch (err) {
203
+ setError(readError(err));
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ };
208
+
209
+ const selectionLabel =
210
+ mode === "parent"
211
+ ? "Se elige un solo producto padre."
212
+ : `${selected.length} producto(s) pasarán a ser variantes.`;
213
+
214
+ return (
215
+ <Overlay onClick={onClose}>
216
+ <Panel onClick={(event) => event.stopPropagation()}>
217
+ <Header>
218
+ <div>
219
+ <h2>Ligar productos</h2>
220
+ <p>
221
+ {articleName || `Producto ${articleId}`}
222
+ {articleUpc ? ` · UPC ${articleUpc}` : ""}
223
+ </p>
224
+ </div>
225
+ <button type="button" className="close" onClick={onClose} aria-label="Cerrar">
226
+ ×
227
+ </button>
228
+ </Header>
229
+
230
+ {step === "search" && (
231
+ <ModeSwitch>
232
+ <button
233
+ type="button"
234
+ className={mode === "parent" ? "active" : ""}
235
+ onClick={() => setMode("parent")}
236
+ >
237
+ Este producto es variante de otro
238
+ </button>
239
+ <button
240
+ type="button"
241
+ className={mode === "children" ? "active" : ""}
242
+ onClick={() => setMode("children")}
243
+ >
244
+ Agregar variantes a este producto
245
+ </button>
246
+ </ModeSwitch>
247
+ )}
248
+
249
+ <Body>
250
+ {error && <Message className="error">{error}</Message>}
251
+
252
+ {step === "search" && (
253
+ <>
254
+ <SearchRow onSubmit={search}>
255
+ <input
256
+ type="text"
257
+ value={query}
258
+ placeholder="Busca por UPC, SKU o nombre"
259
+ onChange={(event) => setQuery(event.target.value)}
260
+ />
261
+ <button type="submit" disabled={loading}>
262
+ {loading ? "Buscando…" : "Buscar"}
263
+ </button>
264
+ </SearchRow>
265
+
266
+ {/* El backend explica por qué un UPC existente no es ligable. Sin
267
+ esto el usuario ve "sin resultados" y no sabe qué hacer. */}
268
+ {rejected && (
269
+ <Message className="error">
270
+ {rejected.name || rejected.upc}: {ERROR_TEXT[rejected.code]}
271
+ </Message>
272
+ )}
273
+
274
+ {searched && !candidates.length && !rejected && (
275
+ <Message>
276
+ No hay productos que puedas ligar con esa búsqueda. Solo
277
+ aparecen productos activos de la misma compañía que todavía no
278
+ pertenecen a otra familia.
279
+ </Message>
280
+ )}
281
+
282
+ {candidates.length > 0 && (
283
+ <List>
284
+ {candidates.map((candidate) => (
285
+ <li key={candidate.articleId}>
286
+ <label>
287
+ <input
288
+ type={mode === "parent" ? "radio" : "checkbox"}
289
+ name="candidate"
290
+ checked={selected.includes(candidate.articleId)}
291
+ onChange={() => toggle(candidate.articleId)}
292
+ />
293
+ <span>
294
+ {candidate.name || `Producto ${candidate.articleId}`}
295
+ <span className="meta">
296
+ UPC {candidate.upc || "—"}
297
+ {candidate.categoryName ? ` · ${candidate.categoryName}` : ""}
298
+ </span>
299
+ </span>
300
+ {candidate.variantsCount > 0 && (
301
+ <span className="count">
302
+ {candidate.variantsCount} variante
303
+ {candidate.variantsCount === 1 ? "" : "s"}
304
+ </span>
305
+ )}
306
+ </label>
307
+ </li>
308
+ ))}
309
+ </List>
310
+ )}
311
+ </>
312
+ )}
313
+
314
+ {step === "inherit" && (
315
+ <>
316
+ <Section>
317
+ <h3>¿Qué se hereda del producto padre?</h3>
318
+ <label className="choice">
319
+ <input
320
+ type="radio"
321
+ checked={inheritMode === "all-filled"}
322
+ onChange={() => setInheritMode("all-filled")}
323
+ />
324
+ <span>
325
+ Todos los atributos con valor
326
+ <span className="hint">
327
+ {attributes.length} atributo(s) con información en el padre.
328
+ </span>
329
+ </span>
330
+ </label>
331
+ <label className="choice">
332
+ <input
333
+ type="radio"
334
+ checked={inheritMode === "selected"}
335
+ onChange={() => setInheritMode("selected")}
336
+ />
337
+ <span>
338
+ Elegir cuáles
339
+ <span className="hint">
340
+ Lo que no elijas conserva el valor que ya tiene la variante.
341
+ </span>
342
+ </span>
343
+ </label>
344
+ </Section>
345
+
346
+ {inheritMode === "selected" && activeRetailerId && !nameScope && (
347
+ <Message>
348
+ El producto padre no comparte la cadena que tienes abierta, así
349
+ que los nombres de abajo son los de otra de sus cadenas.
350
+ </Message>
351
+ )}
352
+
353
+ {inheritMode === "selected" && (
354
+ <AttributeList>
355
+ {attributes.map((attribute) => (
356
+ <li key={attribute.id}>
357
+ <label>
358
+ <input
359
+ type="checkbox"
360
+ checked={chosenAttributes.includes(attribute.id)}
361
+ onChange={() => toggleAttribute(attribute.id)}
362
+ />
363
+ <span>
364
+ {attribute.name}
365
+ {attribute.unconfigured && (
366
+ <span className="note">
367
+ sin configuración en las cadenas del padre
368
+ </span>
369
+ )}
370
+ {attribute.ambiguousName && (
371
+ <span className="note">
372
+ se llama distinto en otra cadena
373
+ </span>
374
+ )}
375
+ </span>
376
+ <span className="value" title={attribute.value}>
377
+ {attribute.value}
378
+ </span>
379
+ </label>
380
+ </li>
381
+ ))}
382
+ </AttributeList>
383
+ )}
384
+
385
+ <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>
410
+ Cadenas y categorías
411
+ <span className="hint">
412
+ Las parejas cadena-categoría del padre se copian a la variante.
413
+ </span>
414
+ </span>
415
+ </label>
416
+ </Section>
417
+
418
+ {/* Es la diferencia entre link y propagate, y es la que sorprende:
419
+ aquí lo elegido se escribe encima aunque la variante lo hubiera
420
+ editado a mano. Decirlo antes evita el reclamo después. */}
421
+ <Message>
422
+ Lo que elijas heredar reemplaza el valor actual de la variante,
423
+ incluso si alguien lo había capturado a mano.
424
+ </Message>
425
+ </>
426
+ )}
427
+ </Body>
428
+
429
+ <Footer>
430
+ <span className="summary">
431
+ {step === "search" ? selectionLabel : "Revisa la herencia antes de ligar."}
432
+ </span>
433
+ <div className="actions">
434
+ {step === "inherit" && (
435
+ <button
436
+ type="button"
437
+ className="secondary"
438
+ onClick={() => setStep("search")}
439
+ disabled={loading}
440
+ >
441
+ Atrás
442
+ </button>
443
+ )}
444
+ <button type="button" className="secondary" onClick={onClose} disabled={loading}>
445
+ Cancelar
446
+ </button>
447
+ {step === "search" ? (
448
+ <button
449
+ type="button"
450
+ className="primary"
451
+ onClick={goToInheritance}
452
+ disabled={loading || !selected.length}
453
+ >
454
+ Continuar
455
+ </button>
456
+ ) : (
457
+ <button
458
+ type="button"
459
+ className="primary"
460
+ onClick={submit}
461
+ disabled={
462
+ loading ||
463
+ // Solo estorba cuando hay algo que elegir y no se eligió nada.
464
+ // Con el padre sin atributos heredables, ligar sigue siendo
465
+ // válido: se heredan imágenes, descripciones y cadenas.
466
+ (inheritMode === "selected" &&
467
+ attributes.length > 0 &&
468
+ !chosenAttributes.length)
469
+ }
470
+ >
471
+ {loading ? "Ligando…" : "Ligar"}
472
+ </button>
473
+ )}
474
+ </div>
475
+ </Footer>
476
+ </Panel>
477
+ </Overlay>
478
+ );
479
+ };