contentoh-components-library 21.6.23 → 21.6.25
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 +852 -340
- package/dist/components/organisms/LinkVariantsModal/styles.js +40 -18
- package/dist/components/pages/ProviderProductEdition/index.js +193 -110
- package/dist/components/pages/ProviderProductEdition/styles.js +4 -1
- package/package.json +1 -1
- package/src/_utils/helper.js +40 -0
- package/src/components/organisms/LinkVariantsModal/index.js +774 -283
- package/src/components/organisms/LinkVariantsModal/styles.js +228 -72
- package/src/components/pages/ProviderProductEdition/index.js +71 -9
- package/src/components/pages/ProviderProductEdition/styles.js +6 -3
|
@@ -2,6 +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
6
|
|
|
6
7
|
import {
|
|
7
8
|
Overlay,
|
|
@@ -13,7 +14,10 @@ import {
|
|
|
13
14
|
List,
|
|
14
15
|
AttributeList,
|
|
15
16
|
Section,
|
|
16
|
-
|
|
17
|
+
Block,
|
|
18
|
+
Choices,
|
|
19
|
+
Tabs,
|
|
20
|
+
Paging,
|
|
17
21
|
ImageGrid,
|
|
18
22
|
AddRetailer,
|
|
19
23
|
Message,
|
|
@@ -69,10 +73,9 @@ export const LinkVariantsModal = ({
|
|
|
69
73
|
articleName,
|
|
70
74
|
articleUpc,
|
|
71
75
|
activeRetailerId,
|
|
72
|
-
retailersAvailable,
|
|
73
|
-
categoriesAvailable,
|
|
74
76
|
token,
|
|
75
77
|
endpoint = process.env.REACT_APP_VARIANTS_ENDPOINT,
|
|
78
|
+
filtersEndpoint = process.env.REACT_APP_CREATE_PRODUCTS_FILTERS,
|
|
76
79
|
onClose,
|
|
77
80
|
onLinked,
|
|
78
81
|
}) => {
|
|
@@ -84,6 +87,9 @@ export const LinkVariantsModal = ({
|
|
|
84
87
|
const [rejected, setRejected] = useState(null);
|
|
85
88
|
const [searched, setSearched] = useState(false);
|
|
86
89
|
const [selected, setSelected] = useState([]);
|
|
90
|
+
const [total, setTotal] = useState(0);
|
|
91
|
+
const [hasMore, setHasMore] = useState(false);
|
|
92
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
87
93
|
|
|
88
94
|
// Lo que el padre ofrece en cada dimensión, y lo que el usuario eligió de
|
|
89
95
|
// cada una. Se guardan por separado porque se eligen por separado: heredar
|
|
@@ -101,14 +107,62 @@ export const LinkVariantsModal = ({
|
|
|
101
107
|
categoryRetailers: [],
|
|
102
108
|
});
|
|
103
109
|
const [nameScope, setNameScope] = useState(null);
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
|
|
111
|
+
// Un modo por dimensión, y ninguno decidido de entrada: el usuario tiene que
|
|
112
|
+
// pasar por las cuatro antes de poder ligar. Preseleccionar "todo" hacía que
|
|
113
|
+
// se pudiera ligar sin haber mirado nada, que es justo lo que hay que evitar
|
|
114
|
+
// en una operación que sobrescribe valores capturados.
|
|
115
|
+
const [modes, setModes] = useState({
|
|
116
|
+
categoryRetailers: null,
|
|
117
|
+
descriptions: null,
|
|
118
|
+
attributes: null,
|
|
119
|
+
images: null,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
Al ligar varios productos, la herencia puede ser la misma para todos o
|
|
124
|
+
distinta para cada uno.
|
|
125
|
+
|
|
126
|
+
Por defecto es la misma: es lo que se quiere casi siempre y evita repetir
|
|
127
|
+
cuatro decisiones por producto. Al desmarcarlo aparecen las pestañas y cada
|
|
128
|
+
producto guarda su propia selección en `perChild`.
|
|
129
|
+
*/
|
|
130
|
+
// Desmarcado a propósito: aplicar la misma herencia a varios productos es una
|
|
131
|
+
// decisión, no un valor por omisión. Marcado de entrada se ligaban todos igual
|
|
132
|
+
// sin que nadie lo hubiera elegido.
|
|
133
|
+
const [applyToAll, setApplyToAll] = useState(false);
|
|
134
|
+
const [activeChild, setActiveChild] = useState(null);
|
|
135
|
+
const [perChild, setPerChild] = useState({});
|
|
106
136
|
|
|
107
137
|
// Cadenas que el padre NO tiene y el usuario decidió agregar a la variante.
|
|
138
|
+
/*
|
|
139
|
+
Imágenes que la base lista pero cuyo archivo no está en S3.
|
|
140
|
+
|
|
141
|
+
Pasa: hay filas de rel_article_image con un src válido que apunta a un objeto
|
|
142
|
+
que ya no existe —comprobado, CloudFront responde 404—. Sin esto el paso 4
|
|
143
|
+
muestra una tarjeta rota, con el texto alternativo donde debería ir la foto, y
|
|
144
|
+
deja marcar para heredar algo que no se puede heredar.
|
|
145
|
+
|
|
146
|
+
No se puede saber desde el servidor sin ir a S3 por cada imagen, así que se
|
|
147
|
+
detecta al fallar la carga y se descuenta de la lista y del conteo.
|
|
148
|
+
*/
|
|
149
|
+
const [brokenImages, setBrokenImages] = useState([]);
|
|
150
|
+
|
|
108
151
|
const [extraRetailers, setExtraRetailers] = useState([]);
|
|
109
152
|
const [newRetailer, setNewRetailer] = useState("");
|
|
153
|
+
const [newDepartment, setNewDepartment] = useState("");
|
|
110
154
|
const [newCategory, setNewCategory] = useState("");
|
|
111
155
|
|
|
156
|
+
// Las categorías no son una lista plana: dependen de la cadena y del
|
|
157
|
+
// departamento. Es la misma cascada que usa el alta de productos, y se sirve
|
|
158
|
+
// del mismo endpoint para no acabar con dos árboles de categorías distintos.
|
|
159
|
+
const [filters, setFilters] = useState({
|
|
160
|
+
retailers: [],
|
|
161
|
+
departments: [],
|
|
162
|
+
categories: [],
|
|
163
|
+
});
|
|
164
|
+
const [loadingFilters, setLoadingFilters] = useState(false);
|
|
165
|
+
|
|
112
166
|
const [loading, setLoading] = useState(false);
|
|
113
167
|
const [error, setError] = useState(null);
|
|
114
168
|
|
|
@@ -137,6 +191,8 @@ export const LinkVariantsModal = ({
|
|
|
137
191
|
setSelected([]);
|
|
138
192
|
setRejected(null);
|
|
139
193
|
setSearched(false);
|
|
194
|
+
setTotal(0);
|
|
195
|
+
setHasMore(false);
|
|
140
196
|
setError(null);
|
|
141
197
|
setStep("search");
|
|
142
198
|
}, [mode]);
|
|
@@ -153,6 +209,8 @@ export const LinkVariantsModal = ({
|
|
|
153
209
|
});
|
|
154
210
|
setCandidates(data.candidates || []);
|
|
155
211
|
setRejected(data.rejected || null);
|
|
212
|
+
setTotal(data.total ?? (data.candidates || []).length);
|
|
213
|
+
setHasMore(!!data.hasMore);
|
|
156
214
|
setSearched(true);
|
|
157
215
|
} catch (err) {
|
|
158
216
|
setError(readError(err));
|
|
@@ -161,6 +219,34 @@ export const LinkVariantsModal = ({
|
|
|
161
219
|
}
|
|
162
220
|
};
|
|
163
221
|
|
|
222
|
+
/*
|
|
223
|
+
Trae la siguiente página y la agrega a la lista.
|
|
224
|
+
|
|
225
|
+
Se acumula en vez de reemplazar porque la selección vive en esta pantalla: si
|
|
226
|
+
la página 2 borrara la 1, lo marcado antes desaparecería de la vista sin
|
|
227
|
+
dejar de estar seleccionado, y el usuario acabaría ligando productos que ya
|
|
228
|
+
no ve.
|
|
229
|
+
*/
|
|
230
|
+
const loadMore = async () => {
|
|
231
|
+
setLoadingMore(true);
|
|
232
|
+
setError(null);
|
|
233
|
+
try {
|
|
234
|
+
const data = await request("search", {
|
|
235
|
+
mode,
|
|
236
|
+
articleId,
|
|
237
|
+
query: query.trim(),
|
|
238
|
+
offset: candidates.length,
|
|
239
|
+
});
|
|
240
|
+
setCandidates((current) => [...current, ...(data.candidates || [])]);
|
|
241
|
+
setTotal(data.total ?? total);
|
|
242
|
+
setHasMore(!!data.hasMore);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
setError(readError(err));
|
|
245
|
+
} finally {
|
|
246
|
+
setLoadingMore(false);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
164
250
|
const toggle = (id) => {
|
|
165
251
|
setSelected((current) => {
|
|
166
252
|
if (mode === "parent") return current[0] === id ? [] : [id];
|
|
@@ -191,17 +277,23 @@ export const LinkVariantsModal = ({
|
|
|
191
277
|
|
|
192
278
|
setOffered(listas);
|
|
193
279
|
setNameScope(data.nameScope ?? null);
|
|
194
|
-
// Todo preseleccionado: heredar es el caso normal, y quitar lo que sobra
|
|
195
|
-
// es menos trabajo que marcar de cero lo que casi siempre se quiere.
|
|
196
280
|
setChosen({
|
|
197
|
-
attributes:
|
|
198
|
-
descriptions:
|
|
199
|
-
images:
|
|
200
|
-
categoryRetailers:
|
|
201
|
-
|
|
202
|
-
|
|
281
|
+
attributes: [],
|
|
282
|
+
descriptions: [],
|
|
283
|
+
images: [],
|
|
284
|
+
categoryRetailers: [],
|
|
285
|
+
});
|
|
286
|
+
setModes({
|
|
287
|
+
categoryRetailers: null,
|
|
288
|
+
descriptions: null,
|
|
289
|
+
attributes: null,
|
|
290
|
+
images: null,
|
|
203
291
|
});
|
|
204
292
|
setExtraRetailers([]);
|
|
293
|
+
setBrokenImages([]);
|
|
294
|
+
setPerChild({});
|
|
295
|
+
setApplyToAll(false);
|
|
296
|
+
setActiveChild(mode === "children" ? selected[0] : null);
|
|
205
297
|
setStep("inherit");
|
|
206
298
|
} catch (err) {
|
|
207
299
|
setError(readError(err));
|
|
@@ -222,13 +314,77 @@ export const LinkVariantsModal = ({
|
|
|
222
314
|
});
|
|
223
315
|
};
|
|
224
316
|
|
|
225
|
-
const
|
|
317
|
+
const chooseMode = (dimension, modo, ids) => {
|
|
318
|
+
setModes((current) => ({ ...current, [dimension]: modo }));
|
|
226
319
|
setChosen((current) => ({
|
|
227
320
|
...current,
|
|
228
|
-
|
|
321
|
+
// "Todos" marca la lista completa para que se vea qué va a bajar; al pasar
|
|
322
|
+
// a "elegir" se parte de cero, que es lo que la palabra promete.
|
|
323
|
+
[dimension]: modo === "all" ? ids : [],
|
|
229
324
|
}));
|
|
230
325
|
};
|
|
231
326
|
|
|
327
|
+
const loadFilters = useCallback(
|
|
328
|
+
async (retailer, department) => {
|
|
329
|
+
setLoadingFilters(true);
|
|
330
|
+
try {
|
|
331
|
+
const res = await axios.get(
|
|
332
|
+
`${filtersEndpoint}?retailerSelected=${
|
|
333
|
+
retailer ?? ""
|
|
334
|
+
}&departmentSelected=${department ?? ""}`,
|
|
335
|
+
{ headers: { Authorization: token } }
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// readPayload YA devuelve el cuerpo parseado. Volver a buscar `body`
|
|
339
|
+
// dentro dejaba la lista siempre vacía, y por eso el selector de cadenas
|
|
340
|
+
// no mostraba nada.
|
|
341
|
+
const lista = readPayload(res)?.data ?? [];
|
|
342
|
+
|
|
343
|
+
setFilters((current) => {
|
|
344
|
+
if (!retailer) {
|
|
345
|
+
return {
|
|
346
|
+
...current,
|
|
347
|
+
retailers: lista.map((item) => ({
|
|
348
|
+
value: item.id,
|
|
349
|
+
label: item.name,
|
|
350
|
+
})),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (!department) {
|
|
354
|
+
return {
|
|
355
|
+
...current,
|
|
356
|
+
departments: lista.map((item) => ({
|
|
357
|
+
value: item.category,
|
|
358
|
+
label: item.category,
|
|
359
|
+
})),
|
|
360
|
+
categories: [],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
...current,
|
|
365
|
+
categories: lista.map((item) => ({
|
|
366
|
+
value: item.id,
|
|
367
|
+
label: item.name,
|
|
368
|
+
})),
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error("linkVariants/filters: ", error);
|
|
373
|
+
} finally {
|
|
374
|
+
setLoadingFilters(false);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
[filtersEndpoint, token]
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Las cadenas del catálogo se piden al entrar al paso de herencia, no al abrir
|
|
381
|
+
// el modal: quien solo busca y cancela no llega a necesitarlas.
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (step === "inherit" && !filters.retailers.length) {
|
|
384
|
+
loadFilters(null, null);
|
|
385
|
+
}
|
|
386
|
+
}, [step, filters.retailers.length, loadFilters]);
|
|
387
|
+
|
|
232
388
|
const addRetailer = () => {
|
|
233
389
|
const retailerId = Number(newRetailer);
|
|
234
390
|
const categoryId = Number(newCategory);
|
|
@@ -242,11 +398,11 @@ export const LinkVariantsModal = ({
|
|
|
242
398
|
|
|
243
399
|
if (yaEsta) return;
|
|
244
400
|
|
|
245
|
-
const retailer =
|
|
246
|
-
(item) => Number(item.
|
|
401
|
+
const retailer = filters.retailers.find(
|
|
402
|
+
(item) => Number(item.value) === retailerId
|
|
247
403
|
);
|
|
248
|
-
const category =
|
|
249
|
-
(item) => Number(item.
|
|
404
|
+
const category = filters.categories.find(
|
|
405
|
+
(item) => Number(item.value) === categoryId
|
|
250
406
|
);
|
|
251
407
|
|
|
252
408
|
setExtraRetailers((current) => [
|
|
@@ -255,8 +411,8 @@ export const LinkVariantsModal = ({
|
|
|
255
411
|
clave,
|
|
256
412
|
retailerId,
|
|
257
413
|
categoryId,
|
|
258
|
-
retailerName: retailer?.
|
|
259
|
-
categoryName: category?.
|
|
414
|
+
retailerName: retailer?.label ?? `Cadena ${retailerId}`,
|
|
415
|
+
categoryName: category?.label ?? `Categoría ${categoryId}`,
|
|
260
416
|
},
|
|
261
417
|
]);
|
|
262
418
|
setChosen((current) => ({
|
|
@@ -264,38 +420,117 @@ export const LinkVariantsModal = ({
|
|
|
264
420
|
categoryRetailers: [...current.categoryRetailers, clave],
|
|
265
421
|
}));
|
|
266
422
|
setNewRetailer("");
|
|
423
|
+
setNewDepartment("");
|
|
267
424
|
setNewCategory("");
|
|
268
425
|
};
|
|
269
426
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
setError(null);
|
|
427
|
+
/*
|
|
428
|
+
Convierte una selección de pantalla en el bloque `inherit` de la API.
|
|
273
429
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
430
|
+
Con el modo en "todos" se manda la palabra y no la lista que se pintó: si
|
|
431
|
+
entre la carga del modal y el guardado alguien agregó un valor al padre,
|
|
432
|
+
"todos" sigue significando todos. La lista sería una foto ya vieja.
|
|
433
|
+
|
|
434
|
+
Las parejas cadena+categoría viajan como objetos, no como ids, porque pueden
|
|
435
|
+
ser cadenas que el padre no tiene: ahí no hay nada que heredar, se asigna.
|
|
436
|
+
*/
|
|
437
|
+
// Las que de verdad se pueden heredar: la base puede listar filas cuyo archivo
|
|
438
|
+
// ya no existe, y esas no cuentan ni para la lista ni para el "todas".
|
|
439
|
+
const imagenesUtiles = offered.images.filter(
|
|
440
|
+
(image) => !brokenImages.includes(image.id)
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const buildInherit = (seleccion) => {
|
|
444
|
+
const parejas = [
|
|
277
445
|
...offered.categoryRetailers.map((item) => ({
|
|
278
446
|
clave: `${item.retailerId}-${item.categoryId}`,
|
|
279
447
|
retailerId: item.retailerId,
|
|
280
448
|
categoryId: item.categoryId,
|
|
281
449
|
})),
|
|
282
|
-
...extraRetailers,
|
|
450
|
+
...(seleccion.extraRetailers ?? []),
|
|
283
451
|
];
|
|
284
452
|
|
|
285
|
-
|
|
453
|
+
return {
|
|
286
454
|
attributes:
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.
|
|
455
|
+
seleccion.modes.attributes === "all"
|
|
456
|
+
? "all-filled"
|
|
457
|
+
: seleccion.chosen.attributes,
|
|
458
|
+
descriptions:
|
|
459
|
+
seleccion.modes.descriptions === "all"
|
|
460
|
+
? "all"
|
|
461
|
+
: seleccion.chosen.descriptions,
|
|
462
|
+
images:
|
|
463
|
+
seleccion.modes.images === "all" ? "all" : seleccion.chosen.images,
|
|
464
|
+
categoryRetailers: parejas
|
|
465
|
+
.filter((par) => seleccion.chosen.categoryRetailers.includes(par.clave))
|
|
292
466
|
.map(({ retailerId, categoryId }) => ({ retailerId, categoryId })),
|
|
293
467
|
};
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const seleccionVacia = () => ({
|
|
471
|
+
modes: {
|
|
472
|
+
categoryRetailers: null,
|
|
473
|
+
descriptions: null,
|
|
474
|
+
attributes: null,
|
|
475
|
+
images: null,
|
|
476
|
+
},
|
|
477
|
+
chosen: {
|
|
478
|
+
attributes: [],
|
|
479
|
+
descriptions: [],
|
|
480
|
+
images: [],
|
|
481
|
+
categoryRetailers: [],
|
|
482
|
+
},
|
|
483
|
+
extraRetailers: [],
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/*
|
|
487
|
+
Cambia de pestaña conservando lo elegido en la anterior. Sin esto, moverse
|
|
488
|
+
entre productos borraría el trabajo hecho en cada uno.
|
|
489
|
+
*/
|
|
490
|
+
const switchChild = (id) => {
|
|
491
|
+
setPerChild((current) => ({
|
|
492
|
+
...current,
|
|
493
|
+
[activeChild]: { modes, chosen, extraRetailers },
|
|
494
|
+
}));
|
|
495
|
+
|
|
496
|
+
const guardado = perChild[id] ?? seleccionVacia();
|
|
497
|
+
setModes(guardado.modes);
|
|
498
|
+
setChosen(guardado.chosen);
|
|
499
|
+
setExtraRetailers(guardado.extraRetailers ?? []);
|
|
500
|
+
setActiveChild(id);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const submit = async () => {
|
|
504
|
+
setLoading(true);
|
|
505
|
+
setError(null);
|
|
506
|
+
|
|
507
|
+
const inherit = buildInherit({ modes, chosen, extraRetailers });
|
|
508
|
+
|
|
509
|
+
// Con las pestañas abiertas cada producto manda la suya. La selección de la
|
|
510
|
+
// pestaña a la vista todavía no está en perChild, así que se agrega aquí: si
|
|
511
|
+
// no, lo último que tocó el usuario se perdería al guardar.
|
|
512
|
+
const inheritByChild =
|
|
513
|
+
!applyToAll && mode === "children"
|
|
514
|
+
? selected.reduce((acc, id) => {
|
|
515
|
+
const guardado =
|
|
516
|
+
id === activeChild
|
|
517
|
+
? { modes, chosen, extraRetailers }
|
|
518
|
+
: perChild[id];
|
|
519
|
+
if (guardado) acc[id] = buildInherit(guardado);
|
|
520
|
+
return acc;
|
|
521
|
+
}, {})
|
|
522
|
+
: undefined;
|
|
294
523
|
|
|
295
524
|
const body =
|
|
296
525
|
mode === "parent"
|
|
297
526
|
? { mode, articleId, parentArticleId: selected[0], inherit }
|
|
298
|
-
: {
|
|
527
|
+
: {
|
|
528
|
+
mode,
|
|
529
|
+
parentArticleId: articleId,
|
|
530
|
+
childrenArticleIds: selected,
|
|
531
|
+
inherit,
|
|
532
|
+
...(inheritByChild ? { inheritByChild } : {}),
|
|
533
|
+
};
|
|
299
534
|
|
|
300
535
|
try {
|
|
301
536
|
const data = await request("link", body);
|
|
@@ -315,6 +550,63 @@ export const LinkVariantsModal = ({
|
|
|
315
550
|
}
|
|
316
551
|
};
|
|
317
552
|
|
|
553
|
+
// Una dimensión está decidida si el usuario eligió un modo, o si el padre no
|
|
554
|
+
// ofrece nada en ella —no se puede pedir que decida sobre una lista vacía—.
|
|
555
|
+
const decidida = (dimension) => {
|
|
556
|
+
if (modes[dimension] !== null) return true;
|
|
557
|
+
if (dimension === "categoryRetailers") {
|
|
558
|
+
return offered.categoryRetailers.length === 0 && extraRetailers.length === 0;
|
|
559
|
+
}
|
|
560
|
+
// Una imagen cuyo archivo no existe no es algo sobre lo que se pueda decidir.
|
|
561
|
+
return dimension === "images"
|
|
562
|
+
? imagenesUtiles.length === 0
|
|
563
|
+
: offered[dimension].length === 0;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const DIMENSIONES = [
|
|
567
|
+
"categoryRetailers",
|
|
568
|
+
"descriptions",
|
|
569
|
+
"attributes",
|
|
570
|
+
"images",
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
// Con pestañas, que la que estoy viendo esté completa no basta: ligar escribe
|
|
574
|
+
// en todos, así que todos tienen que estar decididos. Si no, un producto se
|
|
575
|
+
// ligaría sin heredar nada solo porque nadie abrió su pestaña.
|
|
576
|
+
const decididoPara = (id) => {
|
|
577
|
+
const guardado =
|
|
578
|
+
id === activeChild ? { modes, chosen, extraRetailers } : perChild[id];
|
|
579
|
+
if (!guardado) return false;
|
|
580
|
+
|
|
581
|
+
return DIMENSIONES.every((dimension) => {
|
|
582
|
+
if (guardado.modes[dimension] !== null) return true;
|
|
583
|
+
if (dimension === "categoryRetailers") {
|
|
584
|
+
return (
|
|
585
|
+
offered.categoryRetailers.length === 0 &&
|
|
586
|
+
(guardado.extraRetailers ?? []).length === 0
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return dimension === "images"
|
|
590
|
+
? imagenesUtiles.length === 0
|
|
591
|
+
: offered[dimension].length === 0;
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const todoDecidido =
|
|
596
|
+
step === "inherit" &&
|
|
597
|
+
(applyToAll || mode !== "children"
|
|
598
|
+
? DIMENSIONES.every(decidida)
|
|
599
|
+
: selected.every(decididoPara));
|
|
600
|
+
|
|
601
|
+
const faltantes = [
|
|
602
|
+
["categoryRetailers", "cadenas"],
|
|
603
|
+
["descriptions", "descripciones"],
|
|
604
|
+
["attributes", "atributos"],
|
|
605
|
+
["images", "imágenes"],
|
|
606
|
+
]
|
|
607
|
+
.filter(([clave]) => !decidida(clave))
|
|
608
|
+
.map(([, nombre]) => nombre);
|
|
609
|
+
|
|
318
610
|
const selectionLabel =
|
|
319
611
|
mode === "parent"
|
|
320
612
|
? "Se elige un solo producto padre."
|
|
@@ -331,7 +623,12 @@ export const LinkVariantsModal = ({
|
|
|
331
623
|
{articleUpc ? ` · UPC ${articleUpc}` : ""}
|
|
332
624
|
</p>
|
|
333
625
|
</div>
|
|
334
|
-
<button
|
|
626
|
+
<button
|
|
627
|
+
type="button"
|
|
628
|
+
className="close"
|
|
629
|
+
onClick={onClose}
|
|
630
|
+
aria-label="Cerrar"
|
|
631
|
+
>
|
|
335
632
|
×
|
|
336
633
|
</button>
|
|
337
634
|
</Header>
|
|
@@ -403,7 +700,9 @@ export const LinkVariantsModal = ({
|
|
|
403
700
|
{candidate.name || `Producto ${candidate.articleId}`}
|
|
404
701
|
<span className="meta">
|
|
405
702
|
UPC {candidate.upc || "—"}
|
|
406
|
-
{candidate.categoryName
|
|
703
|
+
{candidate.categoryName
|
|
704
|
+
? ` · ${candidate.categoryName}`
|
|
705
|
+
: ""}
|
|
407
706
|
</span>
|
|
408
707
|
</span>
|
|
409
708
|
{candidate.variantsCount > 0 && (
|
|
@@ -417,129 +716,293 @@ export const LinkVariantsModal = ({
|
|
|
417
716
|
))}
|
|
418
717
|
</List>
|
|
419
718
|
)}
|
|
719
|
+
|
|
720
|
+
{/* Cuántos hay de verdad. Sin esto la lista se corta en 20 y no hay
|
|
721
|
+
forma de saber si eso es todo o solo el principio. */}
|
|
722
|
+
{searched && candidates.length > 0 && (
|
|
723
|
+
<Paging>
|
|
724
|
+
<span>
|
|
725
|
+
Mostrando {candidates.length} de {total}
|
|
726
|
+
</span>
|
|
727
|
+
{hasMore && (
|
|
728
|
+
<button
|
|
729
|
+
type="button"
|
|
730
|
+
onClick={loadMore}
|
|
731
|
+
disabled={loadingMore}
|
|
732
|
+
>
|
|
733
|
+
{loadingMore ? "Cargando…" : "Cargar más"}
|
|
734
|
+
</button>
|
|
735
|
+
)}
|
|
736
|
+
</Paging>
|
|
737
|
+
)}
|
|
420
738
|
</>
|
|
421
739
|
)}
|
|
422
740
|
|
|
423
741
|
{step === "inherit" && (
|
|
424
742
|
<>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
<label className="choice">
|
|
442
|
-
<input
|
|
443
|
-
type="radio"
|
|
444
|
-
checked={inheritMode === "selected"}
|
|
445
|
-
onChange={() => setInheritMode("selected")}
|
|
446
|
-
/>
|
|
447
|
-
<span>
|
|
448
|
-
Elegir cuáles
|
|
449
|
-
<span className="hint">
|
|
450
|
-
Lo que no elijas conserva el valor que ya tiene la variante.
|
|
743
|
+
{mode === "children" && selected.length > 1 && (
|
|
744
|
+
<>
|
|
745
|
+
<label className="apply-all">
|
|
746
|
+
<input
|
|
747
|
+
type="checkbox"
|
|
748
|
+
checked={applyToAll}
|
|
749
|
+
onChange={(event) => {
|
|
750
|
+
setApplyToAll(event.target.checked);
|
|
751
|
+
if (event.target.checked) setPerChild({});
|
|
752
|
+
}}
|
|
753
|
+
/>
|
|
754
|
+
<span>
|
|
755
|
+
Aplicar lo mismo a los {selected.length} productos
|
|
756
|
+
<span className="hint">
|
|
757
|
+
Desmárcalo si alguno debe heredar algo distinto.
|
|
758
|
+
</span>
|
|
451
759
|
</span>
|
|
452
|
-
</
|
|
453
|
-
</label>
|
|
454
|
-
</Section>
|
|
760
|
+
</label>
|
|
455
761
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
762
|
+
{!applyToAll && (
|
|
763
|
+
<Tabs>
|
|
764
|
+
{selected.map((id, indice) => {
|
|
765
|
+
const producto = candidates.find(
|
|
766
|
+
(item) => item.articleId === id
|
|
767
|
+
);
|
|
768
|
+
return (
|
|
769
|
+
<button
|
|
770
|
+
key={id}
|
|
771
|
+
type="button"
|
|
772
|
+
className={id === activeChild ? "active" : ""}
|
|
773
|
+
onClick={() => switchChild(id)}
|
|
774
|
+
>
|
|
775
|
+
<span className="orden">{indice + 1}</span>
|
|
776
|
+
{producto?.name || `Producto ${id}`}
|
|
777
|
+
{decididoPara(id) && (
|
|
778
|
+
<span className="listo" aria-label="listo">
|
|
779
|
+
✓
|
|
780
|
+
</span>
|
|
781
|
+
)}
|
|
782
|
+
</button>
|
|
783
|
+
);
|
|
784
|
+
})}
|
|
785
|
+
</Tabs>
|
|
786
|
+
)}
|
|
787
|
+
</>
|
|
461
788
|
)}
|
|
462
789
|
|
|
463
|
-
{
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
790
|
+
{/* El orden no es casual: primero se decide en qué cadenas vive la
|
|
791
|
+
variante, porque de eso depende qué atributos le aplican. Las
|
|
792
|
+
imágenes van al final: son la decisión más visual y la que menos
|
|
793
|
+
arrastra a las demás. */}
|
|
794
|
+
|
|
795
|
+
<Block>
|
|
796
|
+
<h3>
|
|
797
|
+
<span className="paso">1</span> Cadenas y categorías
|
|
798
|
+
</h3>
|
|
799
|
+
<Choices>
|
|
800
|
+
<label>
|
|
801
|
+
<input
|
|
802
|
+
type="radio"
|
|
803
|
+
name={`categoryRetailers-${activeChild ?? "todos"}`}
|
|
804
|
+
checked={modes.categoryRetailers === "all"}
|
|
805
|
+
onChange={() =>
|
|
806
|
+
chooseMode(
|
|
807
|
+
"categoryRetailers",
|
|
808
|
+
"all",
|
|
809
|
+
offered.categoryRetailers.map(
|
|
810
|
+
(item) => `${item.retailerId}-${item.categoryId}`
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
/>
|
|
815
|
+
<span>
|
|
816
|
+
Todas las del producto padre
|
|
817
|
+
<span className="hint">
|
|
818
|
+
{offered.categoryRetailers.length} cadena(s).
|
|
819
|
+
</span>
|
|
820
|
+
</span>
|
|
821
|
+
</label>
|
|
822
|
+
<label>
|
|
823
|
+
<input
|
|
824
|
+
type="radio"
|
|
825
|
+
name={`categoryRetailers-${activeChild ?? "todos"}`}
|
|
826
|
+
checked={modes.categoryRetailers === "some"}
|
|
827
|
+
onChange={() =>
|
|
828
|
+
chooseMode("categoryRetailers", "some", [])
|
|
829
|
+
}
|
|
830
|
+
/>
|
|
831
|
+
<span>
|
|
832
|
+
Elegir cuáles
|
|
833
|
+
<span className="hint">
|
|
834
|
+
También puedes agregar una cadena que el padre no tenga.
|
|
835
|
+
</span>
|
|
836
|
+
</span>
|
|
837
|
+
</label>
|
|
838
|
+
</Choices>
|
|
839
|
+
|
|
840
|
+
<List>
|
|
841
|
+
{[
|
|
842
|
+
...offered.categoryRetailers.map((item) => ({
|
|
843
|
+
...item,
|
|
844
|
+
clave: `${item.retailerId}-${item.categoryId}`,
|
|
845
|
+
delPadre: true,
|
|
846
|
+
})),
|
|
847
|
+
...extraRetailers.map((item) => ({
|
|
848
|
+
...item,
|
|
849
|
+
delPadre: false,
|
|
850
|
+
})),
|
|
851
|
+
].map((item) => (
|
|
852
|
+
<li key={item.clave}>
|
|
467
853
|
<label>
|
|
468
854
|
<input
|
|
469
855
|
type="checkbox"
|
|
470
|
-
|
|
471
|
-
|
|
856
|
+
disabled={modes.categoryRetailers === "all"}
|
|
857
|
+
checked={chosen.categoryRetailers.includes(
|
|
858
|
+
item.clave
|
|
859
|
+
)}
|
|
860
|
+
onChange={() =>
|
|
861
|
+
toggleChoice("categoryRetailers", item.clave)
|
|
862
|
+
}
|
|
863
|
+
/>
|
|
864
|
+
<img
|
|
865
|
+
className="logo"
|
|
866
|
+
src={getRetailerPicture(item.retailerId, 24, 24)}
|
|
867
|
+
alt=""
|
|
472
868
|
/>
|
|
473
869
|
<span>
|
|
474
|
-
{
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
)}
|
|
480
|
-
{attribute.ambiguousName && (
|
|
481
|
-
<span className="note">
|
|
482
|
-
se llama distinto en otra cadena
|
|
483
|
-
</span>
|
|
484
|
-
)}
|
|
485
|
-
</span>
|
|
486
|
-
<span className="value" title={attribute.value}>
|
|
487
|
-
{attribute.value}
|
|
870
|
+
{item.retailerName}
|
|
871
|
+
<span className="meta">
|
|
872
|
+
{item.categoryName || "Sin categoría"}
|
|
873
|
+
{!item.delPadre && " · no la tiene el padre"}
|
|
874
|
+
</span>
|
|
488
875
|
</span>
|
|
489
876
|
</label>
|
|
490
877
|
</li>
|
|
491
878
|
))}
|
|
492
|
-
</
|
|
493
|
-
)}
|
|
879
|
+
</List>
|
|
494
880
|
|
|
495
|
-
|
|
496
|
-
|
|
881
|
+
<AddRetailer>
|
|
882
|
+
<select
|
|
883
|
+
value={newRetailer}
|
|
884
|
+
onChange={(event) => {
|
|
885
|
+
const valor = event.target.value;
|
|
886
|
+
setNewRetailer(valor);
|
|
887
|
+
setNewDepartment("");
|
|
888
|
+
setNewCategory("");
|
|
889
|
+
if (valor) loadFilters(valor, null);
|
|
890
|
+
}}
|
|
891
|
+
>
|
|
892
|
+
<option value="">Agregar otra cadena…</option>
|
|
893
|
+
{filters.retailers.map((item) => (
|
|
894
|
+
<option key={item.value} value={item.value}>
|
|
895
|
+
{item.label}
|
|
896
|
+
</option>
|
|
897
|
+
))}
|
|
898
|
+
</select>
|
|
899
|
+
<select
|
|
900
|
+
value={newDepartment}
|
|
901
|
+
disabled={!newRetailer || !filters.departments.length}
|
|
902
|
+
onChange={(event) => {
|
|
903
|
+
const valor = event.target.value;
|
|
904
|
+
setNewDepartment(valor);
|
|
905
|
+
setNewCategory("");
|
|
906
|
+
if (valor) loadFilters(newRetailer, valor);
|
|
907
|
+
}}
|
|
908
|
+
>
|
|
909
|
+
<option value="">Departamento…</option>
|
|
910
|
+
{filters.departments.map((item) => (
|
|
911
|
+
<option key={item.value} value={item.value}>
|
|
912
|
+
{item.label}
|
|
913
|
+
</option>
|
|
914
|
+
))}
|
|
915
|
+
</select>
|
|
916
|
+
<select
|
|
917
|
+
value={newCategory}
|
|
918
|
+
disabled={!newDepartment || !filters.categories.length}
|
|
919
|
+
onChange={(event) => setNewCategory(event.target.value)}
|
|
920
|
+
>
|
|
921
|
+
<option value="">Categoría…</option>
|
|
922
|
+
{filters.categories.map((item) => (
|
|
923
|
+
<option key={item.value} value={item.value}>
|
|
924
|
+
{item.label}
|
|
925
|
+
</option>
|
|
926
|
+
))}
|
|
927
|
+
</select>
|
|
497
928
|
<button
|
|
498
929
|
type="button"
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
onClick={() =>
|
|
502
|
-
setOpenSection(
|
|
503
|
-
openSection === "descriptions" ? null : "descriptions"
|
|
504
|
-
)
|
|
505
|
-
}
|
|
930
|
+
onClick={addRetailer}
|
|
931
|
+
disabled={!newRetailer || !newCategory || loadingFilters}
|
|
506
932
|
>
|
|
507
|
-
|
|
508
|
-
<span className="count">
|
|
509
|
-
{chosen.descriptions.length} de{" "}
|
|
510
|
-
{offered.descriptions.length}
|
|
511
|
-
</span>
|
|
933
|
+
{loadingFilters ? "…" : "Agregar"}
|
|
512
934
|
</button>
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
935
|
+
</AddRetailer>
|
|
936
|
+
</Block>
|
|
937
|
+
|
|
938
|
+
<Block>
|
|
939
|
+
<h3>
|
|
940
|
+
<span className="paso">2</span> Descripciones
|
|
941
|
+
</h3>
|
|
942
|
+
{offered.descriptions.length ? (
|
|
943
|
+
<>
|
|
944
|
+
<Choices>
|
|
945
|
+
<label>
|
|
946
|
+
<input
|
|
947
|
+
type="radio"
|
|
948
|
+
name={`descriptions-${activeChild ?? "todos"}`}
|
|
949
|
+
checked={modes.descriptions === "all"}
|
|
950
|
+
onChange={() =>
|
|
951
|
+
chooseMode(
|
|
952
|
+
"descriptions",
|
|
953
|
+
"all",
|
|
954
|
+
offered.descriptions.map((item) => item.id)
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
/>
|
|
958
|
+
<span>
|
|
959
|
+
Todas las que tienen valor
|
|
960
|
+
<span className="hint">
|
|
961
|
+
{offered.descriptions.length} descripción(es) con
|
|
962
|
+
información en el padre.
|
|
963
|
+
</span>
|
|
964
|
+
</span>
|
|
965
|
+
</label>
|
|
966
|
+
<label>
|
|
967
|
+
<input
|
|
968
|
+
type="radio"
|
|
969
|
+
name={`descriptions-${activeChild ?? "todos"}`}
|
|
970
|
+
checked={modes.descriptions === "some"}
|
|
971
|
+
onChange={() =>
|
|
972
|
+
chooseMode("descriptions", "some", [])
|
|
973
|
+
}
|
|
974
|
+
/>
|
|
975
|
+
<span>
|
|
976
|
+
Elegir cuáles
|
|
977
|
+
<span className="hint">
|
|
978
|
+
Lo que no elijas conserva el valor que ya tiene la
|
|
979
|
+
variante.
|
|
980
|
+
</span>
|
|
981
|
+
</span>
|
|
982
|
+
</label>
|
|
983
|
+
</Choices>
|
|
530
984
|
|
|
531
|
-
{openSection === "descriptions" &&
|
|
532
|
-
(offered.descriptions.length ? (
|
|
533
985
|
<AttributeList>
|
|
534
986
|
{offered.descriptions.map((item) => (
|
|
535
987
|
<li key={item.id}>
|
|
536
988
|
<label>
|
|
537
989
|
<input
|
|
538
990
|
type="checkbox"
|
|
991
|
+
disabled={modes.descriptions === "all"}
|
|
539
992
|
checked={chosen.descriptions.includes(item.id)}
|
|
540
|
-
onChange={() =>
|
|
993
|
+
onChange={() =>
|
|
994
|
+
toggleChoice("descriptions", item.id)
|
|
995
|
+
}
|
|
541
996
|
/>
|
|
542
|
-
<span>
|
|
997
|
+
<span>
|
|
998
|
+
{item.name}
|
|
999
|
+
{item.duplicated && (
|
|
1000
|
+
<span className="note">
|
|
1001
|
+
el padre tiene {item.rows} versiones de este
|
|
1002
|
+
texto; se copian todas
|
|
1003
|
+
</span>
|
|
1004
|
+
)}
|
|
1005
|
+
</span>
|
|
543
1006
|
<span className="value" title={item.value}>
|
|
544
1007
|
{item.value}
|
|
545
1008
|
</span>
|
|
@@ -547,167 +1010,190 @@ export const LinkVariantsModal = ({
|
|
|
547
1010
|
</li>
|
|
548
1011
|
))}
|
|
549
1012
|
</AttributeList>
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
<button
|
|
558
|
-
type="button"
|
|
559
|
-
className="toggle"
|
|
560
|
-
aria-expanded={openSection === "images"}
|
|
561
|
-
onClick={() =>
|
|
562
|
-
setOpenSection(openSection === "images" ? null : "images")
|
|
563
|
-
}
|
|
564
|
-
>
|
|
565
|
-
Imágenes
|
|
566
|
-
<span className="count">
|
|
567
|
-
{chosen.images.length} de {offered.images.length}
|
|
568
|
-
</span>
|
|
569
|
-
</button>
|
|
570
|
-
{offered.images.length > 0 && (
|
|
571
|
-
<button
|
|
572
|
-
type="button"
|
|
573
|
-
className="all"
|
|
574
|
-
onClick={() =>
|
|
575
|
-
toggleAll(
|
|
576
|
-
"images",
|
|
577
|
-
offered.images.map((item) => item.id)
|
|
578
|
-
)
|
|
579
|
-
}
|
|
580
|
-
>
|
|
581
|
-
{chosen.images.length === offered.images.length
|
|
582
|
-
? "Ninguna"
|
|
583
|
-
: "Todas"}
|
|
584
|
-
</button>
|
|
585
|
-
)}
|
|
586
|
-
</SectionHead>
|
|
1013
|
+
</>
|
|
1014
|
+
) : (
|
|
1015
|
+
<Message>
|
|
1016
|
+
El producto padre no tiene descripciones que heredar.
|
|
1017
|
+
</Message>
|
|
1018
|
+
)}
|
|
1019
|
+
</Block>
|
|
587
1020
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1021
|
+
<Block>
|
|
1022
|
+
<h3>
|
|
1023
|
+
<span className="paso">3</span> Atributos de la ficha técnica
|
|
1024
|
+
</h3>
|
|
1025
|
+
{offered.attributes.length ? (
|
|
1026
|
+
<>
|
|
1027
|
+
<Choices>
|
|
1028
|
+
<label>
|
|
1029
|
+
<input
|
|
1030
|
+
type="radio"
|
|
1031
|
+
name={`attributes-${activeChild ?? "todos"}`}
|
|
1032
|
+
checked={modes.attributes === "all"}
|
|
1033
|
+
onChange={() =>
|
|
1034
|
+
chooseMode(
|
|
1035
|
+
"attributes",
|
|
1036
|
+
"all",
|
|
1037
|
+
offered.attributes.map((item) => item.id)
|
|
1038
|
+
)
|
|
596
1039
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1040
|
+
/>
|
|
1041
|
+
<span>
|
|
1042
|
+
Todos los que tienen valor
|
|
1043
|
+
<span className="hint">
|
|
1044
|
+
{offered.attributes.length} atributo(s) con
|
|
1045
|
+
información en el padre.
|
|
1046
|
+
</span>
|
|
1047
|
+
</span>
|
|
1048
|
+
</label>
|
|
1049
|
+
<label>
|
|
1050
|
+
<input
|
|
1051
|
+
type="radio"
|
|
1052
|
+
name={`attributes-${activeChild ?? "todos"}`}
|
|
1053
|
+
checked={modes.attributes === "some"}
|
|
1054
|
+
onChange={() => chooseMode("attributes", "some", [])}
|
|
1055
|
+
/>
|
|
1056
|
+
<span>
|
|
1057
|
+
Elegir cuáles
|
|
1058
|
+
<span className="hint">
|
|
1059
|
+
Lo que no elijas conserva el valor que ya tiene la
|
|
1060
|
+
variante.
|
|
1061
|
+
</span>
|
|
1062
|
+
</span>
|
|
1063
|
+
</label>
|
|
1064
|
+
</Choices>
|
|
612
1065
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
setOpenSection(
|
|
621
|
-
openSection === "retailers" ? null : "retailers"
|
|
622
|
-
)
|
|
623
|
-
}
|
|
624
|
-
>
|
|
625
|
-
Cadenas y categorías
|
|
626
|
-
<span className="count">
|
|
627
|
-
{chosen.categoryRetailers.length} seleccionada(s)
|
|
628
|
-
</span>
|
|
629
|
-
</button>
|
|
630
|
-
</SectionHead>
|
|
1066
|
+
{activeRetailerId && !nameScope && (
|
|
1067
|
+
<Message>
|
|
1068
|
+
El producto padre no comparte la cadena que tienes
|
|
1069
|
+
abierta, así que los nombres de abajo son los de otra de
|
|
1070
|
+
sus cadenas.
|
|
1071
|
+
</Message>
|
|
1072
|
+
)}
|
|
631
1073
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
{[
|
|
636
|
-
...offered.categoryRetailers.map((item) => ({
|
|
637
|
-
...item,
|
|
638
|
-
clave: `${item.retailerId}-${item.categoryId}`,
|
|
639
|
-
delPadre: true,
|
|
640
|
-
})),
|
|
641
|
-
...extraRetailers.map((item) => ({
|
|
642
|
-
...item,
|
|
643
|
-
delPadre: false,
|
|
644
|
-
})),
|
|
645
|
-
].map((item) => (
|
|
646
|
-
<li key={item.clave}>
|
|
1074
|
+
<AttributeList>
|
|
1075
|
+
{offered.attributes.map((attribute) => (
|
|
1076
|
+
<li key={attribute.id}>
|
|
647
1077
|
<label>
|
|
648
1078
|
<input
|
|
649
1079
|
type="checkbox"
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
)}
|
|
1080
|
+
disabled={modes.attributes === "all"}
|
|
1081
|
+
checked={chosen.attributes.includes(attribute.id)}
|
|
653
1082
|
onChange={() =>
|
|
654
|
-
toggleChoice("
|
|
1083
|
+
toggleChoice("attributes", attribute.id)
|
|
655
1084
|
}
|
|
656
1085
|
/>
|
|
657
1086
|
<span>
|
|
658
|
-
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1087
|
+
{attribute.name}
|
|
1088
|
+
{attribute.unconfigured && (
|
|
1089
|
+
<span className="note">
|
|
1090
|
+
sin configuración en las cadenas del padre
|
|
1091
|
+
</span>
|
|
1092
|
+
)}
|
|
1093
|
+
{attribute.ambiguousName && (
|
|
1094
|
+
<span className="note">
|
|
1095
|
+
se llama distinto en otra cadena
|
|
1096
|
+
</span>
|
|
1097
|
+
)}
|
|
1098
|
+
</span>
|
|
1099
|
+
<span className="value" title={attribute.value}>
|
|
1100
|
+
{attribute.value}
|
|
663
1101
|
</span>
|
|
664
1102
|
</label>
|
|
665
1103
|
</li>
|
|
666
1104
|
))}
|
|
667
|
-
</
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1105
|
+
</AttributeList>
|
|
1106
|
+
</>
|
|
1107
|
+
) : (
|
|
1108
|
+
<Message>
|
|
1109
|
+
El producto padre no tiene atributos con valor que heredar.
|
|
1110
|
+
</Message>
|
|
1111
|
+
)}
|
|
1112
|
+
</Block>
|
|
1113
|
+
|
|
1114
|
+
<Block>
|
|
1115
|
+
<h3>
|
|
1116
|
+
<span className="paso">4</span> Imágenes
|
|
1117
|
+
</h3>
|
|
1118
|
+
{imagenesUtiles.length ? (
|
|
1119
|
+
<>
|
|
1120
|
+
<Choices>
|
|
1121
|
+
<label>
|
|
1122
|
+
<input
|
|
1123
|
+
type="radio"
|
|
1124
|
+
name={`images-${activeChild ?? "todos"}`}
|
|
1125
|
+
checked={modes.images === "all"}
|
|
1126
|
+
onChange={() =>
|
|
1127
|
+
chooseMode(
|
|
1128
|
+
"images",
|
|
1129
|
+
"all",
|
|
1130
|
+
imagenesUtiles.map((item) => item.id)
|
|
1131
|
+
)
|
|
1132
|
+
}
|
|
1133
|
+
/>
|
|
1134
|
+
<span>
|
|
1135
|
+
Todas
|
|
1136
|
+
<span className="hint">
|
|
1137
|
+
{imagenesUtiles.length} imagen(es) en el padre.
|
|
1138
|
+
</span>
|
|
1139
|
+
</span>
|
|
1140
|
+
</label>
|
|
1141
|
+
<label>
|
|
1142
|
+
<input
|
|
1143
|
+
type="radio"
|
|
1144
|
+
name={`images-${activeChild ?? "todos"}`}
|
|
1145
|
+
checked={modes.images === "some"}
|
|
1146
|
+
onChange={() => chooseMode("images", "some", [])}
|
|
1147
|
+
/>
|
|
1148
|
+
<span>
|
|
1149
|
+
Elegir cuáles
|
|
1150
|
+
<span className="hint">
|
|
1151
|
+
Útil cuando la variante cambia de color o de
|
|
1152
|
+
empaque.
|
|
1153
|
+
</span>
|
|
1154
|
+
</span>
|
|
1155
|
+
</label>
|
|
1156
|
+
</Choices>
|
|
1157
|
+
|
|
1158
|
+
<ImageGrid>
|
|
1159
|
+
{imagenesUtiles.map((image) => (
|
|
1160
|
+
<label
|
|
1161
|
+
key={image.id}
|
|
1162
|
+
className={[
|
|
1163
|
+
chosen.images.includes(image.id) ? "picked" : "",
|
|
1164
|
+
modes.images === "all" ? "locked" : "",
|
|
1165
|
+
]
|
|
1166
|
+
.filter(Boolean)
|
|
1167
|
+
.join(" ")}
|
|
1168
|
+
>
|
|
1169
|
+
<input
|
|
1170
|
+
type="checkbox"
|
|
1171
|
+
disabled={modes.images === "all"}
|
|
1172
|
+
checked={chosen.images.includes(image.id)}
|
|
1173
|
+
onChange={() => toggleChoice("images", image.id)}
|
|
1174
|
+
/>
|
|
1175
|
+
<img
|
|
1176
|
+
src={getProductImage(image.src)}
|
|
1177
|
+
alt=""
|
|
1178
|
+
onError={() =>
|
|
1179
|
+
setBrokenImages((current) =>
|
|
1180
|
+
current.includes(image.id)
|
|
1181
|
+
? current
|
|
1182
|
+
: [...current, image.id]
|
|
1183
|
+
)
|
|
1184
|
+
}
|
|
1185
|
+
/>
|
|
1186
|
+
<span>{image.imageType || "Sin tipo de toma"}</span>
|
|
1187
|
+
</label>
|
|
1188
|
+
))}
|
|
1189
|
+
</ImageGrid>
|
|
708
1190
|
</>
|
|
1191
|
+
) : (
|
|
1192
|
+
<Message>
|
|
1193
|
+
El producto padre no tiene imágenes que heredar.
|
|
1194
|
+
</Message>
|
|
709
1195
|
)}
|
|
710
|
-
</
|
|
1196
|
+
</Block>
|
|
711
1197
|
|
|
712
1198
|
{/* Es la diferencia entre link y propagate, y es la que sorprende:
|
|
713
1199
|
aquí lo elegido se escribe encima aunque la variante lo hubiera
|
|
@@ -722,7 +1208,15 @@ export const LinkVariantsModal = ({
|
|
|
722
1208
|
|
|
723
1209
|
<Footer>
|
|
724
1210
|
<span className="summary">
|
|
725
|
-
{step === "search"
|
|
1211
|
+
{step === "search"
|
|
1212
|
+
? selectionLabel
|
|
1213
|
+
: todoDecidido
|
|
1214
|
+
? "Todo revisado. Puedes ligar."
|
|
1215
|
+
: !applyToAll && mode === "children"
|
|
1216
|
+
? `Faltan ${
|
|
1217
|
+
selected.filter((id) => !decididoPara(id)).length
|
|
1218
|
+
} producto(s) por revisar.`
|
|
1219
|
+
: `Falta decidir: ${faltantes.join(", ")}.`}
|
|
726
1220
|
</span>
|
|
727
1221
|
<div className="actions">
|
|
728
1222
|
{step === "inherit" && (
|
|
@@ -735,7 +1229,12 @@ export const LinkVariantsModal = ({
|
|
|
735
1229
|
Atrás
|
|
736
1230
|
</button>
|
|
737
1231
|
)}
|
|
738
|
-
<button
|
|
1232
|
+
<button
|
|
1233
|
+
type="button"
|
|
1234
|
+
className="secondary"
|
|
1235
|
+
onClick={onClose}
|
|
1236
|
+
disabled={loading}
|
|
1237
|
+
>
|
|
739
1238
|
Cancelar
|
|
740
1239
|
</button>
|
|
741
1240
|
{step === "search" ? (
|
|
@@ -752,15 +1251,7 @@ export const LinkVariantsModal = ({
|
|
|
752
1251
|
type="button"
|
|
753
1252
|
className="primary"
|
|
754
1253
|
onClick={submit}
|
|
755
|
-
disabled={
|
|
756
|
-
loading ||
|
|
757
|
-
// Solo estorba cuando hay algo que elegir y no se eligió nada.
|
|
758
|
-
// Con el padre sin atributos heredables, ligar sigue siendo
|
|
759
|
-
// válido: se heredan imágenes, descripciones y cadenas.
|
|
760
|
-
(inheritMode === "selected" &&
|
|
761
|
-
offered.attributes.length > 0 &&
|
|
762
|
-
!chosen.attributes.length)
|
|
763
|
-
}
|
|
1254
|
+
disabled={loading || !todoDecidido}
|
|
764
1255
|
>
|
|
765
1256
|
{loading ? "Ligando…" : "Ligar"}
|
|
766
1257
|
</button>
|