contentoh-components-library 21.6.50 → 21.6.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.env.test +0 -1
  2. package/dist/components/atoms/IconFile/index.js +2 -85
  3. package/dist/components/atoms/IconFile/styles.js +1 -1
  4. package/dist/components/atoms/ProductImage/ProductImage.stories.js +2 -70
  5. package/dist/components/atoms/ProductImage/index.js +27 -100
  6. package/dist/components/atoms/ProductImage/styles.js +1 -3
  7. package/dist/components/molecules/EditionActiveImage/index.js +2 -4
  8. package/dist/components/molecules/GalleryElement/index.js +4 -13
  9. package/dist/components/molecules/GalleryHeader/index.js +24 -96
  10. package/dist/components/molecules/GalleryHeader/styles.js +1 -4
  11. package/dist/components/molecules/ImageSelector/index.js +0 -1
  12. package/dist/components/organisms/ImagePreviewer/index.js +0 -1
  13. package/dist/components/organisms/ProductImageModal/index.js +2 -3
  14. package/dist/components/organisms/PropagateChangesModal/index.js +6 -1
  15. package/dist/components/pages/ProviderProductEdition/context/provider-product-edition.context.js +10 -8
  16. package/dist/components/pages/ProviderProductEdition/index.js +189 -72
  17. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.context.js +25 -338
  18. package/dist/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +2 -12
  19. package/dist/components/pages/RetailerProductEdition/index.js +579 -631
  20. package/dist/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +0 -1
  21. package/dist/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +145 -45
  22. package/dist/global-files/data.js +3 -16
  23. package/dist/index.js +90 -142
  24. package/package.json +1 -1
  25. package/src/components/atoms/IconFile/index.js +1 -28
  26. package/src/components/atoms/IconFile/styles.js +0 -8
  27. package/src/components/atoms/ProductImage/ProductImage.stories.js +0 -38
  28. package/src/components/atoms/ProductImage/index.js +20 -95
  29. package/src/components/atoms/ProductImage/styles.js +7 -92
  30. package/src/components/molecules/EditionActiveImage/index.js +2 -2
  31. package/src/components/molecules/GalleryElement/index.js +0 -9
  32. package/src/components/molecules/GalleryHeader/index.js +13 -65
  33. package/src/components/molecules/GalleryHeader/styles.js +1 -2
  34. package/src/components/molecules/ImageSelector/index.js +0 -1
  35. package/src/components/organisms/ImagePreviewer/index.js +0 -1
  36. package/src/components/organisms/ProductImageModal/index.js +0 -1
  37. package/src/components/organisms/PropagateChangesModal/index.js +10 -3
  38. package/src/components/pages/ProviderProductEdition/context/provider-product-edition.context.jsx +2 -1
  39. package/src/components/pages/ProviderProductEdition/index.js +111 -6
  40. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.context.jsx +2 -202
  41. package/src/components/pages/RetailerProductEdition/context/provider-product-edition.reducer.js +0 -12
  42. package/src/components/pages/RetailerProductEdition/index.js +258 -363
  43. package/src/components/pages/RetailerProductEdition/stories/ImageEditor.stories.js +0 -1
  44. package/src/components/pages/RetailerProductEdition/stories/TextEditor.stories.js +104 -51
  45. package/src/global-files/data.js +2 -17
  46. package/src/index.js +0 -4
  47. package/.claude/launch.json +0 -14
  48. package/dist/ai/prompts/attribute.prompt.js +0 -28
  49. package/dist/ai/prompts/description.prompt.js +0 -33
  50. package/dist/ai/schemas/attribute.schema.js +0 -35
  51. package/dist/ai/schemas/description.schema.js +0 -27
  52. package/dist/components/atoms/AssetPreview/AssetPreview.stories.js +0 -118
  53. package/dist/components/atoms/AssetPreview/index.js +0 -150
  54. package/dist/components/atoms/AssetPreview/styles.js +0 -28
  55. package/dist/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +0 -147
  56. package/dist/components/molecules/GalleryAssetElement/index.js +0 -178
  57. package/dist/components/molecules/GalleryAssetElement/styles.js +0 -26
  58. package/dist/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +0 -56
  59. package/dist/components/molecules/GalleryTrayTabs/index.js +0 -60
  60. package/dist/components/molecules/GalleryTrayTabs/styles.js +0 -20
  61. package/dist/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +0 -171
  62. package/dist/components/organisms/DigitalAssetsTray/index.js +0 -211
  63. package/dist/components/organisms/DigitalAssetsTray/styles.js +0 -22
  64. package/dist/global-files/digital-assets/assetTypes.js +0 -480
  65. package/dist/global-files/digital-assets/assets.actions.js +0 -64
  66. package/dist/global-files/digital-assets/assets.adapter.js +0 -304
  67. package/dist/global-files/digital-assets/assets.mock.js +0 -136
  68. package/dist/global-files/digital-assets/assets.reducer.js +0 -220
  69. package/dist/global-files/digital-assets/uploadAssets.js +0 -179
  70. package/dist/global-files/digital-assets/useDigitalAssets.js +0 -522
  71. package/docs/activos-digitales.md +0 -273
  72. package/src/components/atoms/AssetPreview/AssetPreview.stories.js +0 -78
  73. package/src/components/atoms/AssetPreview/index.js +0 -119
  74. package/src/components/atoms/AssetPreview/styles.js +0 -190
  75. package/src/components/molecules/GalleryAssetElement/GalleryAssetElement.stories.js +0 -102
  76. package/src/components/molecules/GalleryAssetElement/index.js +0 -162
  77. package/src/components/molecules/GalleryAssetElement/styles.js +0 -159
  78. package/src/components/molecules/GalleryTrayTabs/GalleryTrayTabs.stories.js +0 -32
  79. package/src/components/molecules/GalleryTrayTabs/index.js +0 -41
  80. package/src/components/molecules/GalleryTrayTabs/styles.js +0 -52
  81. package/src/components/organisms/DigitalAssetsTray/DigitalAssetsTray.stories.js +0 -101
  82. package/src/components/organisms/DigitalAssetsTray/index.js +0 -189
  83. package/src/components/organisms/DigitalAssetsTray/styles.js +0 -183
  84. package/src/global-files/digital-assets/assetTypes.js +0 -419
  85. package/src/global-files/digital-assets/assets.actions.js +0 -50
  86. package/src/global-files/digital-assets/assets.adapter.js +0 -229
  87. package/src/global-files/digital-assets/assets.mock.js +0 -122
  88. package/src/global-files/digital-assets/assets.reducer.js +0 -197
  89. package/src/global-files/digital-assets/uploadAssets.js +0 -109
  90. package/src/global-files/digital-assets/useDigitalAssets.js +0 -329
