@visns-studio/visns-components 5.12.7 → 5.12.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/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.9",
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,311 @@
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
+ // No categories available - show a simple dropzone without categorization
226
+ console.log('CategorizedDropZone: No categories, showing all files', {
227
+ dataKey,
228
+ files: entityData[dataKey] || [],
229
+ entityData
230
+ });
231
+
232
+ return (
233
+ <div className={styles.categorizedDropZone}>
234
+ <div className={styles.noCategoryContent}>
235
+ <DropZone
236
+ fetchData={fetchData || (() => {
237
+ console.log('Files updated (no categories)');
238
+ })}
239
+ files={entityData[dataKey] || []}
240
+ settings={{
241
+ url: url,
242
+ method: method,
243
+ type: 'gallery',
244
+ data: {
245
+ file_relationship: file_relationship,
246
+ file_description: '',
247
+ },
248
+ model: entityData?.constructor?.name || 'Design',
249
+ folder: folder,
250
+ filetype: filetype,
251
+ deleteUrl: deleteUrl
252
+ }}
253
+ />
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ return (
260
+ <div className={styles.categorizedDropZone}>
261
+ <div className={styles.categoryTabs}>
262
+ {finalCategories.map(category => (
263
+ <button
264
+ key={category.id}
265
+ className={`${styles.categoryTab} ${activeCategory === category.id ? styles.active : ''}`}
266
+ onClick={() => handleCategoryChange(category.id)}
267
+ >
268
+ {category.label}
269
+ {categorizedFiles[category.id] && categorizedFiles[category.id].length > 0 && (
270
+ <span className={styles.fileCount}>
271
+ ({categorizedFiles[category.id].length})
272
+ </span>
273
+ )}
274
+ </button>
275
+ ))}
276
+ </div>
277
+
278
+ <div className={styles.categoryContent}>
279
+ {activeCategory && (
280
+ <div className={styles.categoryPanel}>
281
+ {finalCategories.find(cat => cat.id === activeCategory)?.description && (
282
+ <p className={styles.categoryDescription}>
283
+ {finalCategories.find(cat => cat.id === activeCategory)?.description}
284
+ </p>
285
+ )}
286
+
287
+ <DropZone
288
+ fetchData={fetchData || (() => {
289
+ // Default fetch function - refresh current category files
290
+ console.log('Files updated for category:', activeCategory);
291
+ })}
292
+ files={getCurrentCategoryFiles()}
293
+ settings={{
294
+ url: url,
295
+ method: method,
296
+ type: 'gallery',
297
+ data: {
298
+ file_relationship: file_relationship,
299
+ file_description: `category:${activeCategory} - ${finalCategories.find(cat => cat.id === activeCategory)?.label || activeCategory}`,
300
+ },
301
+ model: entityData?.constructor?.name || 'Design'
302
+ }}
303
+ />
304
+ </div>
305
+ )}
306
+ </div>
307
+ </div>
308
+ );
309
+ };
310
+
311
+ export default CategorizedDropZone;
@@ -7,7 +7,7 @@ import { arrayMoveImmutable } from 'array-move';
7
7
  import Popup from 'reactjs-popup';
8
8
  import imageCompression from 'browser-image-compression';
9
9
 
10
- import { confirmDialog } from '../utils/ConfirmDialog';
10
+ import { confirmDialog } from './utils/ConfirmDialog';
11
11
  import CustomFetch from './Fetch';
12
12
  import Gallery from './Gallery';
13
13
  import styles from './styles/DropZone.module.css';
