@visns-studio/visns-components 5.5.1 → 5.5.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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "@nivo/line": "^0.88.0",
10
10
  "@nivo/pie": "^0.88.0",
11
11
  "@tinymce/tinymce-react": "^6.1.0",
12
- "@vitejs/plugin-react": "^4.3.4",
12
+ "@vitejs/plugin-react": "^4.4.0",
13
13
  "add": "^2.0.6",
14
14
  "akar-icons": "^1.9.31",
15
15
  "array-move": "^4.0.0",
@@ -17,7 +17,7 @@
17
17
  "axios": "^1.8.4",
18
18
  "browser-image-compression": "^2.0.2",
19
19
  "dayjs": "^1.11.13",
20
- "fabric": "^6.6.2",
20
+ "fabric": "^6.6.4",
21
21
  "file-saver": "^2.0.5",
22
22
  "framer-motion": "^11.18.2",
23
23
  "html-react-parser": "^5.2.3",
@@ -54,12 +54,13 @@
54
54
  "reactjs-popup": "^2.0.6",
55
55
  "style-loader": "^4.0.0",
56
56
  "swapy": "^1.0.5",
57
- "sweetalert2": "^11.18.0",
57
+ "sweetalert2": "^11.19.1",
58
58
  "truncate": "^3.0.0",
59
59
  "uuid": "^10.0.0",
60
60
  "validator": "^13.15.0",
61
61
  "vite": "^5.4.18",
62
- "yarn": "^1.22.22"
62
+ "yarn": "^1.22.22",
63
+ "yet-another-react-lightbox": "^3.22.0"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@babel/core": "^7.26.10",
@@ -81,7 +82,7 @@
81
82
  "react-dom": "^17.0.0 || ^18.0.0"
82
83
  },
83
84
  "name": "@visns-studio/visns-components",
84
- "version": "5.5.1",
85
+ "version": "5.5.3",
85
86
  "description": "Various packages to assist in the development of our Custom Applications.",
86
87
  "main": "src/index.js",
