@mgonza02/goal-image-gallery 1.2.0 → 1.2.3
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/CHANGELOG.md +23 -0
- package/README.md +11 -0
- package/lib/GoalImageGallery.js +52 -7
- package/lib/components/ImageGalleryEmptyState.js +5 -3
- package/lib/components/ImageGalleryGrid.js +12 -3
- package/lib/components/ImageGalleryItem.js +43 -2
- package/lib/hooks/use-image-gallery.js +69 -11
- package/lib/index.d.ts +52 -0
- package/lib/index.js +26 -26
- package/lib/utils/image-utils.js +83 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to the GoalImageGallery project will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.3] - 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`defaultImageCode`**: Highlights the default image in the grid.
|
|
12
|
+
- **`onSetAsDefault(imageCode)`**: Star action when `multiple` and at least two images (requires handler).
|
|
13
|
+
- **`onDelete(imageCode)`**, **`onUpdate(oldCode, newCode)`**, **`onAdd(imageCode)`**: Fired after successful gallery operations.
|
|
14
|
+
|
|
15
|
+
## [1.2.2] - 2026-10-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`maxItems`**: Cap gallery size; hides the add tile and blocks uploads beyond the limit.
|
|
19
|
+
- **`allowDelete` / `minItems`**: Configurable delete in single- and multi-image mode (delete was previously limited to multi with 2+ images).
|
|
20
|
+
- **`minImageWidth` / `minImageHeight`**: Minimum dimension constraints, passed to custom selectors and `validateImageFile`.
|
|
21
|
+
- **`validateImageFile`**, **`validateImageFileSync`**, **`countGalleryImages`** exported from `image-utils`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Delete API payload includes `action: 'delete'`, `image_codes`, and `deletedImageCode` for backends that branch on operation type.
|
|
25
|
+
|
|
26
|
+
## [1.2.1] - 2026-05-22
|
|
27
|
+
|
|
28
|
+
### 🐛 Bug Fixes
|
|
29
|
+
- **Stack alignItems**: Moved `alignItems` prop inside the `sx` prop in `ImageGalleryEmptyState` component to prevent React from passing it down as an unrecognized attribute to DOM elements.
|
|
30
|
+
|
|
8
31
|
## [1.2.0] - 2026-04-29
|
|
9
32
|
|
|
10
33
|
### ⬆️ Dependency Upgrades
|
package/README.md
CHANGED
|
@@ -312,6 +312,16 @@ const CustomImageSelector = ({
|
|
|
312
312
|
| `imageHandlerApi` | `function` | `undefined` | No | API function for image operations (required for editing) |
|
|
313
313
|
| `afterUpload` | `function` | `undefined` | No | Callback function called after successful upload |
|
|
314
314
|
| `multiple` | `boolean` | `false` | No | Allow multiple image selection and upload |
|
|
315
|
+
| `maxItems` | `number` | `undefined` | No | Maximum number of images in the gallery (hides add control when reached) |
|
|
316
|
+
| `allowDelete` | `boolean` | `true` | No | Show delete action when `canEdit` is enabled |
|
|
317
|
+
| `minItems` | `number` | `0` | No | Minimum images that must remain (disables delete at this count) |
|
|
318
|
+
| `minImageWidth` | `number` | `undefined` | No | Minimum image width in pixels (forwarded to selector / `validateImageFile`) |
|
|
319
|
+
| `minImageHeight` | `number` | `undefined` | No | Minimum image height in pixels (forwarded to selector / `validateImageFile`) |
|
|
320
|
+
| `defaultImageCode` | `string` | `undefined` | No | Marks which image is the default in the grid |
|
|
321
|
+
| `onSetAsDefault` | `(imageCode) => void` | `undefined` | No | User chose default (only when `multiple` and 2+ images) |
|
|
322
|
+
| `onDelete` | `(imageCode) => void` | `undefined` | No | After successful delete |
|
|
323
|
+
| `onUpdate` | `(oldImageCode, newImageCode) => void` | `undefined` | No | After successful replace |
|
|
324
|
+
| `onAdd` | `(imageCode) => void` | `undefined` | No | After each new image is added |
|
|
315
325
|
|
|
316
326
|
### Display Props
|
|
317
327
|
|
|
@@ -1106,6 +1116,7 @@ When contributing to this component:
|
|
|
1106
1116
|
For detailed information about changes, new features, and updates across versions, see [CHANGELOG.md](CHANGELOG.md).
|
|
1107
1117
|
|
|
1108
1118
|
### Recent Updates
|
|
1119
|
+
- **v1.2.1**: Fixed warning about invalid `alignItems` prop being passed to DOM elements by Stack components
|
|
1109
1120
|
- **v1.2.0**: Upgraded to MUI v9, migrated to pnpm, Docker build pipeline, SOLID principles documentation
|
|
1110
1121
|
- **v1.1.1**: Modal responsiveness, accessibility improvements, styled-components replaced with MUI Box
|
|
1111
1122
|
- **v1.1.0**: Fixed handleAfterUpload array structure for bulk uploads
|
package/lib/GoalImageGallery.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useMediaQuery } from "@mui/material";
|
|
|
5
5
|
import ImageGalleryGrid from "./components/ImageGalleryGrid";
|
|
6
6
|
import ImageGalleryModal from "./components/ImageGalleryModal";
|
|
7
7
|
import { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } from "./hooks/use-image-gallery";
|
|
8
|
-
import { downloadImage } from "./utils/image-utils";
|
|
8
|
+
import { countGalleryImages, downloadImage } from "./utils/image-utils";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* GoalImageGallery - Advanced React Image Gallery Component
|
|
@@ -233,7 +233,17 @@ const GoalImageGallery = _ref => {
|
|
|
233
233
|
hasPermission = () => true,
|
|
234
234
|
currentCompany = 0,
|
|
235
235
|
slot = {},
|
|
236
|
-
slotProps = {}
|
|
236
|
+
slotProps = {},
|
|
237
|
+
maxItems,
|
|
238
|
+
allowDelete = true,
|
|
239
|
+
minItems = 0,
|
|
240
|
+
minImageWidth,
|
|
241
|
+
minImageHeight,
|
|
242
|
+
defaultImageCode,
|
|
243
|
+
onSetAsDefault,
|
|
244
|
+
onDelete,
|
|
245
|
+
onUpdate,
|
|
246
|
+
onAdd
|
|
237
247
|
} = _ref;
|
|
238
248
|
const theme = useTheme();
|
|
239
249
|
const [updatable, setUpdatable] = useState(false);
|
|
@@ -241,8 +251,21 @@ const GoalImageGallery = _ref => {
|
|
|
241
251
|
selector: ImageSelector
|
|
242
252
|
} = slot;
|
|
243
253
|
const {
|
|
244
|
-
selector: imageSelectorProps
|
|
254
|
+
selector: imageSelectorProps = {}
|
|
245
255
|
} = slotProps;
|
|
256
|
+
const currentImageCount = countGalleryImages((Array.isArray(imageCodes) ? imageCodes : (imageCodes || "").split(/[\s,]+/).filter(Boolean)).map((code, index) => ({
|
|
257
|
+
id: index,
|
|
258
|
+
image_code: code
|
|
259
|
+
})));
|
|
260
|
+
const mergedSelectorProps = {
|
|
261
|
+
...imageSelectorProps,
|
|
262
|
+
maxItems,
|
|
263
|
+
currentImageCount,
|
|
264
|
+
minImageWidth,
|
|
265
|
+
minImageHeight,
|
|
266
|
+
minWidth: imageSelectorProps.minWidth ?? minImageWidth,
|
|
267
|
+
minHeight: imageSelectorProps.minHeight ?? minImageHeight
|
|
268
|
+
};
|
|
246
269
|
|
|
247
270
|
// Initialize permissions
|
|
248
271
|
useEffect(() => {
|
|
@@ -259,12 +282,14 @@ const GoalImageGallery = _ref => {
|
|
|
259
282
|
zoomLevel,
|
|
260
283
|
loading,
|
|
261
284
|
removable,
|
|
285
|
+
canSetDefault,
|
|
262
286
|
setZoomLevel,
|
|
263
287
|
handleOpenImage,
|
|
264
288
|
handleCloseImage,
|
|
265
289
|
handleImageSelect,
|
|
266
290
|
handleCancel,
|
|
267
291
|
handleRemoveImage,
|
|
292
|
+
handleSetAsDefault,
|
|
268
293
|
handleAfterUpload
|
|
269
294
|
} = useImageGallery({
|
|
270
295
|
imageCodes,
|
|
@@ -275,7 +300,14 @@ const GoalImageGallery = _ref => {
|
|
|
275
300
|
ownerEntity,
|
|
276
301
|
currentCompany,
|
|
277
302
|
onRefresh,
|
|
278
|
-
showError
|
|
303
|
+
showError,
|
|
304
|
+
maxItems,
|
|
305
|
+
allowDelete,
|
|
306
|
+
minItems,
|
|
307
|
+
onSetAsDefault,
|
|
308
|
+
onDelete,
|
|
309
|
+
onUpdate,
|
|
310
|
+
onAdd
|
|
279
311
|
});
|
|
280
312
|
|
|
281
313
|
// Zoom and navigation handlers
|
|
@@ -338,7 +370,7 @@ const GoalImageGallery = _ref => {
|
|
|
338
370
|
multiple: multiple,
|
|
339
371
|
title: multiple ? "Seleccionar Imágenes" : "Seleccionar Imagen",
|
|
340
372
|
uploadMessage: multiple ? "Arrastra imágenes aquí o haz clic para seleccionar" : "Arrastra una imagen aquí o haz clic para seleccionar",
|
|
341
|
-
...
|
|
373
|
+
...mergedSelectorProps
|
|
342
374
|
}) : /*#__PURE__*/_jsxs("div", {
|
|
343
375
|
style: {
|
|
344
376
|
padding: "20px",
|
|
@@ -380,7 +412,10 @@ const GoalImageGallery = _ref => {
|
|
|
380
412
|
onImageClick: handleOpenImage,
|
|
381
413
|
onImageSelect: handleImageSelect,
|
|
382
414
|
onRemoveImage: handleRemoveImage,
|
|
383
|
-
|
|
415
|
+
onSetAsDefault: handleSetAsDefault,
|
|
416
|
+
onDownloadImage: handleDownloadImage,
|
|
417
|
+
canSetDefault: canSetDefault,
|
|
418
|
+
defaultImageCode: defaultImageCode
|
|
384
419
|
}), /*#__PURE__*/_jsx(ImageGalleryModal, {
|
|
385
420
|
open: openImage !== null,
|
|
386
421
|
imageUrl: openImage,
|
|
@@ -413,6 +448,16 @@ GoalImageGallery.propTypes = {
|
|
|
413
448
|
hasPermission: PropTypes.func,
|
|
414
449
|
currentCompany: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
415
450
|
slot: PropTypes.object,
|
|
416
|
-
slotProps: PropTypes.object
|
|
451
|
+
slotProps: PropTypes.object,
|
|
452
|
+
maxItems: PropTypes.number,
|
|
453
|
+
allowDelete: PropTypes.bool,
|
|
454
|
+
minItems: PropTypes.number,
|
|
455
|
+
minImageWidth: PropTypes.number,
|
|
456
|
+
minImageHeight: PropTypes.number,
|
|
457
|
+
defaultImageCode: PropTypes.string,
|
|
458
|
+
onSetAsDefault: PropTypes.func,
|
|
459
|
+
onDelete: PropTypes.func,
|
|
460
|
+
onUpdate: PropTypes.func,
|
|
461
|
+
onAdd: PropTypes.func
|
|
417
462
|
};
|
|
418
463
|
export default GoalImageGallery;
|
|
@@ -18,7 +18,9 @@ const ImageGalleryEmptyState = _ref => {
|
|
|
18
18
|
return /*#__PURE__*/_jsx(StyledEmptyStateContainer, {
|
|
19
19
|
children: /*#__PURE__*/_jsxs(Stack, {
|
|
20
20
|
spacing: 3,
|
|
21
|
-
|
|
21
|
+
sx: {
|
|
22
|
+
alignItems: 'center'
|
|
23
|
+
},
|
|
22
24
|
children: [/*#__PURE__*/_jsx(ImageIcon, {
|
|
23
25
|
sx: {
|
|
24
26
|
fontSize: 64,
|
|
@@ -42,9 +44,9 @@ const ImageGalleryEmptyState = _ref => {
|
|
|
42
44
|
}), /*#__PURE__*/_jsxs(Stack, {
|
|
43
45
|
direction: "row",
|
|
44
46
|
spacing: 1,
|
|
45
|
-
alignItems: "center",
|
|
46
47
|
sx: {
|
|
47
|
-
mt: 2
|
|
48
|
+
mt: 2,
|
|
49
|
+
alignItems: 'center'
|
|
48
50
|
},
|
|
49
51
|
children: [/*#__PURE__*/_jsx(Chip, {
|
|
50
52
|
icon: /*#__PURE__*/_jsx(UploadIcon, {}),
|
|
@@ -25,7 +25,10 @@ const ImageGalleryGrid = _ref => {
|
|
|
25
25
|
onImageClick,
|
|
26
26
|
onImageSelect,
|
|
27
27
|
onRemoveImage,
|
|
28
|
-
|
|
28
|
+
onSetAsDefault,
|
|
29
|
+
onDownloadImage,
|
|
30
|
+
canSetDefault = false,
|
|
31
|
+
defaultImageCode
|
|
29
32
|
} = _ref;
|
|
30
33
|
const theme = useTheme();
|
|
31
34
|
if (imageList.length === 0) {
|
|
@@ -70,7 +73,10 @@ const ImageGalleryGrid = _ref => {
|
|
|
70
73
|
onImageClick: onImageClick,
|
|
71
74
|
onImageSelect: onImageSelect,
|
|
72
75
|
onRemoveImage: onRemoveImage,
|
|
73
|
-
|
|
76
|
+
onSetAsDefault: onSetAsDefault,
|
|
77
|
+
onDownloadImage: onDownloadImage,
|
|
78
|
+
canSetDefault: canSetDefault,
|
|
79
|
+
isDefault: !!defaultImageCode && image.image_code === defaultImageCode
|
|
74
80
|
})
|
|
75
81
|
}, image.id))
|
|
76
82
|
})
|
|
@@ -93,6 +99,9 @@ ImageGalleryGrid.propTypes = {
|
|
|
93
99
|
onImageClick: PropTypes.func.isRequired,
|
|
94
100
|
onImageSelect: PropTypes.func.isRequired,
|
|
95
101
|
onRemoveImage: PropTypes.func.isRequired,
|
|
96
|
-
|
|
102
|
+
onSetAsDefault: PropTypes.func,
|
|
103
|
+
onDownloadImage: PropTypes.func.isRequired,
|
|
104
|
+
canSetDefault: PropTypes.bool,
|
|
105
|
+
defaultImageCode: PropTypes.string
|
|
97
106
|
};
|
|
98
107
|
export default ImageGalleryGrid;
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { Box, IconButton, Skeleton, Tooltip, Typography, Button } from '@mui/material';
|
|
4
4
|
import { alpha, useTheme } from '@mui/material/styles';
|
|
5
|
-
import { ChangeCircle as ChangeIcon, Download as DownloadIcon, Visibility as ViewIcon, Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
|
5
|
+
import { ChangeCircle as ChangeIcon, Download as DownloadIcon, Visibility as ViewIcon, Add as AddIcon, Delete as DeleteIcon, Star as StarIcon, StarBorder as StarBorderIcon } from '@mui/icons-material';
|
|
6
6
|
import { getImageContainerStyles, getImageOverlayStyles } from '../styles/styled-components';
|
|
7
7
|
import { handleImageError } from '../utils/image-utils';
|
|
8
8
|
|
|
@@ -21,9 +21,12 @@ const ImageGalleryItem = _ref => {
|
|
|
21
21
|
removable = false,
|
|
22
22
|
allowDownload = false,
|
|
23
23
|
showImageInfo = false,
|
|
24
|
+
canSetDefault = false,
|
|
25
|
+
isDefault = false,
|
|
24
26
|
onImageClick,
|
|
25
27
|
onImageSelect,
|
|
26
28
|
onRemoveImage,
|
|
29
|
+
onSetAsDefault,
|
|
27
30
|
onDownloadImage
|
|
28
31
|
} = _ref;
|
|
29
32
|
const theme = useTheme();
|
|
@@ -65,7 +68,31 @@ const ImageGalleryItem = _ref => {
|
|
|
65
68
|
}
|
|
66
69
|
return /*#__PURE__*/_jsxs(Box, {
|
|
67
70
|
sx: getImageContainerStyles(theme),
|
|
68
|
-
children: [
|
|
71
|
+
children: [isDefault && /*#__PURE__*/_jsxs(Box, {
|
|
72
|
+
sx: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
top: 8,
|
|
75
|
+
left: 8,
|
|
76
|
+
zIndex: 2,
|
|
77
|
+
display: 'flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: 0.5,
|
|
80
|
+
px: 1,
|
|
81
|
+
py: 0.25,
|
|
82
|
+
borderRadius: 1,
|
|
83
|
+
backgroundColor: alpha(theme.palette.warning.main, 0.92),
|
|
84
|
+
color: theme.palette.warning.contrastText
|
|
85
|
+
},
|
|
86
|
+
children: [/*#__PURE__*/_jsx(StarIcon, {
|
|
87
|
+
sx: {
|
|
88
|
+
fontSize: 16
|
|
89
|
+
}
|
|
90
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
|
91
|
+
variant: "caption",
|
|
92
|
+
fontWeight: 600,
|
|
93
|
+
children: "Predeterminada"
|
|
94
|
+
})]
|
|
95
|
+
}), loading && /*#__PURE__*/_jsx(Skeleton, {
|
|
69
96
|
variant: "rectangular",
|
|
70
97
|
width: "100%",
|
|
71
98
|
height: 200,
|
|
@@ -111,6 +138,17 @@ const ImageGalleryItem = _ref => {
|
|
|
111
138
|
},
|
|
112
139
|
children: /*#__PURE__*/_jsx(DownloadIcon, {})
|
|
113
140
|
})
|
|
141
|
+
}), canSetDefault && !isDefault && /*#__PURE__*/_jsx(Tooltip, {
|
|
142
|
+
title: "Usar como predeterminada",
|
|
143
|
+
children: /*#__PURE__*/_jsx(IconButton, {
|
|
144
|
+
size: "small",
|
|
145
|
+
onClick: e => {
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
onSetAsDefault?.(image);
|
|
148
|
+
},
|
|
149
|
+
color: "warning",
|
|
150
|
+
children: /*#__PURE__*/_jsx(StarBorderIcon, {})
|
|
151
|
+
})
|
|
114
152
|
}), updatable && /*#__PURE__*/_jsx(Tooltip, {
|
|
115
153
|
title: "Cambiar imagen",
|
|
116
154
|
children: /*#__PURE__*/_jsx(IconButton, {
|
|
@@ -163,9 +201,12 @@ ImageGalleryItem.propTypes = {
|
|
|
163
201
|
removable: PropTypes.bool,
|
|
164
202
|
allowDownload: PropTypes.bool,
|
|
165
203
|
showImageInfo: PropTypes.bool,
|
|
204
|
+
canSetDefault: PropTypes.bool,
|
|
205
|
+
isDefault: PropTypes.bool,
|
|
166
206
|
onImageClick: PropTypes.func.isRequired,
|
|
167
207
|
onImageSelect: PropTypes.func.isRequired,
|
|
168
208
|
onRemoveImage: PropTypes.func.isRequired,
|
|
209
|
+
onSetAsDefault: PropTypes.func,
|
|
169
210
|
onDownloadImage: PropTypes.func.isRequired
|
|
170
211
|
};
|
|
171
212
|
export default ImageGalleryItem;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { countGalleryImages } from '../utils/image-utils';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Custom hook for managing image gallery state
|
|
@@ -13,7 +14,14 @@ export const useImageGallery = _ref => {
|
|
|
13
14
|
ownerEntity,
|
|
14
15
|
currentCompany,
|
|
15
16
|
onRefresh,
|
|
16
|
-
showError
|
|
17
|
+
showError,
|
|
18
|
+
maxItems,
|
|
19
|
+
allowDelete = true,
|
|
20
|
+
minItems = 0,
|
|
21
|
+
onSetAsDefault,
|
|
22
|
+
onDelete,
|
|
23
|
+
onUpdate,
|
|
24
|
+
onAdd
|
|
17
25
|
} = _ref;
|
|
18
26
|
const [openImage, setOpenImage] = useState(null);
|
|
19
27
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
|
@@ -23,6 +31,7 @@ export const useImageGallery = _ref => {
|
|
|
23
31
|
const [zoomLevel, setZoomLevel] = useState(1);
|
|
24
32
|
const [loading, setLoading] = useState(false);
|
|
25
33
|
const [removable, setRemovable] = useState(false);
|
|
34
|
+
const [canSetDefault, setCanSetDefault] = useState(false);
|
|
26
35
|
|
|
27
36
|
// Process image codes into image list
|
|
28
37
|
useEffect(() => {
|
|
@@ -40,17 +49,19 @@ export const useImageGallery = _ref => {
|
|
|
40
49
|
} else {
|
|
41
50
|
newList = [];
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
const imageCount = newList.length;
|
|
53
|
+
const canRemoveMore = imageCount > minItems;
|
|
54
|
+
setRemovable(!!(updatable && allowDelete && imageCount > 0 && canRemoveMore));
|
|
55
|
+
const atMaxItems = maxItems != null && imageCount >= maxItems;
|
|
56
|
+
if (updatable && multiple && !atMaxItems) {
|
|
47
57
|
newList.push({
|
|
48
58
|
image_code: '+',
|
|
49
59
|
id: newList.length
|
|
50
60
|
});
|
|
51
61
|
}
|
|
62
|
+
setCanSetDefault(!!(multiple && imageCount >= 2 && onSetAsDefault));
|
|
52
63
|
setImageList(newList);
|
|
53
|
-
}, [imageCodes, canEdit, multiple, updatable]);
|
|
64
|
+
}, [imageCodes, canEdit, multiple, updatable, maxItems, allowDelete, minItems, onSetAsDefault]);
|
|
54
65
|
|
|
55
66
|
// Image handlers
|
|
56
67
|
const handleOpenImage = useCallback(function (imageUrl) {
|
|
@@ -64,9 +75,16 @@ export const useImageGallery = _ref => {
|
|
|
64
75
|
setZoomLevel(1);
|
|
65
76
|
}, []);
|
|
66
77
|
const handleImageSelect = useCallback(image => {
|
|
78
|
+
if (image === null && maxItems != null) {
|
|
79
|
+
const currentCount = countGalleryImages(imageList);
|
|
80
|
+
if (currentCount >= maxItems) {
|
|
81
|
+
showError?.(`Máximo ${maxItems} imágenes permitidas`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
67
85
|
setSelectedImage(image);
|
|
68
86
|
setSelectingImage(true);
|
|
69
|
-
}, []);
|
|
87
|
+
}, [imageList, maxItems, showError]);
|
|
70
88
|
const handleCancel = useCallback(() => {
|
|
71
89
|
setSelectingImage(false);
|
|
72
90
|
}, []);
|
|
@@ -83,9 +101,13 @@ export const useImageGallery = _ref => {
|
|
|
83
101
|
...ownerEntity,
|
|
84
102
|
companyId: currentCompany,
|
|
85
103
|
image_code: imagesToSave,
|
|
86
|
-
|
|
104
|
+
image_codes: imagesToSave,
|
|
105
|
+
company_id: currentCompany,
|
|
106
|
+
action: 'delete',
|
|
107
|
+
deletedImageCode: image.image_code
|
|
87
108
|
});
|
|
88
109
|
if (result.success) {
|
|
110
|
+
onDelete?.(image.image_code);
|
|
89
111
|
onRefresh?.(result);
|
|
90
112
|
} else {
|
|
91
113
|
showError(result.message);
|
|
@@ -95,7 +117,13 @@ export const useImageGallery = _ref => {
|
|
|
95
117
|
} finally {
|
|
96
118
|
setLoading(false);
|
|
97
119
|
}
|
|
98
|
-
}, [ownerEntity, currentCompany, imageList, onRefresh, showError, imageHandlerApi]);
|
|
120
|
+
}, [ownerEntity, currentCompany, imageList, onRefresh, onDelete, showError, imageHandlerApi]);
|
|
121
|
+
const handleSetAsDefault = useCallback(image => {
|
|
122
|
+
if (!image?.image_code || image.image_code === '+') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
onSetAsDefault?.(image.image_code);
|
|
126
|
+
}, [onSetAsDefault]);
|
|
99
127
|
|
|
100
128
|
/**
|
|
101
129
|
* Handles the upload completion for single or multiple images
|
|
@@ -149,19 +177,47 @@ export const useImageGallery = _ref => {
|
|
|
149
177
|
}
|
|
150
178
|
});
|
|
151
179
|
}
|
|
180
|
+
if (maxItems != null && imagesToSave.length > maxItems) {
|
|
181
|
+
showError?.(`Máximo ${maxItems} imágenes permitidas (se intentaron guardar ${imagesToSave.length})`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const existingCodes = imageList.reduce((acc, img) => {
|
|
185
|
+
if (img.image_code !== '+') {
|
|
186
|
+
acc.push(img.image_code);
|
|
187
|
+
}
|
|
188
|
+
return acc;
|
|
189
|
+
}, []);
|
|
152
190
|
const result = await imageHandlerApi({
|
|
153
191
|
...ownerEntity,
|
|
154
192
|
companyId: currentCompany,
|
|
155
193
|
company_id: currentCompany,
|
|
156
|
-
image_code: imagesToSave
|
|
194
|
+
image_code: imagesToSave,
|
|
195
|
+
image_codes: imagesToSave
|
|
157
196
|
});
|
|
158
197
|
if (result.success) {
|
|
198
|
+
if (currentImageCode) {
|
|
199
|
+
const newPrimaryCode = imagesArray[0]?.code;
|
|
200
|
+
if (newPrimaryCode && newPrimaryCode !== currentImageCode) {
|
|
201
|
+
onUpdate?.(currentImageCode, newPrimaryCode);
|
|
202
|
+
}
|
|
203
|
+
imagesArray.slice(1).forEach(image => {
|
|
204
|
+
if (image?.code) {
|
|
205
|
+
onAdd?.(image.code);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
imagesArray.forEach(image => {
|
|
210
|
+
if (image?.code && !existingCodes.includes(image.code)) {
|
|
211
|
+
onAdd?.(image.code);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
159
215
|
setSelectingImage(false);
|
|
160
216
|
onRefresh?.(result);
|
|
161
217
|
} else {
|
|
162
218
|
showError(result.message);
|
|
163
219
|
}
|
|
164
|
-
}, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, showError]);
|
|
220
|
+
}, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, onAdd, onUpdate, showError, maxItems]);
|
|
165
221
|
return {
|
|
166
222
|
openImage,
|
|
167
223
|
selectedImageIndex,
|
|
@@ -171,12 +227,14 @@ export const useImageGallery = _ref => {
|
|
|
171
227
|
zoomLevel,
|
|
172
228
|
loading,
|
|
173
229
|
removable,
|
|
230
|
+
canSetDefault,
|
|
174
231
|
setZoomLevel,
|
|
175
232
|
handleOpenImage,
|
|
176
233
|
handleCloseImage,
|
|
177
234
|
handleImageSelect,
|
|
178
235
|
handleCancel,
|
|
179
236
|
handleRemoveImage,
|
|
237
|
+
handleSetAsDefault,
|
|
180
238
|
handleAfterUpload
|
|
181
239
|
};
|
|
182
240
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -23,6 +23,26 @@ export interface GoalImageGalleryProps {
|
|
|
23
23
|
slotProps?: {
|
|
24
24
|
selector?: object;
|
|
25
25
|
};
|
|
26
|
+
/** Maximum number of images allowed in the gallery */
|
|
27
|
+
maxItems?: number;
|
|
28
|
+
/** When false, hides delete actions */
|
|
29
|
+
allowDelete?: boolean;
|
|
30
|
+
/** Minimum images that must remain (delete disabled at or below this count) */
|
|
31
|
+
minItems?: number;
|
|
32
|
+
/** Minimum image width in pixels (passed to selector and validateImageFile) */
|
|
33
|
+
minImageWidth?: number;
|
|
34
|
+
/** Minimum image height in pixels (passed to selector and validateImageFile) */
|
|
35
|
+
minImageHeight?: number;
|
|
36
|
+
/** Image code marked as default (badge + star state) */
|
|
37
|
+
defaultImageCode?: string;
|
|
38
|
+
/** Called when user sets an image as default (multi-image galleries with 2+ images) */
|
|
39
|
+
onSetAsDefault?: (imageCode: string) => void;
|
|
40
|
+
/** Called after a successful delete */
|
|
41
|
+
onDelete?: (imageCode: string) => void;
|
|
42
|
+
/** Called after a successful replace upload */
|
|
43
|
+
onUpdate?: (oldImageCode: string, newImageCode: string) => void;
|
|
44
|
+
/** Called after each new image is added */
|
|
45
|
+
onAdd?: (imageCode: string) => void;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
// Image item interface
|
|
@@ -54,6 +74,13 @@ export interface UseImageGalleryOptions {
|
|
|
54
74
|
currentCompany?: string;
|
|
55
75
|
onRefresh?: (result: any) => void;
|
|
56
76
|
showError?: (message: string) => void;
|
|
77
|
+
maxItems?: number;
|
|
78
|
+
allowDelete?: boolean;
|
|
79
|
+
minItems?: number;
|
|
80
|
+
onSetAsDefault?: (imageCode: string) => void;
|
|
81
|
+
onDelete?: (imageCode: string) => void;
|
|
82
|
+
onUpdate?: (oldImageCode: string, newImageCode: string) => void;
|
|
83
|
+
onAdd?: (imageCode: string) => void;
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
export interface UseImageGalleryReturn {
|
|
@@ -65,12 +92,14 @@ export interface UseImageGalleryReturn {
|
|
|
65
92
|
zoomLevel: number;
|
|
66
93
|
loading: boolean;
|
|
67
94
|
removable: boolean;
|
|
95
|
+
canSetDefault: boolean;
|
|
68
96
|
setZoomLevel: (level: number) => void;
|
|
69
97
|
handleOpenImage: (imageUrl: string, index?: number) => void;
|
|
70
98
|
handleCloseImage: () => void;
|
|
71
99
|
handleImageSelect: (image: ImageItem | null) => void;
|
|
72
100
|
handleCancel: () => void;
|
|
73
101
|
handleRemoveImage: (image: ImageItem) => Promise<void>;
|
|
102
|
+
handleSetAsDefault: (image: ImageItem) => void;
|
|
74
103
|
handleAfterUpload: (images: UploadedImage | UploadedImage[]) => Promise<void>;
|
|
75
104
|
}
|
|
76
105
|
|
|
@@ -105,7 +134,10 @@ export interface ImageGalleryGridProps {
|
|
|
105
134
|
onImageClick: (imageUrl: string, index: number) => void;
|
|
106
135
|
onImageSelect: (image: ImageItem | null) => void;
|
|
107
136
|
onRemoveImage: (image: ImageItem) => void;
|
|
137
|
+
onSetAsDefault?: (image: ImageItem) => void;
|
|
108
138
|
onDownloadImage: (imageCode: string) => void;
|
|
139
|
+
canSetDefault?: boolean;
|
|
140
|
+
defaultImageCode?: string;
|
|
109
141
|
}
|
|
110
142
|
|
|
111
143
|
export interface ImageGalleryModalProps {
|
|
@@ -135,7 +167,10 @@ export interface ImageGalleryItemProps {
|
|
|
135
167
|
onImageClick: (imageUrl: string, index: number) => void;
|
|
136
168
|
onImageSelect: (image: ImageItem) => void;
|
|
137
169
|
onRemoveImage: (image: ImageItem) => void;
|
|
170
|
+
onSetAsDefault?: (image: ImageItem) => void;
|
|
138
171
|
onDownloadImage: (imageCode: string) => void;
|
|
172
|
+
canSetDefault?: boolean;
|
|
173
|
+
isDefault?: boolean;
|
|
139
174
|
}
|
|
140
175
|
|
|
141
176
|
export interface ImageGalleryEmptyStateProps {
|
|
@@ -200,3 +235,20 @@ export declare const debounce: Debounce;
|
|
|
200
235
|
export declare const isTouchDevice: IsTouchDevice;
|
|
201
236
|
export declare const getImageDimensions: GetImageDimensions;
|
|
202
237
|
export declare const createImagePreview: CreateImagePreview;
|
|
238
|
+
export declare const countGalleryImages: (imageList: ImageItem[]) => number;
|
|
239
|
+
export declare const validateImageFileSync: (
|
|
240
|
+
file: File,
|
|
241
|
+
options?: {
|
|
242
|
+
maxSize?: number;
|
|
243
|
+
allowedTypes?: string[];
|
|
244
|
+
}
|
|
245
|
+
) => { isValid: boolean; errors: string[] };
|
|
246
|
+
export declare const validateImageFile: (
|
|
247
|
+
file: File,
|
|
248
|
+
options?: {
|
|
249
|
+
maxSize?: number;
|
|
250
|
+
allowedTypes?: string[];
|
|
251
|
+
minWidth?: number;
|
|
252
|
+
minHeight?: number;
|
|
253
|
+
}
|
|
254
|
+
) => Promise<{ isValid: boolean; errors: string[]; width?: number; height?: number }>;
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } fr
|
|
|
20
20
|
export { StyledImageContainer, ImageOverlay, StyledModalContainer, StyledModalHeader, StyledModalBody, StyledModalFooter, StyledEmptyStateContainer, StyledImageGridContainer } from './styles/styled-components';
|
|
21
21
|
|
|
22
22
|
// Utility exports
|
|
23
|
-
export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview } from './utils/image-utils';
|
|
23
|
+
export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview, countGalleryImages, validateImageFile, validateImageFileSync } from './utils/image-utils';
|
|
24
24
|
|
|
25
25
|
// Examples and documentation
|
|
26
26
|
export { default as GoalImageGalleryExamples } from './examples';
|
|
@@ -66,25 +66,9 @@ export const utils = {
|
|
|
66
66
|
* @param {Object} options - Validation options
|
|
67
67
|
* @returns {Object} - Validation result
|
|
68
68
|
*/
|
|
69
|
-
validateImageFile
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
maxSize = 5 * 1024 * 1024,
|
|
73
|
-
// 5MB
|
|
74
|
-
allowedTypes = FEATURES.SUPPORTED_FORMATS
|
|
75
|
-
} = options;
|
|
76
|
-
const errors = [];
|
|
77
|
-
if (file.size > maxSize) {
|
|
78
|
-
errors.push(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
|
|
79
|
-
}
|
|
80
|
-
if (!allowedTypes.includes(file.type)) {
|
|
81
|
-
errors.push(`File type ${file.type} is not supported`);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
isValid: errors.length === 0,
|
|
85
|
-
errors
|
|
86
|
-
};
|
|
87
|
-
},
|
|
69
|
+
validateImageFile,
|
|
70
|
+
validateImageFileSync,
|
|
71
|
+
countGalleryImages,
|
|
88
72
|
/**
|
|
89
73
|
* Create image handler API wrapper
|
|
90
74
|
* @param {Function} uploadFunction - Upload function
|
|
@@ -136,13 +120,29 @@ export const CONFIG = {
|
|
|
136
120
|
};
|
|
137
121
|
|
|
138
122
|
// Export version info
|
|
139
|
-
export const VERSION = '2.
|
|
123
|
+
export const VERSION = '1.2.3';
|
|
140
124
|
export const CHANGELOG = [{
|
|
141
|
-
version: '2.
|
|
142
|
-
date: '
|
|
143
|
-
changes: ['Added
|
|
125
|
+
version: '1.2.3',
|
|
126
|
+
date: '2026-10-12',
|
|
127
|
+
changes: ['Added defaultImageCode, onSetAsDefault, onDelete, onUpdate, and onAdd lifecycle callbacks', 'Set-as-default control shown only when multiple is true and there are at least two images']
|
|
128
|
+
}, {
|
|
129
|
+
version: '1.2.2',
|
|
130
|
+
date: '2026-10-12',
|
|
131
|
+
changes: ['Added allowDelete, maxItems, minItems, minImageWidth, and minImageHeight props', 'Delete is available for single and multiple galleries when editing (respects minItems)', 'Exported validateImageFile async helper with minimum dimension checks']
|
|
132
|
+
}, {
|
|
133
|
+
version: '1.2.1',
|
|
134
|
+
date: '2026-05-22',
|
|
135
|
+
changes: ['Fixed warning about invalid alignItems prop being passed to DOM elements by Stack components']
|
|
136
|
+
}, {
|
|
137
|
+
version: '1.2.0',
|
|
138
|
+
date: '2026-04-29',
|
|
139
|
+
changes: ['Upgraded to MUI v9', 'Migrated to pnpm package manager', 'Implemented Docker build pipeline', 'Added SOLID principles documentation']
|
|
140
|
+
}, {
|
|
141
|
+
version: '1.1.1',
|
|
142
|
+
date: '2025-07-14',
|
|
143
|
+
changes: ['Enhanced modal responsiveness and accessibility', 'Replaced styled-components with MUI Box']
|
|
144
144
|
}, {
|
|
145
145
|
version: '1.0.0',
|
|
146
|
-
date: '
|
|
147
|
-
changes: ['Initial release
|
|
146
|
+
date: '2025-07-11',
|
|
147
|
+
changes: ['Initial release with multi-method upload support', 'Enhanced UI/UX with Material Design 3', 'Added keyboard navigation and accessibility features']
|
|
148
148
|
}];
|
package/lib/utils/image-utils.js
CHANGED
|
@@ -47,13 +47,94 @@ export const getGridColumns = (isMobile, isTablet, multiple) => {
|
|
|
47
47
|
if (isTablet) return multiple ? 4 : 6; // 3 or 2 columns on tablet
|
|
48
48
|
return multiple ? 3 : 4; // 4 or 3 columns on desktop
|
|
49
49
|
};
|
|
50
|
+
const DEFAULT_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Validate image file type
|
|
53
54
|
*/
|
|
54
55
|
export const isValidImageFile = file => {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
return DEFAULT_IMAGE_TYPES.includes(file.type);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Count gallery images (excludes the "+" add placeholder).
|
|
61
|
+
*/
|
|
62
|
+
export const countGalleryImages = function () {
|
|
63
|
+
let imageList = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
64
|
+
return imageList.filter(img => img?.image_code && img.image_code !== '+').length;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate image file size and type (sync). Use validateImageFile for dimension checks.
|
|
69
|
+
*/
|
|
70
|
+
export const validateImageFileSync = function (file) {
|
|
71
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
72
|
+
const {
|
|
73
|
+
maxSize = 25 * 1024 * 1024,
|
|
74
|
+
allowedTypes = DEFAULT_IMAGE_TYPES
|
|
75
|
+
} = options;
|
|
76
|
+
const errors = [];
|
|
77
|
+
if (!file) {
|
|
78
|
+
errors.push('No se ha seleccionado ningún archivo');
|
|
79
|
+
} else {
|
|
80
|
+
if (maxSize && file.size > maxSize) {
|
|
81
|
+
errors.push(`El tamaño del archivo supera el límite de ${formatFileSize(maxSize)}`);
|
|
82
|
+
}
|
|
83
|
+
if (allowedTypes.length && !allowedTypes.includes(file.type)) {
|
|
84
|
+
errors.push(`El tipo de archivo ${file.type || 'desconocido'} no está permitido`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
isValid: errors.length === 0,
|
|
89
|
+
errors
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate image file including optional minimum width and height (async).
|
|
95
|
+
*/
|
|
96
|
+
export const validateImageFile = async function (file) {
|
|
97
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
98
|
+
const {
|
|
99
|
+
minWidth,
|
|
100
|
+
minHeight,
|
|
101
|
+
minImageWidth,
|
|
102
|
+
minImageHeight,
|
|
103
|
+
...syncOptions
|
|
104
|
+
} = options;
|
|
105
|
+
const resolvedMinWidth = minWidth ?? minImageWidth;
|
|
106
|
+
const resolvedMinHeight = minHeight ?? minImageHeight;
|
|
107
|
+
const syncResult = validateImageFileSync(file, syncOptions);
|
|
108
|
+
if (!syncResult.isValid || !resolvedMinWidth && !resolvedMinHeight) {
|
|
109
|
+
return syncResult;
|
|
110
|
+
}
|
|
111
|
+
const objectUrl = URL.createObjectURL(file);
|
|
112
|
+
try {
|
|
113
|
+
const {
|
|
114
|
+
width,
|
|
115
|
+
height
|
|
116
|
+
} = await getImageDimensions(objectUrl);
|
|
117
|
+
const errors = [...syncResult.errors];
|
|
118
|
+
if (resolvedMinWidth && width < resolvedMinWidth) {
|
|
119
|
+
errors.push(`El ancho mínimo es ${resolvedMinWidth}px (la imagen tiene ${width}px)`);
|
|
120
|
+
}
|
|
121
|
+
if (resolvedMinHeight && height < resolvedMinHeight) {
|
|
122
|
+
errors.push(`El alto mínimo es ${resolvedMinHeight}px (la imagen tiene ${height}px)`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
isValid: errors.length === 0,
|
|
126
|
+
errors,
|
|
127
|
+
width,
|
|
128
|
+
height
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
return {
|
|
132
|
+
isValid: false,
|
|
133
|
+
errors: [...syncResult.errors, 'No se pudieron leer las dimensiones de la imagen']
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
URL.revokeObjectURL(objectUrl);
|
|
137
|
+
}
|
|
57
138
|
};
|
|
58
139
|
|
|
59
140
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgonza02/goal-image-gallery",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Advanced, accessible, and highly customizable React image gallery component with multi-method upload, responsive design, and extensive TypeScript support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|