contentoh-components-library 21.6.22 → 21.6.24
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.
- package/.env.test +1 -0
- package/dist/_utils/helper.js +47 -2
- package/dist/components/organisms/LinkVariantsModal/index.js +891 -186
- package/dist/components/organisms/LinkVariantsModal/styles.js +29 -5
- package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +22 -13
- package/dist/components/pages/ProviderProductEdition/index.js +393 -110
- package/dist/components/pages/ProviderProductEdition/styles.js +2 -2
- package/package.json +1 -1
- package/src/_utils/helper.js +40 -0
- package/src/components/organisms/LinkVariantsModal/index.js +813 -117
- package/src/components/organisms/LinkVariantsModal/styles.js +256 -0
- package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +14 -1
- package/src/components/pages/ProviderProductEdition/index.js +223 -3
- package/src/components/pages/ProviderProductEdition/styles.js +68 -0
|
@@ -2,6 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
|
|
4
4
|
import { readPayload, ApiError } from "./api";
|
|
5
|
+
import {
|
|
6
|
+
getProductImage,
|
|
7
|
+
getRetailerPicture,
|
|
8
|
+
} from "../../../_utils/helper";
|
|
5
9
|
|
|
6
10
|
import {
|
|
7
11
|
Overlay,
|
|
@@ -13,6 +17,12 @@ import {
|
|
|
13
17
|
List,
|
|
14
18
|
AttributeList,
|
|
15
19
|
Section,
|
|
20
|
+
Block,
|
|
21
|
+
Choices,
|
|
22
|
+
Tabs,
|
|
23
|
+
Paging,
|
|
24
|
+
ImageGrid,
|
|
25
|
+
AddRetailer,
|
|
16
26
|
Message,
|
|
17
27
|
Footer,
|
|
18
28
|
} from "./styles";
|
|
@@ -68,6 +78,7 @@ export const LinkVariantsModal = ({
|
|
|
68
78
|
activeRetailerId,
|
|
69
79
|
token,
|
|
70
80
|
endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
|
|
81
|
+
filtersEndpoint = process.env.REACT_APP_CREATE_PRODUCTS_FILTERS,
|
|
71
82
|
onClose,
|
|
72
83
|
onLinked,
|
|
73
84
|
}) => {
|
|
@@ -79,14 +90,65 @@ export const LinkVariantsModal = ({
|
|
|
79
90
|
const [rejected, setRejected] = useState(null);
|
|
80
91
|
const [searched, setSearched] = useState(false);
|
|
81
92
|
const [selected, setSelected] = useState([]);
|
|
93
|
+
const [total, setTotal] = useState(0);
|
|
94
|
+
const [hasMore, setHasMore] = useState(false);
|
|
95
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
82
96
|
|
|
83
|
-
|
|
97
|
+
// Lo que el padre ofrece en cada dimensión, y lo que el usuario eligió de
|
|
98
|
+
// cada una. Se guardan por separado porque se eligen por separado: heredar
|
|
99
|
+
// todos los atributos no dice nada sobre qué fotos quiere la variante.
|
|
100
|
+
const [offered, setOffered] = useState({
|
|
101
|
+
attributes: [],
|
|
102
|
+
descriptions: [],
|
|
103
|
+
images: [],
|
|
104
|
+
categoryRetailers: [],
|
|
105
|
+
});
|
|
106
|
+
const [chosen, setChosen] = useState({
|
|
107
|
+
attributes: [],
|
|
108
|
+
descriptions: [],
|
|
109
|
+
images: [],
|
|
110
|
+
categoryRetailers: [],
|
|
111
|
+
});
|
|
84
112
|
const [nameScope, setNameScope] = useState(null);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
|
|
114
|
+
// Un modo por dimensión, y ninguno decidido de entrada: el usuario tiene que
|
|
115
|
+
// pasar por las cuatro antes de poder ligar. Preseleccionar "todo" hacía que
|
|
116
|
+
// se pudiera ligar sin haber mirado nada, que es justo lo que hay que evitar
|
|
117
|
+
// en una operación que sobrescribe valores capturados.
|
|
118
|
+
const [modes, setModes] = useState({
|
|
119
|
+
categoryRetailers: null,
|
|
120
|
+
descriptions: null,
|
|
121
|
+
attributes: null,
|
|
122
|
+
images: null,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
Al ligar varios productos, la herencia puede ser la misma para todos o
|
|
127
|
+
distinta para cada uno.
|
|
128
|
+
|
|
129
|
+
Por defecto es la misma: es lo que se quiere casi siempre y evita repetir
|
|
130
|
+
cuatro decisiones por producto. Al desmarcarlo aparecen las pestañas y cada
|
|
131
|
+
producto guarda su propia selección en `perChild`.
|
|
132
|
+
*/
|
|
133
|
+
const [applyToAll, setApplyToAll] = useState(true);
|
|
134
|
+
const [activeChild, setActiveChild] = useState(null);
|
|
135
|
+
const [perChild, setPerChild] = useState({});
|
|
136
|
+
|
|
137
|
+
// Cadenas que el padre NO tiene y el usuario decidió agregar a la variante.
|
|
138
|
+
const [extraRetailers, setExtraRetailers] = useState([]);
|
|
139
|
+
const [newRetailer, setNewRetailer] = useState("");
|
|
140
|
+
const [newDepartment, setNewDepartment] = useState("");
|
|
141
|
+
const [newCategory, setNewCategory] = useState("");
|
|
142
|
+
|
|
143
|
+
// Las categorías no son una lista plana: dependen de la cadena y del
|
|
144
|
+
// departamento. Es la misma cascada que usa el alta de productos, y se sirve
|
|
145
|
+
// del mismo endpoint para no acabar con dos árboles de categorías distintos.
|
|
146
|
+
const [filters, setFilters] = useState({
|
|
147
|
+
retailers: [],
|
|
148
|
+
departments: [],
|
|
149
|
+
categories: [],
|
|
150
|
+
});
|
|
151
|
+
const [loadingFilters, setLoadingFilters] = useState(false);
|
|
90
152
|
|
|
91
153
|
const [loading, setLoading] = useState(false);
|
|
92
154
|
const [error, setError] = useState(null);
|
|
@@ -116,6 +178,8 @@ export const LinkVariantsModal = ({
|
|
|
116
178
|
setSelected([]);
|
|
117
179
|
setRejected(null);
|
|
118
180
|
setSearched(false);
|
|
181
|
+
setTotal(0);
|
|
182
|
+
setHasMore(false);
|
|
119
183
|
setError(null);
|
|
120
184
|
setStep("search");
|
|
121
185
|
}, [mode]);
|
|
@@ -132,6 +196,8 @@ export const LinkVariantsModal = ({
|
|
|
132
196
|
});
|
|
133
197
|
setCandidates(data.candidates || []);
|
|
134
198
|
setRejected(data.rejected || null);
|
|
199
|
+
setTotal(data.total ?? (data.candidates || []).length);
|
|
200
|
+
setHasMore(!!data.hasMore);
|
|
135
201
|
setSearched(true);
|
|
136
202
|
} catch (err) {
|
|
137
203
|
setError(readError(err));
|
|
@@ -140,6 +206,34 @@ export const LinkVariantsModal = ({
|
|
|
140
206
|
}
|
|
141
207
|
};
|
|
142
208
|
|
|
209
|
+
/*
|
|
210
|
+
Trae la siguiente página y la agrega a la lista.
|
|
211
|
+
|
|
212
|
+
Se acumula en vez de reemplazar porque la selección vive en esta pantalla: si
|
|
213
|
+
la página 2 borrara la 1, lo marcado antes desaparecería de la vista sin
|
|
214
|
+
dejar de estar seleccionado, y el usuario acabaría ligando productos que ya
|
|
215
|
+
no ve.
|
|
216
|
+
*/
|
|
217
|
+
const loadMore = async () => {
|
|
218
|
+
setLoadingMore(true);
|
|
219
|
+
setError(null);
|
|
220
|
+
try {
|
|
221
|
+
const data = await request("search", {
|
|
222
|
+
mode,
|
|
223
|
+
articleId,
|
|
224
|
+
query: query.trim(),
|
|
225
|
+
offset: candidates.length,
|
|
226
|
+
});
|
|
227
|
+
setCandidates((current) => [...current, ...(data.candidates || [])]);
|
|
228
|
+
setTotal(data.total ?? total);
|
|
229
|
+
setHasMore(!!data.hasMore);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
setError(readError(err));
|
|
232
|
+
} finally {
|
|
233
|
+
setLoadingMore(false);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
143
237
|
const toggle = (id) => {
|
|
144
238
|
setSelected((current) => {
|
|
145
239
|
if (mode === "parent") return current[0] === id ? [] : [id];
|
|
@@ -160,9 +254,32 @@ export const LinkVariantsModal = ({
|
|
|
160
254
|
parentArticleId,
|
|
161
255
|
retailerId: activeRetailerId,
|
|
162
256
|
});
|
|
163
|
-
|
|
257
|
+
|
|
258
|
+
const listas = {
|
|
259
|
+
attributes: data.attributes || [],
|
|
260
|
+
descriptions: data.descriptions || [],
|
|
261
|
+
images: data.images || [],
|
|
262
|
+
categoryRetailers: data.categoryRetailers || [],
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
setOffered(listas);
|
|
164
266
|
setNameScope(data.nameScope ?? null);
|
|
165
|
-
|
|
267
|
+
setChosen({
|
|
268
|
+
attributes: [],
|
|
269
|
+
descriptions: [],
|
|
270
|
+
images: [],
|
|
271
|
+
categoryRetailers: [],
|
|
272
|
+
});
|
|
273
|
+
setModes({
|
|
274
|
+
categoryRetailers: null,
|
|
275
|
+
descriptions: null,
|
|
276
|
+
attributes: null,
|
|
277
|
+
images: null,
|
|
278
|
+
});
|
|
279
|
+
setExtraRetailers([]);
|
|
280
|
+
setPerChild({});
|
|
281
|
+
setApplyToAll(true);
|
|
282
|
+
setActiveChild(mode === "children" ? selected[0] : null);
|
|
166
283
|
setStep("inherit");
|
|
167
284
|
} catch (err) {
|
|
168
285
|
setError(readError(err));
|
|
@@ -171,33 +288,229 @@ export const LinkVariantsModal = ({
|
|
|
171
288
|
}
|
|
172
289
|
};
|
|
173
290
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
current
|
|
177
|
-
|
|
178
|
-
|
|
291
|
+
const toggleChoice = (dimension, id) => {
|
|
292
|
+
setChosen((current) => {
|
|
293
|
+
const lista = current[dimension];
|
|
294
|
+
return {
|
|
295
|
+
...current,
|
|
296
|
+
[dimension]: lista.includes(id)
|
|
297
|
+
? lista.filter((item) => item !== id)
|
|
298
|
+
: [...lista, id],
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const chooseMode = (dimension, modo, ids) => {
|
|
304
|
+
setModes((current) => ({ ...current, [dimension]: modo }));
|
|
305
|
+
setChosen((current) => ({
|
|
306
|
+
...current,
|
|
307
|
+
// "Todos" marca la lista completa para que se vea qué va a bajar; al pasar
|
|
308
|
+
// a "elegir" se parte de cero, que es lo que la palabra promete.
|
|
309
|
+
[dimension]: modo === "all" ? ids : [],
|
|
310
|
+
}));
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const loadFilters = useCallback(
|
|
314
|
+
async (retailer, department) => {
|
|
315
|
+
setLoadingFilters(true);
|
|
316
|
+
try {
|
|
317
|
+
const res = await axios.get(
|
|
318
|
+
`${filtersEndpoint}?retailerSelected=${retailer ?? ""}&departmentSelected=${
|
|
319
|
+
department ?? ""
|
|
320
|
+
}`,
|
|
321
|
+
{ headers: { Authorization: token } }
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const { body } = readPayload(res);
|
|
325
|
+
const lista = (body ? JSON.parse(body) : res.data)?.data ?? [];
|
|
326
|
+
|
|
327
|
+
setFilters((current) => {
|
|
328
|
+
if (!retailer) {
|
|
329
|
+
return {
|
|
330
|
+
...current,
|
|
331
|
+
retailers: lista.map((item) => ({ value: item.id, label: item.name })),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (!department) {
|
|
335
|
+
return {
|
|
336
|
+
...current,
|
|
337
|
+
departments: lista.map((item) => ({
|
|
338
|
+
value: item.category,
|
|
339
|
+
label: item.category,
|
|
340
|
+
})),
|
|
341
|
+
categories: [],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...current,
|
|
346
|
+
categories: lista.map((item) => ({ value: item.id, label: item.name })),
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error("linkVariants/filters: ", error);
|
|
351
|
+
} finally {
|
|
352
|
+
setLoadingFilters(false);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
[filtersEndpoint, token]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Las cadenas del catálogo se piden al entrar al paso de herencia, no al abrir
|
|
359
|
+
// el modal: quien solo busca y cancela no llega a necesitarlas.
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (step === "inherit" && !filters.retailers.length) {
|
|
362
|
+
loadFilters(null, null);
|
|
363
|
+
}
|
|
364
|
+
}, [step, filters.retailers.length, loadFilters]);
|
|
365
|
+
|
|
366
|
+
const addRetailer = () => {
|
|
367
|
+
const retailerId = Number(newRetailer);
|
|
368
|
+
const categoryId = Number(newCategory);
|
|
369
|
+
if (!retailerId || !categoryId) return;
|
|
370
|
+
|
|
371
|
+
const clave = `${retailerId}-${categoryId}`;
|
|
372
|
+
const yaEsta =
|
|
373
|
+
offered.categoryRetailers.some(
|
|
374
|
+
(item) => `${item.retailerId}-${item.categoryId}` === clave
|
|
375
|
+
) || extraRetailers.some((item) => item.clave === clave);
|
|
376
|
+
|
|
377
|
+
if (yaEsta) return;
|
|
378
|
+
|
|
379
|
+
const retailer = filters.retailers.find(
|
|
380
|
+
(item) => Number(item.value) === retailerId
|
|
381
|
+
);
|
|
382
|
+
const category = filters.categories.find(
|
|
383
|
+
(item) => Number(item.value) === categoryId
|
|
179
384
|
);
|
|
385
|
+
|
|
386
|
+
setExtraRetailers((current) => [
|
|
387
|
+
...current,
|
|
388
|
+
{
|
|
389
|
+
clave,
|
|
390
|
+
retailerId,
|
|
391
|
+
categoryId,
|
|
392
|
+
retailerName: retailer?.label ?? `Cadena ${retailerId}`,
|
|
393
|
+
categoryName: category?.label ?? `Categoría ${categoryId}`,
|
|
394
|
+
},
|
|
395
|
+
]);
|
|
396
|
+
setChosen((current) => ({
|
|
397
|
+
...current,
|
|
398
|
+
categoryRetailers: [...current.categoryRetailers, clave],
|
|
399
|
+
}));
|
|
400
|
+
setNewRetailer("");
|
|
401
|
+
setNewDepartment("");
|
|
402
|
+
setNewCategory("");
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/*
|
|
406
|
+
Convierte una selección de pantalla en el bloque `inherit` de la API.
|
|
407
|
+
|
|
408
|
+
Con el modo en "todos" se manda la palabra y no la lista que se pintó: si
|
|
409
|
+
entre la carga del modal y el guardado alguien agregó un valor al padre,
|
|
410
|
+
"todos" sigue significando todos. La lista sería una foto ya vieja.
|
|
411
|
+
|
|
412
|
+
Las parejas cadena+categoría viajan como objetos, no como ids, porque pueden
|
|
413
|
+
ser cadenas que el padre no tiene: ahí no hay nada que heredar, se asigna.
|
|
414
|
+
*/
|
|
415
|
+
const buildInherit = (seleccion) => {
|
|
416
|
+
const parejas = [
|
|
417
|
+
...offered.categoryRetailers.map((item) => ({
|
|
418
|
+
clave: `${item.retailerId}-${item.categoryId}`,
|
|
419
|
+
retailerId: item.retailerId,
|
|
420
|
+
categoryId: item.categoryId,
|
|
421
|
+
})),
|
|
422
|
+
...(seleccion.extraRetailers ?? []),
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
attributes:
|
|
427
|
+
seleccion.modes.attributes === "all"
|
|
428
|
+
? "all-filled"
|
|
429
|
+
: seleccion.chosen.attributes,
|
|
430
|
+
descriptions:
|
|
431
|
+
seleccion.modes.descriptions === "all"
|
|
432
|
+
? "all"
|
|
433
|
+
: seleccion.chosen.descriptions,
|
|
434
|
+
images: seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
|
|
435
|
+
categoryRetailers: parejas
|
|
436
|
+
.filter((par) => seleccion.chosen.categoryRetailers.includes(par.clave))
|
|
437
|
+
.map(({ retailerId, categoryId }) => ({ retailerId, categoryId })),
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const seleccionVacia = () => ({
|
|
442
|
+
modes: {
|
|
443
|
+
categoryRetailers: null,
|
|
444
|
+
descriptions: null,
|
|
445
|
+
attributes: null,
|
|
446
|
+
images: null,
|
|
447
|
+
},
|
|
448
|
+
chosen: {
|
|
449
|
+
attributes: [],
|
|
450
|
+
descriptions: [],
|
|
451
|
+
images: [],
|
|
452
|
+
categoryRetailers: [],
|
|
453
|
+
},
|
|
454
|
+
extraRetailers: [],
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
/*
|
|
458
|
+
Cambia de pestaña conservando lo elegido en la anterior. Sin esto, moverse
|
|
459
|
+
entre productos borraría el trabajo hecho en cada uno.
|
|
460
|
+
*/
|
|
461
|
+
const switchChild = (id) => {
|
|
462
|
+
setPerChild((current) => ({
|
|
463
|
+
...current,
|
|
464
|
+
[activeChild]: { modes, chosen, extraRetailers },
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
const guardado = perChild[id] ?? seleccionVacia();
|
|
468
|
+
setModes(guardado.modes);
|
|
469
|
+
setChosen(guardado.chosen);
|
|
470
|
+
setExtraRetailers(guardado.extraRetailers ?? []);
|
|
471
|
+
setActiveChild(id);
|
|
180
472
|
};
|
|
181
473
|
|
|
182
474
|
const submit = async () => {
|
|
183
475
|
setLoading(true);
|
|
184
476
|
setError(null);
|
|
185
477
|
|
|
186
|
-
const inherit = {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
478
|
+
const inherit = buildInherit({ modes, chosen, extraRetailers });
|
|
479
|
+
|
|
480
|
+
// Con las pestañas abiertas cada producto manda la suya. La selección de la
|
|
481
|
+
// pestaña a la vista todavía no está en perChild, así que se agrega aquí: si
|
|
482
|
+
// no, lo último que tocó el usuario se perdería al guardar.
|
|
483
|
+
const inheritByChild =
|
|
484
|
+
!applyToAll && mode === "children"
|
|
485
|
+
? selected.reduce((acc, id) => {
|
|
486
|
+
const guardado =
|
|
487
|
+
id === activeChild ? { modes, chosen, extraRetailers } : perChild[id];
|
|
488
|
+
if (guardado) acc[id] = buildInherit(guardado);
|
|
489
|
+
return acc;
|
|
490
|
+
}, {})
|
|
491
|
+
: undefined;
|
|
192
492
|
|
|
193
493
|
const body =
|
|
194
494
|
mode === "parent"
|
|
195
495
|
? { mode, articleId, parentArticleId: selected[0], inherit }
|
|
196
|
-
: {
|
|
496
|
+
: {
|
|
497
|
+
mode,
|
|
498
|
+
parentArticleId: articleId,
|
|
499
|
+
childrenArticleIds: selected,
|
|
500
|
+
inherit,
|
|
501
|
+
...(inheritByChild ? { inheritByChild } : {}),
|
|
502
|
+
};
|
|
197
503
|
|
|
198
504
|
try {
|
|
199
505
|
const data = await request("link", body);
|
|
200
|
-
|
|
506
|
+
// El modo importa para quien escucha: en `children` este producto GANA
|
|
507
|
+
// variantes; en `parent` pasa a SER una. Son efectos opuestos sobre la
|
|
508
|
+
// misma ficha, así que quien refresque el estado necesita distinguirlos.
|
|
509
|
+
onLinked?.({
|
|
510
|
+
...data,
|
|
511
|
+
mode,
|
|
512
|
+
parentArticleId: mode === "parent" ? selected[0] : articleId,
|
|
513
|
+
});
|
|
201
514
|
onClose?.();
|
|
202
515
|
} catch (err) {
|
|
203
516
|
setError(readError(err));
|
|
@@ -206,6 +519,53 @@ export const LinkVariantsModal = ({
|
|
|
206
519
|
}
|
|
207
520
|
};
|
|
208
521
|
|
|
522
|
+
// Una dimensión está decidida si el usuario eligió un modo, o si el padre no
|
|
523
|
+
// ofrece nada en ella —no se puede pedir que decida sobre una lista vacía—.
|
|
524
|
+
const decidida = (dimension) =>
|
|
525
|
+
modes[dimension] !== null ||
|
|
526
|
+
(dimension === "categoryRetailers"
|
|
527
|
+
? offered.categoryRetailers.length === 0 && extraRetailers.length === 0
|
|
528
|
+
: offered[dimension].length === 0);
|
|
529
|
+
|
|
530
|
+
const DIMENSIONES = [
|
|
531
|
+
"categoryRetailers",
|
|
532
|
+
"descriptions",
|
|
533
|
+
"attributes",
|
|
534
|
+
"images",
|
|
535
|
+
];
|
|
536
|
+
|
|
537
|
+
// Con pestañas, que la que estoy viendo esté completa no basta: ligar escribe
|
|
538
|
+
// en todos, así que todos tienen que estar decididos. Si no, un producto se
|
|
539
|
+
// ligaría sin heredar nada solo porque nadie abrió su pestaña.
|
|
540
|
+
const decididoPara = (id) => {
|
|
541
|
+
const guardado =
|
|
542
|
+
id === activeChild ? { modes, chosen, extraRetailers } : perChild[id];
|
|
543
|
+
if (!guardado) return false;
|
|
544
|
+
|
|
545
|
+
return DIMENSIONES.every((dimension) => {
|
|
546
|
+
if (guardado.modes[dimension] !== null) return true;
|
|
547
|
+
return dimension === "categoryRetailers"
|
|
548
|
+
? offered.categoryRetailers.length === 0 &&
|
|
549
|
+
(guardado.extraRetailers ?? []).length === 0
|
|
550
|
+
: offered[dimension].length === 0;
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const todoDecidido =
|
|
555
|
+
step === "inherit" &&
|
|
556
|
+
(applyToAll || mode !== "children"
|
|
557
|
+
? DIMENSIONES.every(decidida)
|
|
558
|
+
: selected.every(decididoPara));
|
|
559
|
+
|
|
560
|
+
const faltantes = [
|
|
561
|
+
["categoryRetailers", "cadenas"],
|
|
562
|
+
["descriptions", "descripciones"],
|
|
563
|
+
["attributes", "atributos"],
|
|
564
|
+
["images", "imágenes"],
|
|
565
|
+
]
|
|
566
|
+
.filter(([clave]) => !decidida(clave))
|
|
567
|
+
.map(([, nombre]) => nombre);
|
|
568
|
+
|
|
209
569
|
const selectionLabel =
|
|
210
570
|
mode === "parent"
|
|
211
571
|
? "Se elige un solo producto padre."
|
|
@@ -308,112 +668,450 @@ export const LinkVariantsModal = ({
|
|
|
308
668
|
))}
|
|
309
669
|
</List>
|
|
310
670
|
)}
|
|
671
|
+
|
|
672
|
+
{/* Cuántos hay de verdad. Sin esto la lista se corta en 20 y no hay
|
|
673
|
+
forma de saber si eso es todo o solo el principio. */}
|
|
674
|
+
{searched && candidates.length > 0 && (
|
|
675
|
+
<Paging>
|
|
676
|
+
<span>
|
|
677
|
+
Mostrando {candidates.length} de {total}
|
|
678
|
+
</span>
|
|
679
|
+
{hasMore && (
|
|
680
|
+
<button type="button" onClick={loadMore} disabled={loadingMore}>
|
|
681
|
+
{loadingMore ? "Cargando…" : "Cargar más"}
|
|
682
|
+
</button>
|
|
683
|
+
)}
|
|
684
|
+
</Paging>
|
|
685
|
+
)}
|
|
311
686
|
</>
|
|
312
687
|
)}
|
|
313
688
|
|
|
314
689
|
{step === "inherit" && (
|
|
315
690
|
<>
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
691
|
+
{mode === "children" && selected.length > 1 && (
|
|
692
|
+
<>
|
|
693
|
+
<label className="apply-all">
|
|
694
|
+
<input
|
|
695
|
+
type="checkbox"
|
|
696
|
+
checked={applyToAll}
|
|
697
|
+
onChange={(event) => {
|
|
698
|
+
setApplyToAll(event.target.checked);
|
|
699
|
+
if (event.target.checked) setPerChild({});
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
<span>
|
|
703
|
+
Aplicar lo mismo a los {selected.length} productos
|
|
704
|
+
<span className="hint">
|
|
705
|
+
Desmárcalo si alguno debe heredar algo distinto.
|
|
706
|
+
</span>
|
|
328
707
|
</span>
|
|
329
|
-
</
|
|
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>
|
|
708
|
+
</label>
|
|
345
709
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
710
|
+
{!applyToAll && (
|
|
711
|
+
<Tabs>
|
|
712
|
+
{selected.map((id, indice) => {
|
|
713
|
+
const producto = candidates.find(
|
|
714
|
+
(item) => item.articleId === id
|
|
715
|
+
);
|
|
716
|
+
return (
|
|
717
|
+
<button
|
|
718
|
+
key={id}
|
|
719
|
+
type="button"
|
|
720
|
+
className={id === activeChild ? "active" : ""}
|
|
721
|
+
onClick={() => switchChild(id)}
|
|
722
|
+
>
|
|
723
|
+
<span className="orden">{indice + 1}</span>
|
|
724
|
+
{producto?.name || `Producto ${id}`}
|
|
725
|
+
{decididoPara(id) && (
|
|
726
|
+
<span className="listo" aria-label="listo">
|
|
727
|
+
✓
|
|
728
|
+
</span>
|
|
729
|
+
)}
|
|
730
|
+
</button>
|
|
731
|
+
);
|
|
732
|
+
})}
|
|
733
|
+
</Tabs>
|
|
734
|
+
)}
|
|
735
|
+
</>
|
|
351
736
|
)}
|
|
352
737
|
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
738
|
+
{/* El orden no es casual: primero se decide en qué cadenas vive la
|
|
739
|
+
variante, porque de eso depende qué atributos le aplican. Las
|
|
740
|
+
imágenes van al final: son la decisión más visual y la que menos
|
|
741
|
+
arrastra a las demás. */}
|
|
742
|
+
|
|
743
|
+
<Block>
|
|
744
|
+
<h3>
|
|
745
|
+
<span className="paso">1</span> Cadenas y categorías
|
|
746
|
+
</h3>
|
|
747
|
+
<Choices>
|
|
748
|
+
<label>
|
|
749
|
+
<input
|
|
750
|
+
type="radio"
|
|
751
|
+
checked={modes.categoryRetailers === "all"}
|
|
752
|
+
onChange={() =>
|
|
753
|
+
chooseMode(
|
|
754
|
+
"categoryRetailers",
|
|
755
|
+
"all",
|
|
756
|
+
offered.categoryRetailers.map(
|
|
757
|
+
(item) => `${item.retailerId}-${item.categoryId}`
|
|
758
|
+
)
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
/>
|
|
762
|
+
<span>
|
|
763
|
+
Todas las del producto padre
|
|
764
|
+
<span className="hint">
|
|
765
|
+
{offered.categoryRetailers.length} cadena(s).
|
|
766
|
+
</span>
|
|
767
|
+
</span>
|
|
768
|
+
</label>
|
|
769
|
+
<label>
|
|
770
|
+
<input
|
|
771
|
+
type="radio"
|
|
772
|
+
checked={modes.categoryRetailers === "some"}
|
|
773
|
+
onChange={() => chooseMode("categoryRetailers", "some", [])}
|
|
774
|
+
/>
|
|
775
|
+
<span>
|
|
776
|
+
Elegir cuáles
|
|
777
|
+
<span className="hint">
|
|
778
|
+
También puedes agregar una cadena que el padre no tenga.
|
|
779
|
+
</span>
|
|
780
|
+
</span>
|
|
781
|
+
</label>
|
|
782
|
+
</Choices>
|
|
783
|
+
|
|
784
|
+
<List>
|
|
785
|
+
{[
|
|
786
|
+
...offered.categoryRetailers.map((item) => ({
|
|
787
|
+
...item,
|
|
788
|
+
clave: `${item.retailerId}-${item.categoryId}`,
|
|
789
|
+
delPadre: true,
|
|
790
|
+
})),
|
|
791
|
+
...extraRetailers.map((item) => ({ ...item, delPadre: false })),
|
|
792
|
+
].map((item) => (
|
|
793
|
+
<li key={item.clave}>
|
|
357
794
|
<label>
|
|
358
795
|
<input
|
|
359
796
|
type="checkbox"
|
|
360
|
-
|
|
361
|
-
|
|
797
|
+
disabled={modes.categoryRetailers === "all"}
|
|
798
|
+
checked={chosen.categoryRetailers.includes(item.clave)}
|
|
799
|
+
onChange={() =>
|
|
800
|
+
toggleChoice("categoryRetailers", item.clave)
|
|
801
|
+
}
|
|
802
|
+
/>
|
|
803
|
+
<img
|
|
804
|
+
className="logo"
|
|
805
|
+
src={getRetailerPicture(item.retailerId, 24, 24)}
|
|
806
|
+
alt=""
|
|
807
|
+
/>
|
|
808
|
+
<span>
|
|
809
|
+
{item.retailerName}
|
|
810
|
+
<span className="meta">
|
|
811
|
+
{item.categoryName || "Sin categoría"}
|
|
812
|
+
{!item.delPadre && " · no la tiene el padre"}
|
|
813
|
+
</span>
|
|
814
|
+
</span>
|
|
815
|
+
</label>
|
|
816
|
+
</li>
|
|
817
|
+
))}
|
|
818
|
+
</List>
|
|
819
|
+
|
|
820
|
+
<AddRetailer>
|
|
821
|
+
<select
|
|
822
|
+
value={newRetailer}
|
|
823
|
+
onChange={(event) => {
|
|
824
|
+
const valor = event.target.value;
|
|
825
|
+
setNewRetailer(valor);
|
|
826
|
+
setNewDepartment("");
|
|
827
|
+
setNewCategory("");
|
|
828
|
+
if (valor) loadFilters(valor, null);
|
|
829
|
+
}}
|
|
830
|
+
>
|
|
831
|
+
<option value="">Agregar otra cadena…</option>
|
|
832
|
+
{filters.retailers.map((item) => (
|
|
833
|
+
<option key={item.value} value={item.value}>
|
|
834
|
+
{item.label}
|
|
835
|
+
</option>
|
|
836
|
+
))}
|
|
837
|
+
</select>
|
|
838
|
+
<select
|
|
839
|
+
value={newDepartment}
|
|
840
|
+
disabled={!newRetailer || !filters.departments.length}
|
|
841
|
+
onChange={(event) => {
|
|
842
|
+
const valor = event.target.value;
|
|
843
|
+
setNewDepartment(valor);
|
|
844
|
+
setNewCategory("");
|
|
845
|
+
if (valor) loadFilters(newRetailer, valor);
|
|
846
|
+
}}
|
|
847
|
+
>
|
|
848
|
+
<option value="">Departamento…</option>
|
|
849
|
+
{filters.departments.map((item) => (
|
|
850
|
+
<option key={item.value} value={item.value}>
|
|
851
|
+
{item.label}
|
|
852
|
+
</option>
|
|
853
|
+
))}
|
|
854
|
+
</select>
|
|
855
|
+
<select
|
|
856
|
+
value={newCategory}
|
|
857
|
+
disabled={!newDepartment || !filters.categories.length}
|
|
858
|
+
onChange={(event) => setNewCategory(event.target.value)}
|
|
859
|
+
>
|
|
860
|
+
<option value="">Categoría…</option>
|
|
861
|
+
{filters.categories.map((item) => (
|
|
862
|
+
<option key={item.value} value={item.value}>
|
|
863
|
+
{item.label}
|
|
864
|
+
</option>
|
|
865
|
+
))}
|
|
866
|
+
</select>
|
|
867
|
+
<button
|
|
868
|
+
type="button"
|
|
869
|
+
onClick={addRetailer}
|
|
870
|
+
disabled={!newRetailer || !newCategory || loadingFilters}
|
|
871
|
+
>
|
|
872
|
+
{loadingFilters ? "…" : "Agregar"}
|
|
873
|
+
</button>
|
|
874
|
+
</AddRetailer>
|
|
875
|
+
</Block>
|
|
876
|
+
|
|
877
|
+
<Block>
|
|
878
|
+
<h3>
|
|
879
|
+
<span className="paso">2</span> Descripciones
|
|
880
|
+
</h3>
|
|
881
|
+
{offered.descriptions.length ? (
|
|
882
|
+
<>
|
|
883
|
+
<Choices>
|
|
884
|
+
<label>
|
|
885
|
+
<input
|
|
886
|
+
type="radio"
|
|
887
|
+
checked={modes.descriptions === "all"}
|
|
888
|
+
onChange={() =>
|
|
889
|
+
chooseMode(
|
|
890
|
+
"descriptions",
|
|
891
|
+
"all",
|
|
892
|
+
offered.descriptions.map((item) => item.id)
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
/>
|
|
896
|
+
<span>
|
|
897
|
+
Todas las que tienen valor
|
|
898
|
+
<span className="hint">
|
|
899
|
+
{offered.descriptions.length} descripción(es) con
|
|
900
|
+
información en el padre.
|
|
901
|
+
</span>
|
|
902
|
+
</span>
|
|
903
|
+
</label>
|
|
904
|
+
<label>
|
|
905
|
+
<input
|
|
906
|
+
type="radio"
|
|
907
|
+
checked={modes.descriptions === "some"}
|
|
908
|
+
onChange={() => chooseMode("descriptions", "some", [])}
|
|
909
|
+
/>
|
|
910
|
+
<span>
|
|
911
|
+
Elegir cuáles
|
|
912
|
+
<span className="hint">
|
|
913
|
+
Lo que no elijas conserva el valor que ya tiene la
|
|
914
|
+
variante.
|
|
915
|
+
</span>
|
|
916
|
+
</span>
|
|
917
|
+
</label>
|
|
918
|
+
</Choices>
|
|
919
|
+
|
|
920
|
+
<AttributeList>
|
|
921
|
+
{offered.descriptions.map((item) => (
|
|
922
|
+
<li key={item.id}>
|
|
923
|
+
<label>
|
|
924
|
+
<input
|
|
925
|
+
type="checkbox"
|
|
926
|
+
disabled={modes.descriptions === "all"}
|
|
927
|
+
checked={chosen.descriptions.includes(item.id)}
|
|
928
|
+
onChange={() => toggleChoice("descriptions", item.id)}
|
|
929
|
+
/>
|
|
930
|
+
<span>
|
|
931
|
+
{item.name}
|
|
932
|
+
{item.duplicated && (
|
|
933
|
+
<span className="note">
|
|
934
|
+
el padre tiene {item.rows} versiones de este
|
|
935
|
+
texto; se copian todas
|
|
936
|
+
</span>
|
|
937
|
+
)}
|
|
938
|
+
</span>
|
|
939
|
+
<span className="value" title={item.value}>
|
|
940
|
+
{item.value}
|
|
941
|
+
</span>
|
|
942
|
+
</label>
|
|
943
|
+
</li>
|
|
944
|
+
))}
|
|
945
|
+
</AttributeList>
|
|
946
|
+
</>
|
|
947
|
+
) : (
|
|
948
|
+
<Message>
|
|
949
|
+
El producto padre no tiene descripciones que heredar.
|
|
950
|
+
</Message>
|
|
951
|
+
)}
|
|
952
|
+
</Block>
|
|
953
|
+
|
|
954
|
+
<Block>
|
|
955
|
+
<h3>
|
|
956
|
+
<span className="paso">3</span> Atributos de la ficha técnica
|
|
957
|
+
</h3>
|
|
958
|
+
{offered.attributes.length ? (
|
|
959
|
+
<>
|
|
960
|
+
<Choices>
|
|
961
|
+
<label>
|
|
962
|
+
<input
|
|
963
|
+
type="radio"
|
|
964
|
+
checked={modes.attributes === "all"}
|
|
965
|
+
onChange={() =>
|
|
966
|
+
chooseMode(
|
|
967
|
+
"attributes",
|
|
968
|
+
"all",
|
|
969
|
+
offered.attributes.map((item) => item.id)
|
|
970
|
+
)
|
|
971
|
+
}
|
|
972
|
+
/>
|
|
973
|
+
<span>
|
|
974
|
+
Todos los que tienen valor
|
|
975
|
+
<span className="hint">
|
|
976
|
+
{offered.attributes.length} atributo(s) con información
|
|
977
|
+
en el padre.
|
|
978
|
+
</span>
|
|
979
|
+
</span>
|
|
980
|
+
</label>
|
|
981
|
+
<label>
|
|
982
|
+
<input
|
|
983
|
+
type="radio"
|
|
984
|
+
checked={modes.attributes === "some"}
|
|
985
|
+
onChange={() => chooseMode("attributes", "some", [])}
|
|
362
986
|
/>
|
|
363
987
|
<span>
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
988
|
+
Elegir cuáles
|
|
989
|
+
<span className="hint">
|
|
990
|
+
Lo que no elijas conserva el valor que ya tiene la
|
|
991
|
+
variante.
|
|
992
|
+
</span>
|
|
993
|
+
</span>
|
|
994
|
+
</label>
|
|
995
|
+
</Choices>
|
|
996
|
+
|
|
997
|
+
{activeRetailerId && !nameScope && (
|
|
998
|
+
<Message>
|
|
999
|
+
El producto padre no comparte la cadena que tienes abierta,
|
|
1000
|
+
así que los nombres de abajo son los de otra de sus cadenas.
|
|
1001
|
+
</Message>
|
|
1002
|
+
)}
|
|
1003
|
+
|
|
1004
|
+
<AttributeList>
|
|
1005
|
+
{offered.attributes.map((attribute) => (
|
|
1006
|
+
<li key={attribute.id}>
|
|
1007
|
+
<label>
|
|
1008
|
+
<input
|
|
1009
|
+
type="checkbox"
|
|
1010
|
+
disabled={modes.attributes === "all"}
|
|
1011
|
+
checked={chosen.attributes.includes(attribute.id)}
|
|
1012
|
+
onChange={() => toggleChoice("attributes", attribute.id)}
|
|
1013
|
+
/>
|
|
1014
|
+
<span>
|
|
1015
|
+
{attribute.name}
|
|
1016
|
+
{attribute.unconfigured && (
|
|
1017
|
+
<span className="note">
|
|
1018
|
+
sin configuración en las cadenas del padre
|
|
1019
|
+
</span>
|
|
1020
|
+
)}
|
|
1021
|
+
{attribute.ambiguousName && (
|
|
1022
|
+
<span className="note">
|
|
1023
|
+
se llama distinto en otra cadena
|
|
1024
|
+
</span>
|
|
1025
|
+
)}
|
|
368
1026
|
</span>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
<span className="note">
|
|
372
|
-
se llama distinto en otra cadena
|
|
1027
|
+
<span className="value" title={attribute.value}>
|
|
1028
|
+
{attribute.value}
|
|
373
1029
|
</span>
|
|
374
|
-
|
|
1030
|
+
</label>
|
|
1031
|
+
</li>
|
|
1032
|
+
))}
|
|
1033
|
+
</AttributeList>
|
|
1034
|
+
</>
|
|
1035
|
+
) : (
|
|
1036
|
+
<Message>
|
|
1037
|
+
El producto padre no tiene atributos con valor que heredar.
|
|
1038
|
+
</Message>
|
|
1039
|
+
)}
|
|
1040
|
+
</Block>
|
|
1041
|
+
|
|
1042
|
+
<Block>
|
|
1043
|
+
<h3>
|
|
1044
|
+
<span className="paso">4</span> Imágenes
|
|
1045
|
+
</h3>
|
|
1046
|
+
{offered.images.length ? (
|
|
1047
|
+
<>
|
|
1048
|
+
<Choices>
|
|
1049
|
+
<label>
|
|
1050
|
+
<input
|
|
1051
|
+
type="radio"
|
|
1052
|
+
checked={modes.images === "all"}
|
|
1053
|
+
onChange={() =>
|
|
1054
|
+
chooseMode(
|
|
1055
|
+
"images",
|
|
1056
|
+
"all",
|
|
1057
|
+
offered.images.map((item) => item.id)
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
/>
|
|
1061
|
+
<span>
|
|
1062
|
+
Todas
|
|
1063
|
+
<span className="hint">
|
|
1064
|
+
{offered.images.length} imagen(es) en el padre.
|
|
1065
|
+
</span>
|
|
375
1066
|
</span>
|
|
376
|
-
|
|
377
|
-
|
|
1067
|
+
</label>
|
|
1068
|
+
<label>
|
|
1069
|
+
<input
|
|
1070
|
+
type="radio"
|
|
1071
|
+
checked={modes.images === "some"}
|
|
1072
|
+
onChange={() => chooseMode("images", "some", [])}
|
|
1073
|
+
/>
|
|
1074
|
+
<span>
|
|
1075
|
+
Elegir cuáles
|
|
1076
|
+
<span className="hint">
|
|
1077
|
+
Útil cuando la variante cambia de color o de empaque.
|
|
1078
|
+
</span>
|
|
378
1079
|
</span>
|
|
379
1080
|
</label>
|
|
380
|
-
</
|
|
381
|
-
))}
|
|
382
|
-
</AttributeList>
|
|
383
|
-
)}
|
|
1081
|
+
</Choices>
|
|
384
1082
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
</
|
|
415
|
-
|
|
416
|
-
</
|
|
1083
|
+
<ImageGrid>
|
|
1084
|
+
{offered.images.map((image) => (
|
|
1085
|
+
<label
|
|
1086
|
+
key={image.id}
|
|
1087
|
+
className={[
|
|
1088
|
+
chosen.images.includes(image.id) ? "picked" : "",
|
|
1089
|
+
modes.images === "all" ? "locked" : "",
|
|
1090
|
+
]
|
|
1091
|
+
.filter(Boolean)
|
|
1092
|
+
.join(" ")}
|
|
1093
|
+
>
|
|
1094
|
+
<input
|
|
1095
|
+
type="checkbox"
|
|
1096
|
+
disabled={modes.images === "all"}
|
|
1097
|
+
checked={chosen.images.includes(image.id)}
|
|
1098
|
+
onChange={() => toggleChoice("images", image.id)}
|
|
1099
|
+
/>
|
|
1100
|
+
<img
|
|
1101
|
+
src={getProductImage(image.src)}
|
|
1102
|
+
alt={image.imageType || "Imagen del producto"}
|
|
1103
|
+
/>
|
|
1104
|
+
<span>{image.imageType || "Sin tipo de toma"}</span>
|
|
1105
|
+
</label>
|
|
1106
|
+
))}
|
|
1107
|
+
</ImageGrid>
|
|
1108
|
+
</>
|
|
1109
|
+
) : (
|
|
1110
|
+
<Message>
|
|
1111
|
+
El producto padre no tiene imágenes que heredar.
|
|
1112
|
+
</Message>
|
|
1113
|
+
)}
|
|
1114
|
+
</Block>
|
|
417
1115
|
|
|
418
1116
|
{/* Es la diferencia entre link y propagate, y es la que sorprende:
|
|
419
1117
|
aquí lo elegido se escribe encima aunque la variante lo hubiera
|
|
@@ -428,7 +1126,13 @@ export const LinkVariantsModal = ({
|
|
|
428
1126
|
|
|
429
1127
|
<Footer>
|
|
430
1128
|
<span className="summary">
|
|
431
|
-
{step === "search"
|
|
1129
|
+
{step === "search"
|
|
1130
|
+
? selectionLabel
|
|
1131
|
+
: todoDecidido
|
|
1132
|
+
? "Todo revisado. Puedes ligar."
|
|
1133
|
+
: !applyToAll && mode === "children"
|
|
1134
|
+
? `Faltan ${selected.filter((id) => !decididoPara(id)).length} producto(s) por revisar.`
|
|
1135
|
+
: `Falta decidir: ${faltantes.join(", ")}.`}
|
|
432
1136
|
</span>
|
|
433
1137
|
<div className="actions">
|
|
434
1138
|
{step === "inherit" && (
|
|
@@ -458,15 +1162,7 @@ export const LinkVariantsModal = ({
|
|
|
458
1162
|
type="button"
|
|
459
1163
|
className="primary"
|
|
460
1164
|
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
|
-
}
|
|
1165
|
+
disabled={loading || !todoDecidido}
|
|
470
1166
|
>
|
|
471
1167
|
{loading ? "Ligando…" : "Ligar"}
|
|
472
1168
|
</button>
|