hazo_collab_forms 1.9.1 → 1.9.2

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.
Files changed (80) hide show
  1. package/CHANGE_LOG.md +25 -0
  2. package/README.md +153 -40
  3. package/dist/components/_internal_form_set.d.ts +13 -16
  4. package/dist/components/_internal_form_set.d.ts.map +1 -1
  5. package/dist/components/_internal_form_set.js +18 -2
  6. package/dist/components/_internal_form_set.js.map +1 -1
  7. package/dist/components/collab_form_file_upload.d.ts +38 -71
  8. package/dist/components/collab_form_file_upload.d.ts.map +1 -1
  9. package/dist/components/collab_form_file_upload.js +185 -280
  10. package/dist/components/collab_form_file_upload.js.map +1 -1
  11. package/dist/components/hazo_collab_form_base.d.ts +16 -6
  12. package/dist/components/hazo_collab_form_base.d.ts.map +1 -1
  13. package/dist/components/hazo_collab_form_base.js +19 -10
  14. package/dist/components/hazo_collab_form_base.js.map +1 -1
  15. package/dist/components/hazo_collab_form_doc.d.ts +8 -11
  16. package/dist/components/hazo_collab_form_doc.d.ts.map +1 -1
  17. package/dist/components/hazo_collab_form_doc.js +7 -5
  18. package/dist/components/hazo_collab_form_doc.js.map +1 -1
  19. package/dist/components/hazo_collab_form_group.d.ts +4 -3
  20. package/dist/components/hazo_collab_form_group.d.ts.map +1 -1
  21. package/dist/components/hazo_collab_form_group.js +3 -3
  22. package/dist/components/hazo_collab_form_group.js.map +1 -1
  23. package/dist/components/hazo_collab_form_view/context.d.ts.map +1 -1
  24. package/dist/components/hazo_collab_form_view/context.js +4 -0
  25. package/dist/components/hazo_collab_form_view/context.js.map +1 -1
  26. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts +3 -3
  27. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts.map +1 -1
  28. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js.map +1 -1
  29. package/dist/components/hazo_collab_form_view/index.d.ts +1 -1
  30. package/dist/components/hazo_collab_form_view/index.d.ts.map +1 -1
  31. package/dist/components/hazo_collab_form_view/index.js.map +1 -1
  32. package/dist/components/hazo_collab_form_view/types.d.ts +14 -9
  33. package/dist/components/hazo_collab_form_view/types.d.ts.map +1 -1
  34. package/dist/components/hazo_collab_form_view/views/edit_view.d.ts.map +1 -1
  35. package/dist/components/hazo_collab_form_view/views/edit_view.js +3 -1
  36. package/dist/components/hazo_collab_form_view/views/edit_view.js.map +1 -1
  37. package/dist/components/hazo_collab_form_view/views/summary_view.d.ts.map +1 -1
  38. package/dist/components/hazo_collab_form_view/views/summary_view.js +13 -13
  39. package/dist/components/hazo_collab_form_view/views/summary_view.js.map +1 -1
  40. package/dist/components/index.d.ts +4 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components/index.js +2 -0
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/shared/summary_files/summary_files.d.ts +10 -5
  45. package/dist/components/shared/summary_files/summary_files.d.ts.map +1 -1
  46. package/dist/components/shared/summary_files/summary_files.js +35 -14
  47. package/dist/components/shared/summary_files/summary_files.js.map +1 -1
  48. package/dist/components/shared/summary_utils/file_helpers.d.ts +7 -2
  49. package/dist/components/shared/summary_utils/file_helpers.d.ts.map +1 -1
  50. package/dist/components/shared/summary_utils/file_helpers.js +12 -2
  51. package/dist/components/shared/summary_utils/file_helpers.js.map +1 -1
  52. package/dist/components/shared/summary_utils/index.d.ts +1 -1
  53. package/dist/components/shared/summary_utils/index.d.ts.map +1 -1
  54. package/dist/components/shared/summary_utils/index.js +1 -1
  55. package/dist/components/shared/summary_utils/index.js.map +1 -1
  56. package/dist/components/shared/use_base_form_field.d.ts +13 -10
  57. package/dist/components/shared/use_base_form_field.d.ts.map +1 -1
  58. package/dist/components/shared/use_base_form_field.js +2 -1
  59. package/dist/components/shared/use_base_form_field.js.map +1 -1
  60. package/dist/types/file_attachment.d.ts +55 -0
  61. package/dist/types/file_attachment.d.ts.map +1 -0
  62. package/dist/types/file_attachment.js +54 -0
  63. package/dist/types/file_attachment.js.map +1 -0
  64. package/dist/types/file_manager.d.ts +60 -0
  65. package/dist/types/file_manager.d.ts.map +1 -0
  66. package/dist/types/file_manager.js +8 -0
  67. package/dist/types/file_manager.js.map +1 -0
  68. package/dist/types/index.d.ts +3 -0
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/dist/types/index.js +1 -0
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/index.d.ts +2 -0
  73. package/dist/utils/index.d.ts.map +1 -1
  74. package/dist/utils/index.js +1 -0
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/utils/use_file_status.d.ts +29 -0
  77. package/dist/utils/use_file_status.d.ts.map +1 -0
  78. package/dist/utils/use_file_status.js +68 -0
  79. package/dist/utils/use_file_status.js.map +1 -0
  80. package/package.json +4 -1
@@ -1,36 +1,31 @@
1
1
  /**
2
2
  * File upload component for collaboration forms
3
- * Provides drag-and-drop file upload, file list display, and file management
3
+ * Provides drag-and-drop file upload, file list display, and file management.
4
+ *
5
+ * Uses FileManagerCallbacks for upload/remove operations and FormFileAttachment
6
+ * as the data shape stored in form_data.
4
7
  */
5
8
  'use client';
6
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
10
  import React, { useRef, useState, useCallback } from 'react';
8
11
  import { cn } from '../utils/cn.js';
9
12
  import { use_logger } from '../logger/context.js';
10
- import { API_ENDPOINTS } from '../config/api_endpoints.js';
11
- import { HiTrash, HiPaperClip, HiPhotograph, HiDocument, HiDocumentText, HiCheckCircle, HiLockClosed, HiLockOpen } from 'react-icons/hi';
12
- import { FaSpinner } from 'react-icons/fa';
13
+ import { HiTrash, HiPaperClip, HiPhotograph, HiDocument, HiDocumentText, HiLockClosed, HiLockOpen, HiRefresh } from 'react-icons/hi';
13
14
  /**
14
- * Get file icon based on file type
15
+ * Get file icon based on MIME type and filename
15
16
  */
