@visns-studio/visns-components 5.12.7 → 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
@@ -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.7",
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;
@@ -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 {
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 */
@@ -100,6 +101,7 @@ export {
100
101
  DatePickerPortal,
101
102
  Download,
102
103
  DropZone,
104
+ CategorizedDropZone,
103
105
  Field,
104
106
  Form,
105
107
  GalleryModal,