hazo_collab_forms 1.9.0 → 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 (195) hide show
  1. package/CHANGE_LOG.md +25 -0
  2. package/README.md +157 -44
  3. package/dist/components/_internal_form_set.d.ts +30 -30
  4. package/dist/components/_internal_form_set.d.ts.map +1 -1
  5. package/dist/components/_internal_form_set.js +31 -12
  6. package/dist/components/_internal_form_set.js.map +1 -1
  7. package/dist/components/collab_form_file_upload.d.ts +40 -74
  8. package/dist/components/collab_form_file_upload.d.ts.map +1 -1
  9. package/dist/components/collab_form_file_upload.js +191 -275
  10. package/dist/components/collab_form_file_upload.js.map +1 -1
  11. package/dist/components/hazo_add_field_dialog/components/pending_field_item.d.ts +1 -1
  12. package/dist/components/hazo_add_field_dialog/components/pending_field_item.js +2 -2
  13. package/dist/components/hazo_add_field_dialog/components/pending_field_item.js.map +1 -1
  14. package/dist/components/hazo_add_field_dialog/components/pending_field_list.d.ts +1 -1
  15. package/dist/components/hazo_add_field_dialog/hazo_add_field_dialog.js +7 -7
  16. package/dist/components/hazo_add_field_dialog/hazo_add_field_dialog.js.map +1 -1
  17. package/dist/components/hazo_add_field_dialog/types.d.ts +4 -4
  18. package/dist/components/hazo_add_field_dialog/types.d.ts.map +1 -1
  19. package/dist/components/hazo_add_field_dialog/utils/library_to_pending.js +2 -2
  20. package/dist/components/hazo_add_field_dialog/utils/library_to_pending.js.map +1 -1
  21. package/dist/components/hazo_add_group_dialog/hazo_add_group_dialog.js +6 -6
  22. package/dist/components/hazo_add_group_dialog/hazo_add_group_dialog.js.map +1 -1
  23. package/dist/components/hazo_collab_form_base.d.ts +26 -20
  24. package/dist/components/hazo_collab_form_base.d.ts.map +1 -1
  25. package/dist/components/hazo_collab_form_base.js +22 -13
  26. package/dist/components/hazo_collab_form_base.js.map +1 -1
  27. package/dist/components/hazo_collab_form_checkbox.d.ts +1 -24
  28. package/dist/components/hazo_collab_form_checkbox.d.ts.map +1 -1
  29. package/dist/components/hazo_collab_form_checkbox.js +15 -141
  30. package/dist/components/hazo_collab_form_checkbox.js.map +1 -1
  31. package/dist/components/hazo_collab_form_combo.d.ts +0 -44
  32. package/dist/components/hazo_collab_form_combo.d.ts.map +1 -1
  33. package/dist/components/hazo_collab_form_combo.js +24 -199
  34. package/dist/components/hazo_collab_form_combo.js.map +1 -1
  35. package/dist/components/hazo_collab_form_data_table.d.ts.map +1 -1
  36. package/dist/components/hazo_collab_form_data_table.js +2 -1
  37. package/dist/components/hazo_collab_form_data_table.js.map +1 -1
  38. package/dist/components/hazo_collab_form_date.d.ts +1 -59
  39. package/dist/components/hazo_collab_form_date.d.ts.map +1 -1
  40. package/dist/components/hazo_collab_form_date.js +34 -194
  41. package/dist/components/hazo_collab_form_date.js.map +1 -1
  42. package/dist/components/hazo_collab_form_doc.d.ts +8 -11
  43. package/dist/components/hazo_collab_form_doc.d.ts.map +1 -1
  44. package/dist/components/hazo_collab_form_doc.js +15 -12
  45. package/dist/components/hazo_collab_form_doc.js.map +1 -1
  46. package/dist/components/hazo_collab_form_group.d.ts +4 -3
  47. package/dist/components/hazo_collab_form_group.d.ts.map +1 -1
  48. package/dist/components/hazo_collab_form_group.js +5 -5
  49. package/dist/components/hazo_collab_form_group.js.map +1 -1
  50. package/dist/components/hazo_collab_form_inputbox.d.ts +0 -17
  51. package/dist/components/hazo_collab_form_inputbox.d.ts.map +1 -1
  52. package/dist/components/hazo_collab_form_inputbox.js +25 -148
  53. package/dist/components/hazo_collab_form_inputbox.js.map +1 -1
  54. package/dist/components/hazo_collab_form_radio.d.ts +1 -36
  55. package/dist/components/hazo_collab_form_radio.d.ts.map +1 -1
  56. package/dist/components/hazo_collab_form_radio.js +18 -143
  57. package/dist/components/hazo_collab_form_radio.js.map +1 -1
  58. package/dist/components/hazo_collab_form_textarea.d.ts +0 -17
  59. package/dist/components/hazo_collab_form_textarea.d.ts.map +1 -1
  60. package/dist/components/hazo_collab_form_textarea.js +14 -141
  61. package/dist/components/hazo_collab_form_textarea.js.map +1 -1
  62. package/dist/components/hazo_collab_form_view/context.d.ts.map +1 -1
  63. package/dist/components/hazo_collab_form_view/context.js +4 -0
  64. package/dist/components/hazo_collab_form_view/context.js.map +1 -1
  65. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts +36 -0
  66. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.d.ts.map +1 -0
  67. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js +82 -0
  68. package/dist/components/hazo_collab_form_view/hooks/use_view_callbacks.js.map +1 -0
  69. package/dist/components/hazo_collab_form_view/index.d.ts +1 -1
  70. package/dist/components/hazo_collab_form_view/index.d.ts.map +1 -1
  71. package/dist/components/hazo_collab_form_view/index.js.map +1 -1
  72. package/dist/components/hazo_collab_form_view/types.d.ts +18 -12
  73. package/dist/components/hazo_collab_form_view/types.d.ts.map +1 -1
  74. package/dist/components/hazo_collab_form_view/views/edit_view.d.ts.map +1 -1
  75. package/dist/components/hazo_collab_form_view/views/edit_view.js +11 -81
  76. package/dist/components/hazo_collab_form_view/views/edit_view.js.map +1 -1
  77. package/dist/components/hazo_collab_form_view/views/summary_view.d.ts.map +1 -1
  78. package/dist/components/hazo_collab_form_view/views/summary_view.js +58 -278
  79. package/dist/components/hazo_collab_form_view/views/summary_view.js.map +1 -1
  80. package/dist/components/hazo_edit_field_dialog/hazo_edit_field_dialog.js +8 -8
  81. package/dist/components/hazo_edit_field_dialog/hazo_edit_field_dialog.js.map +1 -1
  82. package/dist/components/hazo_edit_field_dialog/types.d.ts +4 -4
  83. package/dist/components/hazo_edit_field_dialog/types.d.ts.map +1 -1
  84. package/dist/components/hazo_edit_group_dialog/hazo_edit_group_dialog.js +15 -15
  85. package/dist/components/hazo_edit_group_dialog/hazo_edit_group_dialog.js.map +1 -1
  86. package/dist/components/hazo_edit_group_dialog/types.d.ts +4 -4
  87. package/dist/components/hazo_edit_group_dialog/types.d.ts.map +1 -1
  88. package/dist/components/hazo_field_library/hooks/use_elements.d.ts.map +1 -1
  89. package/dist/components/hazo_field_library/hooks/use_elements.js +2 -1
  90. package/dist/components/hazo_field_library/hooks/use_elements.js.map +1 -1
  91. package/dist/components/hazo_field_selector_dialog/utils.d.ts.map +1 -1
  92. package/dist/components/hazo_field_selector_dialog/utils.js +2 -1
  93. package/dist/components/hazo_field_selector_dialog/utils.js.map +1 -1
  94. package/dist/components/hazo_template_generator/components/section_tree.js +1 -1
  95. package/dist/components/hazo_template_generator/components/section_tree.js.map +1 -1
  96. package/dist/components/hazo_template_generator/components/tabs/basic_tab.d.ts.map +1 -1
  97. package/dist/components/hazo_template_generator/components/tabs/basic_tab.js +6 -3
  98. package/dist/components/hazo_template_generator/components/tabs/basic_tab.js.map +1 -1
  99. package/dist/components/hazo_template_generator/components/tabs/section_tab.d.ts.map +1 -1
  100. package/dist/components/hazo_template_generator/components/tabs/section_tab.js +8 -4
  101. package/dist/components/hazo_template_generator/components/tabs/section_tab.js.map +1 -1
  102. package/dist/components/hazo_template_generator/types.d.ts.map +1 -1
  103. package/dist/components/hazo_template_generator/types.js +6 -3
  104. package/dist/components/hazo_template_generator/types.js.map +1 -1
  105. package/dist/components/hazo_template_generator/utils/validation.js +1 -1
  106. package/dist/components/hazo_template_generator/utils/validation.js.map +1 -1
  107. package/dist/components/index.d.ts +4 -0
  108. package/dist/components/index.d.ts.map +1 -1
  109. package/dist/components/index.js +2 -0
  110. package/dist/components/index.js.map +1 -1
  111. package/dist/components/shared/base_field_layout.d.ts +40 -0
  112. package/dist/components/shared/base_field_layout.d.ts.map +1 -0
  113. package/dist/components/shared/base_field_layout.js +34 -0
  114. package/dist/components/shared/base_field_layout.js.map +1 -0
  115. package/dist/components/shared/file_settings/file_settings_checkboxes.d.ts +13 -13
  116. package/dist/components/shared/file_settings/file_settings_checkboxes.d.ts.map +1 -1
  117. package/dist/components/shared/file_settings/file_settings_checkboxes.js +8 -25
  118. package/dist/components/shared/file_settings/file_settings_checkboxes.js.map +1 -1
  119. package/dist/components/shared/row_color_utils.d.ts +16 -0
  120. package/dist/components/shared/row_color_utils.d.ts.map +1 -0
  121. package/dist/components/shared/row_color_utils.js +21 -0
  122. package/dist/components/shared/row_color_utils.js.map +1 -0
  123. package/dist/components/shared/strip_base_props.d.ts +16 -0
  124. package/dist/components/shared/strip_base_props.d.ts.map +1 -0
  125. package/dist/components/shared/strip_base_props.js +91 -0
  126. package/dist/components/shared/strip_base_props.js.map +1 -0
  127. package/dist/components/shared/summary_files/summary_files.d.ts +10 -5
  128. package/dist/components/shared/summary_files/summary_files.d.ts.map +1 -1
  129. package/dist/components/shared/summary_files/summary_files.js +35 -14
  130. package/dist/components/shared/summary_files/summary_files.js.map +1 -1
  131. package/dist/components/shared/summary_utils/file_helpers.d.ts +16 -0
  132. package/dist/components/shared/summary_utils/file_helpers.d.ts.map +1 -0
  133. package/dist/components/shared/summary_utils/file_helpers.js +26 -0
  134. package/dist/components/shared/summary_utils/file_helpers.js.map +1 -0
  135. package/dist/components/shared/summary_utils/index.d.ts +1 -0
  136. package/dist/components/shared/summary_utils/index.d.ts.map +1 -1
  137. package/dist/components/shared/summary_utils/index.js +1 -0
  138. package/dist/components/shared/summary_utils/index.js.map +1 -1
  139. package/dist/components/shared/unified_field_controls/index.d.ts +1 -0
  140. package/dist/components/shared/unified_field_controls/index.d.ts.map +1 -1
  141. package/dist/components/shared/unified_field_controls/index.js +2 -1
  142. package/dist/components/shared/unified_field_controls/index.js.map +1 -1
  143. package/dist/components/shared/unified_field_controls/use_field_control_props.d.ts +68 -0
  144. package/dist/components/shared/unified_field_controls/use_field_control_props.d.ts.map +1 -0
  145. package/dist/components/shared/unified_field_controls/use_field_control_props.js +105 -0
  146. package/dist/components/shared/unified_field_controls/use_field_control_props.js.map +1 -0
  147. package/dist/components/shared/use_base_form_field.d.ts +193 -0
  148. package/dist/components/shared/use_base_form_field.d.ts.map +1 -0
  149. package/dist/components/shared/use_base_form_field.js +265 -0
  150. package/dist/components/shared/use_base_form_field.js.map +1 -0
  151. package/dist/config/api_endpoints.d.ts +19 -0
  152. package/dist/config/api_endpoints.d.ts.map +1 -0
  153. package/dist/config/api_endpoints.js +19 -0
  154. package/dist/config/api_endpoints.js.map +1 -0
  155. package/dist/config/defaults.d.ts +17 -0
  156. package/dist/config/defaults.d.ts.map +1 -0
  157. package/dist/config/defaults.js +17 -0
  158. package/dist/config/defaults.js.map +1 -0
  159. package/dist/config/index.d.ts +3 -0
  160. package/dist/config/index.d.ts.map +1 -0
  161. package/dist/config/index.js +3 -0
  162. package/dist/config/index.js.map +1 -0
  163. package/dist/types/file_attachment.d.ts +55 -0
  164. package/dist/types/file_attachment.d.ts.map +1 -0
  165. package/dist/types/file_attachment.js +54 -0
  166. package/dist/types/file_attachment.js.map +1 -0
  167. package/dist/types/file_manager.d.ts +60 -0
  168. package/dist/types/file_manager.d.ts.map +1 -0
  169. package/dist/types/file_manager.js +8 -0
  170. package/dist/types/file_manager.js.map +1 -0
  171. package/dist/types/index.d.ts +3 -0
  172. package/dist/types/index.d.ts.map +1 -1
  173. package/dist/types/index.js +1 -0
  174. package/dist/types/index.js.map +1 -1
  175. package/dist/utils/filter_dom_props.d.ts.map +1 -1
  176. package/dist/utils/filter_dom_props.js +2 -1
  177. package/dist/utils/filter_dom_props.js.map +1 -1
  178. package/dist/utils/index.d.ts +2 -0
  179. package/dist/utils/index.d.ts.map +1 -1
  180. package/dist/utils/index.js +1 -0
  181. package/dist/utils/index.js.map +1 -1
  182. package/dist/utils/use_chat_messages_check.d.ts.map +1 -1
  183. package/dist/utils/use_chat_messages_check.js +4 -2
  184. package/dist/utils/use_chat_messages_check.js.map +1 -1
  185. package/dist/utils/use_collab_chat.d.ts.map +1 -1
  186. package/dist/utils/use_collab_chat.js +2 -1
  187. package/dist/utils/use_collab_chat.js.map +1 -1
  188. package/dist/utils/use_field_chat_status.d.ts.map +1 -1
  189. package/dist/utils/use_field_chat_status.js +5 -3
  190. package/dist/utils/use_field_chat_status.js.map +1 -1
  191. package/dist/utils/use_file_status.d.ts +29 -0
  192. package/dist/utils/use_file_status.d.ts.map +1 -0
  193. package/dist/utils/use_file_status.js +68 -0
  194. package/dist/utils/use_file_status.js.map +1 -0
  195. package/package.json +4 -1
