@visns-studio/visns-components 5.12.6 → 5.12.8

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
@@ -24,13 +24,13 @@
24
24
  "dayjs": "^1.11.13",
25
25
  "fabric": "^6.7.0",
26
26
  "file-saver": "^2.0.5",
27
- "framer-motion": "^12.19.2",
27
+ "framer-motion": "^12.23.0",
28
28
  "html-react-parser": "^5.2.5",
29
29
  "lodash": "^4.17.21",
30
30
  "lodash.debounce": "^4.0.8",
31
- "lucide-react": "^0.518.0",
31
+ "lucide-react": "^0.525.0",
32
32
  "moment": "^2.30.1",
33
- "motion": "^12.19.2",
33
+ "motion": "^12.23.0",
34
34
  "numeral": "^2.0.6",
35
35
  "pluralize": "^8.0.0",
36
36
  "qrcode.react": "^4.2.0",
@@ -47,10 +47,10 @@
47
47
  "react-reveal": "^1.2.2",
48
48
  "react-router-dom": "^6.30.1",
49
49
  "react-select": "^5.10.1",
50
- "react-signature-pad-wrapper": "^4.1.0",
50
+ "react-signature-pad-wrapper": "^4.1.1",
51
51
  "react-slugify": "^4.0.1",
52
52
  "react-sortable-hoc": "^2.0.0",
53
- "react-to-print": "^3.1.0",
53
+ "react-to-print": "^3.1.1",
54
54
  "react-toastify": "^11.0.5",
55
55
  "react-toggle": "^4.1.3",
56
56
  "react-tooltip": "^5.29.1",
@@ -58,19 +58,19 @@
58
58
  "reactjs-popup": "^2.0.6",
59
59
  "style-loader": "^4.0.0",
60
60
  "swapy": "^1.0.5",
61
- "sweetalert2": "^11.22.1",
61
+ "sweetalert2": "^11.22.2",
62
62
  "tesseract.js": "^6.0.1",
63
63
  "truncate": "^3.0.0",
64
64
  "uuid": "^11.1.0",
65
65
  "validator": "^13.15.15",
66
66
  "vite": "^6.3.5",
67
67
  "yarn": "^1.22.22",
68
- "yet-another-react-lightbox": "^3.23.4"
68
+ "yet-another-react-lightbox": "^3.24.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@babel/core": "^7.27.7",
72
- "@babel/plugin-transform-runtime": "^7.27.4",
73
- "@babel/preset-env": "^7.27.2",
71
+ "@babel/core": "^7.28.0",
72
+ "@babel/plugin-transform-runtime": "^7.28.0",
73
+ "@babel/preset-env": "^7.28.0",
74
74
  "@babel/preset-react": "^7.27.1",
75
75
  "babel-loader": "^10.0.0",
76
76
  "copy-webpack-plugin": "^13.0.0",