16
- function get_file_icon(file_type, file_name) {
17
- const extension = file_name.split('.').pop()?.toLowerCase() || '';
18
- const mime_type = file_type.toLowerCase();
19
- // Image types
20
- if (mime_type.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension)) {
17
+ function get_file_icon(mime_type, file_name) {
18
+ const extension = (file_name || '').split('.').pop()?.toLowerCase() || '';
19
+ const mime = (mime_type || '').toLowerCase();
20
+ if (mime.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension)) {
21
21
  return _jsx(HiPhotograph, { className: "h-8 w-8 text-blue-500" });
22
22
  }
23
- // PDF
24
- if (mime_type === 'application/pdf' || extension === 'pdf') {
23
+ if (mime === 'application/pdf' || extension === 'pdf') {
25
24
  return _jsx(HiDocument, { className: "h-8 w-8 text-red-500" });
26
25
  }
27
- // Document types
28
- if (mime_type.includes('document') ||
29
- mime_type.includes('text') ||
30
- ['doc', 'docx', 'txt', 'rtf', 'odt'].includes(extension)) {
26
+ if (mime.includes('document') || mime.includes('text') || ['doc', 'docx', 'txt', 'rtf', 'odt'].includes(extension)) {
31
27
  return _jsx(HiDocumentText, { className: "h-8 w-8 text-green-500" });
32
28
  }
33
- // Default
34
29
  return _jsx(HiPaperClip, { className: "h-8 w-8 text-gray-500" });
35
30
  }
36
31
  /**
@@ -44,10 +39,26 @@ function format_file_size(bytes) {
44
39
  const i = Math.floor(Math.log(bytes) / Math.log(k));
45
40
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
46
41
  }
42
+ /**
43
+ * Get status badge for a file
44
+ */
45
+ function StatusBadge({ status }) {
46
+ if (!status || status === 'active')
47
+ return null;
48
+ const config = {
49
+ missing: { label: 'Missing', className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300' },
50
+ soft_deleted: { label: 'Deleted', className: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
51
+ orphaned: { label: 'Orphaned', className: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300' },
52
+ };
53
+ const cfg = config[status];
54
+ if (!cfg)
55
+ return null;
56
+ return (_jsx("span", { className: cn('text-[10px] px-1 py-0.5 rounded font-medium', cfg.className), children: cfg.label }));
57
+ }
47
58
  /**
48
59
  * Collaboration form file upload component
49
60
  */
50
- export function CollabFormFileUpload({ field_id_final, accept_files_public = false, accept_files_private = false, files_dir, max_size, min_files, max_files = 10, file_accept, file_processor, file_validator, files: controlled_files, on_files_change, component_ref, upload_endpoint = API_ENDPOINTS.file_upload, private_files_permission, private_files: controlled_private_files, on_private_files_change, can_access_private_files: can_access_private_files_prop, }) {
61
+ export function CollabFormFileUpload({ field_id_final, accept_files_public = false, accept_files_private = false, files_dir, max_size, min_files, max_files = 10, file_accept, file_validator, file_manager, files: controlled_files, on_files_change, private_files: controlled_private_files, on_private_files_change, private_files_permission, can_access_private_files: can_access_private_files_prop, show_file_status = false, allow_reupload = false, file_statuses = {}, scope_id, uploaded_by, }) {
51
62
  const logger = use_logger();
52
63
  const file_input_ref = useRef(null);
53
64
  const private_file_input_ref = useRef(null);
@@ -57,125 +68,64 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
57
68
  const [is_dragging_private, set_is_dragging_private] = useState(false);
58
69
  const [upload_errors, set_upload_errors] = useState({});
59
70
  const [uploading_files, set_uploading_files] = useState(new Set());
60
- const [processing_files, set_processing_files] = useState(new Set());
61
- const [processed_files, set_processed_files] = useState(new Set());
62
71
  const [has_permission, set_has_permission] = useState(false);
63
72
  const [permission_loading, set_permission_loading] = useState(true);
64
73
  // Use controlled files if provided, otherwise use internal state
65
74
  const files = controlled_files !== undefined ? controlled_files : internal_files;
66
75
  const is_controlled = controlled_files !== undefined;
67
- // Use controlled private files if provided, otherwise use internal state
68
76
  const private_files = controlled_private_files !== undefined ? controlled_private_files : internal_private_files;
69
77
  const is_private_controlled = controlled_private_files !== undefined;
70
- // Check permission using hazo_auth when accept_files_private is true
78
+ // Check permission for private files
71
79
  React.useEffect(() => {
72
80
  if (!accept_files_private) {
73
81
  set_permission_loading(false);
74
82
  set_has_permission(false);
75
83
  return;
76
84
  }
77
- // If permission is provided via prop (e.g. from role-based resolution), use that
78
85
  if (can_access_private_files_prop !== undefined) {
79
86
  set_has_permission(can_access_private_files_prop);
80
87
  set_permission_loading(false);
81
88
  return;
82
89
  }
83
- // Need private_files_permission for API check
84
- if (!private_files_permission) {
85
- set_permission_loading(false);
86
- set_has_permission(false);
87
- return;
88
- }
89
- // Otherwise, check via hazo_auth API
90
- const check_permission = async () => {
91
- try {
92
- set_permission_loading(true);
93
- const response = await fetch(API_ENDPOINTS.auth_get, {
94
- credentials: 'include',
95
- });
96
- if (response.ok) {
97
- const auth_data = await response.json();
98
- if (auth_data.authenticated && auth_data.permissions) {
99
- const has_perm = auth_data.permissions.includes(private_files_permission);
100
- set_has_permission(has_perm);
101
- }
102
- else {
103
- set_has_permission(false);
104
- }
105
- }
106
- else {
107
- set_has_permission(false);
108
- }
109
- }
110
- catch (error) {
111
- logger.warn('[CollabFormFileUpload] Error checking permission', {
112
- permission: private_files_permission,
113
- error: error instanceof Error ? error.message : String(error),
114
- });
115
- set_has_permission(false);
116
- }
117
- finally {
118
- set_permission_loading(false);
119
- }
120
- };
121
- check_permission();
122
- }, [accept_files_private, private_files_permission, can_access_private_files_prop]);
123
- // Determine if private files section should be shown
90
+ // Without explicit prop, default to false for private files
91
+ set_permission_loading(false);
92
+ set_has_permission(false);
93
+ }, [accept_files_private, can_access_private_files_prop]);
124
94
  const show_private_section = !!(accept_files_private && has_permission && !permission_loading);
125
95
  /**
126
96
  * Update files state (public files)
127
97
  */
128
98
  const update_files = useCallback((new_files) => {
129
- // Ensure all files have visibility set to 'public'
130
- const files_with_visibility = new_files.map(f => ({
131
- ...f,
132
- visibility: f.visibility || 'public',
133
- }));
134
99
  if (!is_controlled) {
135
- set_internal_files(files_with_visibility);
136
- }
137
- if (on_files_change) {
138
- on_files_change(files_with_visibility);
100
+ set_internal_files(new_files);
139
101
  }
102
+ on_files_change?.(new_files);
140
103
  }, [is_controlled, on_files_change]);
141
104
  /**
142
105
  * Update private files state
143
106
  */
144
107
  const update_private_files = useCallback((new_files) => {
145
- // Ensure all files have visibility set to 'private'
146
- const files_with_visibility = new_files.map(f => ({
147
- ...f,
148
- visibility: 'private',
149
- }));
150
108
  if (!is_private_controlled) {
151
- set_internal_private_files(files_with_visibility);
152
- }
153
- if (on_private_files_change) {
154
- on_private_files_change(files_with_visibility);
109
+ set_internal_private_files(new_files);
155
110
  }
111
+ on_private_files_change?.(new_files);
156
112
  }, [is_private_controlled, on_private_files_change]);
157
113
  /**
158
114
  * Toggle file visibility (move between public and private)
159
115
  */
160
116
  const toggle_file_visibility = useCallback((file_id, current_visibility) => {
161
117
  if (current_visibility === 'public') {
162
- // Move from public to private
163
118
  const file_to_move = files.find(f => f.file_id === file_id);
164
119
  if (file_to_move) {
165
- const new_public_files = files.filter(f => f.file_id !== file_id);
166
- const moved_file = { ...file_to_move, visibility: 'private' };
167
- update_files(new_public_files);
168
- update_private_files([...private_files, moved_file]);
120
+ update_files(files.filter(f => f.file_id !== file_id));
121
+ update_private_files([...private_files, { ...file_to_move, visibility: 'private' }]);
169
122
  }
170
123
  }
171
124
  else {
172
- // Move from private to public
173
125
  const file_to_move = private_files.find(f => f.file_id === file_id);
174
126
  if (file_to_move) {
175
- const new_private_files = private_files.filter(f => f.file_id !== file_id);
176
- const moved_file = { ...file_to_move, visibility: 'public' };
177
- update_private_files(new_private_files);
178
- update_files([...files, moved_file]);
127
+ update_private_files(private_files.filter(f => f.file_id !== file_id));
128
+ update_files([...files, { ...file_to_move, visibility: 'public' }]);
179
129
  }
180
130
  }
181
131
  }, [files, private_files, update_files, update_private_files]);
@@ -183,124 +133,68 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
183
133
  * Validate file before upload
184
134
  */
185
135
  const validate_file = useCallback((file) => {
186
- // Run custom validator first (if provided)
187
136
  if (file_validator) {
188
137
  const custom_error = file_validator(file);
189
- if (custom_error) {
138
+ if (custom_error)
190
139
  return custom_error;
191
- }
192
140
  }
193
- // Check file count
194
141
  if (files.length >= max_files) {
195
142
  return `Maximum ${max_files} files allowed`;
196
143
  }
197
- // Check file size
198
144
  if (max_size && file.size > max_size) {
199
145
  return `File size exceeds maximum of ${format_file_size(max_size)}`;
200
146
  }
201
- // Check file type (if file_accept is specified)
202
147
  if (file_accept) {
203
- const accept_patterns = file_accept.split(',').map((p) => p.trim());
148
+ const accept_patterns = file_accept.split(',').map(p => p.trim());
204
149
  const file_extension = '.' + file.name.split('.').pop()?.toLowerCase();
205
150
  const file_mime = file.type;
206
- const matches = accept_patterns.some((pattern) => {
207
- if (pattern.startsWith('.')) {
208
- // Extension match
151
+ const matches = accept_patterns.some(pattern => {
152
+ if (pattern.startsWith('.'))
209
153
  return file_extension === pattern.toLowerCase();
210
- }
211
- else if (pattern.includes('/*')) {
212
- // MIME type wildcard (e.g., "image/*")
213
- const base_type = pattern.split('/')[0];
214
- return file_mime.startsWith(base_type + '/');
215
- }
216
- else {
217
- // Exact MIME type match
218
- return file_mime === pattern;
219
- }
154
+ if (pattern.includes('/*'))
155
+ return file_mime.startsWith(pattern.split('/')[0] + '/');
156
+ return file_mime === pattern;
220
157
  });
221
- if (!matches) {
158
+ if (!matches)
222
159
  return `File type not allowed. Accepted: ${file_accept}`;
223
- }
224
160
  }
225
161
  return null;
226
162
  }, [files.length, max_files, max_size, file_accept, file_validator]);
227
163
  /**
228
- * Upload file to server
164
+ * Upload file via file_manager.upload callback
229
165
  */
230
166
  const upload_file = useCallback(async (file, visibility = 'public') => {
231
- const file_id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
232
- const uploading_id = `${file_id}-${file.name}`;
167
+ const uploading_id = `${Date.now()}-${file.name}`;
233
168
  try {
234
- set_uploading_files((prev) => new Set(prev).add(uploading_id));
235
- const form_data = new FormData();
236
- form_data.append('file', file);
237
- form_data.append('field_id', field_id_final);
238
- if (files_dir) {
239
- form_data.append('files_dir', files_dir);
240
- }
241
- // Add visibility to the form data
242
- form_data.append('visibility', visibility);
243
- // Add required permission for private files (server-side validation)
244
- if (visibility === 'private' && private_files_permission) {
245
- form_data.append('required_permission', private_files_permission);
246
- }
247
- const response = await fetch(upload_endpoint, {
248
- method: 'POST',
249
- body: form_data,
250
- credentials: 'include',
251
- });
252
- if (!response.ok) {
253
- const error_data = await response.json().catch(() => ({ error: 'Upload failed' }));
254
- throw new Error(error_data.error || `Upload failed: ${response.statusText}`);
255
- }
256
- const result = await response.json();
257
- const file_data = {
258
- file_path: result.file_path || result.path || '',
259
- file_name: file.name,
260
- file_size: file.size,
261
- file_type: file.type,
262
- file_id,
263
- uploaded_at: new Date(),
169
+ set_uploading_files(prev => new Set(prev).add(uploading_id));
170
+ const attachment = await file_manager.upload(file, {
171
+ entity_type: 'form_field',
172
+ entity_id: field_id_final,
264
173
  visibility,
265
- };
266
- // Call file processor if provided
267
- if (file_processor && component_ref) {
268
- set_processing_files((prev) => new Set(prev).add(file_data.file_id));
269
- try {
270
- await file_processor(file_data, component_ref);
271
- set_processed_files((prev) => new Set(prev).add(file_data.file_id));
272
- }
273
- finally {
274
- set_processing_files((prev) => {
275
- const next = new Set(prev);
276
- next.delete(file_data.file_id);
277
- return next;
278
- });
279
- }
280
- }
281
- set_upload_errors((prev) => {
174
+ scope_id,
175
+ uploaded_by,
176
+ metadata: files_dir ? { files_dir } : undefined,
177
+ });
178
+ set_upload_errors(prev => {
282
179
  const next = { ...prev };
283
180
  delete next[uploading_id];
284
181
  return next;
285
182
  });
286
- return file_data;
183
+ return attachment;
287
184
  }
288
185
  catch (error) {
289
186
  const error_message = error instanceof Error ? error.message : 'Upload failed';
290
- set_upload_errors((prev) => ({
291
- ...prev,
292
- [uploading_id]: error_message,
293
- }));
187
+ set_upload_errors(prev => ({ ...prev, [uploading_id]: error_message }));
294
188
  return null;
295
189
  }
296
190
  finally {
297
- set_uploading_files((prev) => {
191
+ set_uploading_files(prev => {
298
192
  const next = new Set(prev);
299
193
  next.delete(uploading_id);
300
194
  return next;
301
195
  });
302
196
  }
303
- }, [field_id_final, files_dir, upload_endpoint, file_processor, component_ref, private_files_permission]);
197
+ }, [field_id_final, file_manager, files_dir, scope_id, uploaded_by]);
304
198
  /**
305
199
  * Handle file selection (public files)
306
200
  */
@@ -309,8 +203,7 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
309
203
  return;
310
204
  const files_to_upload = [];
311
205
  const errors = {};
312
- // Validate all files first
313
- Array.from(selected_files).forEach((file) => {
206
+ Array.from(selected_files).forEach(file => {
314
207
  const error = validate_file(file);
315
208
  if (error) {
316
209
  errors[file.name] = error;
@@ -319,16 +212,14 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
319
212
  files_to_upload.push(file);
320
213
  }
321
214
  });
322
- // Set validation errors
323
215
  if (Object.keys(errors).length > 0) {
324
- set_upload_errors((prev) => ({ ...prev, ...errors }));
216
+ set_upload_errors(prev => ({ ...prev, ...errors }));
325
217
  }
326
- // Upload valid files as public
327
- const upload_promises = files_to_upload.map((file) => upload_file(file, 'public'));
328
- const uploaded_files = await Promise.all(upload_promises);
329
- const successful_uploads = uploaded_files.filter((f) => f !== null);
330
- if (successful_uploads.length > 0) {
331
- update_files([...files, ...successful_uploads]);
218
+ const upload_promises = files_to_upload.map(file => upload_file(file, 'public'));
219
+ const uploaded = await Promise.all(upload_promises);
220
+ const successful = uploaded.filter((f) => f !== null);
221
+ if (successful.length > 0) {
222
+ update_files([...files, ...successful]);
332
223
  }
333
224
  }, [files, validate_file, upload_file, update_files]);
334
225
  /**
@@ -339,8 +230,7 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
339
230
  return;
340
231
  const files_to_upload = [];
341
232
  const errors = {};
342
- // Validate all files first (use same validation as public)
343
- Array.from(selected_files).forEach((file) => {
233
+ Array.from(selected_files).forEach(file => {
344
234
  const error = validate_file(file);
345
235
  if (error) {
346
236
  errors[file.name] = error;
@@ -349,21 +239,17 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
349
239
  files_to_upload.push(file);
350
240
  }
351
241
  });
352
- // Set validation errors
353
242
  if (Object.keys(errors).length > 0) {
354
- set_upload_errors((prev) => ({ ...prev, ...errors }));
243
+ set_upload_errors(prev => ({ ...prev, ...errors }));
355
244
  }
356
- // Upload valid files as private
357
- const upload_promises = files_to_upload.map((file) => upload_file(file, 'private'));
358
- const uploaded_files = await Promise.all(upload_promises);
359
- const successful_uploads = uploaded_files.filter((f) => f !== null);
360
- if (successful_uploads.length > 0) {
361
- update_private_files([...private_files, ...successful_uploads]);
245
+ const upload_promises = files_to_upload.map(file => upload_file(file, 'private'));
246
+ const uploaded = await Promise.all(upload_promises);
247
+ const successful = uploaded.filter((f) => f !== null);
248
+ if (successful.length > 0) {
249
+ update_private_files([...private_files, ...successful]);
362
250
  }
363
251
  }, [private_files, validate_file, upload_file, update_private_files]);
364
- /**
365
- * Handle drag and drop
366
- */
252
+ // Drag handlers (public)
367
253
  const handle_drag_enter = useCallback((e) => {
368
254
  e.preventDefault();
369
255
  e.stopPropagation();
@@ -384,9 +270,7 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
384
270
  set_is_dragging(false);
385
271
  handle_files_selected(e.dataTransfer.files);
386
272
  }, [handle_files_selected]);
387
- /**
388
- * Handle private drag and drop
389
- */
273
+ // Drag handlers (private)
390
274
  const handle_private_drag_enter = useCallback((e) => {
391
275
  e.preventDefault();
392
276
  e.stopPropagation();
@@ -403,104 +287,136 @@ export function CollabFormFileUpload({ field_id_final, accept_files_public = fal
403
287
  set_is_dragging_private(false);
404
288
  handle_private_files_selected(e.dataTransfer.files);
405
289
  }, [handle_private_files_selected]);
406
- /**
407
- * Handle file input change
408
- */
290
+ // Input handlers
409
291
  const handle_input_change = useCallback((e) => {
410
292
  handle_files_selected(e.target.files);
411
- // Reset input to allow selecting the same file again
412
- if (file_input_ref.current) {
293
+ if (file_input_ref.current)
413
294
  file_input_ref.current.value = '';
414
- }
415
295
  }, [handle_files_selected]);
416
- /**
417
- * Handle private file input change
418
- */
419
296
  const handle_private_input_change = useCallback((e) => {
420
297
  handle_private_files_selected(e.target.files);
421
- // Reset input to allow selecting the same file again
422
- if (private_file_input_ref.current) {
298
+ if (private_file_input_ref.current)
423
299
  private_file_input_ref.current.value = '';
424
- }
425
300
  }, [handle_private_files_selected]);
426
301
  /**
427
- * Handle file delete (public files)
302
+ * Handle file delete calls file_manager.remove then updates state
428
303
  */
429
- const handle_delete_file = useCallback((file_id) => {
430
- const new_files = files.filter((f) => f.file_id !== file_id);
431
- // Check minimum files constraint
432
- if (min_files !== undefined && new_files.length < min_files) {
433
- set_upload_errors((prev) => ({
434
- ...prev,
435
- delete_error: `Minimum ${min_files} files required`,
436
- }));
304
+ const handle_delete_file = useCallback(async (file_id) => {
305
+ const file = files.find(f => f.file_id === file_id);
306
+ if (!file)
307
+ return;
308
+ if (min_files !== undefined && files.length <= min_files) {
309
+ set_upload_errors(prev => ({ ...prev, delete_error: `Minimum ${min_files} files required` }));
437
310
  return;
438
311
  }
439
- update_files(new_files);
440
- set_upload_errors((prev) => {
441
- const next = { ...prev };
442
- delete next.delete_error;
443
- return next;
444
- });
445
- }, [files, min_files, update_files]);
312
+ try {
313
+ await file_manager.remove(file.file_id, file.ref_id);
314
+ }
315
+ catch (error) {
316
+ logger.warn('[CollabFormFileUpload] Error removing file reference', {
317
+ file_id, ref_id: file.ref_id,
318
+ error: error instanceof Error ? error.message : String(error),
319
+ });
320
+ }
321
+ update_files(files.filter(f => f.file_id !== file_id));
322
+ set_upload_errors(prev => { const next = { ...prev }; delete next.delete_error; return next; });
323
+ }, [files, min_files, update_files, file_manager, logger]);
446
324
  /**
447
325
  * Handle private file delete
448
326
  */
449
- const handle_delete_private_file = useCallback((file_id) => {
450
- const new_files = private_files.filter((f) => f.file_id !== file_id);
451
- update_private_files(new_files);
452
- }, [private_files, update_private_files]);
453
- /**
454
- * Get file URL - handles both public and private files
455
- */
456
- const get_file_url = useCallback((file_path, visibility) => {
457
- if (file_path.startsWith('/')) {
458
- return file_path;
327
+ const handle_delete_private_file = useCallback(async (file_id) => {
328
+ const file = private_files.find(f => f.file_id === file_id);
329
+ if (!file)
330
+ return;
331
+ try {
332
+ await file_manager.remove(file.file_id, file.ref_id);
459
333
  }
460
- // For private files, use the private files endpoint with permission query
461
- if (visibility === 'private' && private_files_permission) {
462
- return `/api/collab-forms/private-files/${encodeURIComponent(file_path)}?permission=${encodeURIComponent(private_files_permission)}`;
334
+ catch (error) {
335
+ logger.warn('[CollabFormFileUpload] Error removing private file reference', {
336
+ file_id, ref_id: file.ref_id,
337
+ error: error instanceof Error ? error.message : String(error),
338
+ });
463
339
  }
464
- return `/api/collab-forms/files/${encodeURIComponent(file_path)}`;
465
- }, [private_files_permission]);
466
- // Build accordion title based on which file types are enabled
340
+ update_private_files(private_files.filter(f => f.file_id !== file_id));
341
+ }, [private_files, update_private_files, file_manager, logger]);
342
+ /**
343
+ * Handle re-upload for a missing file
344
+ */
345
+ const handle_reupload = useCallback(async (original_file_id, visibility) => {
346
+ if (!file_manager.reupload)
347
+ return;
348
+ // Create a file input and trigger selection
349
+ const input = document.createElement('input');
350
+ input.type = 'file';
351
+ if (file_accept)
352
+ input.accept = file_accept;
353
+ input.onchange = async () => {
354
+ const file = input.files?.[0];
355
+ if (!file)
356
+ return;
357
+ const uploading_id = `reupload-${original_file_id}`;
358
+ set_uploading_files(prev => new Set(prev).add(uploading_id));
359
+ try {
360
+ const new_attachment = await file_manager.reupload(file, original_file_id, {
361
+ entity_type: 'form_field',
362
+ entity_id: field_id_final,
363
+ visibility,
364
+ scope_id,
365
+ uploaded_by,
366
+ });
367
+ // Replace the old attachment with the new one
368
+ if (visibility === 'public') {
369
+ update_files(files.map(f => f.file_id === original_file_id ? new_attachment : f));
370
+ }
371
+ else {
372
+ update_private_files(private_files.map(f => f.file_id === original_file_id ? new_attachment : f));
373
+ }
374
+ }
375
+ catch (error) {
376
+ set_upload_errors(prev => ({
377
+ ...prev,
378
+ [uploading_id]: error instanceof Error ? error.message : 'Re-upload failed',
379
+ }));
380
+ }
381
+ finally {
382
+ set_uploading_files(prev => {
383
+ const next = new Set(prev);
384
+ next.delete(uploading_id);
385
+ return next;
386
+ });
387
+ }
388
+ };
389
+ input.click();
390
+ }, [file_manager, field_id_final, file_accept, scope_id, uploaded_by, files, private_files, update_files, update_private_files]);
391
+ /**
392
+ * Get download URL for a file
393
+ */
394
+ const get_file_url = useCallback((attachment) => {
395
+ return file_manager.get_download_url(attachment.file_id, attachment.visibility);
396
+ }, [file_manager]);
397
+ // Build accordion title
467
398
  const accordion_title = accept_files_public && show_private_section
468
399
  ? `Files (${files.length} public, ${private_files.length} private)`
469
400
  : show_private_section && !accept_files_public
470
401
  ? private_files.length > 0 ? `Private Files (${private_files.length})` : 'Private Files'
471
402
  : files.length > 0 ? `Files (${files.length})` : 'Files';
472
- // Always render wrapper div to maintain consistent DOM structure
473
- // This prevents hydration mismatches when file settings change between server and client
474
- return (_jsx("div", { className: "cls_collab_file_upload space-y-2", suppressHydrationWarning: true, children: (accept_files_public || accept_files_private) ? (
475
- /* Dynamic import for Accordion - will be loaded by consuming app */
476
- _jsx(FileUploadAccordion, { field_id_final: field_id_final, title: accordion_title, is_dragging: is_dragging, on_drag_enter: handle_drag_enter, on_drag_leave: handle_drag_leave, on_drag_over: handle_drag_over, on_drop: handle_drop, file_input_ref: file_input_ref, file_accept: file_accept, on_input_change: handle_input_change, files: files, uploading_files: uploading_files, processing_files: processing_files, processed_files: processed_files, upload_errors: upload_errors, get_file_url: get_file_url, on_file_delete: handle_delete_file, get_file_icon: get_file_icon, format_file_size: format_file_size, max_files: max_files, min_files: min_files,
477
- // File mode props
478
- accept_files_public: accept_files_public, show_private_section: show_private_section, private_files: private_files, private_file_input_ref: private_file_input_ref, is_dragging_private: is_dragging_private, on_private_drag_enter: handle_private_drag_enter, on_private_drag_leave: handle_private_drag_leave, on_private_drop: handle_private_drop, on_private_input_change: handle_private_input_change, on_private_file_delete: handle_delete_private_file, on_toggle_visibility: toggle_file_visibility })) : null }));
403
+ return (_jsx("div", { className: "cls_collab_file_upload space-y-2", suppressHydrationWarning: true, children: (accept_files_public || accept_files_private) ? (_jsx(FileUploadAccordion, { field_id_final: field_id_final, title: accordion_title, is_dragging: is_dragging, on_drag_enter: handle_drag_enter, on_drag_leave: handle_drag_leave, on_drag_over: handle_drag_over, on_drop: handle_drop, file_input_ref: file_input_ref, file_accept: file_accept, on_input_change: handle_input_change, files: files, uploading_files: uploading_files, upload_errors: upload_errors, get_file_url: get_file_url, on_file_delete: handle_delete_file, format_file_size: format_file_size, max_files: max_files, min_files: min_files, accept_files_public: accept_files_public, show_private_section: show_private_section, private_files: private_files, private_file_input_ref: private_file_input_ref, is_dragging_private: is_dragging_private, on_private_drag_enter: handle_private_drag_enter, on_private_drag_leave: handle_private_drag_leave, on_private_drop: handle_private_drop, on_private_input_change: handle_private_input_change, on_private_file_delete: handle_delete_private_file, on_toggle_visibility: toggle_file_visibility, show_file_status: show_file_status, allow_reupload: allow_reupload, file_statuses: file_statuses, on_reupload: handle_reupload })) : null }));
479
404
  }
480
405
  /**
481
406
  * File upload accordion component with dynamic imports
482
407
  */
483
- function FileUploadAccordion({ field_id_final, title, is_dragging, on_drag_enter, on_drag_leave, on_drag_over, on_drop, file_input_ref, file_accept, on_input_change, files, uploading_files, processing_files, processed_files, upload_errors, get_file_url, on_file_delete, get_file_icon, format_file_size, max_files, min_files,
484
- // File mode props
485
- accept_files_public = false, show_private_section = false, private_files = [], private_file_input_ref, is_dragging_private = false, on_private_drag_enter, on_private_drag_leave, on_private_drop, on_private_input_change, on_private_file_delete, on_toggle_visibility, }) {
408
+ function FileUploadAccordion({ field_id_final, title, is_dragging, on_drag_enter, on_drag_leave, on_drag_over, on_drop, file_input_ref, file_accept, on_input_change, files, uploading_files, upload_errors, get_file_url, on_file_delete, format_file_size, max_files, min_files, accept_files_public = false, show_private_section = false, private_files = [], private_file_input_ref, is_dragging_private = false, on_private_drag_enter, on_private_drag_leave, on_private_drop, on_private_input_change, on_private_file_delete, on_toggle_visibility, show_file_status = false, allow_reupload = false, file_statuses = {}, on_reupload, }) {
486
409
  const logger = use_logger();
487
410
  const [AccordionComponents, set_accordion_components] = React.useState(null);
488
411
  const [is_loading, set_is_loading] = React.useState(true);
489
412
  const [is_mounted, set_is_mounted] = React.useState(false);
490
- // Track client-side mounting to prevent hydration mismatch
491
- React.useEffect(() => {
492
- set_is_mounted(true);
493
- }, []);
413
+ React.useEffect(() => { set_is_mounted(true); }, []);
494
414
  React.useEffect(() => {
495
- // Only load accordion on client side
496
415
  if (!is_mounted)
497
416
  return;
498
- // Dynamic import - components will be resolved by consuming app's bundler
499
417
  const load_accordion = async () => {
500
418
  try {
501
419
  set_is_loading(true);
502
- // Try to import from consuming app's components directory
503
- // This path will be resolved by Next.js/webpack in the consuming app
504
420
  // @ts-expect-error - These modules are provided by the consuming application
505
421
  const accordion_module = await import('@/components/ui/accordion').catch(() => null);
506
422
  if (accordion_module) {
@@ -528,42 +444,31 @@ accept_files_public = false, show_private_section = false, private_files = [], p
528
444
  };
529
445
  load_accordion();
530
446
  }, [is_mounted]);
531
- // Always render consistent structure to prevent hydration mismatch
532
- // During SSR and initial client render, show empty placeholder
533
- // After mount and accordion load, show actual content
534
447
  if (!is_mounted || is_loading || !AccordionComponents) {
535
448
  return (_jsx("div", { className: "cls_collab_file_upload_loading text-sm text-muted-foreground", suppressHydrationWarning: true }));
536
449
  }
537
450
  const { Accordion, AccordionItem, AccordionTrigger, AccordionContent } = AccordionComponents;
538
- // Default to open if files exist
539
451
  const default_value = files.length > 0 ? 'files' : undefined;
540
- // Determine if files are required (min_files >= 1)
541
452
  const is_files_required = min_files !== undefined && min_files >= 1;
542
453
  /**
543
- * Render a file item with optional visibility toggle
454
+ * Render a single file item
544
455
  */
545
- const render_file_item = (file_data, visibility, delete_handler) => {
546
- const is_uploading = Array.from(uploading_files).some((id) => id.startsWith(file_data.file_id));
547
- const is_processing = processing_files.has(file_data.file_id);
548
- const is_processed = processed_files.has(file_data.file_id);
549
- return (_jsxs("div", { className: "cls_collab_file_item flex-shrink-0 w-24 flex flex-col items-center gap-1 p-2 border rounded-md hover:bg-muted transition-colors relative", children: [_jsxs("a", { href: get_file_url(file_data.file_path, visibility), target: "_blank", rel: "noopener noreferrer", className: cn("flex flex-col items-center gap-1 w-full", is_uploading && "pointer-events-none opacity-50"), "aria-disabled": is_uploading, children: [_jsxs("div", { className: "relative", children: [get_file_icon(file_data.file_type, file_data.file_name), is_processing && (_jsx("div", { className: "absolute -bottom-1 -right-1 bg-background rounded-full p-0.5 shadow-sm border border-border", children: _jsx(FaSpinner, { className: "h-3 w-3 animate-spin text-primary" }) }))] }), _jsx("span", { className: "text-xs text-center truncate w-full", title: file_data.file_name, children: file_data.file_name }), _jsx("span", { className: "text-xs text-muted-foreground", children: format_file_size(file_data.file_size) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [accept_files_public && show_private_section && on_toggle_visibility && (_jsx("button", { type: "button", onClick: () => on_toggle_visibility(file_data.file_id, visibility), className: cn("p-1 transition-colors", visibility === 'private'
456
+ const render_file_item = (attachment, visibility, delete_handler) => {
457
+ const is_uploading = Array.from(uploading_files).some(id => id.includes(attachment.file_id) || id.includes(attachment.file_name));
458
+ const file_status = file_statuses[attachment.file_id] ?? attachment.status;
459
+ const is_missing = file_status === 'missing';
460
+ return (_jsxs("div", { className: "cls_collab_file_item flex-shrink-0 w-24 flex flex-col items-center gap-1 p-2 border rounded-md hover:bg-muted transition-colors relative", children: [_jsxs("a", { href: get_file_url(attachment), target: "_blank", rel: "noopener noreferrer", className: cn("flex flex-col items-center gap-1 w-full", (is_uploading || is_missing) && "pointer-events-none opacity-50"), "aria-disabled": is_uploading || is_missing, children: [_jsx("div", { className: "relative", children: get_file_icon(attachment.mime_type, attachment.file_name) }), _jsx("span", { className: "text-xs text-center truncate w-full", title: attachment.file_name, children: attachment.file_name }), _jsx("span", { className: "text-xs text-muted-foreground", children: format_file_size(attachment.file_size) })] }), show_file_status && file_status && file_status !== 'active' && (_jsx(StatusBadge, { status: file_status })), _jsxs("div", { className: "flex items-center gap-1", children: [allow_reupload && is_missing && on_reupload && (_jsx("button", { type: "button", onClick: () => on_reupload(attachment.file_id, visibility), className: "text-blue-600 hover:text-blue-700 p-1", title: "Re-upload file", "aria-label": `Re-upload ${attachment.file_name}`, children: _jsx(HiRefresh, { className: "h-4 w-4" }) })), accept_files_public && show_private_section && on_toggle_visibility && (_jsx("button", { type: "button", onClick: () => on_toggle_visibility(attachment.file_id, visibility), className: cn("p-1 transition-colors", visibility === 'private'
550
461
  ? "text-amber-600 hover:text-amber-700"
551
- : "text-muted-foreground hover:text-foreground"), disabled: is_uploading, "aria-label": visibility === 'private' ? `Make ${file_data.file_name} public` : `Make ${file_data.file_name} private`, title: visibility === 'private' ? 'Make public' : 'Make private', children: visibility === 'private' ? (_jsx(HiLockClosed, { className: "h-4 w-4" })) : (_jsx(HiLockOpen, { className: "h-4 w-4" })) })), _jsx("button", { type: "button", onClick: () => delete_handler(file_data.file_id), className: "text-destructive hover:text-destructive/80 p-1", disabled: is_uploading, "aria-label": `Delete ${file_data.file_name}`, children: _jsx(HiTrash, { className: "h-4 w-4" }) }), is_processed && !is_processing && (_jsx(HiCheckCircle, { className: "h-4 w-4 text-green-500" }))] }), is_uploading && (_jsx("div", { className: "absolute inset-0 bg-background/50 flex items-center justify-center rounded-md", children: _jsx("div", { className: "text-xs text-muted-foreground", children: "Uploading..." }) }))] }, file_data.file_id));
462
+ : "text-muted-foreground hover:text-foreground"), disabled: is_uploading, "aria-label": visibility === 'private' ? `Make ${attachment.file_name} public` : `Make ${attachment.file_name} private`, title: visibility === 'private' ? 'Make public' : 'Make private', children: visibility === 'private' ? (_jsx(HiLockClosed, { className: "h-4 w-4" })) : (_jsx(HiLockOpen, { className: "h-4 w-4" })) })), _jsx("button", { type: "button", onClick: () => delete_handler(attachment.file_id), className: "text-destructive hover:text-destructive/80 p-1", disabled: is_uploading, "aria-label": `Delete ${attachment.file_name}`, children: _jsx(HiTrash, { className: "h-4 w-4" }) })] }), is_uploading && (_jsx("div", { className: "absolute inset-0 bg-background/50 flex items-center justify-center rounded-md", children: _jsx("div", { className: "text-xs text-muted-foreground", children: "Uploading..." }) }))] }, attachment.file_id));
552
463
  };
553
464
  /**
554
465
  * Render file upload section (drop zone + file list)
555
466
  */
556
467
  const render_file_section = (section_type, section_files, section_is_dragging, section_on_drag_enter, section_on_drag_leave, section_on_drop, section_file_input_ref, section_on_input_change, section_on_delete, input_id_suffix) => (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: cn('cls_collab_file_drop_zone border-2 border-dashed rounded-md p-4 text-center transition-colors', section_is_dragging
557
468
  ? 'border-primary bg-primary/5'
558
- : 'border-input hover:border-primary/50 hover:bg-muted/50', section_files.length >= max_files && 'opacity-50 cursor-not-allowed'), onDragEnter: section_on_drag_enter, onDragLeave: section_on_drag_leave, onDragOver: on_drag_over, onDrop: section_on_drop, children: [_jsx("input", { ref: section_file_input_ref, type: "file", id: `${field_id_final}-${input_id_suffix}`, className: "hidden", accept: file_accept, multiple: true, onChange: section_on_input_change, disabled: section_files.length >= max_files }), _jsxs("label", { htmlFor: `${field_id_final}-${input_id_suffix}`, className: cn('cursor-pointer block', section_files.length >= max_files && 'cursor-not-allowed opacity-50'), children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Drag and drop files here, or click to select" }), file_accept && (_jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: ["Accepted: ", file_accept] }))] })] }), section_files.length > 0 && (_jsx("div", { className: "cls_collab_file_list", children: _jsx("div", { className: "flex overflow-x-auto gap-2 pb-2", children: section_files.map((file_data) => render_file_item(file_data, section_type, section_on_delete)) }) }))] }));
559
- // Wrap Accordion in div with suppressHydrationWarning to prevent React from checking children
560
- // The Accordion content may differ between server and client due to dynamic imports
561
- return (_jsx("div", { suppressHydrationWarning: true, children: _jsx(Accordion, { type: "single", collapsible: true, defaultValue: default_value, className: "w-full", children: _jsxs(AccordionItem, { value: "files", children: [_jsxs(AccordionTrigger, { className: "text-sm font-medium", children: [title, is_files_required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsx(AccordionContent, { children: _jsxs("div", { className: "space-y-4", children: [Object.keys(upload_errors).length > 0 && (_jsx("div", { className: "space-y-1", children: Object.entries(upload_errors).map(([key, error]) => (_jsx("p", { className: "text-sm text-destructive", children: error }, key))) })), accept_files_public && show_private_section ? (_jsxs(Accordion, { type: "multiple", defaultValue: ['public-files', 'private-files'], className: "w-full", children: [_jsxs(AccordionItem, { value: "public-files", className: "border-b-0", children: [_jsx(AccordionTrigger, { className: "text-sm font-medium py-2", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(HiLockOpen, { className: "h-4 w-4 text-muted-foreground" }), _jsxs("span", { children: ["Public Files (", files.length, ")"] })] }) }), _jsx(AccordionContent, { className: "pb-4", children: render_file_section('public', files, is_dragging, on_drag_enter, on_drag_leave, on_drop, file_input_ref, on_input_change, on_file_delete, 'public-file-input') })] }), _jsxs(AccordionItem, { value: "private-files", className: "border-b-0", children: [_jsx(AccordionTrigger, { className: "text-sm font-medium py-2", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(HiLockClosed, { className: "h-4 w-4 text-amber-600" }), _jsxs("span", { children: ["Private Files (", private_files.length, ")"] })] }) }), _jsx(AccordionContent, { className: "pb-4", children: private_file_input_ref && on_private_drag_enter && on_private_drag_leave && on_private_drop && on_private_input_change && on_private_file_delete && render_file_section('private', private_files, is_dragging_private, on_private_drag_enter, on_private_drag_leave, on_private_drop, private_file_input_ref, on_private_input_change, on_private_file_delete, 'private-file-input') })] })] })) : show_private_section && !accept_files_public ? (
562
- /* Private-only section UI */
563
- _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm text-amber-600 mb-2", children: [_jsx(HiLockClosed, { className: "h-4 w-4" }), _jsx("span", { className: "font-medium", children: "Private Files" })] }), private_file_input_ref && on_private_drag_enter && on_private_drag_leave && on_private_drop && on_private_input_change && on_private_file_delete && render_file_section('private', private_files, is_dragging_private, on_private_drag_enter, on_private_drag_leave, on_private_drop, private_file_input_ref, on_private_input_change, on_private_file_delete, 'private-file-input')] })) : accept_files_public ? (
564
- /* Public-only section UI (original behavior) */
565
- _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: cn('cls_collab_file_drop_zone border-2 border-dashed rounded-md p-6 text-center transition-colors', is_dragging
469
+ : 'border-input hover:border-primary/50 hover:bg-muted/50', section_files.length >= max_files && 'opacity-50 cursor-not-allowed'), onDragEnter: section_on_drag_enter, onDragLeave: section_on_drag_leave, onDragOver: on_drag_over, onDrop: section_on_drop, children: [_jsx("input", { ref: section_file_input_ref, type: "file", id: `${field_id_final}-${input_id_suffix}`, className: "hidden", accept: file_accept, multiple: true, onChange: section_on_input_change, disabled: section_files.length >= max_files }), _jsxs("label", { htmlFor: `${field_id_final}-${input_id_suffix}`, className: cn('cursor-pointer block', section_files.length >= max_files && 'cursor-not-allowed opacity-50'), children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Drag and drop files here, or click to select" }), file_accept && (_jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: ["Accepted: ", file_accept] }))] })] }), section_files.length > 0 && (_jsx("div", { className: "cls_collab_file_list", children: _jsx("div", { className: "flex overflow-x-auto gap-2 pb-2", children: section_files.map(attachment => render_file_item(attachment, section_type, section_on_delete)) }) }))] }));
470
+ return (_jsx("div", { suppressHydrationWarning: true, children: _jsx(Accordion, { type: "single", collapsible: true, defaultValue: default_value, className: "w-full", children: _jsxs(AccordionItem, { value: "files", children: [_jsxs(AccordionTrigger, { className: "text-sm font-medium", children: [title, is_files_required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsx(AccordionContent, { children: _jsxs("div", { className: "space-y-4", children: [Object.keys(upload_errors).length > 0 && (_jsx("div", { className: "space-y-1", children: Object.entries(upload_errors).map(([key, error]) => (_jsx("p", { className: "text-sm text-destructive", children: error }, key))) })), accept_files_public && show_private_section ? (_jsxs(Accordion, { type: "multiple", defaultValue: ['public-files', 'private-files'], className: "w-full", children: [_jsxs(AccordionItem, { value: "public-files", className: "border-b-0", children: [_jsx(AccordionTrigger, { className: "text-sm font-medium py-2", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(HiLockOpen, { className: "h-4 w-4 text-muted-foreground" }), _jsxs("span", { children: ["Public Files (", files.length, ")"] })] }) }), _jsx(AccordionContent, { className: "pb-4", children: render_file_section('public', files, is_dragging, on_drag_enter, on_drag_leave, on_drop, file_input_ref, on_input_change, on_file_delete, 'public-file-input') })] }), _jsxs(AccordionItem, { value: "private-files", className: "border-b-0", children: [_jsx(AccordionTrigger, { className: "text-sm font-medium py-2", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(HiLockClosed, { className: "h-4 w-4 text-amber-600" }), _jsxs("span", { children: ["Private Files (", private_files.length, ")"] })] }) }), _jsx(AccordionContent, { className: "pb-4", children: private_file_input_ref && on_private_drag_enter && on_private_drag_leave && on_private_drop && on_private_input_change && on_private_file_delete && render_file_section('private', private_files, is_dragging_private, on_private_drag_enter, on_private_drag_leave, on_private_drop, private_file_input_ref, on_private_input_change, on_private_file_delete, 'private-file-input') })] })] })) : show_private_section && !accept_files_public ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm text-amber-600 mb-2", children: [_jsx(HiLockClosed, { className: "h-4 w-4" }), _jsx("span", { className: "font-medium", children: "Private Files" })] }), private_file_input_ref && on_private_drag_enter && on_private_drag_leave && on_private_drop && on_private_input_change && on_private_file_delete && render_file_section('private', private_files, is_dragging_private, on_private_drag_enter, on_private_drag_leave, on_private_drop, private_file_input_ref, on_private_input_change, on_private_file_delete, 'private-file-input')] })) : accept_files_public ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: cn('cls_collab_file_drop_zone border-2 border-dashed rounded-md p-6 text-center transition-colors', is_dragging
566
471
  ? 'border-primary bg-primary/5'
567
- : 'border-input hover:border-primary/50 hover:bg-muted/50', files.length >= max_files && 'opacity-50 cursor-not-allowed'), onDragEnter: on_drag_enter, onDragLeave: on_drag_leave, onDragOver: on_drag_over, onDrop: on_drop, children: [_jsx("input", { ref: file_input_ref, type: "file", id: `${field_id_final}-file-input`, className: "hidden", accept: file_accept, multiple: true, onChange: on_input_change, disabled: files.length >= max_files }), _jsx("label", { htmlFor: `${field_id_final}-file-input`, className: cn('cursor-pointer block', files.length >= max_files && 'cursor-not-allowed opacity-50'), children: _jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Drag and drop files here, or click to select" }), file_accept && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Accepted: ", file_accept] }))] }) })] }), files.length > 0 && (_jsx("div", { className: "cls_collab_file_list space-y-2", children: _jsx("div", { className: "flex overflow-x-auto gap-2 pb-2", children: files.map((file_data) => render_file_item(file_data, 'public', on_file_delete)) }) }))] })) : null] }) })] }) }) }));
472
+ : 'border-input hover:border-primary/50 hover:bg-muted/50', files.length >= max_files && 'opacity-50 cursor-not-allowed'), onDragEnter: on_drag_enter, onDragLeave: on_drag_leave, onDragOver: on_drag_over, onDrop: on_drop, children: [_jsx("input", { ref: file_input_ref, type: "file", id: `${field_id_final}-file-input`, className: "hidden", accept: file_accept, multiple: true, onChange: on_input_change, disabled: files.length >= max_files }), _jsx("label", { htmlFor: `${field_id_final}-file-input`, className: cn('cursor-pointer block', files.length >= max_files && 'cursor-not-allowed opacity-50'), children: _jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Drag and drop files here, or click to select" }), file_accept && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Accepted: ", file_accept] }))] }) })] }), files.length > 0 && (_jsx("div", { className: "cls_collab_file_list space-y-2", children: _jsx("div", { className: "flex overflow-x-auto gap-2 pb-2", children: files.map(attachment => render_file_item(attachment, 'public', on_file_delete)) }) }))] })) : null] }) })] }) }) }));
568
473
  }
569
474
  //# sourceMappingURL=collab_form_file_upload.js.map