@@ -1,35 +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 { HiTrash, HiPaperClip, HiPhotograph, HiDocument, HiDocumentText, HiCheckCircle, HiLockClosed, HiLockOpen } from 'react-icons/hi';
11
- import { FaSpinner } from 'react-icons/fa';
13
+ import { HiTrash, HiPaperClip, HiPhotograph, HiDocument, HiDocumentText, HiLockClosed, HiLockOpen, HiRefresh } from 'react-icons/hi';
12
14
  /**
13
- * Get file icon based on file type
15
+ * Get file icon based on MIME type and filename
14
16
  */
15
- function get_file_icon(file_type, file_name) {
16
- const extension = file_name.split('.').pop()?.toLowerCase() || '';
17
- const mime_type = file_type.toLowerCase();
18
- // Image types
19
- 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)) {
20
21
  return _jsx(HiPhotograph, { className: "h-8 w-8 text-blue-500" });
21
22
  }
22
- // PDF
23
- if (mime_type === 'application/pdf' || extension === 'pdf') {
23
+ if (mime === 'application/pdf' || extension === 'pdf') {
24
24
  return _jsx(HiDocument, { className: "h-8 w-8 text-red-500" });
25
25
  }
26
- // Document types
27
- if (mime_type.includes('document') ||
28
- mime_type.includes('text') ||
29
- ['doc', 'docx', 'txt', 'rtf', 'odt'].includes(extension)) {
26
+ if (mime.includes('document') || mime.includes('text') || ['doc', 'docx', 'txt', 'rtf', 'odt'].includes(extension)) {
30
27
  return _jsx(HiDocumentText, { className: "h-8 w-8 text-green-500" });
31
28
  }
32
- // Default
33
29
  return _jsx(HiPaperClip, { className: "h-8 w-8 text-gray-500" });
34
30
  }
35
31
  /**
@@ -43,10 +39,26 @@ function format_file_size(bytes) {
43
39
  const i = Math.floor(Math.log(bytes) / Math.log(k));
44
40
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
45
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
+ }
46
58
  /**
47
59
  * Collaboration form file upload component
48
60
  */
49
- export function CollabFormFileUpload({ field_id_final, accept_files, 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/collab-forms/upload-file', enable_file_visibility = false, 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, }) {
50
62
  const logger = use_logger();
51
63
  const file_input_ref = useRef(null);
52
64
  const private_file_input_ref = useRef(null);
@@ -56,119 +68,64 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
56
68
  const [is_dragging_private, set_is_dragging_private] = useState(false);
57
69
  const [upload_errors, set_upload_errors] = useState({});
58
70
  const [uploading_files, set_uploading_files] = useState(new Set());
59
- const [processing_files, set_processing_files] = useState(new Set());
60
- const [processed_files, set_processed_files] = useState(new Set());
61
71
  const [has_permission, set_has_permission] = useState(false);
62
72
  const [permission_loading, set_permission_loading] = useState(true);
63
73
  // Use controlled files if provided, otherwise use internal state
64
74
  const files = controlled_files !== undefined ? controlled_files : internal_files;
65
75
  const is_controlled = controlled_files !== undefined;
66
- // Use controlled private files if provided, otherwise use internal state
67
76
  const private_files = controlled_private_files !== undefined ? controlled_private_files : internal_private_files;
68
77
  const is_private_controlled = controlled_private_files !== undefined;
69
- // Check permission using hazo_auth when enable_file_visibility is true
78
+ // Check permission for private files
70
79
  React.useEffect(() => {
71
- if (!enable_file_visibility || !private_files_permission) {
80
+ if (!accept_files_private) {
72
81
  set_permission_loading(false);
73
82
  set_has_permission(false);
74
83
  return;
75
84
  }
76
- // If permission is provided via prop, use that
77
85
  if (can_access_private_files_prop !== undefined) {
78
86
  set_has_permission(can_access_private_files_prop);
79
87
  set_permission_loading(false);
80
88
  return;
81
89
  }
82
- // Otherwise, check via hazo_auth API
83
- const check_permission = async () => {
84
- try {
85
- set_permission_loading(true);
86
- const response = await fetch('/api/hazo_auth/get_auth', {
87
- credentials: 'include',
88
- });
89
- if (response.ok) {
90
- const auth_data = await response.json();
91
- if (auth_data.authenticated && auth_data.permissions) {
92
- const has_perm = auth_data.permissions.includes(private_files_permission);
93
- set_has_permission(has_perm);
94
- }
95
- else {
96
- set_has_permission(false);
97
- }
98
- }
99
- else {
100
- set_has_permission(false);
101
- }
102
- }
103
- catch (error) {
104
- logger.warn('[CollabFormFileUpload] Error checking permission', {
105
- permission: private_files_permission,
106
- error: error instanceof Error ? error.message : String(error),
107
- });
108
- set_has_permission(false);
109
- }
110
- finally {
111
- set_permission_loading(false);
112
- }
113
- };
114
- check_permission();
115
- }, [enable_file_visibility, private_files_permission, can_access_private_files_prop]);
116
- // Determine if private files section should be shown
117
- const show_private_section = !!(enable_file_visibility && private_files_permission && has_permission && !permission_loading);
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]);
94
+ const show_private_section = !!(accept_files_private && has_permission && !permission_loading);
118
95
  /**
119
96
  * Update files state (public files)
120
97
  */
121
98
  const update_files = useCallback((new_files) => {
122
- // Ensure all files have visibility set to 'public'
123
- const files_with_visibility = new_files.map(f => ({
124
- ...f,
125
- visibility: f.visibility || 'public',
126
- }));
127
99
  if (!is_controlled) {
128
- set_internal_files(files_with_visibility);
129
- }
130
- if (on_files_change) {
131
- on_files_change(files_with_visibility);
100
+ set_internal_files(new_files);
132
101
  }
102
+ on_files_change?.(new_files);
133
103
  }, [is_controlled, on_files_change]);
134
104
  /**
135
105
  * Update private files state
136
106
  */
137
107
  const update_private_files = useCallback((new_files) => {
138
- // Ensure all files have visibility set to 'private'
139
- const files_with_visibility = new_files.map(f => ({
140
- ...f,
141
- visibility: 'private',
142
- }));
143
108
  if (!is_private_controlled) {
144
- set_internal_private_files(files_with_visibility);
145
- }
146
- if (on_private_files_change) {
147
- on_private_files_change(files_with_visibility);
109
+ set_internal_private_files(new_files);
148
110
  }
111
+ on_private_files_change?.(new_files);
149
112
  }, [is_private_controlled, on_private_files_change]);
150
113
  /**
151
114
  * Toggle file visibility (move between public and private)
152
115
  */
153
116
  const toggle_file_visibility = useCallback((file_id, current_visibility) => {
154
117
  if (current_visibility === 'public') {
155
- // Move from public to private
156
118
  const file_to_move = files.find(f => f.file_id === file_id);
157
119
  if (file_to_move) {
158
- const new_public_files = files.filter(f => f.file_id !== file_id);
159
- const moved_file = { ...file_to_move, visibility: 'private' };
160
- update_files(new_public_files);
161
- 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' }]);
162
122
  }
163
123
  }
164
124
  else {
165
- // Move from private to public
166
125
  const file_to_move = private_files.find(f => f.file_id === file_id);
167
126
  if (file_to_move) {
168
- const new_private_files = private_files.filter(f => f.file_id !== file_id);
169
- const moved_file = { ...file_to_move, visibility: 'public' };
170
- update_private_files(new_private_files);
171
- 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' }]);
172
129
  }
173
130
  }
174
131
  }, [files, private_files, update_files, update_private_files]);
@@ -176,124 +133,68 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
176
133
  * Validate file before upload
177
134
  */
178
135
  const validate_file = useCallback((file) => {
179
- // Run custom validator first (if provided)
180
136
  if (file_validator) {
181
137
  const custom_error = file_validator(file);
182
- if (custom_error) {
138
+ if (custom_error)
183
139
  return custom_error;
184
- }
185
140
  }
186
- // Check file count
187
141
  if (files.length >= max_files) {
188
142
  return `Maximum ${max_files} files allowed`;
189
143
  }
190
- // Check file size
191
144
  if (max_size && file.size > max_size) {
192
145
  return `File size exceeds maximum of ${format_file_size(max_size)}`;
193
146
  }
194
- // Check file type (if file_accept is specified)
195
147
  if (file_accept) {
196
- const accept_patterns = file_accept.split(',').map((p) => p.trim());
148
+ const accept_patterns = file_accept.split(',').map(p => p.trim());
197
149
  const file_extension = '.' + file.name.split('.').pop()?.toLowerCase();
198
150
  const file_mime = file.type;
199
- const matches = accept_patterns.some((pattern) => {
200
- if (pattern.startsWith('.')) {
201
- // Extension match
151
+ const matches = accept_patterns.some(pattern => {
152
+ if (pattern.startsWith('.'))
202
153
  return file_extension === pattern.toLowerCase();
203
- }
204
- else if (pattern.includes('/*')) {
205
- // MIME type wildcard (e.g., "image/*")
206
- const base_type = pattern.split('/')[0];
207
- return file_mime.startsWith(base_type + '/');
208
- }
209
- else {
210
- // Exact MIME type match
211
- return file_mime === pattern;
212
- }
154
+ if (pattern.includes('/*'))
155
+ return file_mime.startsWith(pattern.split('/')[0] + '/');
156
+ return file_mime === pattern;
213
157
  });
214
- if (!matches) {
158
+ if (!matches)
215
159
  return `File type not allowed. Accepted: ${file_accept}`;
216
- }
217
160
  }
218
161
  return null;
219
162
  }, [files.length, max_files, max_size, file_accept, file_validator]);
220
163
  /**
221
- * Upload file to server
164
+ * Upload file via file_manager.upload callback
222
165
  */
223
166
  const upload_file = useCallback(async (file, visibility = 'public') => {
224
- const file_id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
225
- const uploading_id = `${file_id}-${file.name}`;
167
+ const uploading_id = `${Date.now()}-${file.name}`;
226
168
  try {
227
- set_uploading_files((prev) => new Set(prev).add(uploading_id));
228
- const form_data = new FormData();
229
- form_data.append('file', file);
230
- form_data.append('field_id', field_id_final);
231
- if (files_dir) {
232
- form_data.append('files_dir', files_dir);
233
- }
234
- // Add visibility to the form data
235
- form_data.append('visibility', visibility);
236
- // Add required permission for private files (server-side validation)
237
- if (visibility === 'private' && private_files_permission) {
238
- form_data.append('required_permission', private_files_permission);
239
- }
240
- const response = await fetch(upload_endpoint, {
241
- method: 'POST',
242
- body: form_data,
243
- credentials: 'include',
244
- });
245
- if (!response.ok) {
246
- const error_data = await response.json().catch(() => ({ error: 'Upload failed' }));
247
- throw new Error(error_data.error || `Upload failed: ${response.statusText}`);
248
- }
249
- const result = await response.json();
250
- const file_data = {
251
- file_path: result.file_path || result.path || '',
252
- file_name: file.name,
253
- file_size: file.size,
254
- file_type: file.type,
255
- file_id,
256
- 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,
257
173
  visibility,
258
- };
259
- // Call file processor if provided
260
- if (file_processor && component_ref) {
261
- set_processing_files((prev) => new Set(prev).add(file_data.file_id));
262
- try {
263
- await file_processor(file_data, component_ref);
264
- set_processed_files((prev) => new Set(prev).add(file_data.file_id));
265
- }
266
- finally {
267
- set_processing_files((prev) => {
268
- const next = new Set(prev);
269
- next.delete(file_data.file_id);
270
- return next;
271
- });
272
- }
273
- }
274
- set_upload_errors((prev) => {
174
+ scope_id,
175
+ uploaded_by,
176
+ metadata: files_dir ? { files_dir } : undefined,
177
+ });
178
+ set_upload_errors(prev => {
275
179
  const next = { ...prev };
276
180
  delete next[uploading_id];
277
181
  return next;
278
182
  });
279
- return file_data;
183
+ return attachment;
280
184
  }
281
185
  catch (error) {
282
186
  const error_message = error instanceof Error ? error.message : 'Upload failed';
283
- set_upload_errors((prev) => ({
284
- ...prev,
285
- [uploading_id]: error_message,
286
- }));
187
+ set_upload_errors(prev => ({ ...prev, [uploading_id]: error_message }));
287
188
  return null;
288
189
  }
289
190
  finally {
290
- set_uploading_files((prev) => {
191
+ set_uploading_files(prev => {
291
192
  const next = new Set(prev);
292
193
  next.delete(uploading_id);
293
194
  return next;
294
195
  });
295
196
  }
296
- }, [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]);
297
198
  /**
298
199
  * Handle file selection (public files)
299
200
  */
@@ -302,8 +203,7 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
302
203
  return;
303
204
  const files_to_upload = [];
304
205
  const errors = {};
305
- // Validate all files first
306
- Array.from(selected_files).forEach((file) => {
206
+ Array.from(selected_files).forEach(file => {
307
207
  const error = validate_file(file);
308
208
  if (error) {
309
209
  errors[file.name] = error;
@@ -312,16 +212,14 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
312
212
  files_to_upload.push(file);
313
213
  }
314
214
  });
315
- // Set validation errors
316
215
  if (Object.keys(errors).length > 0) {
317
- set_upload_errors((prev) => ({ ...prev, ...errors }));
216
+ set_upload_errors(prev => ({ ...prev, ...errors }));
318
217
  }
319
- // Upload valid files as public
320
- const upload_promises = files_to_upload.map((file) => upload_file(file, 'public'));
321
- const uploaded_files = await Promise.all(upload_promises);
322
- const successful_uploads = uploaded_files.filter((f) => f !== null);
323
- if (successful_uploads.length > 0) {
324
- 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]);
325
223
  }
326
224
  }, [files, validate_file, upload_file, update_files]);
327
225
  /**
@@ -332,8 +230,7 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
332
230
  return;
333
231
  const files_to_upload = [];
334
232
  const errors = {};
335
- // Validate all files first (use same validation as public)
336
- Array.from(selected_files).forEach((file) => {
233
+ Array.from(selected_files).forEach(file => {
337
234
  const error = validate_file(file);
338
235
  if (error) {
339
236
  errors[file.name] = error;
@@ -342,21 +239,17 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
342
239
  files_to_upload.push(file);
343
240
  }
344
241
  });
345
- // Set validation errors
346
242
  if (Object.keys(errors).length > 0) {
347
- set_upload_errors((prev) => ({ ...prev, ...errors }));
243
+ set_upload_errors(prev => ({ ...prev, ...errors }));
348
244
  }
349
- // Upload valid files as private
350
- const upload_promises = files_to_upload.map((file) => upload_file(file, 'private'));
351
- const uploaded_files = await Promise.all(upload_promises);
352
- const successful_uploads = uploaded_files.filter((f) => f !== null);
353
- if (successful_uploads.length > 0) {
354
- 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]);
355
250
  }
356
251
  }, [private_files, validate_file, upload_file, update_private_files]);
357
- /**
358
- * Handle drag and drop
359
- */
252
+ // Drag handlers (public)
360
253
  const handle_drag_enter = useCallback((e) => {
361
254
  e.preventDefault();
362
255
  e.stopPropagation();
@@ -377,9 +270,7 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
377
270
  set_is_dragging(false);
378
271
  handle_files_selected(e.dataTransfer.files);
379
272
  }, [handle_files_selected]);
380
- /**
381
- * Handle private drag and drop
382
- */
273
+ // Drag handlers (private)
383
274
  const handle_private_drag_enter = useCallback((e) => {
384
275
  e.preventDefault();
385
276
  e.stopPropagation();
@@ -396,102 +287,136 @@ export function CollabFormFileUpload({ field_id_final, accept_files, files_dir,
396
287
  set_is_dragging_private(false);
397
288
  handle_private_files_selected(e.dataTransfer.files);
398
289
  }, [handle_private_files_selected]);
399
- /**
400
- * Handle file input change
401
- */
290
+ // Input handlers
402
291
  const handle_input_change = useCallback((e) => {
403
292
  handle_files_selected(e.target.files);
404
- // Reset input to allow selecting the same file again
405
- if (file_input_ref.current) {
293
+ if (file_input_ref.current)
406
294
  file_input_ref.current.value = '';
407
- }
408
295
  }, [handle_files_selected]);
409
- /**
410
- * Handle private file input change
411
- */
412
296
  const handle_private_input_change = useCallback((e) => {
413
297
  handle_private_files_selected(e.target.files);
414
- // Reset input to allow selecting the same file again
415
- if (private_file_input_ref.current) {
298
+ if (private_file_input_ref.current)
416
299
  private_file_input_ref.current.value = '';
417
- }
418
300
  }, [handle_private_files_selected]);
419
301
  /**
420
- * Handle file delete (public files)
302
+ * Handle file delete calls file_manager.remove then updates state
421
303
  */
422
- const handle_delete_file = useCallback((file_id) => {
423
- const new_files = files.filter((f) => f.file_id !== file_id);
424
- // Check minimum files constraint
425
- if (min_files !== undefined && new_files.length < min_files) {
426
- set_upload_errors((prev) => ({
427
- ...prev,
428
- delete_error: `Minimum ${min_files} files required`,
429
- }));
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` }));
430
310
  return;
431
311
  }
432
- update_files(new_files);
433
- set_upload_errors((prev) => {
434
- const next = { ...prev };
435
- delete next.delete_error;
436
- return next;
437
- });
438
- }, [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]);
439
324
  /**
440
325
  * Handle private file delete
441
326
  */
442
- const handle_delete_private_file = useCallback((file_id) => {
443
- const new_files = private_files.filter((f) => f.file_id !== file_id);
444
- update_private_files(new_files);
445
- }, [private_files, update_private_files]);
446
- /**
447
- * Get file URL - handles both public and private files
448
- */
449
- const get_file_url = useCallback((file_path, visibility) => {
450
- if (file_path.startsWith('/')) {
451
- 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);
452
333
  }
453
- // For private files, use the private files endpoint with permission query
454
- if (visibility === 'private' && private_files_permission) {
455
- 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
+ });
456
339
  }
457
- return `/api/collab-forms/files/${encodeURIComponent(file_path)}`;
458
- }, [private_files_permission]);
459
- // Build accordion title based on visibility mode
460
- const accordion_title = enable_file_visibility && show_private_section
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
398
+ const accordion_title = accept_files_public && show_private_section
461
399
  ? `Files (${files.length} public, ${private_files.length} private)`
462
- : files.length > 0 ? `Files (${files.length})` : 'Files';
463
- // Always render wrapper div to maintain consistent DOM structure
464
- // This prevents hydration mismatches when accept_files changes between server and client
465
- return (_jsx("div", { className: "cls_collab_file_upload space-y-2", suppressHydrationWarning: true, children: accept_files ? (
466
- /* Dynamic import for Accordion - will be loaded by consuming app */
467
- _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,
468
- // Private files props
469
- enable_file_visibility: enable_file_visibility, 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 }));
400
+ : show_private_section && !accept_files_public
401
+ ? private_files.length > 0 ? `Private Files (${private_files.length})` : 'Private Files'
402
+ : files.length > 0 ? `Files (${files.length})` : 'Files';
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 }));
470
404
  }
471
405
  /**
472
406
  * File upload accordion component with dynamic imports
473
407
  */
474
- 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,
475
- // Private files props
476
- enable_file_visibility = 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, }) {
477
409
  const logger = use_logger();
478
410
  const [AccordionComponents, set_accordion_components] = React.useState(null);
479
411
  const [is_loading, set_is_loading] = React.useState(true);
480
412
  const [is_mounted, set_is_mounted] = React.useState(false);
481
- // Track client-side mounting to prevent hydration mismatch
482
- React.useEffect(() => {
483
- set_is_mounted(true);
484
- }, []);
413
+ React.useEffect(() => { set_is_mounted(true); }, []);
485
414
  React.useEffect(() => {
486
- // Only load accordion on client side
487
415
  if (!is_mounted)
488
416
  return;
489
- // Dynamic import - components will be resolved by consuming app's bundler
490
417
  const load_accordion = async () => {
491
418
  try {
492
419
  set_is_loading(true);
493
- // Try to import from consuming app's components directory
494
- // This path will be resolved by Next.js/webpack in the consuming app
495
420
  // @ts-expect-error - These modules are provided by the consuming application
496
421
  const accordion_module = await import('@/components/ui/accordion').catch(() => null);
497
422
  if (accordion_module) {
@@ -519,40 +444,31 @@ enable_file_visibility = false, show_private_section = false, private_files = []
519
444
  };
520
445
  load_accordion();
521
446
  }, [is_mounted]);
522
- // Always render consistent structure to prevent hydration mismatch
523
- // During SSR and initial client render, show empty placeholder
524
- // After mount and accordion load, show actual content
525
447
  if (!is_mounted || is_loading || !AccordionComponents) {
526
448
  return (_jsx("div", { className: "cls_collab_file_upload_loading text-sm text-muted-foreground", suppressHydrationWarning: true }));
527
449
  }
528
450
  const { Accordion, AccordionItem, AccordionTrigger, AccordionContent } = AccordionComponents;
529
- // Default to open if files exist
530
451
  const default_value = files.length > 0 ? 'files' : undefined;
531
- // Determine if files are required (min_files >= 1)
532
452
  const is_files_required = min_files !== undefined && min_files >= 1;
533
453
  /**
534
- * Render a file item with optional visibility toggle
454
+ * Render a single file item
535
455
  */
536
- const render_file_item = (file_data, visibility, delete_handler) => {
537
- const is_uploading = Array.from(uploading_files).some((id) => id.startsWith(file_data.file_id));
538
- const is_processing = processing_files.has(file_data.file_id);
539
- const is_processed = processed_files.has(file_data.file_id);
540
- 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: [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'
541
461
  ? "text-amber-600 hover:text-amber-700"
542
- : "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));
543
463
  };
544
464
  /**
545
465
  * Render file upload section (drop zone + file list)
546
466
  */
547
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
548
468
  ? 'border-primary bg-primary/5'
549
- : '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)) }) }))] }));
550
- // Wrap Accordion in div with suppressHydrationWarning to prevent React from checking children
551
- // The Accordion content may differ between server and client due to dynamic imports
552
- 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))) })), enable_file_visibility && 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') })] })] })) : (
553
- /* Single-section UI (original behavior) */
554
- _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
555
471
  ? 'border-primary bg-primary/5'
556
- : '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)) }) }))] }))] }) })] }) }) }));
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] }) })] }) }) }));
557
473
  }
558
474
  //# sourceMappingURL=collab_form_file_upload.js.map