contentoh-components-library 21.6.49 → 21.6.50
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/.claude/launch.json +14 -0
- package/.env.test +1 -0
- package/dist/ai/prompts/attribute.prompt.js +28 -0
- package/dist/ai/prompts/description.prompt.js +33 -0
- package/dist/ai/schemas/attribute.schema.js +35 -0
- package/dist/ai/schemas/description.schema.js +27 -0
- package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +118 -0
- package/dist/components/atoms/AssetPreview/index.js +150 -0
- package/dist/components/atoms/AssetPreview/styles.js +28 -0
- package/dist/components/atoms/IconFile/index.js +85 -2
- package/dist/components/atoms/IconFile/styles.js +1 -1
- package/dist/components/atoms/ProductImage/ProductImage.stories.js +70 -2
- package/dist/components/atoms/ProductImage/index.js +100 -27
- package/dist/components/atoms/ProductImage/styles.js +3 -1
- package/dist/components/molecules/EditionActiveImage/index.js +4 -2
- package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +147 -0
- package/dist/components/molecules/GalleryAssetElement/index.js +178 -0
- package/dist/components/molecules/GalleryAssetElement/styles.js +26 -0
- package/dist/components/molecules/GalleryElement/index.js +13 -4
- package/dist/components/molecules/GalleryHeader/index.js +96 -24
- package/dist/components/molecules/GalleryHeader/styles.js +4 -1
- package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +56 -0
- package/dist/components/molecules/GalleryTrayTabs/index.js +60 -0
- package/dist/components/molecules/GalleryTrayTabs/styles.js +20 -0
- package/dist/components/molecules/ImageSelector/index.js +1 -0
- package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +171 -0
- package/dist/components/organisms/DigitalAssetsTray/index.js +211 -0
- package/dist/components/organisms/DigitalAssetsTray/styles.js +22 -0
- package/dist/components/organisms/ImagePreviewer/index.js +1 -0
- package/dist/components/organisms/ProductImageModal/index.js +3 -2
- package/dist/components/organisms/PropagateChangesModal/index.js +12 -1
- package/dist/components/organisms/PropagateChangesModal/non-propagable.js +45 -0
- package/dist/components/pages/ProviderProductEdition/index.js +11 -1
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +330 -15
- package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -2
- package/dist/components/pages/RetailerProductEdition/index.js +605 -414
- package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +45 -145
- package/dist/global-files/data.js +16 -3
- package/dist/global-files/digital-assets/assetTypes.js +480 -0
- package/dist/global-files/digital-assets/assets.actions.js +64 -0
- package/dist/global-files/digital-assets/assets.adapter.js +304 -0
- package/dist/global-files/digital-assets/assets.mock.js +136 -0
- package/dist/global-files/digital-assets/assets.reducer.js +220 -0
- package/dist/global-files/digital-assets/uploadAssets.js +179 -0
- package/dist/global-files/digital-assets/useDigitalAssets.js +522 -0
- package/dist/index.js +142 -90
- package/docs/activos-digitales.md +273 -0
- package/package.json +1 -1
- package/src/components/atoms/AssetPreview/AssetPreview.stories.js +78 -0
- package/src/components/atoms/AssetPreview/index.js +119 -0
- package/src/components/atoms/AssetPreview/styles.js +190 -0
- package/src/components/atoms/IconFile/index.js +28 -1
- package/src/components/atoms/IconFile/styles.js +8 -0
- package/src/components/atoms/ProductImage/ProductImage.stories.js +38 -0
- package/src/components/atoms/ProductImage/index.js +95 -20
- package/src/components/atoms/ProductImage/styles.js +92 -7
- package/src/components/molecules/EditionActiveImage/index.js +2 -2
- package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +102 -0
- package/src/components/molecules/GalleryAssetElement/index.js +162 -0
- package/src/components/molecules/GalleryAssetElement/styles.js +159 -0
- package/src/components/molecules/GalleryElement/index.js +9 -0
- package/src/components/molecules/GalleryHeader/index.js +65 -13
- package/src/components/molecules/GalleryHeader/styles.js +2 -1
- package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +32 -0
- package/src/components/molecules/GalleryTrayTabs/index.js +41 -0
- package/src/components/molecules/GalleryTrayTabs/styles.js +52 -0
- package/src/components/molecules/ImageSelector/index.js +1 -0
- package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +101 -0
- package/src/components/organisms/DigitalAssetsTray/index.js +189 -0
- package/src/components/organisms/DigitalAssetsTray/styles.js +183 -0
- package/src/components/organisms/ImagePreviewer/index.js +1 -0
- package/src/components/organisms/ProductImageModal/index.js +1 -0
- package/src/components/organisms/PropagateChangesModal/index.js +11 -1
- package/src/components/organisms/PropagateChangesModal/non-propagable.js +29 -0
- package/src/components/pages/ProviderProductEdition/index.js +10 -1
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +201 -0
- package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +12 -0
- package/src/components/pages/RetailerProductEdition/index.js +366 -140
- package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +1 -0
- package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +51 -104
- package/src/global-files/data.js +17 -2
- package/src/global-files/digital-assets/assetTypes.js +419 -0
- package/src/global-files/digital-assets/assets.actions.js +50 -0
- package/src/global-files/digital-assets/assets.adapter.js +229 -0
- package/src/global-files/digital-assets/assets.mock.js +122 -0
- package/src/global-files/digital-assets/assets.reducer.js +197 -0
- package/src/global-files/digital-assets/uploadAssets.js +109 -0
- package/src/global-files/digital-assets/useDigitalAssets.js +329 -0
- package/src/index.js +4 -0
|
@@ -15,6 +15,7 @@ ImageEditor.args = {
|
|
|
15
15
|
"Ficha técnica": false,
|
|
16
16
|
Imágenes: true,
|
|
17
17
|
},
|
|
18
|
+
enableDigitalAssets: true,
|
|
18
19
|
token:
|
|
19
20
|
"eyJraWQiOiJEV3owZnNieXg2MXNFcVduN3RCXC81bVhod3ZNbFZIOTgwUnZcL3RjT0lKdEk9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJjN2U1YTZhNC0yYjNmLTRkNTQtYWY5OC05ZTA5YTRjYmMwMWUiLCJjb2duaXRvOmdyb3VwcyI6WyJjb2xhYm9yYWRvcmVzX2NvbnRlbnRvaCJdLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfbFN6UVo0WjdSIiwiY29nbml0bzp1c2VybmFtZSI6ImM3ZTVhNmE0LTJiM2YtNGQ1NC1hZjk4LTllMDlhNGNiYzAxZSIsImF1ZCI6IjUyZDlza2tkY2c4cWpwODhvb2sxdXNlNm1rIiwiZXZlbnRfaWQiOiI5Y2Y2NTNkMS1iOTA1LTQ5ODgtYmI1MC0wY2E5NTllMTJkNTMiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTc2ODQ5Njg1MCwibmFtZSI6IkNvbGFib3JhZG9yIiwicGhvbmVfbnVtYmVyIjoiKzUyMTExMSIsImV4cCI6MTc2ODUwMDQ1MCwiaWF0IjoxNzY4NDk2ODUwLCJlbWFpbCI6Imtpa2lqZTE0NjdAbXRsY3ouY29tIn0.kVyqMGAoe0ye3i882XuO_8xKf7vNzpPPse3852YP70aK4f9vq7o7NP0DhPTv5UnrcwC0Lcu_UOqoplyHxgPbIzrKF6e4Ket_T7KA2GHnO7-d30wmYl2GxMm_pSgb_qP_IZpVj5bmlST26LpEftZDHPAue3eQaZ0xRBwkI5dkVbVWncZz9iVdrUdz5FC5gXuAHyEQzn-669UsywrtlRN5y8EgcxsccNmsL_El5ctGZYiNMgn1mBeS-FvEikhNORoZqS0e8JKaYmL-e2jcrEuShpDE3mJMsm0E_KBUxt_t0FoB-tUlRDWlDkpIWHNidzTYAUfudc908ykQ15b3EdvFVw",
|
|
20
21
|
productSelected: {
|
|
@@ -18,123 +18,70 @@ TextEditor.args = {
|
|
|
18
18
|
token:
|
|
19
19
|
"eyJraWQiOiJEV3owZnNieXg2MXNFcVduN3RCXC81bVhod3ZNbFZIOTgwUnZcL3RjT0lKdEk9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiI4ZDAxMDIxNC01YmZhLTQzYzMtOTZmYi1jNTU2ZGMwNTc3NGIiLCJjb2duaXRvOmdyb3VwcyI6WyJjb2xhYm9yYWRvcmVzX2NvbnRlbnRvaCJdLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfbFN6UVo0WjdSIiwiY29nbml0bzp1c2VybmFtZSI6IjhkMDEwMjE0LTViZmEtNDNjMy05NmZiLWM1NTZkYzA1Nzc0YiIsImF1ZCI6IjUyZDlza2tkY2c4cWpwODhvb2sxdXNlNm1rIiwiZXZlbnRfaWQiOiIzYzc3Y2JjMC02ZTM0LTRhZDQtOGJjMy02MmJmNGRhOTYxMTUiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTc2ODQ5NjU3NywibmFtZSI6IkNvbGFib3JhZG9yIiwicGhvbmVfbnVtYmVyIjoiKzUyMTExMSIsImV4cCI6MTc2ODUwMDE3NywiaWF0IjoxNzY4NDk2NTc3LCJlbWFpbCI6ImthcmFmZTMyMThAbW9tMmtpZC5jb20ifQ.FhBvgESIAX90v1_2auyejto5NrMetq5qHoFxzh9lRr07UsYw2iKdDG29VyJLZ_bPStSUx-424U7FR1ctSD9mU831TKuwSyGyGKLz0mO70Ume7pgt8zfN0C_YWv5MFG4thcMrvMfEuyzfUhvQ944HBjXvqzyjcOdCjsFx9gVEnLh1bsXe1MJrUx8H1KN5gNrrkkTE7Qo3ybyr_WZF1002JsdMaoFg2UP3H5gzcbcuxgtjNuyhqaXGnoCBsPhu6SOAwWLEu1pZMty88OFteEWEdxJxCbzKaj8IomT8U4Tjiti4tshMkXRnxHBqAkGzLDu3ypbG4S31CR7RgDxRIVqntw",
|
|
20
20
|
productSelected: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
articleId: 140554,
|
|
22
|
+
services: {
|
|
23
|
+
datasheets: 0,
|
|
24
|
+
descriptions: 0,
|
|
25
|
+
images: 1,
|
|
26
|
+
},
|
|
27
|
+
orderId: 9981,
|
|
28
|
+
city: "CDMX",
|
|
29
|
+
status: "AA",
|
|
30
|
+
datasheet_status: "NS",
|
|
24
31
|
prio: "none",
|
|
25
|
-
version:
|
|
26
|
-
description_status: "
|
|
27
|
-
images_status: "
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
version: 5,
|
|
33
|
+
description_status: "NS",
|
|
34
|
+
images_status: "AA",
|
|
35
|
+
statusByRetailer: [
|
|
36
|
+
{
|
|
37
|
+
status: "AA",
|
|
38
|
+
service: "images",
|
|
39
|
+
retailer_id: 17,
|
|
40
|
+
task_user_group_id: 5,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
32
43
|
article: {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
id_article: 140554,
|
|
45
|
+
id_category: 264,
|
|
46
|
+
brand: null,
|
|
47
|
+
name: "AOX RI Suplemento alimenticio 30 caps de 600 mg c/u",
|
|
48
|
+
upc: "123456789",
|
|
49
|
+
sku: "123456789",
|
|
50
|
+
timestamp: "2025-06-12T00:02:27.000Z",
|
|
51
|
+
active: 1,
|
|
52
|
+
company_id: 1541,
|
|
53
|
+
company_name: "Uniq Balance",
|
|
36
54
|
country: "México",
|
|
37
|
-
|
|
38
|
-
id_datasheet_especialist:
|
|
55
|
+
id_order: 9981,
|
|
56
|
+
id_datasheet_especialist: null,
|
|
39
57
|
id_datasheet_facilitator: null,
|
|
40
|
-
id_description_especialist:
|
|
58
|
+
id_description_especialist: null,
|
|
41
59
|
id_description_facilitator: null,
|
|
42
|
-
id_images_especialist:
|
|
60
|
+
id_images_especialist: null,
|
|
43
61
|
id_images_facilitator: null,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
name: "Copia prod cadenas 1",
|
|
48
|
-
timestamp: "2025-11-16T00:32:13.000Z",
|
|
49
|
-
upc: "100023",
|
|
50
|
-
sku: "900006",
|
|
62
|
+
id_auditor: null,
|
|
63
|
+
id_recepcionist: null,
|
|
64
|
+
category: "Farmacia|Farmacia OTC|Suplementos Alimenticios",
|
|
51
65
|
categoryRetailer: [
|
|
52
66
|
{
|
|
53
|
-
id_category:
|
|
54
|
-
id_retailer:
|
|
55
|
-
category_name:
|
|
56
|
-
|
|
57
|
-
retailer_name: "Alsuper",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id_category: 3435,
|
|
61
|
-
id_retailer: 21,
|
|
62
|
-
category_name:
|
|
63
|
-
"Bebés Cuidado e Higiene|Bebés Cuidado e Higiene|Bebés Cuidado e Higiene",
|
|
64
|
-
retailer_name: "Anicetos",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id_category: 3438,
|
|
68
|
-
id_retailer: 1008,
|
|
69
|
-
category_name:
|
|
70
|
-
"Categoría Genérica App Olímpica|Categoría Genérica|Categoría Genérica",
|
|
71
|
-
retailer_name: "App Olímpica",
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id_category: 3439,
|
|
75
|
-
id_retailer: 2004,
|
|
76
|
-
category_name:
|
|
77
|
-
"Categoría Genérica Automercado|Categoría Genérica Automercado|Categoría Genérica Automercado",
|
|
78
|
-
retailer_name: "Automercado",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id_category: 4774,
|
|
82
|
-
id_retailer: 1032,
|
|
83
|
-
category_name:
|
|
84
|
-
"Categoría Genérica IMPIRA Colombia|Categoría Genérica|Categoría Genérica",
|
|
85
|
-
retailer_name: "IMPIRA Colombia",
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id_category: 4911,
|
|
89
|
-
id_retailer: 16,
|
|
90
|
-
category_name:
|
|
91
|
-
"Categoría Genérica Rappi|Categoría Genérica|Categoría Genérica",
|
|
92
|
-
retailer_name: "Rappi",
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id_category: 6103,
|
|
96
|
-
id_retailer: 9,
|
|
97
|
-
category_name: "Alimentos y Bebidas|Amazon Súper|Abarrotes",
|
|
98
|
-
retailer_name: "Amazon",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id_category: 6792,
|
|
102
|
-
id_retailer: 32,
|
|
103
|
-
category_name:
|
|
104
|
-
"Categoría Salud y Belleza Coppel|Categoría Salud y Belleza|Categoría Salud y Belleza",
|
|
105
|
-
retailer_name: "Coppel",
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id_category: 7008,
|
|
109
|
-
id_retailer: 3,
|
|
110
|
-
category_name: "Bebés|Accesorios para Bebés|Accesorios para Bebés",
|
|
111
|
-
retailer_name: "Sam´s Club",
|
|
67
|
+
id_category: 264,
|
|
68
|
+
id_retailer: 17,
|
|
69
|
+
category_name: "Farmacia|Farmacia OTC|Suplementos Alimenticios",
|
|
70
|
+
retailer_name: "Genérico",
|
|
112
71
|
},
|
|
113
72
|
],
|
|
114
73
|
},
|
|
115
74
|
retailers: [
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
],
|
|
121
|
-
statusByRetailer: [
|
|
122
|
-
{ status: "RA/AS", service: "datasheet", retailer_id: 3 },
|
|
123
|
-
{ status: "RA/AS", service: "description", retailer_id: 3 },
|
|
124
|
-
{ status: "AA", service: "images", retailer_id: 3 },
|
|
125
|
-
{ status: "AC", service: "datasheet", retailer_id: 16 },
|
|
126
|
-
{ status: "AC", service: "description", retailer_id: 16 },
|
|
127
|
-
{ status: "AC", service: "images", retailer_id: 16 },
|
|
128
|
-
{ status: "AC", service: "datasheet", retailer_id: 32 },
|
|
129
|
-
{ status: "AC", service: "description", retailer_id: 32 },
|
|
130
|
-
{ status: "AC", service: "images", retailer_id: 32 },
|
|
131
|
-
{ status: "PA", service: "datasheet", retailer_id: 1032 },
|
|
132
|
-
{ status: "PA", service: "description", retailer_id: 1032 },
|
|
133
|
-
{ status: "PA", service: "images", retailer_id: 1032 },
|
|
134
|
-
{ status: "PA", service: "datasheet", retailer_id: 2004 },
|
|
135
|
-
{ status: "PA", service: "description", retailer_id: 2004 },
|
|
136
|
-
{ status: "PA", service: "images", retailer_id: 2004 },
|
|
75
|
+
{
|
|
76
|
+
id: 17,
|
|
77
|
+
name: "Genérico",
|
|
78
|
+
},
|
|
137
79
|
],
|
|
80
|
+
variantsCount: 0,
|
|
81
|
+
parent: null,
|
|
82
|
+
parentArticleId: null,
|
|
83
|
+
country: "México",
|
|
84
|
+
upc: "123456789",
|
|
138
85
|
},
|
|
139
86
|
user: {
|
|
140
87
|
id_user: 426,
|
package/src/global-files/data.js
CHANGED
|
@@ -78,17 +78,32 @@ export const getImage = (imageGeneral, width = 250, height = 250) => {
|
|
|
78
78
|
};
|
|
79
79
|
const ext = image.src.split(".");
|
|
80
80
|
const uuid = ext[0].split("-");
|
|
81
|
+
const extension = ext[ext.length - 1]?.toLowerCase();
|
|
82
|
+
/*
|
|
83
|
+
El redimensionador de CloudFront reprocesa el archivo con sharp: sirve
|
|
84
|
+
para una foto de producto y destruye todo lo demás. Un mp4 devuelve un
|
|
85
|
+
error que la etiqueta <video> no puede reproducir, y un gif animado sale
|
|
86
|
+
aplanado a un solo fotograma.
|
|
87
|
+
|
|
88
|
+
Por eso solo los formatos que sharp sabe redimensionar pasan por el
|
|
89
|
+
resizer; el resto se queda con la URL directa a S3, que es la misma que
|
|
90
|
+
usa la descarga de imágenes. Sin esto hacía falta bajar cada video
|
|
91
|
+
completo a base64 solo para generarle una miniatura.
|
|
92
|
+
*/
|
|
93
|
+
const isResizable = ["jpg", "jpeg", "png", "webp", "tiff", "avif"].includes(extension);
|
|
81
94
|
return {
|
|
82
95
|
...image,
|
|
83
96
|
version: image.id_version,
|
|
84
|
-
ext:
|
|
97
|
+
ext: extension,
|
|
85
98
|
uuid:
|
|
86
99
|
uuid.reduce((prevVal, currVal, idx) => {
|
|
87
100
|
if (idx < 2) return "";
|
|
88
101
|
else return idx === 2 ? currVal : prevVal + "-" + currVal;
|
|
89
102
|
}, "") || uuidv4(),
|
|
90
103
|
srcDB: image.src,
|
|
91
|
-
src: image.image_id
|
|
104
|
+
src: image.image_id && !isResizable
|
|
105
|
+
? `https://${process.env.REACT_APP_IMAGES_BUCKET}.s3.amazonaws.com/${image.src}`
|
|
106
|
+
: image.image_id
|
|
92
107
|
? "https://d24s337exbt5zr.cloudfront.net/" +
|
|
93
108
|
Base64.encode(
|
|
94
109
|
JSON.stringify({
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo de activos digitales.
|
|
3
|
+
*
|
|
4
|
+
* Este archivo es la ÚNICA fuente de verdad de qué se puede subir a la bandeja
|
|
5
|
+
* y con qué límites. Ajustar un tipo o un tope es cambiar un dato aquí, no
|
|
6
|
+
* tocar lógica repartida por la pantalla.
|
|
7
|
+
*
|
|
8
|
+
* Un activo NO es un "tipo de toma". Las imágenes de producto se validan contra
|
|
9
|
+
* `image_manager` (orden, obligatoriedad, cupo por cadena); los activos no
|
|
10
|
+
* entran a ese cruce y por eso tienen su propio catálogo.
|
|
11
|
+
*
|
|
12
|
+
* DOS COSAS QUE PARECEN DETALLE Y NO LO SON:
|
|
13
|
+
*
|
|
14
|
+
* 1. El tipo se decide por EXTENSIÓN, no por mime. El navegador no sabe qué es
|
|
15
|
+
* un .psd, un .ai o un .indd: reporta `application/octet-stream`, cadena
|
|
16
|
+
* vacía, o —peor— `application/pdf` para un .ai. Validar por mime dejaría
|
|
17
|
+
* fuera justo los formatos editables que se pidieron.
|
|
18
|
+
* 2. Por lo mismo, el `accept` del dropzone se arma con extensiones además de
|
|
19
|
+
* mimes. Si solo llevara mimes, react-dropzone descartaría esos archivos
|
|
20
|
+
* antes de que nuestra validación llegue a verlos, y el usuario no vería
|
|
21
|
+
* ningún mensaje: simplemente no pasaría nada al soltarlos.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const KB = 1024;
|
|
25
|
+
const MB = 1024 * KB;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cómo se pinta un archivo en pantalla. No es lo mismo que el tipo de activo:
|
|
29
|
+
* un 360 puede ser mp4 (se reproduce) o gif (se muestra), así que la forma de
|
|
30
|
+
* previsualizar se resuelve por el archivo real, no por su categoría.
|
|
31
|
+
*/
|
|
32
|
+
export const ASSET_PREVIEW = {
|
|
33
|
+
VIDEO: "video",
|
|
34
|
+
IMAGE: "image",
|
|
35
|
+
ICON: "icon",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* PROVISIONAL: cantidad máxima de activos por producto. Es el único dato que
|
|
40
|
+
* el manager no respondió. Subido de 10 a 20 porque la lista de tipos creció
|
|
41
|
+
* (documentos y editables, no solo multimedia).
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_ASSETS_PER_PRODUCT = 20;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Duración válida de un video.
|
|
47
|
+
*
|
|
48
|
+
* Hoy la ficha valida `duracion <= 10 || duracion >= 600` mientras el mensaje al
|
|
49
|
+
* usuario dice "entre 15 segundos y 10 minutos". Los dos no pueden ser ciertos.
|
|
50
|
+
* Se conserva el rango que el código valida de verdad, y el mensaje se genera
|
|
51
|
+
* desde estos mismos números para que no se vuelvan a desincronizar.
|
|
52
|
+
*/
|
|
53
|
+
export const VIDEO_DURATION_SECONDS = {
|
|
54
|
+
min: 10,
|
|
55
|
+
max: 600,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const ASSET_TYPES = {
|
|
59
|
+
360: {
|
|
60
|
+
id: "360",
|
|
61
|
+
label: "360",
|
|
62
|
+
description: "Giro del producto, en mp4 o gif animado",
|
|
63
|
+
mimes: ["video/mp4", "image/gif"],
|
|
64
|
+
extensions: ["mp4", "gif"],
|
|
65
|
+
maxBytes: 200 * MB,
|
|
66
|
+
validatesDuration: true,
|
|
67
|
+
},
|
|
68
|
+
video: {
|
|
69
|
+
id: "video",
|
|
70
|
+
label: "Video",
|
|
71
|
+
description: "Video de producto o promocional",
|
|
72
|
+
mimes: ["video/mp4", "video/quicktime", "video/webm"],
|
|
73
|
+
extensions: ["mp4", "mov", "webm"],
|
|
74
|
+
maxBytes: 200 * MB,
|
|
75
|
+
validatesDuration: true,
|
|
76
|
+
},
|
|
77
|
+
gif: {
|
|
78
|
+
id: "gif",
|
|
79
|
+
label: "GIF",
|
|
80
|
+
description: "Animación corta",
|
|
81
|
+
mimes: ["image/gif"],
|
|
82
|
+
extensions: ["gif"],
|
|
83
|
+
maxBytes: 20 * MB,
|
|
84
|
+
validatesDuration: false,
|
|
85
|
+
},
|
|
86
|
+
documento: {
|
|
87
|
+
id: "documento",
|
|
88
|
+
label: "Documento",
|
|
89
|
+
description: "Manual, ficha técnica, certificado",
|
|
90
|
+
mimes: [
|
|
91
|
+
"application/pdf",
|
|
92
|
+
"application/msword",
|
|
93
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
94
|
+
],
|
|
95
|
+
extensions: ["pdf", "doc", "docx"],
|
|
96
|
+
maxBytes: 50 * MB,
|
|
97
|
+
validatesDuration: false,
|
|
98
|
+
},
|
|
99
|
+
hoja_calculo: {
|
|
100
|
+
id: "hoja_calculo",
|
|
101
|
+
label: "Hoja de cálculo",
|
|
102
|
+
description: "Listas de medidas, tablas de tallas, matrices",
|
|
103
|
+
mimes: [
|
|
104
|
+
"application/vnd.ms-excel",
|
|
105
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
106
|
+
"text/csv",
|
|
107
|
+
],
|
|
108
|
+
extensions: ["xlsx", "xls", "csv"],
|
|
109
|
+
maxBytes: 20 * MB,
|
|
110
|
+
validatesDuration: false,
|
|
111
|
+
},
|
|
112
|
+
/*
|
|
113
|
+
Archivos de diseño editables. Es la categoría con más riesgo: un .psd con
|
|
114
|
+
capas o un .aep pasan sin esfuerzo de 1 GB, y ninguno se puede previsualizar
|
|
115
|
+
en el navegador —se muestran con icono y nombre—. El tope de aquí abajo es
|
|
116
|
+
una propuesta, no un dato confirmado.
|
|
117
|
+
*/
|
|
118
|
+
editable: {
|
|
119
|
+
id: "editable",
|
|
120
|
+
label: "Editable",
|
|
121
|
+
description: "Archivo de diseño con capas (Photoshop, Illustrator, InDesign…)",
|
|
122
|
+
mimes: [],
|
|
123
|
+
extensions: [
|
|
124
|
+
"psd", "psb", // Photoshop
|
|
125
|
+
"ai", "eps", // Illustrator
|
|
126
|
+
"indd", "idml", // InDesign
|
|
127
|
+
"aep", // After Effects
|
|
128
|
+
"prproj", // Premiere
|
|
129
|
+
"sketch", "fig", "xd", // Sketch, Figma, Adobe XD
|
|
130
|
+
"afphoto", "afdesign", // Affinity
|
|
131
|
+
"svg",
|
|
132
|
+
],
|
|
133
|
+
maxBytes: 500 * MB,
|
|
134
|
+
validatesDuration: false,
|
|
135
|
+
},
|
|
136
|
+
comprimido: {
|
|
137
|
+
id: "comprimido",
|
|
138
|
+
label: "Comprimido",
|
|
139
|
+
description: "Paquete de varios archivos en un solo zip",
|
|
140
|
+
mimes: ["application/zip", "application/x-rar-compressed", "application/x-7z-compressed"],
|
|
141
|
+
extensions: ["zip", "rar", "7z"],
|
|
142
|
+
maxBytes: 500 * MB,
|
|
143
|
+
validatesDuration: false,
|
|
144
|
+
},
|
|
145
|
+
otro: {
|
|
146
|
+
id: "otro",
|
|
147
|
+
label: "Otro",
|
|
148
|
+
description: "Formato no reconocido",
|
|
149
|
+
mimes: [],
|
|
150
|
+
extensions: [],
|
|
151
|
+
maxBytes: 50 * MB,
|
|
152
|
+
validatesDuration: false,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Orden en que se muestran en el selector de tipo. */
|
|
157
|
+
export const ASSET_TYPE_ORDER = [
|
|
158
|
+
"360",
|
|
159
|
+
"video",
|
|
160
|
+
"gif",
|
|
161
|
+
"documento",
|
|
162
|
+
"hoja_calculo",
|
|
163
|
+
"editable",
|
|
164
|
+
"comprimido",
|
|
165
|
+
"otro",
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
/** Opciones listas para el componente `Select` (espera `{ value, name }`). */
|
|
169
|
+
export const assetTypeOptions = () =>
|
|
170
|
+
ASSET_TYPE_ORDER.map((id) => ({
|
|
171
|
+
value: id,
|
|
172
|
+
name: ASSET_TYPES[id].label,
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
/** Extensiones que el navegador pinta como imagen. */
|
|
176
|
+
export const IMAGE_EXTENSIONS = [
|
|
177
|
+
"jpg",
|
|
178
|
+
"jpeg",
|
|
179
|
+
"png",
|
|
180
|
+
"webp",
|
|
181
|
+
"gif",
|
|
182
|
+
"bmp",
|
|
183
|
+
"avif",
|
|
184
|
+
"svg",
|
|
185
|
+
"tiff",
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Formatos de video que un navegador REPRODUCE de verdad.
|
|
190
|
+
*
|
|
191
|
+
* No es lo mismo que "es un video". Un .mov se sube bien y S3 lo sirve como
|
|
192
|
+
* video/quicktime, pero Chrome y Firefox no lo reproducen: una grabación de
|
|
193
|
+
* pantalla de macOS viene en HEVC o ProRes, que no saben decodificar. Tratarlo
|
|
194
|
+
* como reproducible pinta un recuadro negro sin explicación; mostrarlo como
|
|
195
|
+
* archivo dice la verdad y deja claro lo que sí se puede hacer: descargarlo.
|
|
196
|
+
*/
|
|
197
|
+
export const PLAYABLE_VIDEO_EXTENSIONS = ["mp4", "webm", "ogv", "ogg"];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Cómo pintar un archivo: manda la extensión y el mime queda de respaldo.
|
|
201
|
+
*
|
|
202
|
+
* La extensión manda porque el mime miente en las dos direcciones: un .psd
|
|
203
|
+
* llega sin él, y un .mov llega con uno que empieza por `video/` aunque el
|
|
204
|
+
* navegador no sepa reproducirlo.
|
|
205
|
+
*
|
|
206
|
+
* NUNCA por la URL. `ProductImage` lo hacía con `/\.mp4$/` y por eso fallaba:
|
|
207
|
+
* la URL guardada apunta al redimensionador de CloudFront y no conserva la
|
|
208
|
+
* extensión del archivo, así que un video jamás daba positivo.
|
|
209
|
+
*/
|
|
210
|
+
export const resolvePreviewKind = ({ mime, ext, name } = {}) => {
|
|
211
|
+
const extension = (ext || getExtension(name) || "").toLowerCase();
|
|
212
|
+
|
|
213
|
+
if (extension) {
|
|
214
|
+
if (PLAYABLE_VIDEO_EXTENSIONS.includes(extension)) return ASSET_PREVIEW.VIDEO;
|
|
215
|
+
if (IMAGE_EXTENSIONS.includes(extension)) return ASSET_PREVIEW.IMAGE;
|
|
216
|
+
return ASSET_PREVIEW.ICON;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (mime === "video/mp4" || mime === "video/webm") return ASSET_PREVIEW.VIDEO;
|
|
220
|
+
if (mime?.startsWith("image/")) return ASSET_PREVIEW.IMAGE;
|
|
221
|
+
return ASSET_PREVIEW.ICON;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const getAssetType = (assetTypeId) =>
|
|
225
|
+
ASSET_TYPES[assetTypeId] ?? ASSET_TYPES.otro;
|
|
226
|
+
|
|
227
|
+
export const getExtension = (fileName = "") => {
|
|
228
|
+
const parts = String(fileName).split(".");
|
|
229
|
+
return parts.length > 1 ? parts.pop().toLowerCase() : "";
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
Qué tipo reclama cada extensión, resuelto una sola vez.
|
|
234
|
+
|
|
235
|
+
El orden de `ASSET_TYPE_ORDER` decide los empates: `mp4` y `gif` aparecen
|
|
236
|
+
también en "360", pero gana el tipo genérico porque es el caso común y el
|
|
237
|
+
usuario puede reetiquetarlo en la tarjeta. Mirando el archivo no hay forma de
|
|
238
|
+
saber si un giro es un giro.
|
|
239
|
+
*/
|
|
240
|
+
const EXTENSION_INDEX = ASSET_TYPE_ORDER.reduce((index, id) => {
|
|
241
|
+
ASSET_TYPES[id].extensions.forEach((ext) => {
|
|
242
|
+
if (id !== "360" && !index[ext]) index[ext] = id;
|
|
243
|
+
});
|
|
244
|
+
return index;
|
|
245
|
+
}, {});
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Tipo de activo sugerido para un archivo recién elegido.
|
|
249
|
+
*
|
|
250
|
+
* Manda la extensión. El mime solo se consulta como respaldo para los formatos
|
|
251
|
+
* multimedia, que son los únicos que el navegador reconoce con certeza: para un
|
|
252
|
+
* .psd o un .ai reporta `application/octet-stream` o nada, y para un .ai a veces
|
|
253
|
+
* `application/pdf`, que lo clasificaría como documento.
|
|
254
|
+
*/
|
|
255
|
+
export const suggestAssetType = ({ mime, name } = {}) => {
|
|
256
|
+
const ext = getExtension(name);
|
|
257
|
+
if (EXTENSION_INDEX[ext]) return EXTENSION_INDEX[ext];
|
|
258
|
+
|
|
259
|
+
if (mime?.startsWith("video/")) return "video";
|
|
260
|
+
if (mime === "image/gif") return "gif";
|
|
261
|
+
if (mime === "application/pdf") return "documento";
|
|
262
|
+
|
|
263
|
+
return "otro";
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** Todos los mimes aceptados, sin repetir. */
|
|
267
|
+
export const acceptedMimes = () => [
|
|
268
|
+
...new Set(ASSET_TYPE_ORDER.flatMap((id) => ASSET_TYPES[id].mimes)),
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
/** Todas las extensiones aceptadas, sin repetir y con punto. */
|
|
272
|
+
export const acceptedExtensions = () => [
|
|
273
|
+
...new Set(ASSET_TYPE_ORDER.flatMap((id) => ASSET_TYPES[id].extensions)),
|
|
274
|
+
].map((ext) => `.${ext}`);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Valor de `accept` para react-dropzone (v12: una cadena separada por comas).
|
|
278
|
+
*
|
|
279
|
+
* Lleva mimes Y extensiones. Con solo mimes, react-dropzone descartaría un
|
|
280
|
+
* .psd o un .indd antes de que nuestra validación llegue a verlos —el navegador
|
|
281
|
+
* no les asigna mime— y al soltarlos no pasaría absolutamente nada: ni el
|
|
282
|
+
* archivo, ni un mensaje de por qué.
|
|
283
|
+
*/
|
|
284
|
+
export const buildDropzoneAccept = () => [...acceptedMimes(), ...acceptedExtensions()].join(", ");
|
|
285
|
+
|
|
286
|
+
/** Lista legible de formatos, para mensajes y para la ayuda de la bandeja. */
|
|
287
|
+
export const acceptedFormatsLabel = () => acceptedExtensions()
|
|
288
|
+
.map((ext) => ext.replace(".", "").toUpperCase())
|
|
289
|
+
.join(", ");
|
|
290
|
+
|
|
291
|
+
const GB = 1024 * MB;
|
|
292
|
+
|
|
293
|
+
export const formatBytes = (bytes) => {
|
|
294
|
+
const value = Number(bytes);
|
|
295
|
+
if (!Number.isFinite(value) || value <= 0) return "-";
|
|
296
|
+
if (value < KB) return `${value} B`;
|
|
297
|
+
if (value < MB) return `${Math.round(value / KB)} KB`;
|
|
298
|
+
// Los editables llegan a varios cientos de MB; sin este escalón un archivo de
|
|
299
|
+
// 1.2 GB se anunciaba como "1228.8 MB".
|
|
300
|
+
if (value < GB) return `${(value / MB).toFixed(1)} MB`;
|
|
301
|
+
return `${(value / GB).toFixed(2)} GB`;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** Duración compacta para la tarjeta del activo: "1:05". */
|
|
305
|
+
export const formatDuration = (seconds) => {
|
|
306
|
+
const value = Number(seconds);
|
|
307
|
+
if (!Number.isFinite(value) || value <= 0) return "-";
|
|
308
|
+
const mins = Math.floor(value / 60);
|
|
309
|
+
const secs = Math.round(value % 60);
|
|
310
|
+
return `${mins}:${String(secs).padStart(2, "0")}`;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/** Duración en prosa para los mensajes de error: "10 segundos", "10 minutos". */
|
|
314
|
+
export const humanizeDuration = (seconds) => {
|
|
315
|
+
const value = Number(seconds);
|
|
316
|
+
if (!Number.isFinite(value) || value <= 0) return "-";
|
|
317
|
+
if (value < 60) return `${Math.round(value)} segundos`;
|
|
318
|
+
const mins = value / 60;
|
|
319
|
+
const label = Number.isInteger(mins) ? mins : mins.toFixed(1);
|
|
320
|
+
return `${label} ${mins === 1 ? "minuto" : "minutos"}`;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Valida un archivo ANTES de tocar la red: tipo permitido, peso y cupo.
|
|
325
|
+
*
|
|
326
|
+
* Se separa de la subida a propósito. Da igual que el archivo termine yendo por
|
|
327
|
+
* URL prefirmada o por el `putObject` de hoy: esto no cambia, y es lo que evita
|
|
328
|
+
* que el navegador empiece a leer 200 MB que de todos modos se van a rechazar.
|
|
329
|
+
*
|
|
330
|
+
* @returns {{ valid: boolean, code?: string, message?: string }}
|
|
331
|
+
*/
|
|
332
|
+
export const validateAssetFile = (file, { currentCount = 0 } = {}) => {
|
|
333
|
+
if (!file) {
|
|
334
|
+
return {
|
|
335
|
+
valid: false,
|
|
336
|
+
code: "NO_FILE",
|
|
337
|
+
message: "No se recibió ningún archivo.",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (currentCount >= MAX_ASSETS_PER_PRODUCT) {
|
|
342
|
+
return {
|
|
343
|
+
valid: false,
|
|
344
|
+
code: "MAX_ASSETS",
|
|
345
|
+
message: `Solo puedes almacenar ${MAX_ASSETS_PER_PRODUCT} activos digitales por producto.`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const assetTypeId = suggestAssetType({ mime: file.type, name: file.name });
|
|
350
|
+
const assetType = getAssetType(assetTypeId);
|
|
351
|
+
|
|
352
|
+
if (assetTypeId === "otro") {
|
|
353
|
+
return {
|
|
354
|
+
valid: false,
|
|
355
|
+
code: "UNSUPPORTED_TYPE",
|
|
356
|
+
message: `"${file.name}" no es un formato permitido. Se aceptan: ${acceptedFormatsLabel()}.`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (file.size > assetType.maxBytes) {
|
|
361
|
+
return {
|
|
362
|
+
valid: false,
|
|
363
|
+
code: "MAX_SIZE",
|
|
364
|
+
message: `"${file.name}" pesa ${formatBytes(
|
|
365
|
+
file.size
|
|
366
|
+
)} y el máximo para ${assetType.label} es ${formatBytes(
|
|
367
|
+
assetType.maxBytes
|
|
368
|
+
)}.`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { valid: true, assetType: assetTypeId, warning: warnAssetFile(file, assetTypeId) };
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Aviso para un archivo que SÍ se acepta pero que el usuario no va a poder ver.
|
|
377
|
+
*
|
|
378
|
+
* El caso que sorprende es el video: alguien sube una grabación de pantalla de
|
|
379
|
+
* macOS —un .mov— esperando reproducirla, y el navegador no la decodifica. El
|
|
380
|
+
* archivo sirve igual para entregarlo al retailer, así que no se rechaza; pero
|
|
381
|
+
* enterarse al soltarlo es mucho mejor que enterarse al intentar verlo.
|
|
382
|
+
*
|
|
383
|
+
* No se avisa de un .psd o un .indd: nadie espera previsualizar un archivo de
|
|
384
|
+
* diseño, y un aviso por cada uno sería ruido.
|
|
385
|
+
*/
|
|
386
|
+
export const warnAssetFile = (file, assetTypeId) => {
|
|
387
|
+
if (assetTypeId !== "video" && assetTypeId !== "360") return null;
|
|
388
|
+
|
|
389
|
+
const ext = getExtension(file?.name);
|
|
390
|
+
if (PLAYABLE_VIDEO_EXTENSIONS.includes(ext)) return null;
|
|
391
|
+
|
|
392
|
+
return `"${file?.name}" se guardará y se podrá descargar, pero el navegador no puede reproducir los .${ext}. Conviértelo a MP4 si necesitas verlo aquí.`;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Valida la duración de un video. Va aparte de `validateAssetFile` porque
|
|
397
|
+
* obtenerla es asíncrono (hay que cargar el archivo en un <video>) y no se
|
|
398
|
+
* quiere pagar ese costo en archivos que ya fueron rechazados por peso.
|
|
399
|
+
*/
|
|
400
|
+
export const validateAssetDuration = (seconds, assetTypeId) => {
|
|
401
|
+
const assetType = getAssetType(assetTypeId);
|
|
402
|
+
if (!assetType.validatesDuration) return { valid: true };
|
|
403
|
+
|
|
404
|
+
const value = Number(seconds);
|
|
405
|
+
if (!Number.isFinite(value)) return { valid: true };
|
|
406
|
+
|
|
407
|
+
const { min, max } = VIDEO_DURATION_SECONDS;
|
|
408
|
+
if (value < min || value > max) {
|
|
409
|
+
return {
|
|
410
|
+
valid: false,
|
|
411
|
+
code: "DURATION",
|
|
412
|
+
message: `Los videos deben durar entre ${humanizeDuration(
|
|
413
|
+
min
|
|
414
|
+
)} y ${humanizeDuration(max)}.`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { valid: true };
|
|
419
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulario compartido del estado de activos digitales.
|
|
3
|
+
*
|
|
4
|
+
* Vive aquí, y no dentro del reducer, para que nadie dependa de nadie en la
|
|
5
|
+
* dirección equivocada: el reducer de la ficha (`context/reducers/assets.js`)
|
|
6
|
+
* y el hook de la bandeja (`useDigitalAssets`) importan ambos estas constantes,
|
|
7
|
+
* y ninguno de los dos importa al otro. Es lo que permite probar el reducer
|
|
8
|
+
* suelto en Storybook sin arrastrar la pantalla completa.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ASSET_ACTIONS = {
|
|
12
|
+
/** Reemplaza toda la lista con lo que devolvió el backend. Limpia el rastreo. */
|
|
13
|
+
SET_ASSETS_VALUES: "SET_ASSETS_VALUES",
|
|
14
|
+
/** Agrega activos recién soltados y ya validados. */
|
|
15
|
+
ADD_ASSETS: "ADD_ASSETS",
|
|
16
|
+
/**
|
|
17
|
+
* Cambio hecho por el usuario (reetiquetar el tipo, renombrar). Si el activo
|
|
18
|
+
* ya existe en el backend, queda marcado para enviarse al guardar.
|
|
19
|
+
*/
|
|
20
|
+
UPDATE_ASSET: "UPDATE_ASSET",
|
|
21
|
+
/**
|
|
22
|
+
* Cambio de ciclo de vida de la subida: progreso, estado, error, o la clave
|
|
23
|
+
* de S3 que devuelve el backend. NO marca nada para guardar —el progreso de
|
|
24
|
+
* una barra no es una edición del usuario— y por eso está separado de
|
|
25
|
+
* `UPDATE_ASSET` en vez de ser una bandera.
|
|
26
|
+
*/
|
|
27
|
+
PATCH_ASSET: "PATCH_ASSET",
|
|
28
|
+
/** Quita un activo de la lista. Si ya existía en el backend, lo anota para borrarlo. */
|
|
29
|
+
REMOVE_ASSET: "REMOVE_ASSET",
|
|
30
|
+
SET_SELECTED_ASSETS: "SET_SELECTED_ASSETS",
|
|
31
|
+
TOGGLE_CHECK_ALL_ASSETS: "TOGGLE_CHECK_ALL_ASSETS",
|
|
32
|
+
/** Tras un guardado exitoso: olvida qué cambió y qué se borró. */
|
|
33
|
+
CLEAR_ASSETS_TRACKING: "CLEAR_ASSETS_TRACKING",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const assetsInitialState = {
|
|
37
|
+
/** Lista completa, en el modelo de interfaz de `assets.adapter`. */
|
|
38
|
+
assets_values: [],
|
|
39
|
+
/**
|
|
40
|
+
* Claves de los activos YA GUARDADOS que el usuario modificó. Se guardan
|
|
41
|
+
* claves y no copias del objeto: una copia se queda vieja en cuanto el
|
|
42
|
+
* usuario vuelve a tocar el activo, y al guardar se enviaría el valor
|
|
43
|
+
* anterior. Con la clave, el objeto se lee siempre de `assets_values`.
|
|
44
|
+
*/
|
|
45
|
+
updated_assets: [],
|
|
46
|
+
/** Ids del backend pendientes de borrar. */
|
|
47
|
+
deleted_assets: [],
|
|
48
|
+
selected_assets: [],
|
|
49
|
+
all_assets_checked: false,
|
|
50
|
+
};
|