@conduction/nextcloud-vue 2.56.0-beta.36 → 2.56.0-beta.38

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 (382) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue.js +1 -1
  7. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue2.js +3 -2
  8. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue.js +1 -1
  11. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js +5 -4
  12. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue3.js.map +1 -1
  14. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue.js +1 -1
  15. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue2.js +2 -1
  16. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue2.js.map +1 -1
  17. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  19. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +2 -1
  20. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  21. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  22. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  23. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +376 -6
  24. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  25. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +146 -71
  26. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  27. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue.js +1 -1
  28. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue2.js +2 -1
  29. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue2.js.map +1 -1
  30. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue3.js.map +1 -1
  31. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue.js +1 -1
  32. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +2 -1
  33. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  36. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +6 -5
  37. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue.js +1 -1
  40. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue2.js +2 -1
  41. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue2.js.map +1 -1
  42. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue3.js.map +1 -1
  43. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  44. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +2 -1
  45. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  46. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  47. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  48. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +2 -1
  49. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  52. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +47 -1
  53. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  54. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  55. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  56. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue.js +1 -1
  57. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue2.js +3 -1
  58. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue2.js.map +1 -1
  59. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue3.js.map +1 -1
  60. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue2.js +2 -1
  61. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue2.js.map +1 -1
  62. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue3.js.map +1 -1
  63. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -4
  64. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  67. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  68. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  69. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  70. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue.js +1 -1
  71. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue2.js +2 -1
  72. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue2.js.map +1 -1
  73. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue3.js.map +1 -1
  74. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  75. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
  76. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue.js +1 -1
  79. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue2.js +3 -1
  80. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue2.js.map +1 -1
  81. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue3.js.map +1 -1
  82. package/dist/esm/components/CnFileManager/CnFileManager.vue.js +1 -1
  83. package/dist/esm/components/CnFileManager/CnFileManager.vue2.js +3 -1
  84. package/dist/esm/components/CnFileManager/CnFileManager.vue2.js.map +1 -1
  85. package/dist/esm/components/CnFileManager/CnFileManager.vue3.js.map +1 -1
  86. package/dist/esm/components/CnFilePreview/CnFilePreview.vue.js +1 -1
  87. package/dist/esm/components/CnFilePreview/CnFilePreview.vue2.js +2 -1
  88. package/dist/esm/components/CnFilePreview/CnFilePreview.vue2.js.map +1 -1
  89. package/dist/esm/components/CnFilePreview/CnFilePreview.vue3.js.map +1 -1
  90. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  91. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +49 -1
  92. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  93. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +3 -3
  94. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  95. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue.js +1 -1
  96. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +2 -1
  97. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  98. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  99. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +571 -77
  100. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  101. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  102. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  103. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  104. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  105. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  106. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +2 -1
  107. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  108. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  109. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue.js +1 -1
  110. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue2.js +3 -2
  111. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue2.js.map +1 -1
  112. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue3.js.map +1 -1
  113. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  114. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -0
  115. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  116. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +82 -75
  117. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  118. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue.js +1 -1
  119. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue2.js +5 -4
  120. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue2.js.map +1 -1
  121. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue3.js.map +1 -1
  122. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue.js +1 -1
  123. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue2.js +2 -1
  124. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue2.js.map +1 -1
  125. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue3.js.map +1 -1
  126. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue.js +1 -1
  127. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue2.js +2 -1
  128. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue2.js.map +1 -1
  129. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue3.js.map +1 -1
  130. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue.js +1 -1
  131. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +2 -1
  132. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  133. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  134. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue.js +1 -1
  135. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue2.js +2 -1
  136. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue2.js.map +1 -1
  137. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue3.js.map +1 -1
  138. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue.js +1 -1
  139. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue2.js +2 -1
  140. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue2.js.map +1 -1
  141. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue3.js.map +1 -1
  142. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  143. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
  144. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  145. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  146. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  147. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  148. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  149. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  150. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  151. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue.js +1 -1
  152. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue2.js +2 -1
  153. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue2.js.map +1 -1
  154. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue3.js.map +1 -1
  155. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue2.js +2 -0
  156. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue2.js.map +1 -1
  157. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue3.js +2 -2
  158. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue3.js.map +1 -1
  159. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  160. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  161. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  162. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  163. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  164. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
  165. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  166. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  167. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  168. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue.js +1 -1
  169. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +2 -1
  170. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  171. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  172. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue.js +1 -1
  173. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue2.js +2 -1
  174. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue2.js.map +1 -1
  175. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue3.js.map +1 -1
  176. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  177. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
  178. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  179. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  180. package/dist/esm/components/CnTasksCard/CnTasksCard.vue.js +1 -1
  181. package/dist/esm/components/CnTasksCard/CnTasksCard.vue2.js +2 -1
  182. package/dist/esm/components/CnTasksCard/CnTasksCard.vue2.js.map +1 -1
  183. package/dist/esm/components/CnTasksCard/CnTasksCard.vue3.js.map +1 -1
  184. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  185. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +2 -1
  186. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  187. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  188. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  189. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  190. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  191. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  192. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue.js +1 -1
  193. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue2.js +2 -1
  194. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue2.js.map +1 -1
  195. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue.js +1 -1
  196. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue2.js +2 -1
  197. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue2.js.map +1 -1
  198. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue3.js.map +1 -1
  199. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  200. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  201. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  202. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  203. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  204. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  205. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  206. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  207. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  208. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  209. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  210. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  211. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  212. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  213. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  214. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  215. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  216. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  217. package/dist/esm/composables/useListView.js +21 -0
  218. package/dist/esm/composables/useListView.js.map +1 -1
  219. package/dist/esm/dialogs/CnNoteHistoryDialog.vue.js +1 -1
  220. package/dist/esm/dialogs/CnNoteHistoryDialog.vue2.js +2 -1
  221. package/dist/esm/dialogs/CnNoteHistoryDialog.vue2.js.map +1 -1
  222. package/dist/esm/dialogs/CnNoteHistoryDialog.vue3.js.map +1 -1
  223. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue.js +1 -1
  224. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue2.js +3 -2
  225. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue2.js.map +1 -1
  226. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue3.js.map +1 -1
  227. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue.js +1 -1
  228. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue2.js +4 -3
  229. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue2.js.map +1 -1
  230. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue3.js.map +1 -1
  231. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue.js +1 -1
  232. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -2
  233. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
  234. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
  235. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  236. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +2 -1
  237. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  238. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  239. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  240. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +6 -5
  241. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  242. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  243. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue.js +1 -1
  244. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue2.js +2 -1
  245. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue2.js.map +1 -1
  246. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue3.js.map +1 -1
  247. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue.js +1 -1
  248. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
  249. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
  250. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
  251. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  252. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
  253. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  254. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  255. package/dist/esm/integrations/builtin/email/CnEmailCard.vue.js +1 -1
  256. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js +2 -1
  257. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
  258. package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
  259. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue.js +1 -1
  260. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +2 -1
  261. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  262. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  263. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue.js +1 -1
  264. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue2.js +2 -1
  265. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue2.js.map +1 -1
  266. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue3.js.map +1 -1
  267. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue.js +1 -1
  268. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
  269. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
  270. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
  271. package/dist/esm/l10n/en.json.js +57 -2
  272. package/dist/esm/l10n/en.json.js.map +1 -1
  273. package/dist/esm/l10n/nl.json.js +78 -2
  274. package/dist/esm/l10n/nl.json.js.map +1 -1
  275. package/dist/esm/library-style.js +1 -1
  276. package/dist/esm/nextcloud-vue.css +1541 -1166
  277. package/dist/esm/store/useObjectStore.js +23 -4
  278. package/dist/esm/store/useObjectStore.js.map +1 -1
  279. package/dist/esm/utils/boardColumns.js +58 -2
  280. package/dist/esm/utils/boardColumns.js.map +1 -1
  281. package/dist/esm/utils/boardReferenceColumns.js +330 -0
  282. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  283. package/dist/esm/utils/buildManifest.js +4 -1
  284. package/dist/esm/utils/buildManifest.js.map +1 -1
  285. package/dist/esm/utils/schema.js +5 -4
  286. package/dist/esm/utils/schema.js.map +1 -1
  287. package/dist/esm/utils/userLocale.js +30 -0
  288. package/dist/esm/utils/userLocale.js.map +1 -0
  289. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  290. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  291. package/dist/esm/utils/widgetDispatch.js +8 -2
  292. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  293. package/dist/nextcloud-vue.cjs.js +30291 -27618
  294. package/dist/nextcloud-vue.cjs.js.map +2 -2
  295. package/dist/nextcloud-vue.css +1541 -1166
  296. package/l10n/en.json +57 -2
  297. package/l10n/nl.json +78 -2
  298. package/package.json +1 -1
  299. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  300. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +3 -2
  301. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +5 -4
  302. package/src/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue +2 -1
  303. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +2 -1
  304. package/src/components/CnBoardView/CnBoardView.vue +433 -12
  305. package/src/components/CnCalendarEventPicker/CnCalendarEventPicker.vue +2 -1
  306. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +2 -1
  307. package/src/components/CnChartWidget/CnChartWidget.vue +6 -5
  308. package/src/components/CnCollectivePagePicker/CnCollectivePagePicker.vue +2 -1
  309. package/src/components/CnConversationThread/CnConversationThread.vue +2 -1
  310. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +2 -1
  311. package/src/components/CnDashboardPage/CnDashboardPage.vue +59 -3
  312. package/src/components/CnDataMatrix/CnDataMatrix.vue +2 -1
  313. package/src/components/CnDetailPage/CnDetailHeaderChips.vue +2 -1
  314. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  315. package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
  316. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  317. package/src/components/CnEmailPicker/CnEmailPicker.vue +2 -1
  318. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  319. package/src/components/CnFederationStatus/CnFederationStatus.vue +3 -1
  320. package/src/components/CnFileManager/CnFileManager.vue +2 -1
  321. package/src/components/CnFilePreview/CnFilePreview.vue +2 -1
  322. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +54 -6
  323. package/src/components/CnFollowToggle/CnFollowToggle.vue +2 -1
  324. package/src/components/CnIndexPage/CnIndexPage.vue +610 -25
  325. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  326. package/src/components/CnNotesCard/CnNotesCard.vue +2 -1
  327. package/src/components/CnObjectCalendar/CnObjectCalendar.vue +3 -2
  328. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +36 -0
  329. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +5 -4
  330. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +2 -1
  331. package/src/components/CnObjectSidebar/CnFlowTasksPanel.vue +2 -1
  332. package/src/components/CnObjectSidebar/CnNotesTab.vue +2 -1
  333. package/src/components/CnObjectSidebar/CnTasksTab.vue +2 -1
  334. package/src/components/CnPropertySourceField/CnPropertySourceField.vue +2 -1
  335. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
  336. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  337. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  338. package/src/components/CnRoadmapItem/CnRoadmapItem.vue +2 -1
  339. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +4 -2
  340. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  341. package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
  342. package/src/components/CnStatsBlock/CnStatsBlock.vue +2 -1
  343. package/src/components/CnSummaryAggregates/CnSummaryAggregates.vue +2 -1
  344. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
  345. package/src/components/CnTasksCard/CnTasksCard.vue +2 -1
  346. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +2 -1
  347. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  348. package/src/components/CnTranslatedBadge/CnTranslatedBadge.vue +2 -1
  349. package/src/components/CnVersionHistory/CnVersionHistory.vue +2 -1
  350. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  351. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  352. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  353. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  354. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  355. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  356. package/src/composables/useListView.js +21 -0
  357. package/src/css/board.css +105 -4
  358. package/src/css/dialog.css +4 -2
  359. package/src/css/index-page.css +49 -0
  360. package/src/css/look-board.css +77 -7
  361. package/src/dialogs/CnNoteHistoryDialog.vue +2 -1
  362. package/src/integrations/builtin/activity/CnActivityCard.vue +3 -2
  363. package/src/integrations/builtin/activity/CnActivityTab.vue +4 -3
  364. package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +3 -2
  365. package/src/integrations/builtin/calendar/CnCalendarCard.vue +2 -1
  366. package/src/integrations/builtin/calendar/CnCalendarTab.vue +6 -5
  367. package/src/integrations/builtin/contactmoment/CnContactmomentCard.vue +2 -1
  368. package/src/integrations/builtin/cospend/CnCospendTab.vue +2 -1
  369. package/src/integrations/builtin/deck/CnDeckTab.vue +2 -1
  370. package/src/integrations/builtin/email/CnEmailCard.vue +2 -1
  371. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +2 -1
  372. package/src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue +2 -1
  373. package/src/integrations/builtin/photos/CnPhotosTab.vue +2 -1
  374. package/src/schemas/app-manifest-v2.schema.json +116 -5
  375. package/src/store/useObjectStore.js +23 -4
  376. package/src/utils/boardColumns.js +57 -1
  377. package/src/utils/boardReferenceColumns.js +326 -0
  378. package/src/utils/buildManifest.js +4 -1
  379. package/src/utils/schema.js +5 -4
  380. package/src/utils/userLocale.js +26 -0
  381. package/src/utils/validateManifestV2.compiled.js +3 -3
  382. package/src/utils/widgetDispatch.js +8 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnFileManager.vue2.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString()\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnFileManager.vue2.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { userLocale } from '../../utils/userLocale.js'\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString(userLocale())\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnFileManager.vue3.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue?vue&type=template&id=1b9de2b2&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString()\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_renderSlot","_toDisplayString","_normalizeClass","_createElementVNode","_Fragment","_renderList","_createTextVNode","_withModifiers"],"mappings":";;;AACM,EAAA,KAAK,EAAC,iBAAkB;AAAA,EAAA,aAAW,EAAC,iBAAiB;;;;AACJ,EAAA,KAAK,EAAC,yBAAyB;;;;AAGjE,EAAA,KAAK,EAAC,wBAAwB;;;;AAGzB,EAAA,KAAK,EAAC,8BAA8B;;;;;;AA0B/B,EAAA,KAAK,EAAC,wBAAwB;;;;AAKhD,EAAA,KAAK,EAAC,uBAAuB;;;AAMhC,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAA4B,GAAA;AACnC,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAA4B,GAAA;AAChC,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAA4B,GAAA;AACjC,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,2BAA2B,GAAA;;;;;AA/C5C,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,CA6EK,OA7EL,UA6EK,EAAA;AA5EU,IAAA,CAAA,MAAA,CAAA,KAAM,IAAG,MAAA,CAAA,WAAU,IAAK,IAAA,CAAA,MAAM,CAAC,MAAM;AAAnD,SAAAD,SAAA,EAAA,EAAAC,kBAAA,CAUQ,UAVR,UAUQ,EAAA;AARP,UAAAC,UAAA,CAOM,2BAPN,MAOM;aANK,MAAK,CAAA,KAAA;8BAAfD,kBAEI,CAAA,IAAA,EAFJ,UAEI,EAAAE,eAAA,CADA;;aAEK,MAAW,CAAA,WAAA;8BAApBF,kBAEG,CAAA,GAAA,EAFH,UAEG,EAAAE,eAAA,CADC,MAAU,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;;;;;MAMJ,MAAQ,CAAA,QAAA;sBAApBF,kBAgBK,CAAA,KAAA,EAAA;;UAfJ,KAAK,EAAAG,cAAA,CAAA,CAAC,2BAA0B,EAAA,EAAA,mCAAA,EACe,KAAS,CAAA,UAAA,EAAA,CAAA,CAAA;AACxD,UAAA,aAAW,EAAC,0BAAyB;AACpC,UAAA,UAAQ,wDAAU,QAAU,CAAA,UAAA,IAAA,QAAA,CAAA,UAAA,CAAA,GAAA,IAAA,CAAA,CAAA,EAAA,CAAA,SAAA,CAAA,CAAA,CAAA;AAC5B,UAAA,WAAS,wDAAU,QAAW,CAAA,WAAA,IAAA,QAAA,CAAA,WAAA,CAAA,GAAA,IAAA,CAAA,CAAA,EAAA,CAAA,SAAA,CAAA,CAAA,CAAA;AAC9B,UAAA,MAAI,wDAAU,QAAM,CAAA,MAAA,IAAA,QAAA,CAAA,MAAA,CAAA,GAAA,IAAA,CAAA,CAAA,EAAA,CAAA,SAAA,CAAA,CAAA,CAAA;AACpB,UAAA,OAAK,0CAAE,QAAc,CAAA,cAAA,IAAA,QAAA,CAAA,cAAA,CAAA,GAAA,IAAA,CAAA,CAAA,CAAA;;AACtB,UAAAC,kBAAA,CAAyB,2BAAnB;WACO,MAAY,CAAA,YAAA;AAAzB,eAAAL,SAAA,EAAA,EAAAC,kBAAA,CAAoD,qCAAtB,MAAW,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;;UACzCI,kBAK4B,CAAA,OAAA,EAAA;AALrB,YAAA,GAAG,EAAC,WAAU;AACpB,YAAA,IAAI,EAAC,MAAK;AACT,YAAA,MAAM,EAAE,MAAM,CAAA,MAAA;AACd,YAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AACnB,YAAA,KAAK,EAAC,6BAA4B;AACjC,YAAA,QAAM,0CAAE,QAAiB,CAAA,iBAAA,IAAA,QAAA,CAAA,iBAAA,CAAA,GAAA,IAAA,CAAA,CAAA,CAAA;;;;AAInB,IAAA,CAAA,MAAA,CAAA,KAAK,CAAC,MAAK,KAAA,CAAA;sBAApBJ,kBAEG,CAAA,GAAA,EAFH,UAEG,EAAAE,eAAA,CADC,MAAS,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;AAIb,SAAAH,SAAA,EAAA,EAAAC,kBAAA,CAsCI,MAtCJ,UAsCI,EAAA;4BArCHA,kBAoCI,CAAAK,QAAA,EAAA,IAAA,EAAAC,UAAA,CApCe,MAAK,CAAA,KAAA,EAAA,CAAb;iCAAXN,kBAoCI,CAAA,IAAA,EAAA;AAnCF,cAAA,GAAG,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;AAC1B,cAAA,KAAK,EAAC,uBAAsB;AAC3B,cAAA,cAAY,EAAE,IAAI,CAAC,EAAC,IAAK,IAAI,CAAC,IAAI;cAClC,OAAK,EAAA,MAAA,KAAE,QAAW,CAAA,WAAA,CAAC,IAAI,CAAA,CAAA;;AACxB,cAAAI,kBAAA,CAAkE,MAAlE,EAAA,WAAkE,EAAtBF,eAAA,CAAA,QAAA,CAAA,OAAO,CAAC,IAAI,CAAA,CAAA,EAAA,CAAA,YAAA;AACxD,cAAAE,kBAAA,CAOK,OAPL,WAOK,EAAA;AANJ,gBAAAA,kBAAA,CAA8D,MAA9D,EAAA,WAA8D,EAAlBF,eAAA,CAAA,IAAI,CAAC,IAAK,CAAA,EAAA,CAAA,YAAA;AACtD,gBAAAE,kBAAA,CAIO,SAJP,WAIO,EAAA;AAHH,kBAAAG,eAAA,CAAAL,eAAA,CAAA,QAAA,CAAA,SAAS,CAAC,IAAI,CAAC,IAAI,KAAG,GACzB,EAAA,CAAA,YAAA;AAAgB,kBAAA,CAAA,IAAI,CAAC,UAAU;oCAA/BF,kBAAoF,CAAAK,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;6CAAhD,GAAGH,eAAA,CAAA,QAAA,CAAA,eAAe,CAAC,IAAI,CAAC,UAAU,CAAA,CAAA,EAAA,CAAA,YAAA;;;AACtD,kBAAA,CAAA,IAAI,CAAC,UAAU;oCAA/BF,kBAAmE,CAAAK,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;wCAAjC,KAAE,GAAAH,eAAA,CAAG,IAAI,CAAC,UAAW,CAAA,EAAA,CAAA,YAAA;;;;;cAGzDE,kBAqBK,CAAA,KAAA,EAAA;AArBA,gBAAA,KAAK,EAAC,+BAAgC;AAAC,gBAAA,OAAK,0CAAN,MAAW,EAAA,EAAA,CAAA,MAAA,CAAA,CAAA,CAAA;;AAIrD,gBAAAH,UAAA,CAgBM,IAhBqB,CAAA,MAAA,EAAA,cAAA,EAAA,EAAA,IAAI,EAAE,IAAI,IAArC,MAgBM;AAfS,kBAAA,CAAA,IAAI,CAAC,GAAE,IAAK,IAAI,CAAC,eAAe;oCAA9CD,kBAMQ,CAAA,QAAA,EAAA;;AALP,wBAAA,IAAI,EAAC,QAAO;AACZ,wBAAA,KAAK,EAAC,yBAAwB;AAC7B,wBAAA,KAAK,EAAE,MAAa,CAAA,aAAA;wBACpB,OAAK,EAAAQ,aAAA,CAAA,MAAA,KAAO,QAAU,CAAA,UAAA,CAAC,IAAI,CAAA,CAAA,EAAA,CAAA,MAAA,CAAA,CAAA;yCACzB;;oBAEW,MAAQ,CAAA,QAAA;oCAAvBR,kBAOQ,CAAA,QAAA,EAAA;;AANP,wBAAA,IAAI,EAAC,QAAO;AACZ,wBAAA,KAAK,EAAC,yDAAwD;AAC7D,wBAAA,KAAK,EAAE,MAAW,CAAA,WAAA;wBAClB,QAAQ,EAAE,iBAAW,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAA;wBAC1C,OAAK,EAAAQ,aAAA,CAAA,MAAA,KAAO,QAAQ,CAAA,QAAA,CAAC,IAAI,CAAA,CAAA,EAAA,CAAA,MAAA,CAAA,CAAA;yCACvB,MAAU,CAAA,WAAA,CAAA,EAAA,CAAA,oBAAA,WAAA,CAAA;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnFileManager.vue3.js","sources":["../../../../src/components/CnFileManager/CnFileManager.vue?vue&type=template&id=395592dc&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-file-manager\" data-testid=\"cn-file-manager\">\n\t\t<header v-if=\"title || description || $slots.header\" class=\"cn-file-manager__header\">\n\t\t\t<!-- @slot header Overrides the default title/description header. Defaults to `<h3>{{ title }}</h3>` + `<p>{{ description }}</p>`. -->\n\t\t\t<slot name=\"header\">\n\t\t\t\t<h3 v-if=\"title\" class=\"cn-file-manager__title\">\n\t\t\t\t\t{{ title }}\n\t\t\t\t</h3>\n\t\t\t\t<p v-if=\"description\" class=\"cn-file-manager__description\">\n\t\t\t\t\t{{ description }}\n\t\t\t\t</p>\n\t\t\t</slot>\n\t\t</header>\n\n\t\t<!-- Drop zone — drag a file here to trigger @upload. -->\n\t\t<div v-if=\"!readOnly\"\n\t\t\tclass=\"cn-file-manager__dropzone\"\n\t\t\t:class=\"{ 'cn-file-manager__dropzone--active': isDragging }\"\n\t\t\tdata-testid=\"cn-file-manager-dropzone\"\n\t\t\t@dragover.prevent=\"onDragOver\"\n\t\t\t@dragleave.prevent=\"onDragLeave\"\n\t\t\t@drop.prevent=\"onDrop\"\n\t\t\t@click=\"openFilePicker\">\n\t\t\t<p>{{ dropzoneLabel }}</p>\n\t\t\t<small v-if=\"dropzoneHint\">{{ dropzoneHint }}</small>\n\t\t\t<input ref=\"fileInput\"\n\t\t\t\ttype=\"file\"\n\t\t\t\t:accept=\"accept\"\n\t\t\t\t:multiple=\"multiple\"\n\t\t\t\tclass=\"cn-file-manager__file-input\"\n\t\t\t\t@change=\"onFileInputChange\">\n\t\t</div>\n\n\t\t<!-- Empty state. -->\n\t\t<p v-if=\"files.length === 0\" class=\"cn-file-manager__empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\n\t\t<!-- File list. -->\n\t\t<ul v-else class=\"cn-file-manager__list\">\n\t\t\t<li v-for=\"file in files\"\n\t\t\t\t:key=\"file.id || file.name\"\n\t\t\t\tclass=\"cn-file-manager__item\"\n\t\t\t\t:data-file-id=\"file.id || file.name\"\n\t\t\t\t@click=\"onFileClick(file)\">\n\t\t\t\t<span class=\"cn-file-manager__item-icon\">{{ iconFor(file) }}</span>\n\t\t\t\t<div class=\"cn-file-manager__item-meta\">\n\t\t\t\t\t<span class=\"cn-file-manager__item-name\">{{ file.name }}</span>\n\t\t\t\t\t<small class=\"cn-file-manager__item-sub\">\n\t\t\t\t\t\t{{ humanSize(file.size) }}\n\t\t\t\t\t\t<template v-if=\"file.uploadedAt\"> · {{ formatTimestamp(file.uploadedAt) }}</template>\n\t\t\t\t\t\t<template v-if=\"file.uploadedBy\"> · {{ file.uploadedBy }}</template>\n\t\t\t\t\t</small>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-file-manager__item-actions\" @click.stop>\n\t\t\t\t\t<!-- @slot item-actions Per-file actions. Scope:\n\t\t\t\t\t { file }. Default renders Download / Delete\n\t\t\t\t\t buttons emitting @download / @delete. -->\n\t\t\t\t\t<slot name=\"item-actions\" :file=\"file\">\n\t\t\t\t\t\t<button v-if=\"file.url || file.downloadHandler\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action\"\n\t\t\t\t\t\t\t:title=\"downloadLabel\"\n\t\t\t\t\t\t\t@click.stop=\"onDownload(file)\">\n\t\t\t\t\t\t\t{{ downloadLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t\t<button v-if=\"!readOnly\"\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-file-manager__action cn-file-manager__action--delete\"\n\t\t\t\t\t\t\t:title=\"deleteLabel\"\n\t\t\t\t\t\t\t:disabled=\"deletingIds[file.id || file.name]\"\n\t\t\t\t\t\t\t@click.stop=\"onDelete(file)\">\n\t\t\t\t\t\t\t{{ deleteLabel }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { userLocale } from '../../utils/userLocale.js'\n/**\n * CnFileManager — File list widget with drag-drop upload, per-file\n * actions, and a click-emit for preview / detail.\n *\n * The component does NOT own a network layer — consumers wire the\n * uploads and downloads via the `@upload`, `@download`, `@delete`\n * events. The widget owns the dropzone UI, the per-file row\n * rendering, the icon-by-extension mapping, and the human-readable\n * size + timestamp formatting.\n *\n * For OpenRegister-backed file attachments, the consuming app\n * typically converts the OR `_files` array into the `files[]` prop\n * shape and persists the deletes / uploads back via the usual\n * object-store calls.\n *\n * ```vue\n * <CnFileManager\n * title=\"Attachments\"\n * :files=\"attachments\"\n * accept=\".pdf,.png,.jpg\"\n * :multiple=\"true\"\n * :max-size-mb=\"20\"\n * @upload=\"onUpload\"\n * @download=\"onDownload\"\n * @delete=\"onDelete\"\n * @file-click=\"openPreview\" />\n * ```\n */\nexport default {\n\tname: 'CnFileManager',\n\tprops: {\n\t\t/**\n\t\t * Files to render. Each entry:\n\t\t * `{ id?, name, size?, type?, url?, uploadedAt?, uploadedBy? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tfiles: { type: Array, default: () => [] },\n\t\t/** Optional widget title. */\n\t\ttitle: { type: String, default: '' },\n\t\t/** Optional description. */\n\t\tdescription: { type: String, default: '' },\n\t\t/** Hide the dropzone + delete actions. */\n\t\treadOnly: { type: Boolean, default: false },\n\t\t/** `accept` attribute for the file input. */\n\t\taccept: { type: String, default: '' },\n\t\t/** Allow multi-file selection. */\n\t\tmultiple: { type: Boolean, default: true },\n\t\t/** Per-file max size in megabytes. 0 disables. */\n\t\tmaxSizeMb: { type: Number, default: 0 },\n\t\t/** Dropzone primary label. */\n\t\tdropzoneLabel: { type: String, default: 'Drop files here or click to browse' },\n\t\t/** Dropzone secondary hint. */\n\t\tdropzoneHint: { type: String, default: '' },\n\t\t/** Empty-state text when files[] is empty. */\n\t\temptyLabel: { type: String, default: 'No files attached.' },\n\t\t/** Download button label / title. */\n\t\tdownloadLabel: { type: String, default: 'Download' },\n\t\t/** Delete button label / title. */\n\t\tdeleteLabel: { type: String, default: 'Delete' },\n\t},\n\n\temits: ['delete', 'download', 'file-click', 'upload', 'upload-rejected'],\n\tdata() {\n\t\treturn {\n\t\t\tisDragging: false,\n\t\t\tdeletingIds: {},\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pick the emoji-icon for a file based on extension /\n\t\t * MIME type. Pure presentation; consumers can override\n\t\t * via the `#item-actions` slot if they want full icons.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {string} The icon character.\n\t\t */\n\t\ticonFor(file) {\n\t\t\tconst ext = (file.name || '').split('.').pop().toLowerCase()\n\t\t\tconst type = (file.type || '').toLowerCase()\n\t\t\tif (type.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n\t\t\t\treturn '🖼️'\n\t\t\t}\n\t\t\tif (type.startsWith('video/') || ['mp4', 'mov', 'avi', 'webm'].includes(ext)) {\n\t\t\t\treturn '🎬'\n\t\t\t}\n\t\t\tif (type.startsWith('audio/') || ['mp3', 'wav', 'ogg'].includes(ext)) {\n\t\t\t\treturn '🎵'\n\t\t\t}\n\t\t\tif (['pdf'].includes(ext)) {\n\t\t\t\treturn '📕'\n\t\t\t}\n\t\t\tif (['doc', 'docx', 'odt', 'txt', 'md'].includes(ext)) {\n\t\t\t\treturn '📄'\n\t\t\t}\n\t\t\tif (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) {\n\t\t\t\treturn '📊'\n\t\t\t}\n\t\t\tif (['ppt', 'pptx', 'odp'].includes(ext)) {\n\t\t\t\treturn '📈'\n\t\t\t}\n\t\t\tif (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {\n\t\t\t\treturn '🗜️'\n\t\t\t}\n\t\t\tif (['xml', 'json', 'yml', 'yaml'].includes(ext)) {\n\t\t\t\treturn '🧾'\n\t\t\t}\n\t\t\treturn '📎'\n\t\t},\n\n\t\t/**\n\t\t * Format a byte count as a short human-readable string.\n\t\t *\n\t\t * @param {number} bytes Size in bytes.\n\t\t * @return {string} Like '1.2 MB'.\n\t\t */\n\t\thumanSize(bytes) {\n\t\t\tif (bytes === undefined || bytes === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (bytes < 1024) {\n\t\t\t\treturn `${bytes} B`\n\t\t\t}\n\t\t\tif (bytes < 1024 * 1024) {\n\t\t\t\treturn `${(bytes / 1024).toFixed(1)} KB`\n\t\t\t}\n\t\t\treturn `${(bytes / 1024 / 1024).toFixed(1)} MB`\n\t\t},\n\n\t\t/**\n\t\t * Format an ISO timestamp as a short locale string.\n\t\t *\n\t\t * @param {string} iso ISO datetime.\n\t\t * @return {string} Locale-formatted timestamp.\n\t\t */\n\t\tformatTimestamp(iso) {\n\t\t\ttry {\n\t\t\t\tconst d = new Date(iso)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn iso\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleString(userLocale())\n\t\t\t} catch {\n\t\t\t\treturn iso\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Programmatic open of the file picker. Useful from a\n\t\t * parent button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenFilePicker() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$refs.fileInput) {\n\t\t\t\tthis.$refs.fileInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragover handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver() {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.isDragging = true\n\t\t},\n\n\t\t/**\n\t\t * Dropzone dragleave handler.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tthis.isDragging = false\n\t\t},\n\n\t\t/**\n\t\t * Dropzone drop handler — extracts files + validates size +\n\t\t * emits `@upload`.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tthis.isDragging = false\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst list = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : []\n\t\t\tthis.emitUpload(list)\n\t\t},\n\n\t\t/**\n\t\t * File-input change handler.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonFileInputChange(event) {\n\t\t\tconst list = Array.from(event.target.files || [])\n\t\t\tthis.emitUpload(list)\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * Validate + emit `@upload` with the file batch.\n\t\t *\n\t\t * @param {File[]} list The selected files.\n\t\t * @return {void}\n\t\t */\n\t\temitUpload(list) {\n\t\t\tif (list.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.maxSizeMb > 0) {\n\t\t\t\tconst limit = this.maxSizeMb * 1024 * 1024\n\t\t\t\tconst oversized = list.find((f) => f.size > limit)\n\t\t\t\tif (oversized) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event upload-rejected Emitted when one or more\n\t\t\t\t\t * files in the batch exceed maxSizeMb.\n\t\t\t\t\t * @type {{ reason: 'size', file: File, limitMb: number }}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('upload-rejected', { reason: 'size', file: oversized, limitMb: this.maxSizeMb })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event upload Emitted with the file batch from the\n\t\t\t * dropzone or file picker.\n\t\t\t * @type {File[]}\n\t\t\t */\n\t\t\tthis.$emit('upload', list)\n\t\t},\n\n\t\t/**\n\t\t * Forward a file-row click.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonFileClick(file) {\n\t\t\t/**\n\t\t\t * @event file-click Emitted on row click (not on the\n\t\t\t * action buttons).\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('file-click', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a download action.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDownload(file) {\n\t\t\t/**\n\t\t\t * @event download Emitted when the Download button is\n\t\t\t * clicked.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('download', file)\n\t\t},\n\n\t\t/**\n\t\t * Forward a delete action; sets the in-flight flag so the\n\t\t * button disables until the parent removes the file or\n\t\t * clears it via `setDeleted`.\n\t\t *\n\t\t * @param {object} file The file entry.\n\t\t * @return {void}\n\t\t */\n\t\tonDelete(file) {\n\t\t\tconst id = file.id || file.name\n\t\t\tthis.deletingIds = { ...this.deletingIds, [id]: true }\n\t\t\t/**\n\t\t\t * @event delete Emitted when the Delete button is\n\t\t\t * clicked. Parent persists then removes from files[].\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('delete', file)\n\t\t},\n\n\t\t/**\n\t\t * Public method — clear the in-flight delete marker for a\n\t\t * file id. Use when the parent's persist fails so the\n\t\t * Delete button becomes clickable again.\n\t\t *\n\t\t * @param {string} id File id.\n\t\t * @return {void}\n\t\t */\n\t\tclearDeleting(id) {\n\t\t\tconst next = { ...this.deletingIds }\n\t\t\tdelete next[id]\n\t\t\tthis.deletingIds = next\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-manager {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-file-manager__title {\n\tmargin: 0;\n\tfont-size: 1.1em;\n}\n\n.cn-file-manager__description {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__dropzone {\n\tborder: 2px dashed var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 24px;\n\ttext-align: center;\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n\ttransition: background 0.15s ease, border-color 0.15s ease;\n}\n\n.cn-file-manager__dropzone:hover,\n.cn-file-manager__dropzone--active {\n\tbackground: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n}\n\n.cn-file-manager__dropzone p {\n\tmargin: 0;\n}\n\n.cn-file-manager__dropzone small {\n\tdisplay: block;\n\tmargin-top: 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__file-input {\n\tdisplay: none;\n}\n\n.cn-file-manager__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tmargin: 16px 0;\n\ttext-align: center;\n}\n\n.cn-file-manager__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-file-manager__item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 8px 12px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\tcursor: pointer;\n}\n\n.cn-file-manager__item:hover {\n\tbackground: var(--color-background-darker, var(--color-background-hover));\n}\n\n.cn-file-manager__item-icon {\n\tfont-size: 1.4em;\n}\n\n.cn-file-manager__item-meta {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-file-manager__item-name {\n\tfont-weight: 500;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-file-manager__item-sub {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-file-manager__item-actions {\n\tdisplay: flex;\n\tgap: 4px;\n}\n\n.cn-file-manager__action {\n\tbackground: none;\n\tborder: 1px solid var(--color-border);\n\tpadding: 4px 8px;\n\tborder-radius: var(--border-radius);\n\tcursor: pointer;\n\tfont-size: 0.85em;\n}\n\n.cn-file-manager__action:hover {\n\tbackground: var(--color-main-background);\n}\n\n.cn-file-manager__action--delete {\n\tcolor: var(--color-error);\n}\n\n.cn-file-manager__action:disabled {\n\topacity: 0.4;\n\tcursor: not-allowed;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_renderSlot","_toDisplayString","_normalizeClass","_createElementVNode","_Fragment","_renderList","_createTextVNode","_withModifiers"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnFilePreview.vue2.js';
