@visns-studio/visns-components 5.12.5 → 5.12.6

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
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.12.5",
90
+ "version": "5.12.6",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -54,10 +54,16 @@ import {
54
54
  Eye as EyeOpen,
55
55
  EyeOff as EyeSlashed,
56
56
  File,
57
+ FileText,
58
+ FileSpreadsheet,
59
+ FileImage,
60
+ FileCode,
61
+ FileArchive,
57
62
  Edit as Pencil,
58
63
  Trash2 as TrashCan,
64
+ X as CircleX,
59
65
  } from 'lucide-react';
60
- import { confirmDialog } from '../../utils/ConfirmDialog';
66
+ import { confirmDialog } from '../utils/ConfirmDialog';
61
67
 
62
68
  import 'react-big-calendar/lib/css/react-big-calendar.css';
63
69
  import 'react-big-calendar/lib/addons/dragAndDrop/styles.css';
@@ -79,6 +85,9 @@ import TableFilter from '../TableFilter';
79
85
  import ProposalTemplateSectionManager from '../proposal/ProposalTemplateSectionManager';
80
86
  import ProposalTemplatePreview from '../proposal/ProposalTemplatePreview';
81
87
 
88
+ // Gallery Modal
89
+ import GalleryModal from '../modals/GalleryModal';
90
+
82
91
  import styles from '../styles/GenericDetail.module.scss';
83
92
 
84
93
  const localizer = momentLocalizer(moment);
@@ -127,6 +136,88 @@ const optimizeImageIfNeeded = async (file) => {
127
136
  return file;
128
137
  };
129
138
 