@@ -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.6",
90
+ "version": "5.12.8",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -0,0 +1,286 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import DropZone from './DropZone';
3
+ import CustomFetch from './Fetch';
4
+ import styles from './styles/CategorizedDropZone.module.scss';
5
+
6
+ const CategorizedDropZone = ({
7
+ filetype = 'image',
8
+ folder = 'uploads',
9
+ dataKey = 'files',
10
+ file_relationship = 'files',
11
+ url = '/ajax/files',
12
+ method = 'POST',
13
+ deleteUrl = '/ajax/files/delete',
14
+ categories = [],
15
+ categoriesUrl = null,
16
+ categoriesWhere = [],
17
+ entityData = {},
18
+ routeParams = {},
19
+ fetchData = null
20
+ }) => {
21
+ const [activeCategory, setActiveCategory] = useState(null);
22
+ const [categorizedFiles, setCategorizedFiles] = useState({});
23
+ const [dynamicCategories, setDynamicCategories] = useState([]);
24
+ const [loadingCategories, setLoadingCategories] = useState(false);
25
+
26
+ // Get final categories (dynamic or static)
27
+ const finalCategories = dynamicCategories.length > 0 ? dynamicCategories : categories;
28
+
29
+ // Fetch categories from API if categoriesUrl is provided
30
+ useEffect(() => {
31
+ if (categoriesUrl) {
32
+ setLoadingCategories(true);
33
+
34
+ const requestData = {};
35
+ if (categoriesWhere && categoriesWhere.length > 0) {
36
+ requestData.where = categoriesWhere;
37
+ }
38
+
39
+ CustomFetch(categoriesUrl, 'POST', requestData)
40
+ .then(response => {
41
+ if (response.data && !response.data.error) {
42
+ const categoriesData = Array.isArray(response.data.data) ? response.data.data :
43
+ Array.isArray(response.data) ? response.data : [];
44
+
45
+ // Transform API data to match expected format
46
+ const transformedCategories = categoriesData.map(cat => ({
47
+ id: cat.slug || cat.id,
48
+ label: cat.name || cat.label,
49
+ description: cat.description || ''
50
+ }));
51
+
52
+ setDynamicCategories(transformedCategories);
53
+
54
+ // Set first category as active if none selected
55
+ if (!activeCategory && transformedCategories.length > 0) {
56
+ setActiveCategory(transformedCategories[0].id);
57
+ }
58
+ }
59
+ })
60
+ .catch(error => {
61
+ console.error('Failed to load categories:', error);
62
+ })
63
+ .finally(() => {
64
+ setLoadingCategories(false);
65
+ });
66
+ } else if (categories.length > 0 && !activeCategory) {
67
+ // Set first static category as active
68
+ setActiveCategory(categories[0].id);
69
+ }
70
+ }, [categoriesUrl, categoriesWhere, categories, activeCategory]);
71
+
72
+ useEffect(() => {
73
+ console.log('CategorizedDropZone: Initializing files', {
74
+ finalCategories,
75
+ entityData: entityData[dataKey],
76
+ dataKey
77
+ });
78
+
79
+ // Initialize categorized files structure
80
+ const initialCategorizedFiles = {};
81
+ finalCategories.forEach(category => {
82
+ initialCategorizedFiles[category.id] = [];
83
+ });
84
+
85
+ // If there are existing files, categorize them based on file_description
86
+ if (entityData[dataKey] && Array.isArray(entityData[dataKey])) {
87
+ console.log('CategorizedDropZone: Processing files', entityData[dataKey]);
88
+
89
+ entityData[dataKey].forEach(file => {
90
+ console.log('CategorizedDropZone: Processing file', {
91
+ file_name: file.file_name,
92
+ file_description: file.file_description,
93
+ id: file.id
94
+ });
95
+
96
+ const category = getCategoryFromDescription(file.file_description);
97
+ console.log('CategorizedDropZone: Category extracted', category);
98
+
99
+ if (category && initialCategorizedFiles[category]) {
100
+ initialCategorizedFiles[category].push(file);
101
+ console.log(`CategorizedDropZone: Added file ${file.file_name} to category ${category}`);
102
+ } else {
103
+ console.log(`CategorizedDropZone: No category match for file ${file.file_name}`, {
104
+ extractedCategory: category,
105
+ availableCategories: Object.keys(initialCategorizedFiles)
106
+ });
107
+ }
108
+ });
109
+ }
110
+
111
+ console.log('CategorizedDropZone: Final categorized files', initialCategorizedFiles);
112
+ setCategorizedFiles(initialCategorizedFiles);
113
+ }, [entityData, dataKey, finalCategories]);
114
+
115
+ const getCategoryFromDescription = (description) => {
116
+ if (!description) return null;
117
+
118
+ // Check if the description contains a category identifier (handles both slugs and numeric IDs)
119
+ const categoryMatch = description.match(/^category:([^-\s]+)/i);
120
+ if (categoryMatch) {
121
+ const extractedId = categoryMatch[1];
122
+
123
+ // First, try to find by exact ID match
124
+ const exactMatch = finalCategories.find(cat => cat.id === extractedId);
125
+ if (exactMatch) return extractedId;
126
+
127
+ // If no exact match, try to find by numeric ID if extracted ID is numeric
128
+ if (/^\d+$/.test(extractedId)) {
129
+ const numericMatch = finalCategories.find(cat => cat.id == extractedId);
130
+ if (numericMatch) return numericMatch.id;
131
+ }
132
+
133
+ // Log for debugging
134
+ console.log('Category extraction debug:', {
135
+ description,
136
+ extractedId,
137
+ availableCategories: finalCategories.map(c => ({ id: c.id, label: c.label }))
138
+ });
139
+ }
140
+
141
+ // Fallback: check if description matches any category label
142
+ const matchedCategory = finalCategories.find(cat =>
143
+ description.toLowerCase().includes(cat.label.toLowerCase())
144
+ );
145
+ return matchedCategory?.id || null;
146
+ };
147
+
148
+ const handleCategoryChange = (categoryId) => {
149
+ setActiveCategory(categoryId);
150
+ };
151
+
152
+ const getCurrentCategoryFiles = () => {
153
+ return categorizedFiles[activeCategory] || [];
154
+ };
155
+
156
+ const handleFileUpload = (files) => {
157
+ // Add category prefix to file descriptions
158
+ const activeCategoryData = finalCategories.find(cat => cat.id === activeCategory);
159
+ const categoryPrefix = `category:${activeCategory}`;
160
+
161
+ // Update the files with category information
162
+ const updatedFiles = files.map(file => ({
163
+ ...file,
164
+ file_description: file.file_description ?
165
+ `${categoryPrefix} - ${file.file_description}` :
166
+ `${categoryPrefix} - ${activeCategoryData?.description || activeCategoryData?.label}`
167
+ }));
168
+
169
+ // Update the categorized files state
170
+ setCategorizedFiles(prev => ({
171
+ ...prev,
172
+ [activeCategory]: [...(prev[activeCategory] || []), ...updatedFiles]
173
+ }));
174
+ };
175
+
176
+ const handleFileDelete = (fileId) => {
177
+ // Remove file from the appropriate category
178
+ setCategorizedFiles(prev => {
179
+ const updated = { ...prev };
180
+ Object.keys(updated).forEach(categoryId => {
181
+ updated[categoryId] = updated[categoryId].filter(file => file.id !== fileId);
182
+ });
183
+ return updated;
184
+ });
185
+ };
186
+
187
+ const handleFileUpdate = (updatedFile) => {
188
+ // Update file in the appropriate category
189
+ const newCategory = getCategoryFromDescription(updatedFile.file_description);
190
+
191
+ setCategorizedFiles(prev => {
192
+ const updated = { ...prev };
193
+
194
+ // Remove from old category
195
+ Object.keys(updated).forEach(categoryId => {
196
+ updated[categoryId] = updated[categoryId].filter(file => file.id !== updatedFile.id);
197
+ });
198
+
199
+ // Add to new category
200
+ if (newCategory && updated[newCategory]) {
201
+ updated[newCategory].push(updatedFile);
202
+ }
203
+
204
+ return updated;
205
+ });
206
+ };
207
+
208
+ // Prepare entity data for the current category
209
+ const currentCategoryEntityData = {
210
+ ...entityData,
211
+ [dataKey]: getCurrentCategoryFiles()
212
+ };
213
+
214
+ if (loadingCategories) {
215
+ return (
216
+ <div className={styles.categorizedDropZone}>
217
+ <div className={styles.loadingState}>
218
+ Loading categories...
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ if (finalCategories.length === 0) {
225
+ return (
226
+ <div className={styles.categorizedDropZone}>
227
+ <div className={styles.emptyState}>
228
+ No categories available. Please configure site photo categories in settings.
229
+ </div>
230
+ </div>
231
+ );
232
+ }
233
+
234
+ return (
235
+ <div className={styles.categorizedDropZone}>
236
+ <div className={styles.categoryTabs}>
237
+ {finalCategories.map(category => (
238
+ <button
239
+ key={category.id}
240
+ className={`${styles.categoryTab} ${activeCategory === category.id ? styles.active : ''}`}
241
+ onClick={() => handleCategoryChange(category.id)}
242
+ >
243
+ {category.label}
244
+ {categorizedFiles[category.id] && categorizedFiles[category.id].length > 0 && (
245
+ <span className={styles.fileCount}>
246
+ ({categorizedFiles[category.id].length})
247
+ </span>
248
+ )}
249
+ </button>
250
+ ))}
251
+ </div>
252
+
253
+ <div className={styles.categoryContent}>
254
+ {activeCategory && (
255
+ <div className={styles.categoryPanel}>
256
+ {finalCategories.find(cat => cat.id === activeCategory)?.description && (
257
+ <p className={styles.categoryDescription}>
258
+ {finalCategories.find(cat => cat.id === activeCategory)?.description}
259
+ </p>
260
+ )}
261
+
262
+ <DropZone
263
+ fetchData={fetchData || (() => {
264
+ // Default fetch function - refresh current category files
265
+ console.log('Files updated for category:', activeCategory);
266
+ })}
267
+ files={getCurrentCategoryFiles()}
268
+ settings={{
269
+ url: url,
270
+ method: method,
271
+ type: 'gallery',
272
+ data: {
273
+ file_relationship: file_relationship,
274
+ file_description: `category:${activeCategory} - ${finalCategories.find(cat => cat.id === activeCategory)?.label || activeCategory}`,
275
+ },
276
+ model: entityData?.constructor?.name || 'Design'
277
+ }}
278
+ />
279
+ </div>
280
+ )}
281
+ </div>
282
+ </div>
283
+ );
284
+ };
285
+
286
+ export default CategorizedDropZone;
@@ -37,6 +37,7 @@ import Tools from './sketch/tools';
37
37
  import VisnsAsyncSelect from './AsyncSelect';
38
38
  import VisnsAutocomplete from './Autocomplete';
39
39
  import VisnsDropZone from './DropZone';
40
+ import DatePickerPortal from './utils/DatePickerPortal';
40
41
 
41
42
  import 'react-toggle/style.css';
42
43
  import 'react-datepicker/dist/react-datepicker.css';
@@ -72,6 +73,7 @@ function Field({
72
73
  }) {
73
74
  const editorRef = useRef(null);
74
75
  const fileImageInputRef = useRef(null);
76
+ const containerRef = useRef(null);
75
77
  const [autoValue, setAutoValue] = useState('');
76
78
  const [colorPickerShow, setColorPickerShow] = useState(false);
77
79
  const [counter, setCounter] = useState(0);
@@ -2129,31 +2131,38 @@ function Field({
2129
2131
  case 'date':
2130
2132
  case 'datetime':
2131
2133
  return (
2132
- <DatePicker
2133
- dateFormat={
2134
- settings.format
2135
- ? settings.format
2136
- : settings.type === 'datetime'
2137
- ? 'dd-MM-yyyy hh:mm aa'
2138
- : 'dd-MM-yyyy'
2139
- }
2140
- isClearable
2141
- className={inputClass[settings.id]}
2142
- selected={inputValue || ''}
2143
- showMonthDropdown
2144
- showYearDropdown
2145
- scrollableYearDropdown
2146
- onChange={(date) => onChangeDate(date, settings.id)}
2147
- showTimeSelect={settings.type === 'datetime'}
2148
- showTimeSelectOnly={settings.type === 'time'}
2149
- timeIntervals={
2150
- settings.type === 'time' ? 15 : undefined
2151
- }
2152
- timeCaption={
2153
- settings.type === 'time' ? 'Time' : undefined
2154
- }
2155
- shouldCloseOnSelect={true}
2156
- />
2134
+ <DatePickerPortal
2135
+ containerRef={containerRef}
2136
+ centerScreen={settings.centerScreen}
2137
+ modalContext={settings.modalContext}
2138
+ usePortal={settings.usePortal}
2139
+ >
2140
+ <DatePicker
2141
+ dateFormat={
2142
+ settings.format
2143
+ ? settings.format
2144
+ : settings.type === 'datetime'
2145
+ ? 'dd-MM-yyyy hh:mm aa'
2146
+ : 'dd-MM-yyyy'
2147
+ }
2148
+ isClearable
2149
+ className={inputClass[settings.id]}
2150
+ selected={inputValue || ''}
2151
+ showMonthDropdown
2152
+ showYearDropdown
2153
+ scrollableYearDropdown
2154
+ onChange={(date) => onChangeDate(date, settings.id)}
2155
+ showTimeSelect={settings.type === 'datetime'}
2156
+ showTimeSelectOnly={settings.type === 'time'}
2157
+ timeIntervals={
2158
+ settings.type === 'time' ? 15 : undefined
2159
+ }
2160
+ timeCaption={
2161
+ settings.type === 'time' ? 'Time' : undefined
2162
+ }
2163
+ shouldCloseOnSelect={true}
2164
+ />
2165
+ </DatePickerPortal>
2157
2166
  );
2158
2167
  case 'plaintext':
2159
2168
  if (settings.description) {
@@ -2254,21 +2263,28 @@ function Field({
2254
2263
  );
2255
2264
  case 'time':
2256
2265
  return (
2257
- <DatePicker
2258
- dateFormat="hh:mm aa"
2259
- isClearable
2260
- className={inputClass[settings.id]}
2261
- selected={inputValue || ''}
2262
- showMonthDropdown
2263
- showYearDropdown
2264
- scrollableYearDropdown
2265
- onChange={(date) => onChangeDate(date, settings.id)}
2266
- showTimeSelect={true}
2267
- showTimeSelectOnly={true}
2268
- timeIntervals={15}
2269
- timeCaption={'Time'}
2270
- shouldCloseOnSelect={true}
2271
- />
2266
+ <DatePickerPortal
2267
+ containerRef={containerRef}
2268
+ centerScreen={settings.centerScreen}
2269
+ modalContext={settings.modalContext}
2270
+ usePortal={settings.usePortal}
2271
+ >
2272
+ <DatePicker
2273
+ dateFormat="hh:mm aa"
2274
+ isClearable
2275
+ className={inputClass[settings.id]}
2276
+ selected={inputValue || ''}
2277
+ showMonthDropdown
2278
+ showYearDropdown
2279
+ scrollableYearDropdown
2280
+ onChange={(date) => onChangeDate(date, settings.id)}
2281
+ showTimeSelect={true}
2282
+ showTimeSelectOnly={true}
2283
+ timeIntervals={15}
2284
+ timeCaption={'Time'}
2285
+ shouldCloseOnSelect={true}
2286
+ />
2287
+ </DatePickerPortal>
2272
2288
  );
2273
2289
  case 'toggle':
2274
2290
  return (
@@ -2522,7 +2538,7 @@ function Field({
2522
2538
  }, [counter]);
2523
2539
 
2524
2540
  return (
2525
- <div className={formItemClass} style={formItemStyle}>
2541
+ <div className={formItemClass} style={formItemStyle} ref={containerRef}>
2526
2542
  <label
2527
2543
  className={
2528
2544
  ['richeditor', 'textarea'].includes(settings.type)
@@ -80,6 +80,7 @@ import QrCode from '../QrCode';
80
80
  import StagePopupModal from './StagePopupModal';
81
81
  import Table from '../DataGrid';
82
82
  import TableFilter from '../TableFilter';
83
+ import CategorizedDropZone from '../CategorizedDropZone';
83
84
 
84
85
  // Proposal components
85
86
  import ProposalTemplateSectionManager from '../proposal/ProposalTemplateSectionManager';
@@ -2748,6 +2749,30 @@ function GenericDetail({
2748
2749
  } else {
2749
2750
  return null;
2750
2751
  }
2752
+ case 'categorized_dropzone':
2753
+ // Replace URL parameters like {dataId} with actual values
2754
+ const replaceUrlParams = (url) => {
2755
+ if (!url) return url;
2756
+ return url.replace(/{dataId}/g, routeParams[urlParam] || '');
2757
+ };
2758
+
2759
+ return (
2760
+ <CategorizedDropZone
2761
+ filetype={activeTabConfig.filetype}
2762
+ folder={activeTabConfig.folder}
2763
+ dataKey={activeTabConfig.key}
2764
+ file_relationship={activeTabConfig.file_relationship}
2765
+ url={replaceUrlParams(activeTabConfig.url)}
2766
+ method={activeTabConfig.method}
2767
+ deleteUrl={replaceUrlParams(activeTabConfig.deleteUrl)}
2768
+ categories={activeTabConfig.categories || []}
2769
+ categoriesUrl={activeTabConfig.categoriesUrl}
2770
+ categoriesWhere={activeTabConfig.categoriesWhere}
2771
+ entityData={data}
2772
+ routeParams={routeParams}
2773
+ fetchData={handleReload}
2774
+ />
2775
+ );
2751
2776
  case 'scheduling':
2752
2777
  return (
2753
2778
  <GenericEditableTable
@@ -56,7 +56,20 @@ const StagePopupModal = ({
56
56
  url = url.replace(`{${key}}`, routeParams[key]);
57
57
  });
58
58
 
59
- const response = await CustomFetch(url, 'POST');
59
+ // Prepare request data with where and fields parameters
60
+ const requestData = {};
61
+
62
+ // Add where parameter if available
63
+ if (field.where) {
64
+ requestData.where = field.where;
65
+ }
66
+
67
+ // Add fields parameter if available
68
+ if (field.fields) {
69
+ requestData.fields = field.fields;
70
+ }
71
+
72
+ const response = await CustomFetch(url, 'POST', requestData);
60
73
 
61
74
  if (response.data && !response.data.error) {
62
75
  // Handle both nested (response.data.data) and direct (response.data) array formats
@@ -0,0 +1,217 @@
1
+ // CategorizedDropZone.module.scss
2
+ .categorizedDropZone {
3
+ width: 100%;
4
+ margin-bottom: 20px;
5
+ }
6
+
7
+ .categoryTabs {
8
+ display: flex;
9
+ flex-wrap: wrap;
10
+ gap: 4px;
11
+ margin-bottom: 24px;
12
+ padding: 6px;
13
+ background-color: #f8f9fa;
14
+ border-radius: 8px;
15
+ border: 1px solid #e9ecef;
16
+ }
17
+
18
+ .categoryTab {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 8px;
22
+ padding: 10px 16px;
23
+ background-color: #ffffff;
24
+ border: 1px solid #dee2e6;
25
+ border-radius: 6px;
26
+ cursor: pointer;
27
+ font-size: 14px;
28
+ font-weight: 500;
29
+ color: #6c757d;
30
+ transition: all 0.2s ease;
31
+ white-space: nowrap;
32
+ outline: none;
33
+
34
+ &:hover {
35
+ color: #495057;
36
+ background-color: #f8f9fa;
37
+ border-color: #adb5bd;
38
+ }
39
+
40
+ &.active {
41
+ color: #ffffff;
42
+ background-color: #3498db;
43
+ border-color: #3498db;
44
+
45
+ .fileCount {
46
+ background-color: rgba(255, 255, 255, 0.2);
47
+ color: white;
48
+ border-color: rgba(255, 255, 255, 0.3);
49
+ }
50
+ }
51
+
52
+ &:focus {
53
+ outline: 2px solid #3498db;
54
+ outline-offset: 2px;
55
+ }
56
+ }
57
+
58
+ .fileCount {
59
+ background-color: #e9ecef;
60
+ color: #495057;
61
+ padding: 3px 7px;
62
+ border-radius: 10px;
63
+ font-size: 11px;
64
+ font-weight: 600;
65
+ min-width: 20px;
66
+ text-align: center;
67
+ border: 1px solid #ced4da;
68
+ transition: all 0.2s ease;
69
+ line-height: 1;
70
+ }
71
+
72
+ .categoryContent {
73
+ min-height: 300px;
74
+ }
75
+
76
+ .categoryPanel {
77
+ width: 100%;
78
+ }
79
+
80
+ .categoryTitle {
81
+ font-size: 20px;
82
+ font-weight: 600;
83
+ color: #2c3e50;
84
+ margin: 0 20px 8px 20px;
85
+ padding-bottom: 8px;
86
+ border-bottom: 2px solid #3498db;
87
+ display: block;
88
+ width: calc(100% - 40px);
89
+ }
90
+
91
+ .categoryDescription {
92
+ font-size: 14px;
93
+ color: #6c757d;
94
+ margin: 0 0 24px 0;
95
+ padding: 0 20px;
96
+ font-style: italic;
97
+ }
98
+
99
+ .loadingState,
100
+ .emptyState {
101
+ text-align: center;
102
+ padding: 40px 20px;
103
+ color: #666;
104
+ font-size: 16px;
105
+ }
106
+
107
+ .loadingState {
108
+ background-color: #f8f9fa;
109
+ border-radius: 8px;
110
+ border: 1px dashed #ddd;
111
+ }
112
+
113
+ .emptyState {
114
+ background-color: #fff3cd;
115
+ border: 1px solid #ffeaa7;
116
+ border-radius: 8px;
117
+ color: #856404;
118
+ }
119
+
120
+ // Responsive design
121
+ @media (max-width: 768px) {
122
+ .categoryTabs {
123
+ gap: 3px;
124
+ padding: 4px;
125
+ margin-bottom: 20px;
126
+ }
127
+
128
+ .categoryTab {
129
+ flex: 1;
130
+ justify-content: center;
131
+ padding: 8px 12px;
132
+ font-size: 13px;
133
+ min-width: 0; // Allow tabs to shrink
134
+ }
135
+
136
+ .fileCount {
137
+ font-size: 10px;
138
+ padding: 2px 5px;
139
+ min-width: 16px;
140
+ }
141
+
142
+ .categoryTitle {
143
+ font-size: 18px;
144
+ }
145
+
146
+ .categoryDescription {
147
+ font-size: 13px;
148
+ margin-bottom: 20px;
149
+ }
150
+ }
151
+
152
+ // Very small screens
153
+ @media (max-width: 480px) {
154
+ .categoryTabs {
155
+ flex-direction: column;
156
+ gap: 2px;
157
+ }
158
+
159
+ .categoryTab {
160
+ width: 100%;
161
+ padding: 12px 16px;
162
+ font-size: 14px;
163
+ }
164
+ }
165
+
166
+ // Dark theme support
167
+ @media (prefers-color-scheme: dark) {
168
+ .categoryTabs {
169
+ background-color: #343a40;
170
+ border-color: #495057;
171
+ }
172
+
173
+ .categoryTab {
174
+ background-color: #495057;
175
+ border-color: #6c757d;
176
+ color: #adb5bd;
177
+
178
+ &:hover {
179
+ background-color: #6c757d;
180
+ border-color: #adb5bd;
181
+ color: #f8f9fa;
182
+ }
183
+
184
+ &.active {
185
+ background-color: #3498db;
186
+ border-color: #3498db;
187
+ color: #ffffff;
188
+ }
189
+ }
190
+
191
+ .fileCount {
192
+ background-color: #6c757d;
193
+ color: #f8f9fa;
194
+ border-color: #adb5bd;
195
+ }
196
+
197
+ .categoryTitle {
198
+ color: #f8f9fa;
199
+ border-bottom-color: #3498db;
200
+ }
201
+
202
+ .categoryDescription {
203
+ color: #adb5bd;
204
+ }
205
+
206
+ .loadingState {
207
+ background-color: #343a40;
208
+ border-color: #495057;
209
+ color: #adb5bd;
210
+ }
211
+
212
+ .emptyState {
213
+ background-color: #856404;
214
+ border-color: #ffc107;
215
+ color: #fff3cd;
216
+ }
217
+ }
@@ -237,7 +237,7 @@
237
237
  /* Modal styles */
238
238
  .modalwrap {
239
239
  width: 100%;
240
- max-width: 500px;
240
+ max-width: 800px;
241
241
  }
242
242
 
243
243
  .modal {
@@ -451,15 +451,7 @@ select:not(:placeholder-shown) + .fi__span {
451
451
  width: 100% !important;
452
452
  }
453
453
 
454
- .react-datepicker-popper {
455
- z-index: 99999 !important;
456
- position: fixed !important;
457
- transform: none !important;
458
- top: 50% !important;
459
- left: 50% !important;
460
- margin-top: -150px !important;
461
- margin-left: -150px !important;
462
- }
454
+ // DatePicker positioning removed - handled by global.css
463
455
 
464
456
  .react-datepicker__close-icon:after {
465
457
  background-color: rgba(var(--primary-rgb), 0.45) !important;
@@ -828,15 +828,7 @@ select:not(:placeholder-shown) + .fi__span {
828
828
  width: 100% !important;
829
829
  }
830
830
 
831
- .react-datepicker-popper {
832
- z-index: 99999 !important;
833
- position: fixed !important;
834
- transform: none !important;
835
- top: 50% !important;
836
- left: 50% !important;
837
- margin-top: -150px !important;
838
- margin-left: -150px !important;
839
- }
831
+ // DatePicker positioning removed - handled by global.css
840
832
 
841
833
  .react-datepicker__close-icon:after {
842
834
  background-color: rgba(var(--primary-rgb), 0.45) !important;
@@ -196,7 +196,7 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
196
196
  .inovua-react-toolkit-text-input__input
197
197
  ):not(.inovua-react-toolkit-numeric-input__input):not(
198
198
  .variable-inserter-search-input
199
- ) {
199
+ ):not(.react-datepicker__month-select):not(.react-datepicker__year-select) {
200
200
  background: white;
201
201
  border-radius: var(--br);
202
202
  border: 1px solid rgba(var(--paragraph-color-rgb), 0.15);
@@ -339,29 +339,38 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
339
339
  z-index: 999 !important;
340
340
  }
341
341
 
342
- /* DatePicker z-index fix */
342
+ /* DatePicker proper z-index hierarchy */
343
343
  .react-datepicker-popper {
344
- z-index: 99999 !important;
344
+ z-index: 1060 !important; /* Above Bootstrap modals (1050) */
345
+ position: absolute !important; /* Use absolute positioning for proper context */
346
+ }
347
+
348
+ /* Center-screen positioning for modal contexts */
349
+ .react-datepicker-popper.center-screen {
345
350
  position: fixed !important;
346
- transform: none !important;
347
351
  top: 50% !important;
348
352
  left: 50% !important;
349
- margin-top: -150px !important;
350
- margin-left: -150px !important;
353
+ transform: translate(-50%, -50%) !important;
354
+ z-index: 1060 !important;
355
+ }
356
+
357
+ /* Portal mode for modal contexts */
358
+ .react-datepicker-popper.portal-mode {
359
+ position: fixed !important;
360
+ z-index: 1060 !important;
351
361
  }
352
362
 
353
363
  .react-datepicker {
354
- position: absolute !important;
364
+ position: relative !important;
355
365
  display: inline-block !important;
356
- z-index: 99999 !important;
366
+ z-index: 1 !important;
357
367
  background-color: white !important;
358
368
  border-radius: 0.3rem !important;
359
369
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important;
360
370
  }
361
371
 
362
372
  .react-datepicker-portal {
363
- position: fixed !important;
364
- z-index: 99999 !important;
373
+ z-index: 1060 !important;
365
374
  }
366
375
 
367
376
  /* Fix for specific modal header that's causing issues */
@@ -369,12 +378,9 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
369
378
  z-index: 1 !important;
370
379
  }
371
380
 
372
- /* Make sure popup content doesn't constrain the datepicker */
373
- .popup-content {
374
- overflow: visible !important;
375
- }
376
-
377
- .modal__content {
381
+ /* Conditional overflow visibility only for modal contexts */
382
+ .modal-with-datepicker .popup-content,
383
+ .modal-with-datepicker .modal__content {
378
384
  overflow: visible !important;
379
385
  }
380
386
 
@@ -384,26 +390,179 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
384
390
  border: 1px solid #aeaeae !important;
385
391
  border-radius: 0.3rem !important;
386
392
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
393
+ float: left !important;
394
+ }
395
+
396
+ .react-datepicker {
397
+ position: relative !important;
398
+ display: inline-block !important;
399
+ background-color: white !important;
400
+ border-radius: 0.3rem !important;
401
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important;
402
+ min-width: 385px !important; /* Wider to accommodate month/year dropdowns */
403
+ width: auto !important;
404
+ }
405
+
406
+ /* Ensure time container appears on the right side */
407
+ .react-datepicker__time-container {
408
+ float: right !important;
409
+ border-left: 1px solid #aeaeae !important;
410
+ width: 85px !important;
411
+ display: block !important;
412
+ }
413
+
414
+ /* Fix navigation button positioning */
415
+ .react-datepicker__navigation {
416
+ position: absolute !important;
417
+ top: 2px !important;
418
+ z-index: 1 !important;
419
+ height: 32px !important;
420
+ width: 32px !important;
421
+ cursor: pointer !important;
422
+ }
423
+
424
+ .react-datepicker__navigation--previous {
425
+ left: 2px !important;
426
+ }
427
+
428
+ .react-datepicker__navigation--next {
429
+ right: 2px !important;
430
+ }
431
+
432
+ /* Fix next button positioning when time selector is present */
433
+ .react-datepicker__navigation--next--with-time:not(
434
+ .react-datepicker__navigation--next--with-today-button
435
+ ) {
436
+ right: 87px !important; /* Account for time container width + border */
437
+ }
438
+
439
+ /* Ensure proper clearfix for float layout */
440
+ .react-datepicker::after {
441
+ content: '' !important;
442
+ display: table !important;
443
+ clear: both !important;
444
+ }
445
+
446
+ /* Fix header layout to accommodate navigation buttons */
447
+ .react-datepicker__header {
448
+ text-align: center !important;
449
+ background-color: #f0f0f0 !important;
450
+ border-bottom: 1px solid #aeaeae !important;
451
+ border-top-left-radius: 0.3rem !important;
452
+ border-top-right-radius: 0.3rem !important;
453
+ padding: 8px 0 !important;
454
+ position: relative !important;
455
+ }
456
+
457
+ /* Ensure current month text doesn't interfere with navigation */
458
+ .react-datepicker__current-month,
459
+ .react-datepicker-time__header,
460
+ .react-datepicker-year-header {
461
+ margin-top: 0 !important;
462
+ color: #000 !important;
463
+ font-weight: bold !important;
464
+ font-size: 0.944rem !important;
465
+ padding: 0 40px !important; /* Add padding to avoid navigation button overlap */
387
466
  }
388
467
 
389
- /* Hide the arrow since we're centering the calendar */
468
+ /* Alternative approach: Force specific width for month container */
469
+ .react-datepicker__month-container {
470
+ width: calc(
471
+ 100% - 87px
472
+ ) !important; /* Full width minus time container and border */
473
+ box-sizing: border-box !important;
474
+ min-width: 295px !important; /* Ensure enough space for month/year dropdowns */
475
+ }
476
+
477
+ /* Ensure time container has proper sizing and positioning */
478
+ .react-datepicker__time-container {
479
+ height: auto !important;
480
+ min-height: 250px !important; /* Match typical calendar height */
481
+ }
482
+
483
+ /* Force container to maintain layout */
484
+ .react-datepicker {
485
+ overflow: hidden !important; /* Contain floated children */
486
+ }
487
+
488
+ /* Make month and year select dropdowns more compact */
489
+ .react-datepicker__month-dropdown-container,
490
+ .react-datepicker__year-dropdown-container {
491
+ display: inline-block !important;
492
+ margin: 0 5px !important;
493
+ }
494
+
495
+ /* Target the actual select elements with higher specificity */
496
+ .react-datepicker .react-datepicker__header .react-datepicker__month-select,
497
+ .react-datepicker .react-datepicker__header .react-datepicker__year-select {
498
+ width: auto !important;
499
+ min-width: 70px !important;
500
+ max-width: 90px !important;
501
+ padding: 4px 8px !important;
502
+ font-size: 0.9rem !important;
503
+ border: 1px solid #aeaeae !important;
504
+ border-radius: 4px !important;
505
+ background-color: white !important;
506
+ cursor: pointer !important;
507
+ text-align: center !important;
508
+ margin: 0 2px !important;
509
+ box-sizing: border-box !important;
510
+ }
511
+
512
+ .react-datepicker .react-datepicker__header .react-datepicker__year-select {
513
+ min-width: 60px !important;
514
+ max-width: 70px !important;
515
+ }
516
+
517
+ /* Also target read-view elements for dropdowns */
518
+ .react-datepicker__month-read-view,
519
+ .react-datepicker__year-read-view {
520
+ width: auto !important;
521
+ min-width: 70px !important;
522
+ max-width: 90px !important;
523
+ padding: 4px 8px !important;
524
+ font-size: 0.9rem !important;
525
+ border: 1px solid #aeaeae !important;
526
+ border-radius: 4px !important;
527
+ background-color: white !important;
528
+ cursor: pointer !important;
529
+ text-align: center !important;
530
+ }
531
+
532
+ .react-datepicker__year-read-view {
533
+ min-width: 60px !important;
534
+ max-width: 70px !important;
535
+ }
536
+
537
+ /* Ensure dropdowns fit in header */
538
+ .react-datepicker__header {
539
+ display: flex !important;
540
+ flex-direction: column !important;
541
+ align-items: center !important;
542
+ gap: 8px !important;
543
+ }
544
+
545
+ /* Container for month/year dropdowns */
546
+ .react-datepicker__current-month {
547
+ display: flex !important;
548
+ align-items: center !important;
549
+ justify-content: center !important;
550
+ gap: 8px !important;
551
+ padding: 0 40px !important;
552
+ }
553
+
554
+ /* Show arrow for proper positioning context */
390
555
  .react-datepicker__triangle {
391
- display: none !important;
556
+ display: block !important;
392
557
  }
393
558
 
394
- /* Ensure all containers allow the datepicker to be visible */
395
- .modal,
396
- .popup-content,
397
- .modal__content,
398
- .formItem,
399
- .fi__label,
400
- .dynamicform,
401
- .dynamic__left {
402
- overflow: visible !important;
559
+ /* Hide arrow only for center-screen mode */
560
+ .react-datepicker-popper.center-screen .react-datepicker__triangle {
561
+ display: none !important;
403
562
  }
404
563
 
405
- /* Add a backdrop to make the datepicker stand out */
406
- .react-datepicker-popper::before {
564
+ /* Backdrop only for center-screen mode */
565
+ .react-datepicker-popper.center-screen::before {
407
566
  content: '' !important;
408
567
  position: fixed !important;
409
568
  top: 0 !important;
@@ -466,8 +625,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
466
625
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
467
626
 
468
627
  /* Checkerboard pattern for transparency */
469
- background-image:
470
- linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
628
+ background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
471
629
  linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
472
630
  linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
473
631
  linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
@@ -478,8 +636,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
478
636
  .relation-color-preview:hover {
479
637
  border-color: #adb5bd;
480
638
  transform: scale(1.1);
481
- box-shadow:
482
- inset 0 1px 2px rgba(0, 0, 0, 0.1),
639
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1),
483
640
  0 2px 8px rgba(0, 0, 0, 0.15);
484
641
  }
485
642
 
@@ -535,8 +692,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
535
692
  flex-shrink: 0;
536
693
 
537
694
  /* Checkerboard pattern for transparency */
538
- background-image:
539
- linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
695
+ background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
540
696
  linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
541
697
  linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
542
698
  linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
@@ -545,18 +701,19 @@ form div:has(.react-toggle) input[type='checkbox'] {
545
701
  }
546
702
 
547
703
  /* When no color is selected */
548
- .cpicker__box[style=""] {
549
- background:
550
- linear-gradient(45deg,
551
- transparent 40%,
552
- #dc3545 42%,
553
- #dc3545 58%,
704
+ .cpicker__box[style=''] {
705
+ background: linear-gradient(
706
+ 45deg,
707
+ transparent 40%,
708
+ #dc3545 42%,
709
+ #dc3545 58%,
554
710
  transparent 60%
555
711
  ),
556
- linear-gradient(-45deg,
557
- transparent 40%,
558
- #dc3545 42%,
559
- #dc3545 58%,
712
+ linear-gradient(
713
+ -45deg,
714
+ transparent 40%,
715
+ #dc3545 42%,
716
+ #dc3545 58%,
560
717
  transparent 60%
561
718
  ),
562
719
  #ffffff;
@@ -600,7 +757,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
600
757
  .cpicker-container {
601
758
  padding: 6px;
602
759
  }
603
-
760
+
604
761
  .cpicker-container .w-color-sketch {
605
762
  transform: scale(0.75) !important;
606
763
  }
@@ -610,7 +767,7 @@ form div:has(.react-toggle) input[type='checkbox'] {
610
767
  .cpicker-container {
611
768
  padding: 4px;
612
769
  }
613
-
770
+
614
771
  .cpicker-container .w-color-sketch {
615
772
  transform: scale(0.65) !important;
616
773
  }
@@ -0,0 +1,127 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ /**
5
+ * DatePickerPortal - Smart portal component for DatePicker positioning
6
+ *
7
+ * Automatically detects if the DatePicker is inside a modal context
8
+ * and renders it appropriately to prevent z-index conflicts
9
+ */
10
+ const DatePickerPortal = ({
11
+ children,
12
+ usePortal = null, // null = auto-detect, true = force portal, false = no portal
13
+ modalContext = null, // null = auto-detect, true = force modal styling, false = normal
14
+ centerScreen = false, // true = force center-screen positioning
15
+ containerRef = null // reference to the input container for positioning
16
+ }) => {
17
+ const [shouldUsePortal, setShouldUsePortal] = useState(false);
18
+ const [isModalContext, setIsModalContext] = useState(false);
19
+ const [portalContainer, setPortalContainer] = useState(null);
20
+
21
+ useEffect(() => {
22
+ // Auto-detect modal context if not explicitly set
23
+ if (modalContext === null) {
24
+ const detectModalContext = () => {
25
+ if (containerRef && containerRef.current) {
26
+ // Check if the container is inside a modal
27
+ const modalSelectors = [
28
+ '.modal',
29
+ '.popup-content',
30
+ '.modal__content',
31
+ '.swal2-container',
32
+ '[role="dialog"]',
33
+ '[aria-modal="true"]'
34
+ ];
35
+
36
+ for (const selector of modalSelectors) {
37
+ if (containerRef.current.closest(selector)) {
38
+ setIsModalContext(true);
39
+ setShouldUsePortal(true);
40
+ return;
41
+ }
42
+ }
43
+ }
44
+ setIsModalContext(false);
45
+ setShouldUsePortal(false);
46
+ };
47
+
48
+ detectModalContext();
49
+ } else {
50
+ setIsModalContext(modalContext);
51
+ setShouldUsePortal(modalContext);
52
+ }
53
+ }, [modalContext, containerRef]);
54
+
55
+ useEffect(() => {
56
+ // Override auto-detection if explicitly set
57
+ if (usePortal !== null) {
58
+ setShouldUsePortal(usePortal);
59
+ }
60
+ }, [usePortal]);
61
+
62
+ useEffect(() => {
63
+ // Create portal container
64
+ if (shouldUsePortal) {
65
+ const container = document.createElement('div');
66
+ container.className = 'datepicker-portal-container';
67
+
68
+ // Add appropriate classes based on context
69
+ if (isModalContext || centerScreen) {
70
+ container.classList.add('modal-context');
71
+ }
72
+
73
+ document.body.appendChild(container);
74
+ setPortalContainer(container);
75
+
76
+ // Add modal-context class to body for CSS targeting
77
+ if (isModalContext) {
78
+ document.body.classList.add('modal-with-datepicker');
79
+ }
80
+
81
+ return () => {
82
+ document.body.removeChild(container);
83
+ document.body.classList.remove('modal-with-datepicker');
84
+ };
85
+ }
86
+ }, [shouldUsePortal, isModalContext, centerScreen]);
87
+
88
+ // Clone children and add appropriate classes
89
+ const enhancedChildren = React.Children.map(children, child => {
90
+ if (React.isValidElement(child)) {
91
+ const additionalProps = {};
92
+
93
+ // Add popperClassName for react-datepicker
94
+ if (child.props.popperClassName) {
95
+ additionalProps.popperClassName = child.props.popperClassName;
96
+ }
97
+
98
+ // Add classes based on context
99
+ const classes = [];
100
+ if (isModalContext || centerScreen) {
101
+ classes.push('center-screen');
102
+ }
103
+ if (shouldUsePortal) {
104
+ classes.push('portal-mode');
105
+ }
106
+
107
+ if (classes.length > 0) {
108
+ additionalProps.popperClassName = [
109
+ child.props.popperClassName,
110
+ ...classes
111
+ ].filter(Boolean).join(' ');
112
+ }
113
+
114
+ return React.cloneElement(child, additionalProps);
115
+ }
116
+ return child;
117
+ });
118
+
119
+ // Render with or without portal
120
+ if (shouldUsePortal && portalContainer) {
121
+ return createPortal(enhancedChildren, portalContainer);
122
+ }
123
+
124
+ return enhancedChildren;
125
+ };
126
+
127
+ export default DatePickerPortal;
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** CMS Components */
2
2
  import DataGrid from './components/DataGrid';
3
3
  import DropZone from './components/DropZone';
4
+ import CategorizedDropZone from './components/CategorizedDropZone';
4
5
  import SortableList from './components/sorting/List';
5
6
 
6
7
  /** CRM Components */
@@ -29,6 +30,7 @@ import DataGridSearch from './components/controls/DataGridSearch';
29
30
  import AutoRefreshControls from './components/controls/AutoRefreshControls';
30
31
  import AudioPlayer from './components/controls/AudioPlayer';
31
32
  import GalleryModal from './components/modals/GalleryModal';
33
+ import DatePickerPortal from './components/utils/DatePickerPortal';
32
34
  export * from './components/columns/ColumnRenderers.jsx';
33
35
 
34
36
  /** Auth Specific Components */
@@ -96,8 +98,10 @@ export {
96
98
  CustomFetch,
97
99
  DataGrid,
98
100
  DataGridSearch,
101
+ DatePickerPortal,
99
102
  Download,
100
103
  DropZone,
104
+ CategorizedDropZone,
101
105
  Field,
102
106
  Form,
103
107
  GalleryModal,