@visns-studio/visns-components 5.14.7 → 5.14.9

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/README.md CHANGED
@@ -50,6 +50,7 @@ VISNS Components is a React-based UI component library that provides a set of re
50
50
  - **Download** - File download functionality
51
51
  - **Loader** - Loading indicators
52
52
  - **Notification** - Toast notifications
53
+ - **StandardModal** - Reusable modal component with consistent styling
53
54
  - **Call Popup** - Modal dialogs
54
55
  - **QR Code** - QR code generation and printing
55
56
 
@@ -2312,6 +2313,64 @@ The QrCode component fetches QR code data from the specified URL and displays it
2312
2313
  }
2313
2314
  ```
2314
2315
 
2316
+ #### StandardModal
2317
+
2318
+ A reusable modal component that standardizes the look and feel across all components. Uses the same styling as GenericIndex, GenericDetail, and DataGrid modals, providing consistent modal behavior throughout the application.
2319
+
2320
+ ```jsx
2321
+ import { StandardModal } from '@visns-studio/visns-components';
2322
+
2323
+ // Basic usage (for custom content with header)
2324
+ <StandardModal
2325
+ isOpen={showModal}
2326
+ onClose={() => setShowModal(false)}
2327
+ title="Modal Title"
2328
+ showHeader={true}
2329
+ >
2330
+ <div>Your custom content here</div>
2331
+ </StandardModal>
2332
+
2333
+ // Usage with Form components (no header - Form provides its own)
2334
+ <StandardModal
2335
+ isOpen={modalShow}
2336
+ onClose={modalClose}
2337
+ closeOnDocumentClick={false}
2338
+ verticalAlign="top"
2339
+ >
2340
+ <Form
2341
+ closeModal={modalClose}
2342
+ formSettings={formData}
2343
+ // ... other Form props
2344
+ />
2345
+ </StandardModal>
2346
+ ```
2347
+
2348
+ **Props:**
2349
+
2350
+ - `isOpen` (boolean): Whether the modal is open
2351
+ - `onClose` (function): Function to call when modal should close
2352
+ - `title` (string): Modal title (only used if showHeader is true)
2353
+ - `children` (ReactNode): Modal content
2354
+ - `size` (string): Modal size - 'small' (400px), 'medium' (90vw, default), 'large' (95vw)
2355
+ - `closeOnDocumentClick` (boolean): Whether to close on outside click (default: true)
2356
+ - `closeOnEscape` (boolean): Whether to close on escape key (default: true)
2357
+ - `verticalAlign` (string): Vertical alignment - 'center' (default) or 'top'
2358
+ - `showHeader` (boolean): Whether to show modal header (default: false)
2359
+ - `customStyles` (object): Custom styles to override defaults
2360
+
2361
+ **Size Options:**
2362
+
2363
+ - **Small**: 400px width, ideal for simple dialogs and confirmations
2364
+ - **Medium** (default): 90vw width with 600px min and 1400px max, matches original modal sizing
2365
+ - **Large**: 95vw width with 800px min and 1600px max, for very large content
2366
+
2367
+ **Important Notes:**
2368
+
2369
+ - When wrapping Form components, use `showHeader={false}` (default) since Form components render their own headers
2370
+ - When creating custom modals with simple content, use `showHeader={true}` to display the title and close button
2371
+ - The component automatically handles responsive sizing and vertical alignment
2372
+ - Uses the same CSS classes as existing modals (modalwrap, modal, modal__header, modal__content)
2373
+
2315
2374
  ### Enhanced OCR Template Component
2316
2375
 
2317
2376
  #### OcrTemplateEnhanced (v5.14.0+)
package/package.json CHANGED
@@ -88,7 +88,7 @@
88
88
  "react-dom": "^17.0.0 || ^18.0.0"
89
89
  },
90
90
  "name": "@visns-studio/visns-components",
91
- "version": "5.14.7",
91
+ "version": "5.14.9",
92
92
  "description": "Various packages to assist in the development of our Custom Applications.",
93
93
  "main": "src/index.js",
94
94
  "files": [
@@ -30,7 +30,6 @@ import React, {
30
30
  import { useNavigate } from 'react-router-dom';
31
31
  import debounce from 'lodash.debounce';
32
32
  import moment from 'moment';
33
- import Popup from 'reactjs-popup';
34
33
  import 'reactjs-popup/dist/index.css';
35
34
  import parse from 'html-react-parser';
36
35
  import { saveAs } from 'file-saver';
@@ -80,6 +79,7 @@ import CustomFetch from './Fetch';
80
79
  import Download from './Download';
81
80
  import Form from './Form';
82
81
  import CellWithTooltip from './cells/CellWithTooltip';
82
+ import StandardModal from './generic/StandardModal';
83
83
  import DataGridSearch from './controls/DataGridSearch';
84
84
  import AutoRefreshControls from './controls/AutoRefreshControls';
85
85
  import AudioPlayer from './controls/AudioPlayer';
@@ -3714,55 +3714,32 @@ const DataGrid = forwardRef(
3714
3714
  )}
3715
3715
 
3716
3716
  <AudioPlayer audioSource={audioSource} />
3717
- <Popup
3718
- open={modalShow}
3717
+ <StandardModal
3718
+ isOpen={modalShow}
3719
3719
  onClose={modalClose}
3720
+ title={formData.title || 'Edit Data'}
3720
3721
  closeOnDocumentClick={false}
3721
- contentStyle={{
3722
- width: windowWidth < 768 ? '95vw' : '80vw',
3723
- minWidth: windowWidth < 768 ? '95vw' : '600px',
3724
- maxWidth: '1200px',
3725
- ...(formData.verticalAlign === 'top' && {
3726
- position: 'fixed',
3727
- top: '5vh',
3728
- left: '50%',
3729
- transform: 'translateX(-50%)',
3730
- margin: '0',
3731
- }),
3732
- }}
3733
- nested
3722
+ verticalAlign={formData.verticalAlign || 'center'}
3734
3723
  >
3735
- <div
3736
- className={`${styles.modalwrap} ${
3737
- styles['top--modal']
3738
- } ${styles.modalWide} ${
3739
- formData.verticalAlign === 'top'
3740
- ? 'modal-top-aligned'
3741
- : ''
3742
- }`}
3743
- >
3744
- <div className={styles.modal}>
3745
- <Form
3746
- ajaxSetting={ajaxSetting}
3747
- api={api}
3748
- closeModal={modalClose}
3749
- columnId={formId}
3750
- fetchTable={handleReload}
3751
- formSettings={formData}
3752
- formType={formType}
3753
- gridRef={gridRef}
3754
- mapbox={mapbox}
3755
- modalOpen={modalOpen}
3756
- paramValue={paramValue}
3757
- style={style}
3758
- updateForm={setFormData}
3759
- userProfile={userProfile}
3760
- bulkEditMode={bulkEditMode}
3761
- bulkEditGroupData={bulkEditGroupData}
3762
- />
3763
- </div>
3764
- </div>
3765
- </Popup>
3724
+ <Form
3725
+ ajaxSetting={ajaxSetting}
3726
+ api={api}
3727
+ closeModal={modalClose}
3728
+ columnId={formId}
3729
+ fetchTable={handleReload}
3730
+ formSettings={formData}
3731
+ formType={formType}
3732
+ gridRef={gridRef}
3733
+ mapbox={mapbox}
3734
+ modalOpen={modalOpen}
3735
+ paramValue={paramValue}
3736
+ style={style}
3737
+ updateForm={setFormData}
3738
+ userProfile={userProfile}
3739
+ bulkEditMode={bulkEditMode}
3740
+ bulkEditGroupData={bulkEditGroupData}
3741
+ />
3742
+ </StandardModal>
3766
3743
  <GalleryModal
3767
3744
  modalGalleryShow={modalGalleryShow}
3768
3745
  modalGalleryClose={modalGalleryClose}
@@ -4,15 +4,15 @@ import { Link, useNavigate } from 'react-router-dom';
4
4
  import { ResponsiveBar } from '@nivo/bar';
5
5
  import { ResponsiveLine } from '@nivo/line';
6
6
  import { ResponsivePie } from '@nivo/pie';
7
- import Popup from 'reactjs-popup';
8
7
  import dayjs from 'dayjs';
9
8
  import parse from 'html-react-parser';
10
9
  import { createSwapy } from 'swapy';
11
- import { X, Minus, Plus, Star, Trash2, Eye, RotateCcw, Clock, Database, BarChart3, AlertCircle } from 'lucide-react';
10
+ import { Minus, Plus, Star, Trash2, Eye, RotateCcw, Clock, Database, BarChart3, AlertCircle } from 'lucide-react';
12
11
  import { confirmDialog } from '../utils/ConfirmDialog';
13
12
 
14
13
  import CustomFetch from '../Fetch';
15
14
  import MultiSelect from '../MultiSelect';
15
+ import StandardModal from './StandardModal';
16
16
 
17
17
  import styles from '../styles/GenericDashboard.module.scss';
18
18
 
@@ -1961,63 +1961,49 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1961
1961
  )}
1962
1962
  <div className={styles.grid} ref={container}>
1963
1963
  {renderWidgets()}
1964
- <Popup
1965
- open={showAddWidgetModal}
1964
+ <StandardModal
1965
+ isOpen={showAddWidgetModal}
1966
1966
  onClose={() => setShowAddWidgetModal(false)}
1967
- modal
1967
+ title="Add a Widget"
1968
+ showHeader={true}
1968
1969
  >
1969
- <div className="modalwrap">
1970
- <div className={`${styles.addWidgetPopup} modal`}>
1971
- <div className="modal__header">
1972
- <h1>Add a Widget</h1>
1973
- <button
1974
- className="modal__close"
1975
- onClick={() => setShowAddWidgetModal(false)}
1976
- >
1977
- <X strokeWidth={1} size={24} />
1978
- </button>
1979
- </div>
1980
- <div className="modal__content">
1981
- {availableWidgets.length > 0 ? (
1982
- <div>
1983
- <select
1984
- value={selectedAddWidgetId}
1985
- onChange={(e) =>
1986
- setSelectedAddWidgetId(
1987
- e.target.value
1988
- )
1989
- }
1990
- className={styles.addWidgetSelect}
1991
- >
1992
- <option value="">
1993
- -- Select a widget --
1994
- </option>
1995
- {availableWidgets.map((widget) => (
1996
- <option
1997
- key={widget.id}
1998
- value={widget.id}
1999
- >
2000
- {widget.title}
2001
- </option>
2002
- ))}
2003
- </select>
2004
- <button
2005
- className={styles.addWidgetButton}
2006
- onClick={handleAddWidget}
2007
- disabled={!selectedAddWidgetId}
2008
- >
2009
- Add
2010
- </button>
2011
- </div>
2012
- ) : (
2013
- <div className={styles.noWidgetsMessage}>
2014
- No more widgets available
2015
- </div>
2016
- )}
2017
- </div>
1970
+ {availableWidgets.length > 0 ? (
1971
+ <div>
1972
+ <select
1973
+ value={selectedAddWidgetId}
1974
+ onChange={(e) =>
1975
+ setSelectedAddWidgetId(
1976
+ e.target.value
1977
+ )
1978
+ }
1979
+ className={styles.addWidgetSelect}
1980
+ >
1981
+ <option value="">
1982
+ -- Select a widget --
1983
+ </option>
1984
+ {availableWidgets.map((widget) => (
1985
+ <option
1986
+ key={widget.id}
1987
+ value={widget.id}
1988
+ >
1989
+ {widget.title}
1990
+ </option>
1991
+ ))}
1992
+ </select>
1993
+ <button
1994
+ className={styles.addWidgetButton}
1995
+ onClick={handleAddWidget}
1996
+ disabled={!selectedAddWidgetId}
1997
+ >
1998
+ ADD
1999
+ </button>
2018
2000
  </div>
2019
- </div>
2020
- </Popup>
2001
+ ) : (
2002
+ <div className={styles.noWidgetsMessage}>
2003
+ No more widgets available
2004
+ </div>
2005
+ )}
2006
+ </StandardModal>
2021
2007
  </div>
2022
2008
  </>
2023
2009
  );
@@ -39,7 +39,6 @@ import { Calendar, momentLocalizer } from 'react-big-calendar';
39
39
  import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop';
40
40
  import moment from 'moment';
41
41
  import parse from 'html-react-parser';
42
- import Popup from 'reactjs-popup';
43
42
  import { motion } from 'framer-motion';
44
43
  import Dropzone from 'react-dropzone';
45
44
  import truncate from 'truncate';
@@ -79,6 +78,7 @@ import MergeEntity from '../MergeEntity';
79
78
  import MultiSelect from '../MultiSelect';
80
79
  import QrCode from '../QrCode';
81
80
  import StagePopupModal from './StagePopupModal';
81
+ import StandardModal from './StandardModal';
82
82
  import Table from '../DataGrid';
83
83
  import TableFilter from '../TableFilter';
84
84
  import CategorizedDropZone from '../CategorizedDropZone';
@@ -242,7 +242,7 @@ function GenericDetail({
242
242
 
243
243
  /** Fileupload states */
244
244
  const [files, setFiles] = useState([]);
245
-
245
+
246
246
  // Debug logging for files state changes
247
247
  useEffect(() => {
248
248
  console.log('GenericDetail: Files state changed:', {
@@ -250,7 +250,7 @@ function GenericDetail({
250
250
  filesCount: files ? files.length : 0,
251
251
  filesType: typeof files,
252
252
  isArray: Array.isArray(files),
253
- firstFile: files && files.length > 0 ? files[0] : null
253
+ firstFile: files && files.length > 0 ? files[0] : null,
254
254
  });
255
255
  }, [files]);
256
256
  const [loadingProgress, setLoadingProgress] = useState(0);
@@ -275,6 +275,16 @@ function GenericDetail({
275
275
  const [lightboxOpen, setLightboxOpen] = useState(false);
276
276
  const [lightboxIndex, setLightboxIndex] = useState(0);
277
277
 
278
+ /** Window Width State for Responsive Modal */
279
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
280
+
281
+ // Handle window resize for responsive modal
282
+ useEffect(() => {
283
+ const handleResize = () => setWindowWidth(window.innerWidth);
284
+ window.addEventListener('resize', handleResize);
285
+ return () => window.removeEventListener('resize', handleResize);
286
+ }, []);
287
+
278
288
  // Gallery modal close function
279
289
  const modalGalleryClose = () => {
280
290
  setModalGalleryShow(false);
@@ -1437,7 +1447,7 @@ function GenericDetail({
1437
1447
  return renderLink();
1438
1448
  case 'ocrTemplate':
1439
1449
  return (
1440
- <OcrTemplateEnhanced
1450
+ <OcrTemplateEnhanced
1441
1451
  id={id}
1442
1452
  data={data}
1443
1453
  setData={setData}
@@ -1754,14 +1764,20 @@ function GenericDetail({
1754
1764
  // Helper function to get nested object value using dot notation
1755
1765
  const getNestedValue = (obj, path) => {
1756
1766
  if (!obj || !path) return null;
1757
- return path.split('.').reduce((current, key) => {
1758
- return current && current[key] !== undefined ? current[key] : null;
1759
- }, obj);
1767
+ return path
1768
+ .split('.')
1769
+ .reduce((current, key) => {
1770
+ return current &&
1771
+ current[key] !== undefined
1772
+ ? current[key]
1773
+ : null;
1774
+ }, obj);
1760
1775
  };
1761
1776
 
1762
1777
  // Extract files using nested key support
1763
- const files = activeTabConfig.key
1764
- ? getNestedValue(data, activeTabConfig.key) || []
1778
+ const files = activeTabConfig.key
1779
+ ? getNestedValue(data, activeTabConfig.key) ||
1780
+ []
1765
1781
  : [];
1766
1782
 
1767
1783
  const downloadFile = (file) => {
@@ -1874,7 +1890,9 @@ function GenericDetail({
1874
1890
 
1875
1891
  // Create a fetchData function that refetches the main data
1876
1892
  const handleFetchData = () => {
1877
- console.log('GenericDetail: DropZone fetchData called, refetching main data...');
1893
+ console.log(
1894
+ 'GenericDetail: DropZone fetchData called, refetching main data...'
1895
+ );
1878
1896
  setDataReload(!dataReload);
1879
1897
  };
1880
1898
 
@@ -1889,20 +1907,38 @@ function GenericDetail({
1889
1907
  fetchData={handleFetchData}
1890
1908
  files={files}
1891
1909
  settings={{
1892
- url: replaceDropzoneUrlParams(activeTabConfig.url),
1910
+ url: replaceDropzoneUrlParams(
1911
+ activeTabConfig.url
1912
+ ),
1893
1913
  method: 'PUT',
1894
- type: activeTabConfig.filetype === 'image' ? 'gallery' : 'list',
1914
+ type:
1915
+ activeTabConfig.filetype ===
1916
+ 'image'
1917
+ ? 'gallery'
1918
+ : 'list',
1895
1919
  data: {
1896
- file_relationship: activeTabConfig.file_relationship,
1920
+ file_relationship:
1921
+ activeTabConfig.file_relationship,
1897
1922
  },
1898
- folder: activeTabConfig.folder || 'uploads',
1923
+ folder:
1924
+ activeTabConfig.folder ||
1925
+ 'uploads',
1899
1926
  filetype: activeTabConfig.filetype,
1900
- deleteUrl: replaceDropzoneUrlParams(activeTabConfig.deleteUrl) || '/ajax/files/delete',
1927
+ deleteUrl:
1928
+ replaceDropzoneUrlParams(
1929
+ activeTabConfig.deleteUrl
1930
+ ) || '/ajax/files/delete',
1901
1931
  showDescription: true,
1902
1932
  }}
1903
- url={replaceDropzoneUrlParams(activeTabConfig.url)}
1933
+ url={replaceDropzoneUrlParams(
1934
+ activeTabConfig.url
1935
+ )}
1904
1936
  urlKey={activeTabConfig.urlKey}
1905
- deleteUrl={replaceDropzoneUrlParams(activeTabConfig.deleteUrl) || '/ajax/files/delete'}
1937
+ deleteUrl={
1938
+ replaceDropzoneUrlParams(
1939
+ activeTabConfig.deleteUrl
1940
+ ) || '/ajax/files/delete'
1941
+ }
1906
1942
  entityData={data}
1907
1943
  routeParams={routeParams}
1908
1944
  dataKey={activeTabConfig.key}
@@ -2623,19 +2659,27 @@ function GenericDetail({
2623
2659
  className={
2624
2660
  item.size ===
2625
2661
  'full'
2626
- ? item.type === 'ocrTemplate'
2627
- ? styles['fw-grid-item']
2662
+ ? item.type ===
2663
+ 'ocrTemplate'
2664
+ ? styles[
2665
+ 'fw-grid-item'
2666
+ ]
2628
2667
  : `${styles['fw-grid-item']} ${styles.notecolor}`
2629
2668
  : null
2630
2669
  }
2631
2670
  >
2632
- {item.type !== 'ocrTemplate' && (
2671
+ {item.type !==
2672
+ 'ocrTemplate' && (
2633
2673
  <strong>
2634
2674
  {item.label
2635
2675
  ? `${item.label}:`
2636
2676
  : null}
2637
2677
  </strong>
2638
- )}{item.type !== 'ocrTemplate' ? ' ' : ''}
2678
+ )}
2679
+ {item.type !==
2680
+ 'ocrTemplate'
2681
+ ? ' '
2682
+ : ''}
2639
2683
  {item
2640
2684
  .href
2641
2685
  ?.url &&
@@ -3787,48 +3831,29 @@ function GenericDetail({
3787
3831
  </div>
3788
3832
  </div>
3789
3833
 
3790
- <Popup
3791
- open={modalShow}
3834
+ <StandardModal
3835
+ isOpen={modalShow}
3792
3836
  onClose={modalClose}
3837
+ title={formData.title || 'Edit Details'}
3793
3838
  closeOnDocumentClick={false}
3794
- contentStyle={{
3795
- width: '80vw',
3796
- maxWidth: '1200px',
3797
- ...(formData.verticalAlign === 'top' && {
3798
- position: 'fixed',
3799
- top: '5vh',
3800
- left: '50%',
3801
- transform: 'translateX(-50%)',
3802
- margin: '0',
3803
- }),
3804
- }}
3839
+ verticalAlign={formData.verticalAlign || 'center'}
3805
3840
  >
3806
- <div
3807
- className={`modalwrap top--modal modalWide ${
3808
- formData.verticalAlign === 'top'
3809
- ? 'modal-top-aligned'
3810
- : ''
3811
- }`}
3812
- >
3813
- <div className="modal">
3814
- <Form
3815
- closeModal={modalClose}
3816
- columnId={formId}
3817
- fetchTable={handleReload}
3818
- formSettings={formData}
3819
- formType={formType}
3820
- style={userProfile?.settings?.style || {}}
3821
- updateForm={setFormData}
3822
- userProfile={userProfile}
3823
- paramValue={
3824
- routeParams[urlParam]
3825
- ? routeParams[urlParam]
3826
- : 0
3827
- }
3828
- />
3829
- </div>
3830
- </div>
3831
- </Popup>
3841
+ <Form
3842
+ closeModal={modalClose}
3843
+ columnId={formId}
3844
+ fetchTable={handleReload}
3845
+ formSettings={formData}
3846
+ formType={formType}
3847
+ style={userProfile?.settings?.style || {}}
3848
+ updateForm={setFormData}
3849
+ userProfile={userProfile}
3850
+ paramValue={
3851
+ routeParams[urlParam]
3852
+ ? routeParams[urlParam]
3853
+ : 0
3854
+ }
3855
+ />
3856
+ </StandardModal>
3832
3857
 
3833
3858
  <StagePopupModal
3834
3859
  show={showStagePopup}
@@ -7,7 +7,6 @@ import { saveAs } from 'file-saver';
7
7
  import _ from 'lodash';
8
8
  import parse from 'html-react-parser';
9
9
  import moment from 'moment';
10
- import Popup from 'reactjs-popup';
11
10
 
12
11
  import CustomFetch from '../Fetch';
13
12
  import Download from '../Download';
@@ -16,19 +15,24 @@ import Table from '../DataGrid';
16
15
  import TableFilter from '../TableFilter';
17
16
  import GenericGrid from './GenericGrid';
18
17
  import GenericReportForm from './GenericReportForm';
18
+ import StandardModal from './StandardModal';
19
19
 
20
20
  import styles from '../styles/GenericIndex.module.scss';
21
21
 
22
- function useWindowHeight() {
22
+ function useWindowDimensions() {
23
23
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
24
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
24
25
 
25
26
  useEffect(() => {
26
- const handleResize = () => setWindowHeight(window.innerHeight);
27
+ const handleResize = () => {
28
+ setWindowHeight(window.innerHeight);
29
+ setWindowWidth(window.innerWidth);
30
+ };
27
31
  window.addEventListener('resize', handleResize);
28
32
  return () => window.removeEventListener('resize', handleResize);
29
33
  }, []);
30
34
 
31
- return windowHeight;
35
+ return { windowHeight, windowWidth };
32
36
  }
33
37
 
34
38
  function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
@@ -236,7 +240,7 @@ function GenericIndex({
236
240
  const [customActionModalShow, setCustomActionModalShow] = useState(false);
237
241
  const [customActionFormData, setCustomActionFormData] = useState({});
238
242
  const [customActionConfig, setCustomActionConfig] = useState(null);
239
- const windowHeight = useWindowHeight();
243
+ const { windowHeight, windowWidth } = useWindowDimensions();
240
244
  const { config, setConfig, subnav, setSubnav } = useConfig(
241
245
  setting,
242
246
  setting.tabs,
@@ -1002,41 +1006,22 @@ function GenericIndex({
1002
1006
  </div>
1003
1007
 
1004
1008
  {/* Custom Action Modal */}
1005
- <Popup
1006
- open={customActionModalShow}
1009
+ <StandardModal
1010
+ isOpen={customActionModalShow}
1007
1011
  onClose={closeCustomActionModal}
1012
+ title={customActionFormData.title || 'Custom Action'}
1008
1013
  closeOnDocumentClick={false}
1009
- contentStyle={{
1010
- width: '80vw',
1011
- maxWidth: '1200px',
1012
- ...(customActionFormData.verticalAlign === 'top' && {
1013
- position: 'fixed',
1014
- top: '5vh',
1015
- left: '50%',
1016
- transform: 'translateX(-50%)',
1017
- margin: '0',
1018
- }),
1019
- }}
1014
+ verticalAlign={customActionFormData.verticalAlign || 'center'}
1020
1015
  >
1021
- <div
1022
- className={`modalwrap top--modal modalWide ${
1023
- customActionFormData.verticalAlign === 'top'
1024
- ? 'modal-top-aligned'
1025
- : ''
1026
- }`}
1027
- >
1028
- <div className="modal">
1029
- <Form
1030
- closeModal={closeCustomActionModal}
1031
- formSettings={customActionFormData}
1032
- formType="create"
1033
- fetchTable={handleCustomActionSubmit}
1034
- style={userProfile?.settings?.style || {}}
1035
- userProfile={userProfile}
1036
- />
1037
- </div>
1038
- </div>
1039
- </Popup>
1016
+ <Form
1017
+ closeModal={closeCustomActionModal}
1018
+ formSettings={customActionFormData}
1019
+ formType="create"
1020
+ fetchTable={handleCustomActionSubmit}
1021
+ style={userProfile?.settings?.style || {}}
1022
+ userProfile={userProfile}
1023
+ />
1024
+ </StandardModal>
1040
1025
  </>
1041
1026
  );
1042
1027
  }
@@ -1,13 +1,11 @@
1
1
  import React, { useState, useCallback } from 'react';
2
- import { useNavigate } from 'react-router-dom';
3
2
  import Table from '../DataGrid';
4
- import { Bell, Check, ArrowLeft } from 'lucide-react';
3
+ import { Bell, Check } from 'lucide-react';
5
4
  import { toast } from 'react-toastify';
6
5
  import CustomFetch from '../Fetch';
7
6
  import styles from '../styles/NotificationList.module.scss';
8
7
 
9
8
  const NotificationList = () => {
10
- const navigate = useNavigate();
11
9
  const [loading, setLoading] = useState(false);
12
10
 
13
11
  const markAllAsRead = useCallback(() => {
@@ -50,25 +48,26 @@ const NotificationList = () => {
50
48
  },
51
49
  columns: [
52
50
  {
53
- id: 'data',
54
- jsonData: 'label',
51
+ id: ['data'],
52
+ nameFrom: 'label',
55
53
  label: 'Message',
56
- type: 'json',
57
- flex: 1,
54
+ type: 'relation',
55
+ minWidth: 300,
56
+ wordWrap: true,
58
57
  },
59
58
  {
60
59
  id: 'created_at',
61
60
  label: 'Created At',
62
61
  type: 'datetime',
63
62
  minWidth: 180,
64
- maxWidth: 180,
63
+ maxWidth: 200,
65
64
  },
66
65
  {
67
66
  id: 'read_at',
68
67
  label: 'Read At',
69
68
  type: 'datetime',
70
69
  minWidth: 180,
71
- maxWidth: 180,
70
+ maxWidth: 200,
72
71
  },
73
72
  ],
74
73
  settings: [],
@@ -111,21 +110,6 @@ const NotificationList = () => {
111
110
  </div>
112
111
  </div>
113
112
  </div>
114
- <div className={styles.grid}>
115
- <div className={styles.grid__row}>
116
- <div className={styles.grid__full}>
117
- <div className={styles.backButtonContainer}>
118
- <button
119
- className={styles.backButton}
120
- onClick={() => navigate('/')}
121
- >
122
- <ArrowLeft size={16} />
123
- <span>Back to Dashboard</span>
124
- </button>
125
- </div>
126
- </div>
127
- </div>
128
- </div>
129
113
  </div>
130
114
  );
131
115
  };
@@ -0,0 +1,119 @@
1
+ import React from 'react';
2
+ import Popup from 'reactjs-popup';
3
+ import { X } from 'lucide-react';
4
+ import formStyles from '../styles/Form.module.scss';
5
+
6
+ /**
7
+ * StandardModal Component
8
+ *
9
+ * A reusable modal component that standardizes the look and feel across all components.
10
+ * Uses the same styles as GenericIndex, GenericDetail, and DataGrid via Form.module.scss.
11
+ *
12
+ * @param {Object} props
13
+ * @param {boolean} props.isOpen - Whether the modal is open
14
+ * @param {Function} props.onClose - Function to call when modal should close
15
+ * @param {string} props.title - Modal title (only used if showHeader is true)
16
+ * @param {React.ReactNode} props.children - Modal content
17
+ * @param {string} props.size - Modal size: 'small', 'medium', 'large' (default: 'medium')
18
+ * @param {boolean} props.closeOnDocumentClick - Whether to close on outside click (default: true)
19
+ * @param {boolean} props.closeOnEscape - Whether to close on escape key (default: true)
20
+ * @param {string} props.verticalAlign - Vertical alignment: 'center', 'top' (default: 'center')
21
+ * @param {boolean} props.showHeader - Whether to show modal header (default: false, because Form components have their own headers)
22
+ * @param {Object} props.customStyles - Custom styles to override defaults
23
+ */
24
+ const StandardModal = ({
25
+ isOpen,
26
+ onClose,
27
+ title,
28
+ children,
29
+ size = 'medium',
30
+ closeOnDocumentClick = true,
31
+ closeOnEscape = true,
32
+ verticalAlign = 'center',
33
+ showHeader = false,
34
+ customStyles = {}
35
+ }) => {
36
+ // Apply vertical alignment class if needed
37
+ const getModalClasses = () => {
38
+ let classes = formStyles.modal;
39
+ if (verticalAlign === 'top') {
40
+ classes += ' modal-top-aligned';
41
+ }
42
+ return classes;
43
+ };
44
+
45
+ // Get size-based styles for the popup content
46
+ const getContentStyle = () => {
47
+ const baseStyle = {
48
+ ...customStyles.modal
49
+ };
50
+
51
+ // Apply size-based styles
52
+ switch (size) {
53
+ case 'small':
54
+ return {
55
+ ...baseStyle,
56
+ width: '400px',
57
+ minWidth: '400px',
58
+ maxWidth: '600px'
59
+ };
60
+ case 'large':
61
+ return {
62
+ ...baseStyle,
63
+ width: '95vw',
64
+ minWidth: '800px',
65
+ maxWidth: '1600px'
66
+ };
67
+ case 'medium':
68
+ default:
69
+ // Use the same dimensions as modalWide class (original modal sizing)
70
+ return {
71
+ ...baseStyle,
72
+ width: '90vw',
73
+ minWidth: '600px',
74
+ maxWidth: '1400px'
75
+ };
76
+ }
77
+ };
78
+
79
+ return (
80
+ <Popup
81
+ open={isOpen}
82
+ onClose={onClose}
83
+ modal
84
+ closeOnDocumentClick={closeOnDocumentClick}
85
+ closeOnEscape={closeOnEscape}
86
+ contentStyle={getContentStyle()}
87
+ >
88
+ <div className={formStyles.modalwrap}>
89
+ <div
90
+ className={getModalClasses()}
91
+ >
92
+ {showHeader && (
93
+ <div className={formStyles.modal__header} style={customStyles.header}>
94
+ <h1 style={customStyles.title}>{title}</h1>
95
+ <button
96
+ className={formStyles.modal__close}
97
+ onClick={onClose}
98
+ style={customStyles.closeButton}
99
+ type="button"
100
+ aria-label="Close modal"
101
+ >
102
+ <X strokeWidth={1} size={24} />
103
+ </button>
104
+ </div>
105
+ )}
106
+ {showHeader ? (
107
+ <div className={formStyles.modal__content} style={customStyles.content}>
108
+ {children}
109
+ </div>
110
+ ) : (
111
+ children
112
+ )}
113
+ </div>
114
+ </div>
115
+ </Popup>
116
+ );
117
+ };
118
+
119
+ export default StandardModal;
@@ -145,8 +145,9 @@
145
145
  }
146
146
 
147
147
  .modalWide {
148
- width: 80vw;
149
- max-width: 1200px;
148
+ width: 90vw;
149
+ max-width: 1400px;
150
+ min-width: 600px;
150
151
  }
151
152
 
152
153
  .modal {
@@ -11,6 +11,12 @@
11
11
  grid-template-columns: 1fr 1fr 1fr 1fr;
12
12
  gap: 1em;
13
13
  }
14
+
15
+ /* Enhanced styling for wider modals */
16
+ .modal & form {
17
+ gap: 1.5em;
18
+ max-width: none;
19
+ }
14
20
  }
15
21
 
16
22
  .formItem {
@@ -238,9 +244,10 @@ input[type='file'] {
238
244
  .modal__content {
239
245
  width: 100%;
240
246
  position: relative;
241
- padding: 0.75rem;
242
- max-height: 80vh;
247
+ padding: 1rem;
248
+ max-height: 85vh;
243
249
  overflow-y: auto;
250
+ min-height: 400px;
244
251
 
245
252
  .btn {
246
253
  background: var(--primary-color);
@@ -945,3 +945,27 @@
945
945
  }
946
946
  }
947
947
 
948
+ // Modal styles (consistent with DataGrid)
949
+ .modalwrap {
950
+ background: var(--tertiary-color);
951
+ border-radius: 8px;
952
+ overflow: hidden;
953
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
954
+ }
955
+
956
+ .top--modal {
957
+ position: relative;
958
+ z-index: 1000;
959
+ }
960
+
961
+ .modalWide {
962
+ width: 90vw;
963
+ max-width: 1400px;
964
+ min-width: 600px;
965
+ }
966
+
967
+ .modal {
968
+ width: 100%;
969
+ position: relative;
970
+ }
971
+
@@ -1090,3 +1090,27 @@
1090
1090
  filter: brightness(1.1);
1091
1091
  }
1092
1092
  }
1093
+
1094
+ // Modal styles (consistent with DataGrid)
1095
+ .modalwrap {
1096
+ background: var(--tertiary-color);
1097
+ border-radius: 8px;
1098
+ overflow: hidden;
1099
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
1100
+ }
1101
+
1102
+ .top--modal {
1103
+ position: relative;
1104
+ z-index: 1000;
1105
+ }
1106
+
1107
+ .modalWide {
1108
+ width: 90vw;
1109
+ max-width: 1400px;
1110
+ min-width: 600px;
1111
+ }
1112
+
1113
+ .modal {
1114
+ width: 100%;
1115
+ position: relative;
1116
+ }
@@ -84,20 +84,22 @@
84
84
 
85
85
  /* Notification Box Header */
86
86
  .notiboxHeader {
87
- display: flex;
88
- align-items: center;
89
- justify-content: space-between;
90
- padding: 12px 16px;
91
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
92
- gap: 10px;
87
+ display: flex !important;
88
+ align-items: center !important;
89
+ justify-content: space-between !important;
90
+ padding: 12px 16px !important;
91
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06) !important;
92
+ gap: 12px !important;
93
+ min-height: 48px !important;
93
94
  }
94
95
 
95
96
  .notiboxHeader h3 {
96
- margin: 0;
97
- font-size: 16px;
98
- font-weight: 600;
99
- color: var(--heading-color, #333);
100
- flex-shrink: 0;
97
+ margin: 0 !important;
98
+ font-size: 16px !important;
99
+ font-weight: 600 !important;
100
+ color: #333 !important;
101
+ flex-shrink: 0 !important;
102
+ flex-grow: 1 !important;
101
103
  }
102
104
 
103
105
  /* Notification Scroll Wrap */
@@ -231,42 +233,41 @@
231
233
  }
232
234
 
233
235
  .notiDismiss {
234
- position: absolute;
235
- right: 12px;
236
- top: 10px;
237
- background: transparent;
238
- border: none;
239
- color: #666;
240
- cursor: pointer;
241
- padding: 0;
242
- display: flex;
243
- align-items: center;
244
- justify-content: center;
245
- width: 24px;
246
- height: 24px;
247
- border-radius: 50%;
248
- transition: all 0.2s;
249
- opacity: 0.7;
236
+ position: absolute !important;
237
+ right: 12px !important;
238
+ top: 10px !important;
239
+ background: #f8f9fa !important;
240
+ border: 1px solid #dee2e6 !important;
241
+ color: #6c757d !important;
242
+ cursor: pointer !important;
243
+ padding: 4px !important;
244
+ display: flex !important;
245
+ align-items: center !important;
246
+ justify-content: center !important;
247
+ width: 24px !important;
248
+ height: 24px !important;
249
+ border-radius: 50% !important;
250
+ transition: all 0.2s !important;
251
+ opacity: 1 !important;
252
+ visibility: visible !important;
253
+ z-index: 10 !important;
250
254
  }
251
255
 
252
256
  .notiContent:hover .notiDismiss {
253
- opacity: 1;
257
+ background: #e9ecef !important;
258
+ color: #495057 !important;
259
+ border-color: #adb5bd !important;
254
260
  }
255
261
 
256
262
  .notiDismiss:hover {
257
- background-color: rgba(220, 53, 69, 0.1);
258
- color: #dc3545;
259
- transform: scale(1.1);
263
+ background-color: #dc3545 !important;
264
+ color: #ffffff !important;
265
+ border-color: #dc3545 !important;
260
266
  }
261
267
 
262
268
  .notiDismiss:focus {
263
- outline: none;
264
- box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.3);
265
- background-color: rgba(220, 53, 69, 0.05);
266
- }
267
-
268
- .notiDismiss:active {
269
- transform: scale(0.95);
269
+ outline: none !important;
270
+ box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.3) !important;
270
271
  }
271
272
 
272
273
  /* Empty State */
@@ -322,46 +323,45 @@
322
323
 
323
324
  /* Mark All as Read Button */
324
325
  .markAllBtn {
325
- background-color: var(--primary-color, #0056b3);
326
- color: var(--tertiary-color, white);
327
- padding: 8px 16px;
328
- border-radius: var(--br, 6px);
329
- border: none;
330
- display: flex;
331
- align-items: center;
332
- justify-content: center;
333
- gap: 6px;
334
- font-size: 14px;
335
- font-weight: 500;
336
- cursor: pointer;
337
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
338
- transition: all 0.2s ease;
339
- outline: none;
340
- white-space: nowrap;
341
- flex-shrink: 0;
342
- /* Ensure visibility */
343
- opacity: 1;
326
+ background-color: #6c757d !important;
327
+ color: #ffffff !important;
328
+ padding: 6px 10px !important;
329
+ border-radius: 4px !important;
330
+ border: none !important;
331
+ display: inline-flex !important;
332
+ align-items: center !important;
333
+ justify-content: center !important;
334
+ gap: 4px !important;
335
+ font-size: 12px !important;
336
+ font-weight: 500 !important;
337
+ cursor: pointer !important;
338
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2) !important;
339
+ transition: all 0.2s ease !important;
340
+ outline: none !important;
341
+ white-space: nowrap !important;
342
+ flex-shrink: 0 !important;
343
+ min-width: 120px !important;
344
+ height: 32px !important;
345
+ line-height: 1.2 !important;
346
+ opacity: 1 !important;
347
+ visibility: visible !important;
348
+ position: relative !important;
349
+ z-index: 10 !important;
344
350
  }
345
351
 
346
352
  .markAllBtn:hover:not(:disabled) {
347
- background-color: var(--secondary-color, #004494);
348
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
349
- transform: translateY(-1px);
353
+ background-color: #5a6268 !important;
354
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25) !important;
350
355
  }
351
356
 
352
357
  .markAllBtn:focus:not(:disabled) {
353
- box-shadow: 0 0 0 3px rgba(var(--primary-color-rgb, 0, 86, 179), 0.4);
354
- }
355
-
356
- .markAllBtn:active:not(:disabled) {
357
- transform: translateY(0);
358
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
358
+ box-shadow: 0 0 0 2px rgba(108, 117, 125, 0.4) !important;
359
359
  }
360
360
 
361
361
  .markAllBtn:disabled {
362
- opacity: 0.6;
363
- cursor: not-allowed;
364
- background-color: #6c757d;
362
+ opacity: 0.6 !important;
363
+ cursor: not-allowed !important;
364
+ background-color: #adb5bd !important;
365
365
  }
366
366
 
367
367
  /* Footer Actions */
@@ -374,41 +374,37 @@
374
374
  }
375
375
 
376
376
  .viewAllBtn {
377
- background-color: var(--primary-color, #0056b3);
378
- color: var(--tertiary-color, white);
379
- font-size: 14px;
380
- font-weight: 500;
381
- cursor: pointer;
382
- padding: 10px 20px;
383
- border-radius: var(--br, 6px);
384
- border: none;
385
- width: auto;
386
- min-width: 200px;
387
- text-align: center;
388
- transition: all 0.2s ease;
389
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
390
- outline: none;
391
- position: relative;
392
- z-index: 1;
393
- /* Ensure visibility and proper contrast */
394
- opacity: 1;
395
- display: block;
377
+ background-color: #6c757d !important;
378
+ color: #ffffff !important;
379
+ font-size: 14px !important;
380
+ font-weight: 500 !important;
381
+ cursor: pointer !important;
382
+ padding: 12px 24px !important;
383
+ border-radius: 6px !important;
384
+ border: none !important;
385
+ width: 100% !important;
386
+ max-width: 280px !important;
387
+ text-align: center !important;
388
+ transition: all 0.2s ease !important;
389
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important;
390
+ outline: none !important;
391
+ position: relative !important;
392
+ height: 44px !important;
393
+ opacity: 1 !important;
394
+ visibility: visible !important;
395
+ display: block !important;
396
+ line-height: 1.2 !important;
397
+ z-index: 10 !important;
396
398
  }
397
399
 
398
400
  .viewAllBtn:hover:not(:disabled) {
399
- background-color: var(--secondary-color, #004494);
400
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
401
- transform: translateY(-1px);
401
+ background-color: #5a6268 !important;
402
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25) !important;
402
403
  }
403
404
 
404
405
  .viewAllBtn:focus:not(:disabled) {
405
- box-shadow: 0 0 0 3px rgba(var(--primary-color-rgb, 0, 86, 179), 0.4);
406
- outline: none;
407
- }
408
-
409
- .viewAllBtn:active:not(:disabled) {
410
- transform: translateY(0);
411
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
406
+ box-shadow: 0 0 0 2px rgba(108, 117, 125, 0.4) !important;
407
+ outline: none !important;
412
408
  }
413
409
 
414
410
  /* Bell Animation */
@@ -1,7 +1,7 @@
1
1
  .notificationListContainer {
2
2
  width: 100%;
3
- max-width: 1200px;
4
- margin: 0 auto;
3
+ margin: 0;
4
+ padding: 0;
5
5
  }
6
6
 
7
7
  .grid {
@@ -88,31 +88,3 @@
88
88
  opacity: 0.6;
89
89
  cursor: not-allowed;
90
90
  }
91
-
92
- .backButtonContainer {
93
- display: flex;
94
- justify-content: center;
95
- padding: 16px 0;
96
- }
97
-
98
- .backButton {
99
- display: flex;
100
- align-items: center;
101
- gap: 8px;
102
- background-color: var(--primary-color);
103
- color: white;
104
- border: none;
105
- border-radius: 6px;
106
- padding: 12px 24px;
107
- font-size: 14px;
108
- font-weight: 500;
109
- cursor: pointer;
110
- transition: all 0.2s ease;
111
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
112
- min-width: 200px;
113
- }
114
-
115
- .backButton:hover {
116
- background-color: var(--primary-color-darker, #0056b3);
117
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
118
- }
@@ -65,9 +65,28 @@
65
65
  box-shadow: 0 0 0 2px rgba(107, 114, 128, 0.3) !important;
66
66
  }
67
67
 
68
- /* Make the modal more compact */
68
+ /* Make the modal use full width like GenericIndex */
69
69
  :global(.swal2-popup.swal2-modal) {
70
- max-width: 90%;
70
+ max-width: 95%;
71
+ width: 95%;
72
+ min-width: 800px;
73
+ }
74
+
75
+ /* Responsive adjustments for smaller screens */
76
+ @media (max-width: 1024px) {
77
+ :global(.swal2-popup.swal2-modal) {
78
+ max-width: 90%;
79
+ width: 90%;
80
+ min-width: 600px;
81
+ }
82
+ }
83
+
84
+ @media (max-width: 768px) {
85
+ :global(.swal2-popup.swal2-modal) {
86
+ max-width: 95%;
87
+ width: 95%;
88
+ min-width: 320px;
89
+ }
71
90
  }
72
91
 
73
92
  /* Reduce spacing between elements */
@@ -225,11 +225,6 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
225
225
  backdrop-filter: blur(3px);
226
226
  }
227
227
 
228
- .modalWide {
229
- width: 80vw !important;
230
- max-width: 1200px !important;
231
- }
232
-
233
228
  /* Top-aligned modal styles */
234
229
  .modal-top-aligned {
235
230
  max-height: 90vh !important;
package/src/index.js CHANGED
@@ -72,6 +72,7 @@ import GenericSort from './components/generic/GenericSort';
72
72
  import NotificationList from './components/generic/NotificationList';
73
73
  import OcrTemplateEnhanced from './components/generic/OcrTemplateEnhanced';
74
74
  import StagePopupModal from './components/generic/StagePopupModal';
75
+ import StandardModal from './components/generic/StandardModal';
75
76
 
76
77
  /** Proposal Components */
77
78
  import ProposalTemplateSectionManager from './components/proposal/ProposalTemplateSectionManager';
@@ -145,6 +146,7 @@ export {
145
146
  showConfirmDialog,
146
147
  SortableList,
147
148
  StagePopupModal,
149
+ StandardModal,
148
150
  Table,
149
151
  TableFilter,
150
152
  VariableInserter,