139
+ // Helper function to get accept types based on filetype configuration
140
+ const getAcceptTypes = (filetype) => {
141
+ switch (filetype) {
142
+ case 'document':
143
+ return {
144
+ 'application/pdf': ['.pdf'],
145
+ 'application/msword': ['.doc'],
146
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
147
+ 'application/vnd.ms-excel': ['.xls'],
148
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
149
+ 'application/vnd.ms-powerpoint': ['.ppt'],
150
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
151
+ 'text/plain': ['.txt'],
152
+ 'text/csv': ['.csv'],
153
+ 'application/zip': ['.zip'],
154
+ 'application/x-rar-compressed': ['.rar'],
155
+ 'application/x-7z-compressed': ['.7z']
156
+ };
157
+ case 'image':
158
+ return {
159
+ 'image/jpeg': ['.jpg', '.jpeg'],
160
+ 'image/png': ['.png'],
161
+ 'image/gif': ['.gif'],
162
+ 'image/webp': ['.webp'],
163
+ 'image/svg+xml': ['.svg'],
164
+ 'image/bmp': ['.bmp'],
165
+ 'image/tiff': ['.tiff', '.tif']
166
+ };
167
+ default:
168
+ return undefined; // Accept all file types
169
+ }
170
+ };
171
+
172
+ // Helper function to get the appropriate icon based on file extension
173
+ const getFileIcon = (filename) => {
174
+ const extension = filename.split('.').pop().toLowerCase();
175
+
176
+ switch (extension) {
177
+ case 'pdf':
178
+ return <FileText strokeWidth={2} size={20} color="#dc2626" />;
179
+ case 'doc':
180
+ case 'docx':
181
+ return <FileText strokeWidth={2} size={20} color="#2563eb" />;
182
+ case 'xls':
183
+ case 'xlsx':
184
+ return <FileSpreadsheet strokeWidth={2} size={20} color="#16a34a" />;
185
+ case 'ppt':
186
+ case 'pptx':
187
+ return <FileText strokeWidth={2} size={20} color="#ea580c" />;
188
+ case 'txt':
189
+ case 'csv':
190
+ return <FileCode strokeWidth={2} size={20} color="#6b7280" />;
191
+ case 'zip':
192
+ case 'rar':
193
+ case '7z':
194
+ return <FileArchive strokeWidth={2} size={20} color="#7c3aed" />;
195
+ case 'jpg':
196
+ case 'jpeg':
197
+ case 'png':
198
+ case 'gif':
199
+ case 'webp':
200
+ case 'svg':
201
+ case 'bmp':
202
+ case 'tiff':
203
+ case 'tif':
204
+ return <FileImage strokeWidth={2} size={20} color="#059669" />;
205
+ default:
206
+ return <File strokeWidth={2} size={20} />;
207
+ }
208
+ };
209
+
210
+ // Helper function to format file size
211
+ const formatFileSize = (bytes) => {
212
+ if (!bytes || bytes === 0) return '';
213
+
214
+ const k = 1024;
215
+ const sizes = ['B', 'KB', 'MB', 'GB'];
216
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
217
+
218
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
219
+ };
220
+
130
221
  function GenericDetail({
131
222
  extraUrlParam,
132
223
  layout = 'full',
@@ -158,6 +249,19 @@ function GenericDetail({
158
249
  const [total, setTotal] = useState(0);
159
250
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
160
251
 
252
+ /** Gallery Modal States */
253
+ const [modalGalleryShow, setModalGalleryShow] = useState(false);
254
+ const [galleryData, setGalleryData] = useState([]);
255
+ const [currentItem, setCurrentItem] = useState(null);
256
+ const [lightboxOpen, setLightboxOpen] = useState(false);
257
+ const [lightboxIndex, setLightboxIndex] = useState(0);
258
+
259
+ // Gallery modal close function
260
+ const modalGalleryClose = () => {
261
+ setModalGalleryShow(false);
262
+ setLightboxOpen(false);
263
+ };
264
+
161
265
  /** Stage Popup States */
162
266
  const [showStagePopup, setShowStagePopup] = useState(false);
163
267
  const [stagePopupData, setStagePopupData] = useState(null);
@@ -1445,7 +1549,7 @@ function GenericDetail({
1445
1549
  message:
1446
1550
  'Are you sure you want to delete ' +
1447
1551
  name,
1448
- data: { name: name }, // Add context data
1552
+ data: { name: name },
1449
1553
  buttons: [
1450
1554
  {
1451
1555
  label: 'Yes',
@@ -1473,7 +1577,9 @@ function GenericDetail({
1473
1577
  },
1474
1578
  {
1475
1579
  label: 'No',
1476
- onClick: () => close(),
1580
+ onClick: () => {
1581
+ // Do nothing on cancel
1582
+ },
1477
1583
  },
1478
1584
  ],
1479
1585
  });
@@ -1501,6 +1607,7 @@ function GenericDetail({
1501
1607
  />
1502
1608
  </div>
1503
1609
  <Dropzone
1610
+ accept={getAcceptTypes(activeTabConfig.filetype)}
1504
1611
  onDrop={(acceptedFiles) => {
1505
1612
  const uploadFile = async (file) => {
1506
1613
  // Optimize image if it's an image file
@@ -1695,20 +1802,9 @@ function GenericDetail({
1695
1802
  styles.sortlist
1696
1803
  }
1697
1804
  >
1698
- {data[
1699
- activeTabConfig
1700
- .key
1701
- ] &&
1702
- Array.isArray(
1703
- data[
1704
- activeTabConfig
1705
- .key
1706
- ]
1707
- ) &&
1708
- data[
1709
- activeTabConfig
1710
- .key
1711
- ].map(
1805
+ {files &&
1806
+ Array.isArray(files) &&
1807
+ files.map(
1712
1808
  (a, b) => {
1713
1809
  const imageSource =
1714
1810
  a.base64_encoded_image ||
@@ -1732,6 +1828,33 @@ function GenericDetail({
1732
1828
  alt={
1733
1829
  a.file_name
1734
1830
  }
1831
+ onClick={(e) => {
1832
+ e.stopPropagation();
1833
+ e.preventDefault();
1834
+
1835
+ // Prepare gallery data in the format expected by Lightbox
1836
+ const galleryArray = files
1837
+ .filter(file => {
1838
+ const imageSource = file.base64_encoded_image || file.s3_url || file.file_url;
1839
+ return imageSource;
1840
+ })
1841
+ .map((file) => ({
1842
+ src: file.base64_encoded_image || file.s3_url || file.file_url,
1843
+ caption: file.file_name,
1844
+ width: 320,
1845
+ height: 240,
1846
+ }));
1847
+
1848
+ // Set gallery data for lightbox
1849
+ setGalleryData(galleryArray);
1850
+
1851
+ // Set initial lightbox index
1852
+ setLightboxIndex(b);
1853
+
1854
+ // Open lightbox directly
1855
+ setLightboxOpen(true);
1856
+ }}
1857
+ style={{ cursor: 'pointer' }}
1735
1858
  />
1736
1859
  <TrashCan
1737
1860
  onClick={(
@@ -1788,18 +1911,16 @@ function GenericDetail({
1788
1911
  );
1789
1912
  }}
1790
1913
  >
1791
- <File
1792
- strokeWidth={
1793
- 2
1794
- }
1795
- size={
1796
- 20
1797
- }
1798
- />
1914
+ {getFileIcon(a.file_name)}
1799
1915
  <div className="filename">
1800
- {
1801
- a.file_name
1802
- }
1916
+ <div className="file-name">
1917
+ {a.file_name}
1918
+ </div>
1919
+ {a.file_size && (
1920
+ <div className="file-size">
1921
+ {formatFileSize(a.file_size)}
1922
+ </div>
1923
+ )}
1803
1924
  </div>
1804
1925
  </div>
1805
1926
  <button
@@ -2390,6 +2511,7 @@ function GenericDetail({
2390
2511
  }
2391
2512
  >
2392
2513
  <Dropzone
2514
+ accept={getAcceptTypes(activeTabConfig.filetype)}
2393
2515
  onDrop={(
2394
2516
  acceptedFiles
2395
2517
  ) => {
@@ -2718,6 +2840,17 @@ function GenericDetail({
2718
2840
  }
2719
2841
  }, [subnav, tabs]);
2720
2842
 
2843
+ // Update files when active tab changes for dropzone tabs
2844
+ useEffect(() => {
2845
+ if (activeTabConfig && activeTabConfig.type === 'dropzone' && data) {
2846
+ if (activeTabConfig.file_relationship && data[activeTabConfig.file_relationship]) {
2847
+ setFiles(data[activeTabConfig.file_relationship]);
2848
+ } else if (data.files) {
2849
+ setFiles(data.files);
2850
+ }
2851
+ }
2852
+ }, [activeTabConfig, data]);
2853
+
2721
2854
  useEffect(() => {
2722
2855
  const fetchDropdownOptions = async () => {
2723
2856
  if (activeTabConfig?.columns) {
@@ -3167,6 +3300,20 @@ function GenericDetail({
3167
3300
  entityData={data}
3168
3301
  routeParams={routeParams}
3169
3302
  />
3303
+
3304
+ {/* Gallery Modal for image viewing */}
3305
+ <GalleryModal
3306
+ modalGalleryShow={modalGalleryShow}
3307
+ modalGalleryClose={modalGalleryClose}
3308
+ galleryData={galleryData}
3309
+ setGalleryData={setGalleryData}
3310
+ currentItem={currentItem}
3311
+ form={{}}
3312
+ lightboxOpen={lightboxOpen}
3313
+ setLightboxOpen={setLightboxOpen}
3314
+ lightboxIndex={lightboxIndex}
3315
+ setLightboxIndex={setLightboxIndex}
3316
+ />
3170
3317
  </>
3171
3318
  );
3172
3319
  }
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
4
4
  import imageCompression from 'browser-image-compression';
5
5
  import Vapor from 'laravel-vapor';
6
6
  import Lightbox from 'yet-another-react-lightbox';
7
+ import 'yet-another-react-lightbox/styles.css';
7
8
  import { X, Image as ImageIcon, CloudUpload } from 'lucide-react';
8
9
  import styles from '../styles/DataGrid.module.scss';
9
10
  import CustomFetch from '../Fetch';
@@ -396,7 +397,34 @@ const GalleryModal = ({
396
397
  open={lightboxOpen}
397
398
  close={() => setLightboxOpen(false)}
398
399
  index={lightboxIndex}
399
- slides={galleryData.map((img) => ({ src: img.src }))}
400
+ slides={galleryData.map((img, index) => ({
401
+ src: img.src,
402
+ alt: img.caption || `Image ${index + 1}`,
403
+ title: img.caption
404
+ }))}
405
+ styles={{
406
+ container: {
407
+ backgroundColor: "rgba(0, 0, 0, 0.9)",
408
+ zIndex: 999999
409
+ }
410
+ }}
411
+ controller={{
412
+ closeOnBackdropClick: true,
413
+ closeOnPullUp: true,
414
+ closeOnPullDown: true
415
+ }}
416
+ carousel={{
417
+ finite: true,
418
+ preload: 1,
419
+ padding: "16px",
420
+ spacing: "30%",
421
+ imageFit: "contain"
422
+ }}
423
+ animation={{
424
+ fade: 300,
425
+ swipe: 400
426
+ }}
427
+ portal={{ root: document.body }}
400
428
  />
401
429
  </>
402
430
  );
@@ -333,9 +333,25 @@
333
333
  }
334
334
 
335
335
  .filename {
336
- white-space: nowrap;
336
+ display: flex;
337
+ flex-direction: column;
337
338
  overflow: hidden;
338
- text-overflow: ellipsis;
339
+ flex: 1;
340
+
341
+ .file-name {
342
+ white-space: nowrap;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ font-weight: 500;
346
+ line-height: 1.2;
347
+ }
348
+
349
+ .file-size {
350
+ font-size: 0.75rem;
351
+ color: rgba(var(--primary-rgb), 0.7);
352
+ margin-top: 2px;
353
+ font-weight: 400;
354
+ }
339
355
  }
340
356
  }
341
357
 
@@ -364,26 +380,25 @@
364
380
  }
365
381
 
366
382
  .sortlist {
367
- display: grid;
368
- grid-template-columns: repeat(4, 1fr); /* 4 columns by default */
369
- gap: 16px;
383
+ display: grid !important;
384
+ grid-template-columns: repeat(4, 1fr) !important; /* 4 columns by default */
385
+ gap: 16px !important;
370
386
  padding: 16px 0;
387
+ margin: 0;
371
388
  list-style: none;
389
+ box-sizing: border-box;
372
390
 
373
391
  /* Responsive grid layout */
374
392
  @media (max-width: 1200px) {
375
- grid-template-columns: repeat(3, 1fr); /* 3 columns on medium screens */
393
+ grid-template-columns: repeat(3, 1fr) !important; /* 3 columns on medium screens */
376
394
  }
377
395
 
378
396
  @media (max-width: 768px) {
379
- grid-template-columns: repeat(2, 1fr); /* 2 columns on small screens */
397
+ grid-template-columns: repeat(2, 1fr) !important; /* 2 columns on small screens */
380
398
  }
381
399
 
382
400
  @media (max-width: 480px) {
383
- grid-template-columns: repeat(
384
- 1,
385
- 1fr
386
- ); /* 1 column on very small screens */
401
+ grid-template-columns: repeat(1, 1fr) !important; /* 1 column on very small screens */
387
402
  }
388
403
 
389
404
  li {
@@ -413,6 +428,12 @@
413
428
  width: 100%;
414
429
  height: 100%;
415
430
  object-fit: cover;
431
+ cursor: pointer;
432
+ transition: opacity 0.2s ease;
433
+
434
+ &:hover {
435
+ opacity: 0.9;
436
+ }
416
437
  }
417
438
 
418
439
  .delete {
@@ -819,3 +840,4 @@
819
840
  overflow-x: auto;
820
841
  white-space: pre-wrap;
821
842
  }
843
+