2
2
  import { render } from './CnFilePreview.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-be44a5f6";
5
+ script.__scopeId = "data-v-a604b852";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -3,6 +3,7 @@ import { generateUrl } from '@nextcloud/router';
3
3
  import { NcButton, NcDialog, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue';
4
4
  import { previewKindOf, fileContentUrl } from '../../composables/useFileOpener.js';
5
5
  import { parseDelimited } from '../../utils/parseDelimited.js';
6
+ import { userLocale } from '../../utils/userLocale.js';
6
7
  import { prefixUrl, buildHeaders } from '../../utils/headers.js';
7
8
 
8
9
  /**
@@ -135,7 +136,7 @@ var script = {
135
136
  if (estimate > shown) {
136
137
  return translate('nextcloud-vue', 'Showing the first {shown} of about {total} rows', {
137
138
  shown,
138
- total: estimate.toLocaleString(),
139
+ total: estimate.toLocaleString(userLocale()),
139
140
  })
140
141
  }
141
142
  return translate('nextcloud-vue', 'Showing the first {shown} rows', { shown })
@@ -1 +1 @@
1
- {"version":3,"file":"CnFilePreview.vue2.js","sources":["../../../../src/components/CnFilePreview/CnFilePreview.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcDialog\n\t\tv-if=\"dialog\"\n\t\t:name=\"fileName\"\n\t\tsize=\"large\"\n\t\t@closing=\"$emit('close')\">\n\t\t<div class=\"cn-file-preview\" data-testid=\"cn-file-preview\">\n\t\t\t<NcLoadingIcon v-if=\"loading\" :name=\"loadingLabel\" />\n\t\t\t<NcNoteCard v-else-if=\"failed\" type=\"error\">\n\t\t\t\t{{ failedLabel }}\n\t\t\t</NcNoteCard>\n\t\t\t<template v-else>\n\t\t\t\t<NcNoteCard v-if=\"unreadable\" type=\"warning\" data-testid=\"cn-file-preview-unreadable\">\n\t\t\t\t\t{{ unreadableLabel }}\n\t\t\t\t</NcNoteCard>\n\t\t\t\t<template v-if=\"mode === 'table'\">\n\t\t\t\t\t<div class=\"cn-file-preview__scroll\">\n\t\t\t\t\t\t<table class=\"cn-file-preview__table\">\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t<th v-for=\"(cell, c) in header\" :key=\"c\" scope=\"col\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t<tr v-for=\"(row, r) in body\" :key=\"r\">\n\t\t\t\t\t\t\t\t\t<td v-for=\"(cell, c) in row\" :key=\"c\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p class=\"cn-file-preview__count\" data-testid=\"cn-file-preview-count\">\n\t\t\t\t\t\t{{ countLabel }}\n\t\t\t\t\t</p>\n\t\t\t\t</template>\n\t\t\t\t<pre v-else class=\"cn-file-preview__text\" data-testid=\"cn-file-preview-text\">{{ text }}</pre>\n\t\t\t\t<p v-if=\"truncatedText && mode !== 'table'\" class=\"cn-file-preview__count\">\n\t\t\t\t\t{{ truncatedLabel }}\n\t\t\t\t</p>\n\t\t\t</template>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"filesUrl\"\n\t\t\t\t:href=\"filesUrl\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t{{ openInFilesLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"contentUrl\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"contentUrl\"\n\t\t\t\t:download=\"fileName\">\n\t\t\t\t{{ downloadLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</NcDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcDialog, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport { fileContentUrl, previewKindOf } from '../../composables/useFileOpener.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { parseDelimited } from '../../utils/parseDelimited.js'\n\n/**\n * CnFilePreview — an in-page preview of a data file the Nextcloud Viewer does\n * not show: the first rows of a CSV or TSV as a table, JSON and XML as\n * formatted read-only text, plain text as is. It fetches at most the first\n * megabyte, offers Download and Open in Files, and renders every cell as text,\n * never as HTML. A file that cannot be read as a table shows its raw text with\n * a sentence saying so.\n *\n * ```vue\n * <CnFilePreview :file=\"{ name: 'data.csv', accessUrl: '/s/abc/download', id: 12 }\" @close=\"file = null\" />\n * ```\n */\nexport default {\n\tname: 'CnFilePreview',\n\n\tcomponents: { NcButton, NcDialog, NcLoadingIcon, NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * The file row: `name`, a content URL (`accessUrl`, `downloadUrl` or\n\t\t * `url`), optionally `type` (mime), `size` and `id` (for Open in Files).\n\t\t *\n\t\t * @type {{name?: string, title?: string, type?: string, mimetype?: string, size?: number, id?: number|string, accessUrl?: string, downloadUrl?: string, url?: string}}\n\t\t */\n\t\tfile: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Rows shown for a CSV or TSV, the header excluded. */\n\t\tmaxRows: {\n\t\t\ttype: Number,\n\t\t\tdefault: 100,\n\t\t},\n\n\t\t/** Bytes fetched at most. */\n\t\tmaxBytes: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1048576,\n\t\t},\n\n\t\t/** Wrap the preview in a dialog. Pass `false` to embed it. */\n\t\tdialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when the dialog is closed.\n\t\t *\n\t\t * @event close\n\t\t */\n\t\t'close',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\tfailed: false,\n\t\t\tunreadable: false,\n\t\t\ttext: '',\n\t\t\theader: [],\n\t\t\tbody: [],\n\t\t\tmode: 'text',\n\t\t\tbytesRead: 0,\n\t\t\ttotalBytes: 0,\n\t\t\tsampleTruncated: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tfileName() {\n\t\t\treturn this.file?.name || this.file?.title || ''\n\t\t},\n\n\t\tkind() {\n\t\t\treturn previewKindOf(this.file) || 'text'\n\t\t},\n\n\t\tcontentUrl() {\n\t\t\treturn fileContentUrl(this.file)\n\t\t},\n\n\t\tfilesUrl() {\n\t\t\tconst id = this.file?.id ?? this.file?.fileId\n\t\t\treturn id ? generateUrl('/f/{fileid}', { fileid: id }) : ''\n\t\t},\n\n\t\ttruncatedText() {\n\t\t\treturn this.sampleTruncated\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\n\t\tfailedLabel() {\n\t\t\treturn t('nextcloud-vue', 'The preview could not be loaded.')\n\t\t},\n\n\t\tunreadableLabel() {\n\t\t\treturn t('nextcloud-vue', 'This file could not be read as a table, so the raw text is shown.')\n\t\t},\n\n\t\tdownloadLabel() {\n\t\t\treturn t('nextcloud-vue', 'Download')\n\t\t},\n\n\t\topenInFilesLabel() {\n\t\t\treturn t('nextcloud-vue', 'Open in Files')\n\t\t},\n\n\t\ttruncatedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Only the first part of the file is shown.')\n\t\t},\n\n\t\t/** \"Showing the first 100 of about 40,000 rows\" or \"Showing all 12 rows\". */\n\t\tcountLabel() {\n\t\t\tconst shown = this.body.length\n\t\t\tif (!this.sampleTruncated) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing all {count} rows', { count: shown })\n\t\t\t}\n\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\tconst perRow = this.bytesRead > 0 && shown > 0 ? this.bytesRead / (shown + 1) : 0\n\t\t\tconst estimate = perRow > 0 && size > 0 ? Math.max(shown, Math.round(size / perRow) - 1) : 0\n\t\t\tif (estimate > shown) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} of about {total} rows', {\n\t\t\t\t\tshown,\n\t\t\t\t\ttotal: estimate.toLocaleString(),\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} rows', { shown })\n\t\t},\n\t},\n\n\twatch: {\n\t\tfile: 'load',\n\t},\n\n\tcreated() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\t/** Fetch the first megabyte and shape it for display. */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\tthis.unreadable = false\n\t\t\tconst url = this.contentUrl\n\t\t\tif (!url) {\n\t\t\t\tthis.failed = true\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\theaders: { ...buildHeaders(null), Range: `bytes=0-${this.maxBytes - 1}` },\n\t\t\t\t\tcredentials: 'same-origin',\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`HTTP ${response.status}`)\n\t\t\t\t}\n\t\t\t\tconst full = await response.text()\n\t\t\t\tconst cut = full.length > this.maxBytes\n\t\t\t\tconst raw = cut ? full.slice(0, this.maxBytes) : full\n\t\t\t\tthis.bytesRead = raw.length\n\t\t\t\tconst range = response.headers?.get?.('Content-Range') || ''\n\t\t\t\tconst total = Number(range.split('/')[1])\n\t\t\t\tthis.totalBytes = Number.isFinite(total) && total > 0 ? total : 0\n\t\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\t\tthis.sampleTruncated = cut || size > raw.length\n\t\t\t\tthis.shape(raw)\n\t\t\t} catch {\n\t\t\t\tthis.failed = true\n\t\t\t}\n\t\t\tthis.loading = false\n\t\t},\n\n\t\t/**\n\t\t * Turn the fetched text into a table or formatted text.\n\t\t *\n\t\t * @param {string} raw The fetched text.\n\t\t */\n\t\tshape(raw) {\n\t\t\tif (this.kind === 'csv' || this.kind === 'tsv') {\n\t\t\t\tconst parsed = parseDelimited(raw, this.kind === 'tsv' ? '\\t' : ',', this.maxRows + 1)\n\t\t\t\tif (!parsed.balanced || parsed.rows.length === 0) {\n\t\t\t\t\tthis.mode = 'text'\n\t\t\t\t\tthis.text = raw\n\t\t\t\t\tthis.unreadable = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.mode = 'table'\n\t\t\t\tthis.header = parsed.rows[0]\n\t\t\t\tthis.body = parsed.rows.slice(1, this.maxRows + 1)\n\t\t\t\tthis.sampleTruncated = this.sampleTruncated || parsed.truncated\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.mode = 'text'\n\t\t\tthis.text = this.kind === 'json' ? this.formatJson(raw) : raw\n\t\t},\n\n\t\t/**\n\t\t * Pretty-print JSON; text that does not parse is returned as is.\n\t\t *\n\t\t * @param {string} raw The JSON text.\n\t\t * @return {string} The formatted text.\n\t\t */\n\t\tformatJson(raw) {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(JSON.parse(raw), null, 2)\n\t\t\t} catch {\n\t\t\t\treturn raw\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-preview {\n\tmin-height: 120px;\n}\n\n.cn-file-preview__scroll {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-file-preview__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tfont-size: 13px;\n}\n\n.cn-file-preview__table th,\n.cn-file-preview__table td {\n\tpadding: 6px 10px;\n\ttext-align: start;\n\tborder-bottom: 1px solid var(--color-border);\n\twhite-space: nowrap;\n}\n\n.cn-file-preview__table th {\n\tposition: sticky;\n\ttop: 0;\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n}\n\n.cn-file-preview__count {\n\tmargin: 8px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-file-preview__text {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tmargin: 0;\n\tpadding: 12px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 8px);\n\tfont-size: 12px;\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"CnFilePreview.vue2.js","sources":["../../../../src/components/CnFilePreview/CnFilePreview.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcDialog\n\t\tv-if=\"dialog\"\n\t\t:name=\"fileName\"\n\t\tsize=\"large\"\n\t\t@closing=\"$emit('close')\">\n\t\t<div class=\"cn-file-preview\" data-testid=\"cn-file-preview\">\n\t\t\t<NcLoadingIcon v-if=\"loading\" :name=\"loadingLabel\" />\n\t\t\t<NcNoteCard v-else-if=\"failed\" type=\"error\">\n\t\t\t\t{{ failedLabel }}\n\t\t\t</NcNoteCard>\n\t\t\t<template v-else>\n\t\t\t\t<NcNoteCard v-if=\"unreadable\" type=\"warning\" data-testid=\"cn-file-preview-unreadable\">\n\t\t\t\t\t{{ unreadableLabel }}\n\t\t\t\t</NcNoteCard>\n\t\t\t\t<template v-if=\"mode === 'table'\">\n\t\t\t\t\t<div class=\"cn-file-preview__scroll\">\n\t\t\t\t\t\t<table class=\"cn-file-preview__table\">\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t<th v-for=\"(cell, c) in header\" :key=\"c\" scope=\"col\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t<tr v-for=\"(row, r) in body\" :key=\"r\">\n\t\t\t\t\t\t\t\t\t<td v-for=\"(cell, c) in row\" :key=\"c\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p class=\"cn-file-preview__count\" data-testid=\"cn-file-preview-count\">\n\t\t\t\t\t\t{{ countLabel }}\n\t\t\t\t\t</p>\n\t\t\t\t</template>\n\t\t\t\t<pre v-else class=\"cn-file-preview__text\" data-testid=\"cn-file-preview-text\">{{ text }}</pre>\n\t\t\t\t<p v-if=\"truncatedText && mode !== 'table'\" class=\"cn-file-preview__count\">\n\t\t\t\t\t{{ truncatedLabel }}\n\t\t\t\t</p>\n\t\t\t</template>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"filesUrl\"\n\t\t\t\t:href=\"filesUrl\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t{{ openInFilesLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"contentUrl\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"contentUrl\"\n\t\t\t\t:download=\"fileName\">\n\t\t\t\t{{ downloadLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</NcDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcDialog, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport { fileContentUrl, previewKindOf } from '../../composables/useFileOpener.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { parseDelimited } from '../../utils/parseDelimited.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\n/**\n * CnFilePreview — an in-page preview of a data file the Nextcloud Viewer does\n * not show: the first rows of a CSV or TSV as a table, JSON and XML as\n * formatted read-only text, plain text as is. It fetches at most the first\n * megabyte, offers Download and Open in Files, and renders every cell as text,\n * never as HTML. A file that cannot be read as a table shows its raw text with\n * a sentence saying so.\n *\n * ```vue\n * <CnFilePreview :file=\"{ name: 'data.csv', accessUrl: '/s/abc/download', id: 12 }\" @close=\"file = null\" />\n * ```\n */\nexport default {\n\tname: 'CnFilePreview',\n\n\tcomponents: { NcButton, NcDialog, NcLoadingIcon, NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * The file row: `name`, a content URL (`accessUrl`, `downloadUrl` or\n\t\t * `url`), optionally `type` (mime), `size` and `id` (for Open in Files).\n\t\t *\n\t\t * @type {{name?: string, title?: string, type?: string, mimetype?: string, size?: number, id?: number|string, accessUrl?: string, downloadUrl?: string, url?: string}}\n\t\t */\n\t\tfile: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Rows shown for a CSV or TSV, the header excluded. */\n\t\tmaxRows: {\n\t\t\ttype: Number,\n\t\t\tdefault: 100,\n\t\t},\n\n\t\t/** Bytes fetched at most. */\n\t\tmaxBytes: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1048576,\n\t\t},\n\n\t\t/** Wrap the preview in a dialog. Pass `false` to embed it. */\n\t\tdialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when the dialog is closed.\n\t\t *\n\t\t * @event close\n\t\t */\n\t\t'close',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\tfailed: false,\n\t\t\tunreadable: false,\n\t\t\ttext: '',\n\t\t\theader: [],\n\t\t\tbody: [],\n\t\t\tmode: 'text',\n\t\t\tbytesRead: 0,\n\t\t\ttotalBytes: 0,\n\t\t\tsampleTruncated: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tfileName() {\n\t\t\treturn this.file?.name || this.file?.title || ''\n\t\t},\n\n\t\tkind() {\n\t\t\treturn previewKindOf(this.file) || 'text'\n\t\t},\n\n\t\tcontentUrl() {\n\t\t\treturn fileContentUrl(this.file)\n\t\t},\n\n\t\tfilesUrl() {\n\t\t\tconst id = this.file?.id ?? this.file?.fileId\n\t\t\treturn id ? generateUrl('/f/{fileid}', { fileid: id }) : ''\n\t\t},\n\n\t\ttruncatedText() {\n\t\t\treturn this.sampleTruncated\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\n\t\tfailedLabel() {\n\t\t\treturn t('nextcloud-vue', 'The preview could not be loaded.')\n\t\t},\n\n\t\tunreadableLabel() {\n\t\t\treturn t('nextcloud-vue', 'This file could not be read as a table, so the raw text is shown.')\n\t\t},\n\n\t\tdownloadLabel() {\n\t\t\treturn t('nextcloud-vue', 'Download')\n\t\t},\n\n\t\topenInFilesLabel() {\n\t\t\treturn t('nextcloud-vue', 'Open in Files')\n\t\t},\n\n\t\ttruncatedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Only the first part of the file is shown.')\n\t\t},\n\n\t\t/** \"Showing the first 100 of about 40,000 rows\" or \"Showing all 12 rows\". */\n\t\tcountLabel() {\n\t\t\tconst shown = this.body.length\n\t\t\tif (!this.sampleTruncated) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing all {count} rows', { count: shown })\n\t\t\t}\n\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\tconst perRow = this.bytesRead > 0 && shown > 0 ? this.bytesRead / (shown + 1) : 0\n\t\t\tconst estimate = perRow > 0 && size > 0 ? Math.max(shown, Math.round(size / perRow) - 1) : 0\n\t\t\tif (estimate > shown) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} of about {total} rows', {\n\t\t\t\t\tshown,\n\t\t\t\t\ttotal: estimate.toLocaleString(userLocale()),\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} rows', { shown })\n\t\t},\n\t},\n\n\twatch: {\n\t\tfile: 'load',\n\t},\n\n\tcreated() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\t/** Fetch the first megabyte and shape it for display. */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\tthis.unreadable = false\n\t\t\tconst url = this.contentUrl\n\t\t\tif (!url) {\n\t\t\t\tthis.failed = true\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\theaders: { ...buildHeaders(null), Range: `bytes=0-${this.maxBytes - 1}` },\n\t\t\t\t\tcredentials: 'same-origin',\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`HTTP ${response.status}`)\n\t\t\t\t}\n\t\t\t\tconst full = await response.text()\n\t\t\t\tconst cut = full.length > this.maxBytes\n\t\t\t\tconst raw = cut ? full.slice(0, this.maxBytes) : full\n\t\t\t\tthis.bytesRead = raw.length\n\t\t\t\tconst range = response.headers?.get?.('Content-Range') || ''\n\t\t\t\tconst total = Number(range.split('/')[1])\n\t\t\t\tthis.totalBytes = Number.isFinite(total) && total > 0 ? total : 0\n\t\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\t\tthis.sampleTruncated = cut || size > raw.length\n\t\t\t\tthis.shape(raw)\n\t\t\t} catch {\n\t\t\t\tthis.failed = true\n\t\t\t}\n\t\t\tthis.loading = false\n\t\t},\n\n\t\t/**\n\t\t * Turn the fetched text into a table or formatted text.\n\t\t *\n\t\t * @param {string} raw The fetched text.\n\t\t */\n\t\tshape(raw) {\n\t\t\tif (this.kind === 'csv' || this.kind === 'tsv') {\n\t\t\t\tconst parsed = parseDelimited(raw, this.kind === 'tsv' ? '\\t' : ',', this.maxRows + 1)\n\t\t\t\tif (!parsed.balanced || parsed.rows.length === 0) {\n\t\t\t\t\tthis.mode = 'text'\n\t\t\t\t\tthis.text = raw\n\t\t\t\t\tthis.unreadable = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.mode = 'table'\n\t\t\t\tthis.header = parsed.rows[0]\n\t\t\t\tthis.body = parsed.rows.slice(1, this.maxRows + 1)\n\t\t\t\tthis.sampleTruncated = this.sampleTruncated || parsed.truncated\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.mode = 'text'\n\t\t\tthis.text = this.kind === 'json' ? this.formatJson(raw) : raw\n\t\t},\n\n\t\t/**\n\t\t * Pretty-print JSON; text that does not parse is returned as is.\n\t\t *\n\t\t * @param {string} raw The JSON text.\n\t\t * @return {string} The formatted text.\n\t\t */\n\t\tformatJson(raw) {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(JSON.parse(raw), null, 2)\n\t\t\t} catch {\n\t\t\t\treturn raw\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-preview {\n\tmin-height: 120px;\n}\n\n.cn-file-preview__scroll {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-file-preview__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tfont-size: 13px;\n}\n\n.cn-file-preview__table th,\n.cn-file-preview__table td {\n\tpadding: 6px 10px;\n\ttext-align: start;\n\tborder-bottom: 1px solid var(--color-border);\n\twhite-space: nowrap;\n}\n\n.cn-file-preview__table th {\n\tposition: sticky;\n\ttop: 0;\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n}\n\n.cn-file-preview__count {\n\tmargin: 8px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-file-preview__text {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tmargin: 0;\n\tpadding: 12px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 8px);\n\tfont-size: 12px;\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnFilePreview.vue3.js","sources":["../../../../src/components/CnFilePreview/CnFilePreview.vue?vue&type=template&id=be44a5f6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcDialog\n\t\tv-if=\"dialog\"\n\t\t:name=\"fileName\"\n\t\tsize=\"large\"\n\t\t@closing=\"$emit('close')\">\n\t\t<div class=\"cn-file-preview\" data-testid=\"cn-file-preview\">\n\t\t\t<NcLoadingIcon v-if=\"loading\" :name=\"loadingLabel\" />\n\t\t\t<NcNoteCard v-else-if=\"failed\" type=\"error\">\n\t\t\t\t{{ failedLabel }}\n\t\t\t</NcNoteCard>\n\t\t\t<template v-else>\n\t\t\t\t<NcNoteCard v-if=\"unreadable\" type=\"warning\" data-testid=\"cn-file-preview-unreadable\">\n\t\t\t\t\t{{ unreadableLabel }}\n\t\t\t\t</NcNoteCard>\n\t\t\t\t<template v-if=\"mode === 'table'\">\n\t\t\t\t\t<div class=\"cn-file-preview__scroll\">\n\t\t\t\t\t\t<table class=\"cn-file-preview__table\">\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t<th v-for=\"(cell, c) in header\" :key=\"c\" scope=\"col\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t<tr v-for=\"(row, r) in body\" :key=\"r\">\n\t\t\t\t\t\t\t\t\t<td v-for=\"(cell, c) in row\" :key=\"c\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p class=\"cn-file-preview__count\" data-testid=\"cn-file-preview-count\">\n\t\t\t\t\t\t{{ countLabel }}\n\t\t\t\t\t</p>\n\t\t\t\t</template>\n\t\t\t\t<pre v-else class=\"cn-file-preview__text\" data-testid=\"cn-file-preview-text\">{{ text }}</pre>\n\t\t\t\t<p v-if=\"truncatedText && mode !== 'table'\" class=\"cn-file-preview__count\">\n\t\t\t\t\t{{ truncatedLabel }}\n\t\t\t\t</p>\n\t\t\t</template>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"filesUrl\"\n\t\t\t\t:href=\"filesUrl\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t{{ openInFilesLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"contentUrl\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"contentUrl\"\n\t\t\t\t:download=\"fileName\">\n\t\t\t\t{{ downloadLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</NcDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcDialog, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport { fileContentUrl, previewKindOf } from '../../composables/useFileOpener.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { parseDelimited } from '../../utils/parseDelimited.js'\n\n/**\n * CnFilePreview — an in-page preview of a data file the Nextcloud Viewer does\n * not show: the first rows of a CSV or TSV as a table, JSON and XML as\n * formatted read-only text, plain text as is. It fetches at most the first\n * megabyte, offers Download and Open in Files, and renders every cell as text,\n * never as HTML. A file that cannot be read as a table shows its raw text with\n * a sentence saying so.\n *\n * ```vue\n * <CnFilePreview :file=\"{ name: 'data.csv', accessUrl: '/s/abc/download', id: 12 }\" @close=\"file = null\" />\n * ```\n */\nexport default {\n\tname: 'CnFilePreview',\n\n\tcomponents: { NcButton, NcDialog, NcLoadingIcon, NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * The file row: `name`, a content URL (`accessUrl`, `downloadUrl` or\n\t\t * `url`), optionally `type` (mime), `size` and `id` (for Open in Files).\n\t\t *\n\t\t * @type {{name?: string, title?: string, type?: string, mimetype?: string, size?: number, id?: number|string, accessUrl?: string, downloadUrl?: string, url?: string}}\n\t\t */\n\t\tfile: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Rows shown for a CSV or TSV, the header excluded. */\n\t\tmaxRows: {\n\t\t\ttype: Number,\n\t\t\tdefault: 100,\n\t\t},\n\n\t\t/** Bytes fetched at most. */\n\t\tmaxBytes: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1048576,\n\t\t},\n\n\t\t/** Wrap the preview in a dialog. Pass `false` to embed it. */\n\t\tdialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when the dialog is closed.\n\t\t *\n\t\t * @event close\n\t\t */\n\t\t'close',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\tfailed: false,\n\t\t\tunreadable: false,\n\t\t\ttext: '',\n\t\t\theader: [],\n\t\t\tbody: [],\n\t\t\tmode: 'text',\n\t\t\tbytesRead: 0,\n\t\t\ttotalBytes: 0,\n\t\t\tsampleTruncated: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tfileName() {\n\t\t\treturn this.file?.name || this.file?.title || ''\n\t\t},\n\n\t\tkind() {\n\t\t\treturn previewKindOf(this.file) || 'text'\n\t\t},\n\n\t\tcontentUrl() {\n\t\t\treturn fileContentUrl(this.file)\n\t\t},\n\n\t\tfilesUrl() {\n\t\t\tconst id = this.file?.id ?? this.file?.fileId\n\t\t\treturn id ? generateUrl('/f/{fileid}', { fileid: id }) : ''\n\t\t},\n\n\t\ttruncatedText() {\n\t\t\treturn this.sampleTruncated\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\n\t\tfailedLabel() {\n\t\t\treturn t('nextcloud-vue', 'The preview could not be loaded.')\n\t\t},\n\n\t\tunreadableLabel() {\n\t\t\treturn t('nextcloud-vue', 'This file could not be read as a table, so the raw text is shown.')\n\t\t},\n\n\t\tdownloadLabel() {\n\t\t\treturn t('nextcloud-vue', 'Download')\n\t\t},\n\n\t\topenInFilesLabel() {\n\t\t\treturn t('nextcloud-vue', 'Open in Files')\n\t\t},\n\n\t\ttruncatedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Only the first part of the file is shown.')\n\t\t},\n\n\t\t/** \"Showing the first 100 of about 40,000 rows\" or \"Showing all 12 rows\". */\n\t\tcountLabel() {\n\t\t\tconst shown = this.body.length\n\t\t\tif (!this.sampleTruncated) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing all {count} rows', { count: shown })\n\t\t\t}\n\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\tconst perRow = this.bytesRead > 0 && shown > 0 ? this.bytesRead / (shown + 1) : 0\n\t\t\tconst estimate = perRow > 0 && size > 0 ? Math.max(shown, Math.round(size / perRow) - 1) : 0\n\t\t\tif (estimate > shown) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} of about {total} rows', {\n\t\t\t\t\tshown,\n\t\t\t\t\ttotal: estimate.toLocaleString(),\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} rows', { shown })\n\t\t},\n\t},\n\n\twatch: {\n\t\tfile: 'load',\n\t},\n\n\tcreated() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\t/** Fetch the first megabyte and shape it for display. */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\tthis.unreadable = false\n\t\t\tconst url = this.contentUrl\n\t\t\tif (!url) {\n\t\t\t\tthis.failed = true\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\theaders: { ...buildHeaders(null), Range: `bytes=0-${this.maxBytes - 1}` },\n\t\t\t\t\tcredentials: 'same-origin',\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`HTTP ${response.status}`)\n\t\t\t\t}\n\t\t\t\tconst full = await response.text()\n\t\t\t\tconst cut = full.length > this.maxBytes\n\t\t\t\tconst raw = cut ? full.slice(0, this.maxBytes) : full\n\t\t\t\tthis.bytesRead = raw.length\n\t\t\t\tconst range = response.headers?.get?.('Content-Range') || ''\n\t\t\t\tconst total = Number(range.split('/')[1])\n\t\t\t\tthis.totalBytes = Number.isFinite(total) && total > 0 ? total : 0\n\t\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\t\tthis.sampleTruncated = cut || size > raw.length\n\t\t\t\tthis.shape(raw)\n\t\t\t} catch {\n\t\t\t\tthis.failed = true\n\t\t\t}\n\t\t\tthis.loading = false\n\t\t},\n\n\t\t/**\n\t\t * Turn the fetched text into a table or formatted text.\n\t\t *\n\t\t * @param {string} raw The fetched text.\n\t\t */\n\t\tshape(raw) {\n\t\t\tif (this.kind === 'csv' || this.kind === 'tsv') {\n\t\t\t\tconst parsed = parseDelimited(raw, this.kind === 'tsv' ? '\\t' : ',', this.maxRows + 1)\n\t\t\t\tif (!parsed.balanced || parsed.rows.length === 0) {\n\t\t\t\t\tthis.mode = 'text'\n\t\t\t\t\tthis.text = raw\n\t\t\t\t\tthis.unreadable = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.mode = 'table'\n\t\t\t\tthis.header = parsed.rows[0]\n\t\t\t\tthis.body = parsed.rows.slice(1, this.maxRows + 1)\n\t\t\t\tthis.sampleTruncated = this.sampleTruncated || parsed.truncated\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.mode = 'text'\n\t\t\tthis.text = this.kind === 'json' ? this.formatJson(raw) : raw\n\t\t},\n\n\t\t/**\n\t\t * Pretty-print JSON; text that does not parse is returned as is.\n\t\t *\n\t\t * @param {string} raw The JSON text.\n\t\t * @return {string} The formatted text.\n\t\t */\n\t\tformatJson(raw) {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(JSON.parse(raw), null, 2)\n\t\t\t} catch {\n\t\t\t\treturn raw\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-preview {\n\tmin-height: 120px;\n}\n\n.cn-file-preview__scroll {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-file-preview__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tfont-size: 13px;\n}\n\n.cn-file-preview__table th,\n.cn-file-preview__table td {\n\tpadding: 6px 10px;\n\ttext-align: start;\n\tborder-bottom: 1px solid var(--color-border);\n\twhite-space: nowrap;\n}\n\n.cn-file-preview__table th {\n\tposition: sticky;\n\ttop: 0;\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n}\n\n.cn-file-preview__count {\n\tmargin: 8px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-file-preview__text {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tmargin: 0;\n\tpadding: 12px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 8px);\n\tfont-size: 12px;\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n}\n</style>\n"],"names":["_createBlock","_createElementBlock","_Fragment","_createElementVNode","_openBlock","_renderList","_toDisplayString"],"mappings":"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"}
1
+ {"version":3,"file":"CnFilePreview.vue3.js","sources":["../../../../src/components/CnFilePreview/CnFilePreview.vue?vue&type=template&id=a604b852&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcDialog\n\t\tv-if=\"dialog\"\n\t\t:name=\"fileName\"\n\t\tsize=\"large\"\n\t\t@closing=\"$emit('close')\">\n\t\t<div class=\"cn-file-preview\" data-testid=\"cn-file-preview\">\n\t\t\t<NcLoadingIcon v-if=\"loading\" :name=\"loadingLabel\" />\n\t\t\t<NcNoteCard v-else-if=\"failed\" type=\"error\">\n\t\t\t\t{{ failedLabel }}\n\t\t\t</NcNoteCard>\n\t\t\t<template v-else>\n\t\t\t\t<NcNoteCard v-if=\"unreadable\" type=\"warning\" data-testid=\"cn-file-preview-unreadable\">\n\t\t\t\t\t{{ unreadableLabel }}\n\t\t\t\t</NcNoteCard>\n\t\t\t\t<template v-if=\"mode === 'table'\">\n\t\t\t\t\t<div class=\"cn-file-preview__scroll\">\n\t\t\t\t\t\t<table class=\"cn-file-preview__table\">\n\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t<th v-for=\"(cell, c) in header\" :key=\"c\" scope=\"col\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t<tr v-for=\"(row, r) in body\" :key=\"r\">\n\t\t\t\t\t\t\t\t\t<td v-for=\"(cell, c) in row\" :key=\"c\">\n\t\t\t\t\t\t\t\t\t\t{{ cell }}\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p class=\"cn-file-preview__count\" data-testid=\"cn-file-preview-count\">\n\t\t\t\t\t\t{{ countLabel }}\n\t\t\t\t\t</p>\n\t\t\t\t</template>\n\t\t\t\t<pre v-else class=\"cn-file-preview__text\" data-testid=\"cn-file-preview-text\">{{ text }}</pre>\n\t\t\t\t<p v-if=\"truncatedText && mode !== 'table'\" class=\"cn-file-preview__count\">\n\t\t\t\t\t{{ truncatedLabel }}\n\t\t\t\t</p>\n\t\t\t</template>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"filesUrl\"\n\t\t\t\t:href=\"filesUrl\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener noreferrer\">\n\t\t\t\t{{ openInFilesLabel }}\n\t\t\t</NcButton>\n\t\t\t<NcButton\n\t\t\t\tv-if=\"contentUrl\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"contentUrl\"\n\t\t\t\t:download=\"fileName\">\n\t\t\t\t{{ downloadLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</NcDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcDialog, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport { fileContentUrl, previewKindOf } from '../../composables/useFileOpener.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { parseDelimited } from '../../utils/parseDelimited.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\n/**\n * CnFilePreview — an in-page preview of a data file the Nextcloud Viewer does\n * not show: the first rows of a CSV or TSV as a table, JSON and XML as\n * formatted read-only text, plain text as is. It fetches at most the first\n * megabyte, offers Download and Open in Files, and renders every cell as text,\n * never as HTML. A file that cannot be read as a table shows its raw text with\n * a sentence saying so.\n *\n * ```vue\n * <CnFilePreview :file=\"{ name: 'data.csv', accessUrl: '/s/abc/download', id: 12 }\" @close=\"file = null\" />\n * ```\n */\nexport default {\n\tname: 'CnFilePreview',\n\n\tcomponents: { NcButton, NcDialog, NcLoadingIcon, NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * The file row: `name`, a content URL (`accessUrl`, `downloadUrl` or\n\t\t * `url`), optionally `type` (mime), `size` and `id` (for Open in Files).\n\t\t *\n\t\t * @type {{name?: string, title?: string, type?: string, mimetype?: string, size?: number, id?: number|string, accessUrl?: string, downloadUrl?: string, url?: string}}\n\t\t */\n\t\tfile: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Rows shown for a CSV or TSV, the header excluded. */\n\t\tmaxRows: {\n\t\t\ttype: Number,\n\t\t\tdefault: 100,\n\t\t},\n\n\t\t/** Bytes fetched at most. */\n\t\tmaxBytes: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1048576,\n\t\t},\n\n\t\t/** Wrap the preview in a dialog. Pass `false` to embed it. */\n\t\tdialog: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when the dialog is closed.\n\t\t *\n\t\t * @event close\n\t\t */\n\t\t'close',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\tfailed: false,\n\t\t\tunreadable: false,\n\t\t\ttext: '',\n\t\t\theader: [],\n\t\t\tbody: [],\n\t\t\tmode: 'text',\n\t\t\tbytesRead: 0,\n\t\t\ttotalBytes: 0,\n\t\t\tsampleTruncated: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tfileName() {\n\t\t\treturn this.file?.name || this.file?.title || ''\n\t\t},\n\n\t\tkind() {\n\t\t\treturn previewKindOf(this.file) || 'text'\n\t\t},\n\n\t\tcontentUrl() {\n\t\t\treturn fileContentUrl(this.file)\n\t\t},\n\n\t\tfilesUrl() {\n\t\t\tconst id = this.file?.id ?? this.file?.fileId\n\t\t\treturn id ? generateUrl('/f/{fileid}', { fileid: id }) : ''\n\t\t},\n\n\t\ttruncatedText() {\n\t\t\treturn this.sampleTruncated\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\n\t\tfailedLabel() {\n\t\t\treturn t('nextcloud-vue', 'The preview could not be loaded.')\n\t\t},\n\n\t\tunreadableLabel() {\n\t\t\treturn t('nextcloud-vue', 'This file could not be read as a table, so the raw text is shown.')\n\t\t},\n\n\t\tdownloadLabel() {\n\t\t\treturn t('nextcloud-vue', 'Download')\n\t\t},\n\n\t\topenInFilesLabel() {\n\t\t\treturn t('nextcloud-vue', 'Open in Files')\n\t\t},\n\n\t\ttruncatedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Only the first part of the file is shown.')\n\t\t},\n\n\t\t/** \"Showing the first 100 of about 40,000 rows\" or \"Showing all 12 rows\". */\n\t\tcountLabel() {\n\t\t\tconst shown = this.body.length\n\t\t\tif (!this.sampleTruncated) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing all {count} rows', { count: shown })\n\t\t\t}\n\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\tconst perRow = this.bytesRead > 0 && shown > 0 ? this.bytesRead / (shown + 1) : 0\n\t\t\tconst estimate = perRow > 0 && size > 0 ? Math.max(shown, Math.round(size / perRow) - 1) : 0\n\t\t\tif (estimate > shown) {\n\t\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} of about {total} rows', {\n\t\t\t\t\tshown,\n\t\t\t\t\ttotal: estimate.toLocaleString(userLocale()),\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Showing the first {shown} rows', { shown })\n\t\t},\n\t},\n\n\twatch: {\n\t\tfile: 'load',\n\t},\n\n\tcreated() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\t/** Fetch the first megabyte and shape it for display. */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\tthis.unreadable = false\n\t\t\tconst url = this.contentUrl\n\t\t\tif (!url) {\n\t\t\t\tthis.failed = true\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\theaders: { ...buildHeaders(null), Range: `bytes=0-${this.maxBytes - 1}` },\n\t\t\t\t\tcredentials: 'same-origin',\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`HTTP ${response.status}`)\n\t\t\t\t}\n\t\t\t\tconst full = await response.text()\n\t\t\t\tconst cut = full.length > this.maxBytes\n\t\t\t\tconst raw = cut ? full.slice(0, this.maxBytes) : full\n\t\t\t\tthis.bytesRead = raw.length\n\t\t\t\tconst range = response.headers?.get?.('Content-Range') || ''\n\t\t\t\tconst total = Number(range.split('/')[1])\n\t\t\t\tthis.totalBytes = Number.isFinite(total) && total > 0 ? total : 0\n\t\t\t\tconst size = this.totalBytes || Number(this.file?.size) || 0\n\t\t\t\tthis.sampleTruncated = cut || size > raw.length\n\t\t\t\tthis.shape(raw)\n\t\t\t} catch {\n\t\t\t\tthis.failed = true\n\t\t\t}\n\t\t\tthis.loading = false\n\t\t},\n\n\t\t/**\n\t\t * Turn the fetched text into a table or formatted text.\n\t\t *\n\t\t * @param {string} raw The fetched text.\n\t\t */\n\t\tshape(raw) {\n\t\t\tif (this.kind === 'csv' || this.kind === 'tsv') {\n\t\t\t\tconst parsed = parseDelimited(raw, this.kind === 'tsv' ? '\\t' : ',', this.maxRows + 1)\n\t\t\t\tif (!parsed.balanced || parsed.rows.length === 0) {\n\t\t\t\t\tthis.mode = 'text'\n\t\t\t\t\tthis.text = raw\n\t\t\t\t\tthis.unreadable = true\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.mode = 'table'\n\t\t\t\tthis.header = parsed.rows[0]\n\t\t\t\tthis.body = parsed.rows.slice(1, this.maxRows + 1)\n\t\t\t\tthis.sampleTruncated = this.sampleTruncated || parsed.truncated\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.mode = 'text'\n\t\t\tthis.text = this.kind === 'json' ? this.formatJson(raw) : raw\n\t\t},\n\n\t\t/**\n\t\t * Pretty-print JSON; text that does not parse is returned as is.\n\t\t *\n\t\t * @param {string} raw The JSON text.\n\t\t * @return {string} The formatted text.\n\t\t */\n\t\tformatJson(raw) {\n\t\t\ttry {\n\t\t\t\treturn JSON.stringify(JSON.parse(raw), null, 2)\n\t\t\t} catch {\n\t\t\t\treturn raw\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-file-preview {\n\tmin-height: 120px;\n}\n\n.cn-file-preview__scroll {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-file-preview__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\tfont-size: 13px;\n}\n\n.cn-file-preview__table th,\n.cn-file-preview__table td {\n\tpadding: 6px 10px;\n\ttext-align: start;\n\tborder-bottom: 1px solid var(--color-border);\n\twhite-space: nowrap;\n}\n\n.cn-file-preview__table th {\n\tposition: sticky;\n\ttop: 0;\n\tbackground: var(--color-background-dark);\n\tfont-weight: 600;\n}\n\n.cn-file-preview__count {\n\tmargin: 8px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-file-preview__text {\n\tmax-height: 60vh;\n\toverflow: auto;\n\tmargin: 0;\n\tpadding: 12px;\n\tbackground: var(--color-background-dark);\n\tborder-radius: var(--border-radius-large, 8px);\n\tfont-size: 12px;\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n}\n</style>\n"],"names":["_createBlock","_createElementBlock","_Fragment","_createElementVNode","_openBlock","_renderList","_toDisplayString"],"mappings":";;;AAcO,EAAA,KAAK,EAAC,iBAAE;AAAA,EAAA,aAAA,EAAA,iBAAA;;AAUL,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAyB,GAAA;AAC5B,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAA,GAAA;;AAiBZ,EAAA,KAAM,EAAC,wBAAwB;AAAC,EAAA,aAAW,EAAC,uBAAuB;;;;AAI7D,EAAA,KAAA,EAAA,uBAAA;AAAA,EAAA,aAAA,EAAA,sBAAA;;;;AACN,EAAA,KAAA,EAAA,wBAAA;;;;;;;;;UArCC,MAAM,CAAA,MAAA;;;AACX,QAAA,IAAI,EAAE,QAAO,CAAA,QAAA;AACb,QAAA,IAAG,EAAC,OAAM;AACT,QAAA,SAAM,uCAAE,IAAK,CAAA,KAAA,CAAA,OAAA,CAAA,CAAA,CAAA;;AAuCJ,QAAA,OAAK,gBAOJ;WALJ,QAAQ,CAAA,QAAA;uCAKJ,CAAA,mBAAA,EAAA;;AAJT,gBAAA,IAAI,EAAE,QAAE,CAAA,QAAA;AACT,gBAAA,MAAM,EAAC,QAAA;AACP,gBAAA,GAAG,EAAC,qBAAY;;iCAChB,MAAgB;kDAAb,QAAa,CAAA,gBAAA,CAAA,EAAA,CAAA,YAAA;;;;;WAGV;4BADNA;;AAEA,gBAAA,OAAO,EAAC,SAAC;AACR,gBAAA,IAAI,EAAE,QAAC,CAAA,UAAA;AACP,gBAAA,QAAM,EAAA,QAAA,CAAA,QAAA;;;;;;;;;+BAbH;6BAAA,iBAAA,EAAA;aApCW,KAAA,CAAA,OAAA;8BAAhBA,WAAgB,CAAA,wBAAA,EAAA;;AAAA,kBAAA,IAAA,EAAA,QAAA,CAAA,YAAA;;iBACO,KAAM,CAAA,MAAA;gCAA5BA,WAEY,CAAA,qBAAA,EAAA;;AAFkB,oBAAA,IAAI,EAAC,OAAO;;qCACzC,MAAgB;8DAAH,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;;;;gCAEdC,kBA+BU,CAAAC,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;qBA9BQ,KAAU,CAAA,UAAA;sCAA3BF,WAEQ,CAAA,qBAAA,EAAA;;AAFqB,0BAAA,IAAI,EAAC,SAAE;AAAA,0BAAA,aAAA,EAAA,4BAAA;;2CACnC,MAAoB;4DAAlB,QAAe,CAAA,eAAA,CAAA,EAAA,CAAA,YAAA;;;;;qBAEZ,KAAA,CAAA,IAAA,KAAA,OAAA;sCAAHC,kBAsBD,CAAAC,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;6CAJI,iBAAA,EAAA;AAhBD,4BAAAC,kBAAA,CAeF,SAfE,UAeF,EAAA;gDARO,CAAA,OAAA,EAAA,IAAA,EAAA;gCALPA,kBAIK,CAAA,IAAA,EAAA,IAAA,EAAA;AAHJ,mCAAAC,SAAA,CAAA,IAAA,CAAA,EAAAH,kBAAA,CAEKC,QAFA,EAAA,IAAA,EAAAG,UAAA,CAAA,KAAA,CAAA,MAAA,EAAA,CAAA,IAAA,EAAA,CAAA,KAAA;yDAALJ,kBAEK,CAAA,IAAA,EAAA;AAFA,sCAAA,GAAA,EAAA,CAAA;AAAA,sCAAA,KAAA,EAAA,KAAA;uDACD,IAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;;;gDAUE,CAAA,OAAA,EAAA,IAAA,EAAA;AALP,iCAAAG,SAAA,CAAA,IAAA,CAAA,EAAAH,kBAAA,CAIKC,QAJD,EAAA,IAAA,EAAAG,UAAA,CAAA,KAAA,CAAA,IAAA,EAAA,CAAA,GAAA,EAAA,CAAA,KAAA;uDAAJJ,kBAIK,CAAA,IAAA,EAAA,EAJD,GAAA,EAAA,CAAA,EAAA,EAAA;AACH,qCAAAG,SAAA,CAAA,IAAA,CAAA,EAAAH,kBAAA,CAECC,QAFI,EAAA,IAAA,EAAAG,UAAA,CAAA,GAAA,EAAA,CAAA,IAAA,EAAA,CAAA,KAAA;AAAL,sCAAA,QAAAD,SAAA,EAAA,EAAAH,kBAAA,CAEC,IAFI,EAAA,EAAA,GAAA,EAAA,CAAA,oBACD,IAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;;;;;;0BAMRE,kBAEI,CAAA,GAAA,EAFJ,UAEI,EAAAG,eAAA,CADA,QAAU,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;;sCAGfL,kBAAS,CAAA,KAAA,EAAT,UAAS,EAAAK,eAAA,CAAA,KAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA,CAAA;AACN,oBAAA,CAAA,QAAA,CAAA,aAAA,IAAA,KAAA,CAAA,IAAA,KAAA,OAAA;sCAAHL,kBAEI,CAAA,GAAA,EAFJ,UAEI,EAAAK,eAAA,CADA,QAAY,CAAA,cAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnFilesBrowser.vue2.js';
2
2
  import { render } from './CnFilesBrowser.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-15dbc3b4";
5
+ script.__scopeId = "data-v-120dc0ec";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,5 +1,6 @@
1
1
  import { getCurrentUser } from '@nextcloud/auth';
2
2
  import axios__default from '@nextcloud/axios';
3
+ import { subscribe, unsubscribe } from '@nextcloud/event-bus';
3
4
  import { View, sortNodes, getNewFileMenuEntries, FileType, getFileActions, formatFileSize } from '@nextcloud/files';
4
5
  import { getRootPath, getClient, resultToNode, getRemoteURL, getDefaultPropfind } from '@nextcloud/files/dav';
5
6
  import { translate, translatePlural } from '@nextcloud/l10n';
@@ -30,6 +31,12 @@ let uploadSeq = 0;
30
31
  /** A folder glyph, the icon the Files app's View constructor requires. */
31
32
  const VIEW_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
32
33
 
34
+ // Event-bus channels that mean the open folder may have changed: the page's
35
+ // Refresh signal, and the node events the Files app's actions and uploader emit.
36
+ const FOLDER_CHANGED_CHANNELS = ['cn:page:refresh', 'files:node:created', 'files:node:deleted', 'files:node:updated', 'files:node:moved'];
37
+
38
+ const FOLDER_CHANGED_DEBOUNCE_MS = 300;
39
+
33
40
  var script = {
34
41
  name: 'CnFilesBrowser',
35
42
 
@@ -66,6 +73,10 @@ var script = {
66
73
  // The page's pre-bound action runner (CnPageRenderer provides it);
67
74
  // absent outside a page tree, in which case the bare dispatcher runs.
68
75
  cnDispatchAction: { default: null },
76
+ // The host app's translate function (CnAppRoot provides it). A row
77
+ // action or column the manifest declares is authored in English as
78
+ // the catalogue key, and read through this in the user's language.
79
+ cnTranslate: { default: () => (key) => key },
69
80
  },
70
81
 
71
82
  props: {
@@ -726,6 +737,19 @@ var script = {
726
737
  mounted() {
727
738
  this.loadColumnChoice();
728
739
  this.refresh();
740
+ // Another surface on the page changed the folder: the page's Refresh,
741
+ // a dialog that filed a document, or a Files action whose work ends
742
+ // after its own call returned (the delete confirmation, an upload).
743
+ for (const channel of FOLDER_CHANGED_CHANNELS) {
744
+ subscribe(channel, this.onFolderChanged);
745
+ }
746
+ },
747
+
748
+ beforeUnmount() {
749
+ for (const channel of FOLDER_CHANGED_CHANNELS) {
750
+ unsubscribe(channel, this.onFolderChanged);
751
+ }
752
+ clearTimeout(this._folderChangedTimer);
729
753
  },
730
754
 
731
755
  methods: {
@@ -1013,6 +1037,30 @@ var script = {
1013
1037
  * @param {object} node The node.
1014
1038
  * @return {string} The label.
1015
1039
  */
1040
+ /**
1041
+ * A host action's label in the user's language.
1042
+ *
1043
+ * @spec openspec/changes/files-browser-hears-the-page/specs/files-browser/spec.md#requirement-a-declared-row-action-reads-in-the-users-language
1044
+ * @param {object} action The manifest-declared action.
1045
+ * @return {string} The label.
1046
+ */
1047
+ hostLabel(action) {
1048
+ return this.cnTranslate(String(action?.label ?? ''))
1049
+ },
1050
+
1051
+ /**
1052
+ * Re-read the folder once after a burst of change signals. One upload
1053
+ * of ten files answers ten `files:node:created`, and one PROPFIND
1054
+ * covers them all.
1055
+ *
1056
+ * @spec openspec/changes/files-browser-hears-the-page/specs/files-browser/spec.md#requirement-the-files-browser-re-reads-when-the-page-says-files-changed
1057
+ * @return {void}
1058
+ */
1059
+ onFolderChanged() {
1060
+ clearTimeout(this._folderChangedTimer);
1061
+ this._folderChangedTimer = setTimeout(() => this.refresh(), FOLDER_CHANGED_DEBOUNCE_MS);
1062
+ },
1063
+
1016
1064
  labelOf(action, node) {
1017
1065
  try {
1018
1066
  return action.displayName(this.contextFor(node))
@@ -1404,7 +1452,7 @@ var script = {
1404
1452
  */
1405
1453
  columnLabel(column) {
1406
1454
  if (column && typeof column.label === 'string' && column.label !== '') {
1407
- return column.label
1455
+ return this.cnTranslate(column.label)
1408
1456
  }
1409
1457
  return this.builtInColumnLabels[column && column.key] || (column && column.key) || ''
1410
1458
  },