@@ -189,14 +189,18 @@ function VisnsDropZone({ fetchData, files, settings }) {
189
189
 
190
190
  useEffect(() => {
191
191
  if (data?.length > 0) {
192
+ // Only send the ID field for each file for sorting
193
+ const sortData = data.map(file => ({ id: file.id }));
194
+
192
195
  CustomFetch(
193
196
  '/ajax/files/sort_update',
194
197
  'POST',
195
198
  {
196
- list: data,
199
+ list: sortData,
197
200
  },
198
201
  function (result) {
199
202
  if (result.error === '') {
203
+ console.log('Sort order updated successfully');
200
204
  } else {
201
205
  toast.error(String(result.error));
202
206
  }
@@ -11,7 +11,7 @@ import _ from 'lodash';
11
11
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
12
12
  import { X } from 'lucide-react';
13
13
  import imageCompression from 'browser-image-compression';
14
- import { confirmDialog } from '../utils/ConfirmDialog';
14
+ import { confirmDialog } from './utils/ConfirmDialog';
15
15
 
16
16
  import '@visns-studio/visns-datagrid-enterprise/index.css';
17
17
 
@@ -15,14 +15,18 @@ const ChildSort = ({ item, url }) => {
15
15
 
16
16
  useEffect(() => {
17
17
  if (data.length > 0) {
18
+ // Only send the ID field for each item for sorting
19
+ const sortData = data.map(item => ({ id: item.id }));
20
+
18
21
  CustomFetch(
19
22
  url + '/sort_update',
20
23
  'POST',
21
24
  {
22
- list: data,
25
+ list: sortData,
23
26
  },
24
27
  function (result) {
25
28
  if (result.error === '') {
29
+ console.log('Sort order updated successfully');
26
30
  } else {
27
31
  toast.error(String(result.error));
28
32
  }
@@ -60,14 +64,18 @@ const CmsSort = ({ setting, category = false }) => {
60
64
  useEffect(() => {
61
65
  if (category === false) {
62
66
  if (data.length > 0) {
67
+ // Only send the ID field for each item for sorting
68
+ const sortData = data.map(item => ({ id: item.id }));
69
+
63
70
  CustomFetch(
64
71
  url + '/sort_update',
65
72
  'POST',
66
73
  {
67
- list: data,
74
+ list: sortData,
68
75
  },
69
76
  function (result) {
70
77
  if (result.error === '') {
78
+ console.log('Sort order updated successfully');
71
79
  } else {
72
80
  toast.error(String(result.error));
73
81
  }
@@ -87,10 +87,23 @@ export const showConfirmDialog = ({
87
87
  // Show SweetAlert2 dialog
88
88
  return Swal.fire(swalOptions).then((result) => {
89
89
  if (result.isConfirmed) {
90
- onConfirm();
91
- } else if (result.dismiss === Swal.DismissReason.cancel) {
92
- onCancel();
90
+ try {
91
+ onConfirm();
92
+ } catch (error) {
93
+ console.error('Error executing onConfirm callback:', error);
94
+ }
95
+ } else if (result.dismiss === Swal.DismissReason.cancel || result.isDenied) {
96
+ try {
97
+ onCancel();
98
+ } catch (error) {
99
+ console.error('Error executing onCancel callback:', error);
100
+ }
93
101
  }
102
+
103
+ return result;
104
+ }).catch((error) => {
105
+ console.error('SweetAlert2 error:', error);
106
+ return error;
94
107
  });
95
108
  };
96
109
 
@@ -9,7 +9,7 @@ import dayjs from 'dayjs';
9
9
  import parse from 'html-react-parser';
10
10
  import { createSwapy } from 'swapy';
11
11
  import { X, Minus, Plus, Star, Trash2 } from 'lucide-react';
12
- import { confirmDialog } from '../../utils/ConfirmDialog';
12
+ import { confirmDialog } from '../utils/ConfirmDialog';
13
13
 
14
14
  import CustomFetch from '../Fetch';
15
15
  import MultiSelect from '../MultiSelect';
@@ -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
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
4
4
  import { saveAs } from 'file-saver';
5
5
  import parse from 'html-react-parser';
6
6
  import { Delete, Download as DownloadIcon } from 'lucide-react';
7
- import { confirmDialog } from '../../utils/ConfirmDialog';
7
+ import { confirmDialog } from '../utils/ConfirmDialog';
8
8
 
9
9
  import CustomFetch from '../Fetch';
10
10
  import Download from '../Download';
@@ -4,7 +4,7 @@ import debounce from 'lodash.debounce';
4
4
  import { toast } from 'react-toastify';
5
5
  import { Trash2, ArrowUp, ArrowDown, Copy, Droplets } from 'lucide-react';
6
6
  import { Compact } from '@uiw/react-color';
7
- import { confirmDialog } from '../../utils/ConfirmDialog';
7
+ import { confirmDialog } from '../utils/ConfirmDialog';
8
8
 
9
9
  import CustomFetch from '../Fetch';
10
10
  import MultiSelect from '../MultiSelect';
@@ -34,7 +34,7 @@ import {
34
34
  Edit as Pencil,
35
35
  GripVertical as ChevronVertical,
36
36
  } from 'lucide-react';
37
- import { confirmDialog } from '../../utils/ConfirmDialog';
37
+ import { confirmDialog } from '../utils/ConfirmDialog';
38
38
 
39
39
  import 'react-toggle/style.css';
40
40
  import 'react-datepicker/dist/react-datepicker.css';
@@ -26,11 +26,14 @@ const ChildSort = ({ item, url }) => {
26
26
 
27
27
  useEffect(() => {
28
28
  if (data.length > 0) {
29
+ // Only send the ID field for each item for sorting
30
+ const sortData = data.map(item => ({ id: item.id }));
31
+
29
32
  CustomFetch(
30
33
  `${url}/sort_update`,
31
34
  'POST',
32
35
  {
33
- list: data,
36
+ list: sortData,
34
37
  },
35
38
  function (result) {
36
39
  if (result.error === '') {
@@ -108,6 +111,8 @@ const GenericSort = ({ category = false, layout = 'full' }) => {
108
111
  payload.id = paramValue;
109
112
  } else {
110
113
  fetchUrl = fetchUrl + '/sort_update';
114
+ // For the standard sort_update endpoint, only send IDs
115
+ payload.list = sortData.map(item => ({ id: item.id }));
111
116
  }
112
117
 
113
118
  if (category === false) {
@@ -116,6 +121,8 @@ const GenericSort = ({ category = false, layout = 'full' }) => {
116
121
 
117
122
  if (res.data.error !== '') {
118
123
  toast.error(String(res.data.error));
124
+ } else {
125
+ toast.success('Sort updated successfully');
119
126
  }
120
127
  }
121
128
  }
@@ -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