87
88
  "files": [
@@ -16,6 +16,7 @@ import numeral from 'numeral';
16
16
  import Popup from 'reactjs-popup';
17
17
  import 'reactjs-popup/dist/index.css';
18
18
  import parse from 'html-react-parser';
19
+ import 'react-toggle/style.css';
19
20
  import { saveAs } from 'file-saver';
20
21
  import NumberFilter from '@inovua/reactdatagrid-community/NumberFilter';
21
22
  import SelectFilter from '@inovua/reactdatagrid-community/SelectFilter';
@@ -25,9 +26,12 @@ import ReactDataGrid from '@inovua/reactdatagrid-community';
25
26
  // import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
26
27
  // import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
27
28
  // import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
28
- import { Gallery } from 'react-grid-gallery';
29
+ // import { Gallery } from 'react-grid-gallery';
29
30
  import { confirmAlert } from 'react-confirm-alert';
30
31
  import { toast } from 'react-toastify';
32
+ import imageCompression from 'browser-image-compression';
33
+ import Vapor from 'laravel-vapor';
34
+ import Lightbox from 'yet-another-react-lightbox';
31
35
  import {
32
36
  Alarm,
33
37
  ArrowCounterClockwise,
@@ -59,6 +63,7 @@ import styles from './styles/DataGrid.module.scss';
59
63
 
60
64
  import '@inovua/reactdatagrid-community/index.css';
61
65
  import 'react-confirm-alert/src/react-confirm-alert.css';
66
+ import 'yet-another-react-lightbox/styles.css';
62
67
 
63
68
  import CustomFetch from './Fetch';
64
69
  import Download from './Download';
@@ -166,6 +171,13 @@ const DataGrid = forwardRef(
166
171
  /** Gallery Modal States */
167
172
  const [modalGalleryShow, setModalGalleryShow] = useState(false);
168
173
  const [galleryData, setGalleryData] = useState([]);
174
+ const [uploadProgress, setUploadProgress] = useState(0);
175
+ const [currentItem, setCurrentItem] = useState(null);
176
+ const [isUploading, setIsUploading] = useState(false);
177
+
178
+ /** Lightbox States */
179
+ const [lightboxOpen, setLightboxOpen] = useState(false);
180
+ const [lightboxIndex, setLightboxIndex] = useState(0);
169
181
 
170
182
  /** Gallery Modal Functions */
171
183
  const modalGalleryOpen = () => {
@@ -176,6 +188,149 @@ const DataGrid = forwardRef(
176
188
  setModalGalleryShow(false);
177
189
  };
178
190
 
191
+ const handleFileUpload = async (files) => {
192
+ if (!currentItem || !currentItem.settings.upload) return;
193
+
194
+ setIsUploading(true);
195
+ const uploadPromises = [];
196
+
197
+ for (let i = 0; i < files.length; i++) {
198
+ const file = files[i];
199
+
200
+ try {
201
+ // Optimize image if needed
202
+ let optimizedFile = file;
203
+ if (file.type.startsWith('image/')) {
204
+ const options = {
205
+ maxSizeMB: 1,
206
+ maxWidthOrHeight: 1920,
207
+ useWebWorker: true,
208
+ };
209
+ try {
210
+ optimizedFile = await imageCompression(
211
+ file,
212
+ options
213
+ );
214
+ } catch (err) {
215
+ console.error('Image compression failed:', err);
216
+ // Continue with original file if compression fails
217
+ }
218
+ }
219
+
220
+ // Upload to Vapor
221
+ const uploadPromise = new Promise((resolve, reject) => {
222
+ Vapor.store(optimizedFile, {
223
+ progress: (progress) => {
224
+ setUploadProgress(progress * 100);
225
+ },
226
+ })
227
+ .then((response) => {
228
+ // Construct the URL with the key parameter
229
+ const uploadSettings =
230
+ currentItem.settings.upload;
231
+ const itemData = currentItem.data;
232
+ const keyValue = itemData[uploadSettings.key];
233
+ const url = keyValue
234
+ ? `${uploadSettings.url}/${keyValue}`
235
+ : uploadSettings.url;
236
+
237
+ // Make the PUT request
238
+ CustomFetch(
239
+ url,
240
+ uploadSettings.method || 'POST',
241
+ {
242
+ uuid: response.uuid,
243
+ key: response.key,
244
+ bucket: response.bucket,
245
+ filename: file.name,
246
+ filesize: optimizedFile.size,
247
+ extension: response.extension,
248
+ file_relationship: uploadSettings.id,
249
+ },
250
+ (result) => {
251
+ if (result.error === '') {
252
+ resolve();
253
+ } else {
254
+ toast.error(String(result.error));
255
+ reject(result.error);
256
+ }
257
+ }
258
+ );
259
+ })
260
+ .catch(reject);
261
+ });
262
+
263
+ uploadPromises.push(uploadPromise);
264
+ } catch (error) {
265
+ console.error('Error uploading file:', error);
266
+ toast.error(
267
+ `Error uploading ${file.name}: ${
268
+ error.message || 'Unknown error'
269
+ }`
270
+ );
271
+ }
272
+ }
273
+
274
+ try {
275
+ await Promise.all(uploadPromises);
276
+ toast.success('Files uploaded successfully');
277
+
278
+ // Refresh the gallery data
279
+ if (currentItem && currentItem.settings && currentItem.data) {
280
+ const s = currentItem.settings;
281
+ const d = currentItem.data;
282
+
283
+ // Function to access nested property using a series of keys
284
+ const getNestedValue = (data, keys) =>
285
+ keys.reduce(
286
+ (currentValue, key) =>
287
+ currentValue && currentValue[key] !== undefined
288
+ ? currentValue[key]
289
+ : undefined,
290
+ data
291
+ );
292
+
293
+ // Fetch updated data
294
+ try {
295
+ const res = await CustomFetch(
296
+ `${form.url}/${d[form.primaryKey]}`,
297
+ 'GET',
298
+ {}
299
+ );
300
+
301
+ if (res.data) {
302
+ // Extract the updated nested value
303
+ const updatedNestedValue = getNestedValue(
304
+ res.data,
305
+ s.relation
306
+ );
307
+
308
+ // Update gallery data
309
+ if (
310
+ updatedNestedValue &&
311
+ Array.isArray(updatedNestedValue)
312
+ ) {
313
+ const updatedGalleryArray =
314
+ updatedNestedValue.map((item) => ({
315
+ src: item[s.key],
316
+ caption: item.file_name,
317
+ }));
318
+
319
+ setGalleryData(updatedGalleryArray);
320
+ }
321
+ }
322
+ } catch (error) {
323
+ console.error('Error refreshing gallery data:', error);
324
+ }
325
+ }
326
+ } catch (error) {
327
+ console.error('Error in upload process:', error);
328
+ } finally {
329
+ setIsUploading(false);
330
+ setUploadProgress(0);
331
+ }
332
+ };
333
+
179
334
  /** Modal States */
180
335
  const [formData, setFormData] = useState({});
181
336
  const [formType, setFormType] = useState('');
@@ -601,13 +756,16 @@ const DataGrid = forwardRef(
601
756
  // Extracting nested value using predefined keys from 's.relation'
602
757
  const nestedValue = getNestedValue(d, s.relation);
603
758
 
604
- // Mapping nested values to create gallery array with source and captions
759
+ // Mapping nested values to create gallery array with source, captions, and dimensions
605
760
  const galleryArray = nestedValue.map((item) => ({
606
761
  src: item[s.key],
607
762
  caption: item.file_name,
763
+ width: 320, // Default width for proper grid layout
764
+ height: 240, // Default height for proper grid layout
608
765
  }));
609
766
 
610
767
  setGalleryData(galleryArray);
768
+ setCurrentItem({ data: d, settings: s });
611
769
 
612
770
  modalGalleryOpen();
613
771
  break;
@@ -2461,11 +2619,9 @@ const DataGrid = forwardRef(
2461
2619
  if (shouldRenderButton) {
2462
2620
  return (
2463
2621
  <button
2464
- className={styles.btn}
2465
- style={{
2466
- padding:
2467
- '0.2em 1em 0.05em 1em',
2468
- }}
2622
+ className={
2623
+ styles.clockButton
2624
+ }
2469
2625
  onClick={(e) => {
2470
2626
  e.stopPropagation();
2471
2627
  e.preventDefault();
@@ -2497,9 +2653,9 @@ const DataGrid = forwardRef(
2497
2653
  data-tooltip-content={`${column.label} Timer`}
2498
2654
  strokeWidth={2}
2499
2655
  size={18}
2500
- className={
2501
- styles.tdaction
2502
- }
2656
+ style={{
2657
+ color: 'white',
2658
+ }}
2503
2659
  />
2504
2660
  </button>
2505
2661
  );
@@ -2983,6 +3139,8 @@ const DataGrid = forwardRef(
2983
3139
  open={modalGalleryShow}
2984
3140
  onClose={modalGalleryClose}
2985
3141
  closeOnDocumentClick={false}
3142
+ contentStyle={{ width: 'auto', maxWidth: '90vw' }}
3143
+ overlayStyle={{ background: 'rgba(0, 0, 0, 0.7)' }}
2986
3144
  >
2987
3145
  <div
2988
3146
  className={`${styles.modalwrap} ${styles['top--modal']}`}
@@ -2997,15 +3155,191 @@ const DataGrid = forwardRef(
2997
3155
  <CircleX strokeWidth={1} size={24} />
2998
3156
  </button>
2999
3157
  </div>
3000
- <div className={styles.modal__content}>
3158
+ <div
3159
+ className={`${styles.modal__content} ${styles['modal__content--gallery']}`}
3160
+ >
3161
+ {currentItem?.settings?.upload && (
3162
+ <div className={styles.uploadSection}>
3163
+ <h3>
3164
+ <Image
3165
+ strokeWidth={1.5}
3166
+ size={18}
3167
+ />
3168
+ Upload New Images
3169
+ </h3>
3170
+ <div
3171
+ className={styles.dropzoneContainer}
3172
+ >
3173
+ {isUploading && (
3174
+ <div
3175
+ className={
3176
+ styles.progressBar
3177
+ }
3178
+ >
3179
+ <div
3180
+ className={
3181
+ styles.progressBarInner
3182
+ }
3183
+ style={{
3184
+ width: `${uploadProgress}%`,
3185
+ }}
3186
+ ></div>
3187
+ </div>
3188
+ )}
3189
+ <div
3190
+ className={styles.dropzone}
3191
+ onClick={() =>
3192
+ document
3193
+ .getElementById(
3194
+ 'gallery-file-upload'
3195
+ )
3196
+ .click()
3197
+ }
3198
+ onDragOver={(e) => {
3199
+ e.preventDefault();
3200
+ e.stopPropagation();
3201
+ }}
3202
+ onDragEnter={(e) => {
3203
+ e.preventDefault();
3204
+ e.stopPropagation();
3205
+ }}
3206
+ onDragLeave={(e) => {
3207
+ e.preventDefault();
3208
+ e.stopPropagation();
3209
+ }}
3210
+ onDrop={(e) => {
3211
+ e.preventDefault();
3212
+ e.stopPropagation();
3213
+ const files =
3214
+ e.dataTransfer.files;
3215
+ if (files.length > 0) {
3216
+ handleFileUpload(files);
3217
+ }
3218
+ }}
3219
+ >
3220
+ <CloudUpload
3221
+ strokeWidth={1.5}
3222
+ size={24}
3223
+ />
3224
+ <p>
3225
+ Drag and drop files here or
3226
+ click to select files
3227
+ </p>
3228
+ <input
3229
+ id="gallery-file-upload"
3230
+ type="file"
3231
+ multiple
3232
+ style={{ display: 'none' }}
3233
+ onChange={(e) => {
3234
+ if (
3235
+ e.target.files
3236
+ .length > 0
3237
+ ) {
3238
+ handleFileUpload(
3239
+ e.target.files
3240
+ );
3241
+ e.target.value =
3242
+ null; // Reset input
3243
+ }
3244
+ }}
3245
+ accept="image/*"
3246
+ />
3247
+ </div>
3248
+ </div>
3249
+ </div>
3250
+ )}
3001
3251
  <div className={styles.formcontainer}>
3002
3252
  {galleryData.length > 0 ? (
3003
- <Gallery
3004
- images={galleryData}
3005
- enableImageSelection={false}
3006
- />
3253
+ <div
3254
+ className={styles.galleryContainer}
3255
+ >
3256
+ <div
3257
+ className={
3258
+ styles.galleryInstructions
3259
+ }
3260
+ >
3261
+ <Image
3262
+ strokeWidth={1.5}
3263
+ size={16}
3264
+ />
3265
+ <span>
3266
+ Tap any image to enlarge
3267
+ </span>
3268
+ </div>
3269
+ <div
3270
+ className={
3271
+ styles.customGalleryGrid
3272
+ }
3273
+ >
3274
+ {galleryData.map(
3275
+ (img, index) => (
3276
+ <div
3277
+ key={`gallery-item-${index}`}
3278
+ className={
3279
+ styles.galleryItem
3280
+ }
3281
+ onClick={() => {
3282
+ setLightboxIndex(
3283
+ index
3284
+ );
3285
+ setLightboxOpen(
3286
+ true
3287
+ );
3288
+ }}
3289
+ >
3290
+ <img
3291
+ src={img.src}
3292
+ alt={
3293
+ img.caption ||
3294
+ `Image ${
3295
+ index +
3296
+ 1
3297
+ }`
3298
+ }
3299
+ />
3300
+ {img.caption && (
3301
+ <div
3302
+ className={
3303
+ styles.imageCaption
3304
+ }
3305
+ >
3306
+ {
3307
+ img.caption
3308
+ }
3309
+ </div>
3310
+ )}
3311
+ <div
3312
+ className={
3313
+ styles.galleryImageOverlay
3314
+ }
3315
+ >
3316
+ <span>
3317
+ <Image
3318
+ strokeWidth={
3319
+ 1.5
3320
+ }
3321
+ size={
3322
+ 16
3323
+ }
3324
+ style={{
3325
+ marginRight:
3326
+ '6px',
3327
+ }}
3328
+ />
3329
+ Enlarge
3330
+ </span>
3331
+ </div>
3332
+ </div>
3333
+ )
3334
+ )}
3335
+ </div>
3336
+ </div>
3007
3337
  ) : (
3008
3338
  <div className={styles.noDataMessage}>
3339
+ <Image
3340
+ strokeWidth={1.5}
3341
+ size={20}
3342
+ />
3009
3343
  No images available in the gallery
3010
3344
  </div>
3011
3345
  )}
@@ -3014,6 +3348,14 @@ const DataGrid = forwardRef(
3014
3348
  </div>
3015
3349
  </div>
3016
3350
  </Popup>
3351
+
3352
+ {/* Lightbox for full-size image viewing */}
3353
+ <Lightbox
3354
+ open={lightboxOpen}
3355
+ close={() => setLightboxOpen(false)}
3356
+ index={lightboxIndex}
3357
+ slides={galleryData.map((img) => ({ src: img.src }))}
3358
+ />
3017
3359
  </>
3018
3360
  );
3019
3361
  }
@@ -101,7 +101,7 @@ function MultiSelect({
101
101
 
102
102
  return (
103
103
  <SelectComponent
104
- closeMenuOnSelect={!multi}
104
+ closeMenuOnSelect={!multi || settings.limit === 1}
105
105
  isClearable
106
106
  isSearchable
107
107
  isMulti={multi}
@@ -109,7 +109,19 @@ function MultiSelect({
109
109
  components={{ SelectList }}
110
110
  options={selectOptions}
111
111
  onChange={(inputValue, action) => {
112
- onChange(inputValue, action, settings.id);
112
+ // Special handling for limit=1: keep only the most recent selection
113
+ if (
114
+ settings.limit === 1 &&
115
+ multi &&
116
+ Array.isArray(inputValue) &&
117
+ inputValue.length > 1
118
+ ) {
119
+ const latestSelection = inputValue[inputValue.length - 1];
120
+ const newValue = latestSelection ? [latestSelection] : [];
121
+ onChange(newValue, action, settings.id);
122
+ } else {
123
+ onChange(inputValue, action, settings.id);
124
+ }
113
125
  }}
114
126
  className={
115
127
  className
@@ -130,6 +142,11 @@ function MultiSelect({
130
142
  value={selectValue}
131
143
  onCreateOption={handleCreate}
132
144
  placeholder={placeholder ? placeholder : 'Select...'}
145
+ isOptionDisabled={
146
+ settings.limit && multi && settings.limit > 1
147
+ ? () => selectValue.length >= settings.limit
148
+ : undefined
149
+ }
133
150
  />
134
151
  );
135
152
  }