@@ -1,122 +0,0 @@
1
- /**
2
- * Activos de ejemplo con la forma EXACTA que devolvería el backend, para
3
- * Storybook y pruebas.
4
- *
5
- * Están crudos a propósito, sin pasar por `fromBackend`: así, si el contrato
6
- * cambia, estos objetos se editan y el adaptador se prueba de verdad en vez de
7
- * comparar el modelo de interfaz contra sí mismo.
8
- */
9
-
10
- export const backendAssetsMock = [
11
- {
12
- id: 91,
13
- asset_type: "360",
14
- name: "giro_producto.mp4",
15
- ext: "mp4",
16
- mime: "video/mp4",
17
- size_bytes: 8412160,
18
- duration: 12,
19
- width: 1080,
20
- height: 1080,
21
- src: "id-136038/assets/91-6d0f9a1e-0f0c-4a2b-9f1d-2a7c1b3e5d90.mp4",
22
- poster: "id-136038/assets/91-6d0f9a1e-0f0c-4a2b-9f1d-2a7c1b3e5d90.jpg",
23
- id_retailer: null,
24
- created_at: "2026-09-10T16:22:04.000Z",
25
- },
26
- {
27
- id: 92,
28
- asset_type: "360",
29
- name: "giro_empaque.gif",
30
- ext: "gif",
31
- mime: "image/gif",
32
- size_bytes: 3145728,
33
- duration: null,
34
- width: 800,
35
- height: 800,
36
- src: "id-136038/assets/92-b1c2d3e4-1111-2222-3333-444455556666.gif",
37
- poster: null,
38
- id_retailer: null,
39
- created_at: "2026-09-10T16:24:41.000Z",
40
- },
41
- {
42
- id: 93,
43
- asset_type: "video",
44
- name: "demostracion_uso.mp4",
45
- ext: "mp4",
46
- mime: "video/mp4",
47
- size_bytes: 54525952,
48
- duration: 96,
49
- width: 1920,
50
- height: 1080,
51
- src: "id-136038/assets/93-77aa88bb-99cc-4dd0-8ee1-ff0011223344.mp4",
52
- poster: "id-136038/assets/93-77aa88bb-99cc-4dd0-8ee1-ff0011223344.jpg",
53
- // Activo de una sola cadena: sirve para probar `filterByRetailer`.
54
- id_retailer: 1017,
55
- created_at: "2026-09-11T09:02:17.000Z",
56
- },
57
- {
58
- id: 94,
59
- asset_type: "documento",
60
- name: "manual_de_usuario.pdf",
61
- ext: "pdf",
62
- mime: "application/pdf",
63
- size_bytes: 2418192,
64
- duration: null,
65
- width: null,
66
- height: null,
67
- src: "id-136038/assets/94-cc55dd66-7788-99aa-bbcc-ddeeff001122.pdf",
68
- poster: null,
69
- id_retailer: null,
70
- created_at: "2026-09-12T11:48:00.000Z",
71
- },
72
- {
73
- id: 96,
74
- asset_type: "editable",
75
- name: "arte_empaque.psd",
76
- ext: "psd",
77
- // El navegador no le asigna mime a un .psd: llega vacío. Es el caso que
78
- // obliga a resolver el tipo por extensión.
79
- mime: "",
80
- size_bytes: 268435456,
81
- duration: null,
82
- width: null,
83
- height: null,
84
- src: "id-136038/assets/96-11223344-5566-7788-99aa-bbccddeeff00.psd",
85
- poster: null,
86
- id_retailer: null,
87
- created_at: "2026-09-15T10:12:33.000Z",
88
- },
89
- {
90
- id: 97,
91
- asset_type: "hoja_calculo",
92
- name: "tabla_de_medidas.xlsx",
93
- ext: "xlsx",
94
- mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
95
- size_bytes: 48128,
96
- duration: null,
97
- width: null,
98
- height: null,
99
- src: "id-136038/assets/97-99887766-5544-3322-1100-ffeeddccbbaa.xlsx",
100
- poster: null,
101
- id_retailer: null,
102
- created_at: "2026-09-15T10:14:02.000Z",
103
- },
104
- ];
105
-
106
- /**
107
- * Caso incompleto a propósito: sin `mime`, sin `ext` y sin `url`. El backend
108
- * todavía no existe y es muy probable que las primeras respuestas lleguen así;
109
- * el adaptador tiene que poder deducirlos del nombre y de la clave de S3.
110
- */
111
- export const partialBackendAssetMock = {
112
- id: 95,
113
- name: "ficha_tecnica.pdf",
114
- src: "id-136038/assets/95-aa11bb22-cc33-dd44-ee55-ff6677889900.pdf",
115
- };
116
-
117
- /** `File` falso para probar validaciones sin tocar disco ni red. */
118
- export const makeFileMock = ({
119
- name = "giro_producto.mp4",
120
- type = "video/mp4",
121
- size = 8412160,
122
- } = {}) => ({ name, type, size });
@@ -1,197 +0,0 @@
1
- import { ASSET_ACTIONS, assetsInitialState } from "./assets.actions";
2
- import { assetKey, isPersisted } from "./assets.adapter";
3
-
4
- export { assetsInitialState };
5
-
6
- /*
7
- Estado de los activos digitales de la ficha.
8
-
9
- Separado de `active-state` a propósito: ese reducer ya lleva 344 líneas y toda
10
- la lógica de imágenes, que no comparte nada con esto —los activos no tienen
11
- tipo de toma, ni obligatoriedad, ni cupo por cadena—.
12
-
13
- Vive aquí y no en `context/reducers/` —donde están los otros cuatro— porque no
14
- depende de la pantalla: es una función pura sobre activos, igual que el
15
- adaptador y el catálogo. Así el hook y las historias de Storybook lo usan sin
16
- arrastrar la ficha completa, y `provider-product-edition.reducer.js` solo lo
17
- enchufa con una línea, igual que a los demás.
18
-
19
- El reducer es puro. Liberar los object URL de los archivos locales es un efecto
20
- y se hace en el hook antes de despachar; meterlo aquí rompería poder probar
21
- este archivo sin navegador.
22
- */
23
- export const assetsReducer = (state, action) => {
24
- switch (action.type) {
25
- case ASSET_ACTIONS.SET_ASSETS_VALUES: {
26
- return {
27
- ...state,
28
- assets_values: Array.isArray(action.payload) ? action.payload : [],
29
- // Lo que venga del backend es, por definición, lo ya guardado: no hay
30
- // nada pendiente de enviar.
31
- updated_assets: [],
32
- deleted_assets: [],
33
- selected_assets: [],
34
- all_assets_checked: false,
35
- };
36
- }
37
-
38
- case ASSET_ACTIONS.ADD_ASSETS: {
39
- const incoming = Array.isArray(action.payload)
40
- ? action.payload
41
- : [action.payload];
42
-
43
- return {
44
- ...state,
45
- assets_values: [...state.assets_values, ...incoming],
46
- };
47
- }
48
-
49
- case ASSET_ACTIONS.UPDATE_ASSET: {
50
- const { key, changes } = action.payload;
51
- let touched = null;
52
-
53
- const assets_values = state.assets_values.map((asset) => {
54
- if (assetKey(asset) !== key) return asset;
55
- touched = asset;
56
- return { ...asset, ...changes };
57
- });
58
-
59
- // Un activo que todavía no existe en el backend no se "actualiza": se
60
- // creará con los valores finales, así que no hay nada que rastrear.
61
- const shouldTrack = touched
62
- && isPersisted(touched)
63
- && !state.updated_assets.includes(key);
64
-
65
- return {
66
- ...state,
67
- assets_values,
68
- updated_assets: shouldTrack
69
- ? [...state.updated_assets, key]
70
- : state.updated_assets,
71
- };
72
- }
73
-
74
- case ASSET_ACTIONS.PATCH_ASSET: {
75
- const { key, changes } = action.payload;
76
-
77
- return {
78
- ...state,
79
- assets_values: state.assets_values.map((asset) => (
80
- assetKey(asset) === key ? { ...asset, ...changes } : asset
81
- )),
82
- };
83
- }
84
-
85
- case ASSET_ACTIONS.REMOVE_ASSET: {
86
- const { key } = action.payload;
87
- const target = state.assets_values.find((asset) => assetKey(asset) === key);
88
-
89
- if (!target) return state;
90
-
91
- const assets_values = state.assets_values.filter(
92
- (asset) => assetKey(asset) !== key,
93
- );
94
- const selected_assets = state.selected_assets.filter(
95
- (asset) => assetKey(asset) !== key,
96
- );
97
-
98
- return {
99
- ...state,
100
- assets_values,
101
- // Solo se le pide al backend que borre lo que el backend tiene. Un
102
- // activo local que nunca se subió simplemente desaparece.
103
- deleted_assets: isPersisted(target)
104
- ? [...state.deleted_assets, target.id]
105
- : state.deleted_assets,
106
- // Sin esto, un activo borrado seguiría contando como seleccionado y como
107
- // modificado: el guardado intentaría enviar un activo que ya no existe.
108
- updated_assets: state.updated_assets.filter((item) => item !== key),
109
- selected_assets,
110
- // Se recalcula en vez de apagarse: si estaban todos marcados y se borra
111
- // uno, los que quedan siguen marcados y la casilla de "seleccionar todo"
112
- // debe seguir reflejándolo.
113
- all_assets_checked:
114
- assets_values.length > 0
115
- && selected_assets.length === assets_values.length,
116
- };
117
- }
118
-
119
- case ASSET_ACTIONS.SET_SELECTED_ASSETS: {
120
- const selected = Array.isArray(action.payload) ? action.payload : [];
121
-
122
- return {
123
- ...state,
124
- selected_assets: selected,
125
- all_assets_checked:
126
- state.assets_values.length > 0
127
- && selected.length === state.assets_values.length,
128
- };
129
- }
130
-
131
- case ASSET_ACTIONS.TOGGLE_CHECK_ALL_ASSETS: {
132
- // Con la lista vacía no hay nada que marcar: dejarlo en `true` pintaría la
133
- // casilla de "seleccionar todo" activa sobre una bandeja sin activos.
134
- const isChecked = Boolean(action.payload) && state.assets_values.length > 0;
135
-
136
- return {
137
- ...state,
138
- all_assets_checked: isChecked,
139
- selected_assets: isChecked ? [...state.assets_values] : [],
140
- };
141
- }
142
-
143
- case ASSET_ACTIONS.CLEAR_ASSETS_TRACKING: {
144
- return {
145
- ...state,
146
- updated_assets: [],
147
- deleted_assets: [],
148
- };
149
- }
150
-
151
- default: {
152
- return null;
153
- }
154
- }
155
- };
156
-
157
- /**
158
- * Los tres grupos que espera el backend, listos para `buildSavePayload`.
159
- *
160
- * Se deriva de `assets_values` en el momento de guardar, no se acumula: las
161
- * claves rastreadas dicen QUÉ cambió y el objeto vigente dice CÓMO quedó. Es lo
162
- * que evita enviar el valor anterior cuando el usuario edita dos veces el mismo
163
- * activo antes de guardar.
164
- */
165
- export const selectAssetsToSave = (state) => ({
166
- assets: state.assets_values,
167
- updated: state.assets_values.filter((asset) => state.updated_assets.includes(assetKey(asset))),
168
- deleted: state.deleted_assets,
169
- });
170
-
171
- /** ¿Hay algo sin guardar? Sirve para avisar antes de salir y para habilitar el botón. */
172
- /**
173
- * Desglose de lo que falta por guardar.
174
- *
175
- * Se cuenta por separado —altas, cambios, bajas— y no como un número suelto
176
- * porque "3 cambios sin guardar" no le dice al usuario qué perdería si se va:
177
- * no es lo mismo dejar sin guardar un borrado que un archivo recién subido.
178
- *
179
- * Un activo nuevo solo cuenta si ya está en S3. Uno que todavía se está
180
- * subiendo, o que falló, no es un cambio pendiente de guardar: es un archivo que
181
- * aún no existe, y anunciarlo como tal haría que el contador subiera y bajara
182
- * solo mientras la barra de progreso avanza.
183
- */
184
- export const countUnsavedAssets = (state) => {
185
- const nuevos = state.assets_values.filter(
186
- (asset) => !isPersisted(asset) && Boolean(asset.src),
187
- ).length;
188
-
189
- return {
190
- nuevos,
191
- modificados: state.updated_assets.length,
192
- borrados: state.deleted_assets.length,
193
- total: nuevos + state.updated_assets.length + state.deleted_assets.length,
194
- };
195
- };
196
-
197
- export const hasUnsavedAssets = (state) => countUnsavedAssets(state).total > 0;
@@ -1,109 +0,0 @@
1
- /**
2
- * Subida de activos digitales a S3.
3
- *
4
- * El archivo NO pasa por ningún Lambda ni se convierte a base64. El backend
5
- * firma la subida y el navegador manda el archivo directo al bucket. Es lo que
6
- * permite subir un .psd de 300 MB: el camino que usan hoy las imágenes
7
- * —`FileReader.readAsDataURL` y `putObject` con las llaves de AWS en el
8
- * bundle— carga el archivo entero en memoria y lo infla un tercio.
9
- *
10
- * Es un POST multipart y no un PUT porque el backend firma con
11
- * `createPresignedPost`, la única forma de que S3 aplique de verdad el tope de
12
- * tamaño y el Content-Type. Con un PUT firmado, quien tenga la URL sube lo que
13
- * quiera del tamaño que quiera.
14
- */
15
-
16
- import axios from "axios";
17
-
18
- import { toPayload } from "./assets.adapter";
19
-
20
- /**
21
- * Pide al backend que firme la subida de varios archivos a la vez.
22
- *
23
- * En una sola llamada y no una por archivo: soltar diez archivos son diez
24
- * viajes de ida y vuelta que no hacen falta.
25
- *
26
- * Devuelve `uploads` (los que se firmaron) y `rejected` (los que el servidor no
27
- * aceptó, con su motivo). El servidor valida por su cuenta aunque el front ya
28
- * lo haya hecho: el front se puede saltar, y quien firma decide qué entra al
29
- * bucket.
30
- */
31
- export const requestPresign = async ({ articleId, assets, token }) => {
32
- const { data } = await axios.post(
33
- process.env.REACT_APP_ASSETS_PRESIGN_ENDPOINT,
34
- {
35
- articleId,
36
- files: assets.map((asset) => ({
37
- // El `ref` vuelve tal cual en la respuesta. Emparejar por nombre sería
38
- // frágil: dos archivos que se llamen igual acabarían con la clave del
39
- // otro.
40
- ref: asset.tempId,
41
- ...toPayload(asset),
42
- })),
43
- },
44
- { headers: { Authorization: token } },
45
- );
46
-
47
- // `integration: lambda` de API Gateway devuelve el objeto del handler tal
48
- // cual, con el cuerpo como cadena, en vez de traducirlo a una respuesta HTTP.
49
- const body = typeof data?.body === "string" ? JSON.parse(data.body) : data?.body ?? data;
50
-
51
- if (data?.statusCode && data.statusCode >= 400) {
52
- throw new Error(body?.message || "No se pudo preparar la subida");
53
- }
54
-
55
- return { uploads: body?.uploads ?? [], rejected: body?.rejected ?? [] };
56
- };
57
-
58
- /**
59
- * Sube un archivo con los campos que firmó el backend.
60
- *
61
- * Se usa XMLHttpRequest y no fetch porque fetch todavía no sabe informar del
62
- * progreso de subida. En un .psd de 300 MB la barra no es un adorno: sin ella
63
- * la pantalla parece colgada durante minutos.
64
- *
65
- * El orden del FormData importa: S3 exige que el archivo vaya AL FINAL, después
66
- * de todos los campos de la política. Si va antes, rechaza la petición.
67
- */
68
- export const uploadToS3 = ({ upload, file, onProgress }) => new Promise((resolve, reject) => {
69
- const form = new FormData();
70
- Object.entries(upload.fields).forEach(([name, value]) => form.append(name, value));
71
- form.append("file", file);
72
-
73
- const xhr = new XMLHttpRequest();
74
- xhr.open("POST", upload.url, true);
75
-
76
- xhr.upload.onprogress = (event) => {
77
- if (!event.lengthComputable) return;
78
- onProgress?.(Math.round((event.loaded / event.total) * 100));
79
- };
80
-
81
- xhr.onload = () => {
82
- // S3 responde 204 sin cuerpo cuando acepta el archivo.
83
- if (xhr.status >= 200 && xhr.status < 300) {
84
- resolve(upload.key);
85
- return;
86
- }
87
- /*
88
- Los errores de S3 vienen en XML, no en JSON. Los dos que importan:
89
- EntityTooLarge = el archivo se pasó del tope que firmamos, y AccessDenied
90
- = al rol del Lambda le falta s3:PutObject sobre el prefijo, o sea que se
91
- desplegó solo la función y no el servicio completo.
92
- */
93
- const code = /<Code>(.*?)<\/Code>/.exec(xhr.responseText)?.[1];
94
- reject(new Error(code === "EntityTooLarge"
95
- ? "El archivo supera el tamaño permitido."
96
- : `S3 rechazó la subida (${code || xhr.status}).`));
97
- };
98
-
99
- xhr.onerror = () => reject(new Error(
100
- // Un fallo de red sin estatus casi siempre es CORS: el bucket tiene que
101
- // permitir POST desde el origen de la aplicación. El navegador corta la
102
- // petición antes de mandarla y no deja rastro en ningún log del servidor.
103
- "No se pudo conectar con el almacenamiento. Revisa la configuración CORS del bucket.",
104
- ));
105
-
106
- xhr.onabort = () => reject(new Error("Subida cancelada."));
107
-
108
- xhr.send(form);
109
- });