@conduction/nextcloud-vue 2.85.0 → 2.87.0

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 (301) hide show
  1. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue.js +1 -1
  2. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue2.js +3 -2
  3. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdvancedFormDialog/CnMetadataTab.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue.js +1 -1
  6. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js +5 -4
  7. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue.js +1 -1
  10. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue2.js +2 -1
  11. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  14. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +2 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +156 -14
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +105 -74
  21. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue.js +1 -1
  23. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue2.js +2 -1
  24. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCalendarEventPicker/CnCalendarEventPicker.vue3.js.map +1 -1
  26. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue.js +1 -1
  27. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +2 -1
  28. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +6 -5
  32. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue.js +1 -1
  35. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue2.js +2 -1
  36. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue2.js.map +1 -1
  37. package/dist/esm/components/CnCollectivePagePicker/CnCollectivePagePicker.vue3.js.map +1 -1
  38. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  39. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +2 -1
  40. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  41. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  42. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  43. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +2 -1
  44. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  47. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +2 -1
  48. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  49. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  50. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue.js +1 -1
  51. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue2.js +3 -1
  52. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue2.js.map +1 -1
  53. package/dist/esm/components/CnDataMatrix/CnDataMatrix.vue3.js.map +1 -1
  54. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue2.js +2 -1
  55. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue2.js.map +1 -1
  56. package/dist/esm/components/CnDetailPage/CnDetailHeaderChips.vue3.js.map +1 -1
  57. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue.js +1 -1
  58. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue2.js +2 -1
  59. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue2.js.map +1 -1
  60. package/dist/esm/components/CnEmailPicker/CnEmailPicker.vue3.js.map +1 -1
  61. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue.js +1 -1
  62. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue2.js +3 -1
  63. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue2.js.map +1 -1
  64. package/dist/esm/components/CnFederationStatus/CnFederationStatus.vue3.js.map +1 -1
  65. package/dist/esm/components/CnFileManager/CnFileManager.vue.js +1 -1
  66. package/dist/esm/components/CnFileManager/CnFileManager.vue2.js +3 -1
  67. package/dist/esm/components/CnFileManager/CnFileManager.vue2.js.map +1 -1
  68. package/dist/esm/components/CnFileManager/CnFileManager.vue3.js.map +1 -1
  69. package/dist/esm/components/CnFilePreview/CnFilePreview.vue.js +1 -1
  70. package/dist/esm/components/CnFilePreview/CnFilePreview.vue2.js +2 -1
  71. package/dist/esm/components/CnFilePreview/CnFilePreview.vue2.js.map +1 -1
  72. package/dist/esm/components/CnFilePreview/CnFilePreview.vue3.js.map +1 -1
  73. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  74. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +49 -1
  75. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  76. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +3 -3
  77. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  78. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue.js +1 -1
  79. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +2 -1
  80. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  81. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  82. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -5
  83. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  84. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  85. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  86. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +2 -1
  87. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  88. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  89. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue.js +1 -1
  90. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue2.js +3 -2
  91. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue2.js.map +1 -1
  92. package/dist/esm/components/CnObjectCalendar/CnObjectCalendar.vue3.js.map +1 -1
  93. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  94. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -0
  95. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +82 -75
  97. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  98. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue.js +1 -1
  99. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue2.js +5 -4
  100. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue2.js.map +1 -1
  101. package/dist/esm/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue3.js.map +1 -1
  102. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue.js +1 -1
  103. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue2.js +2 -1
  104. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue2.js.map +1 -1
  105. package/dist/esm/components/CnObjectSidebar/CnAuditTrailTab.vue3.js.map +1 -1
  106. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue.js +1 -1
  107. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue2.js +2 -1
  108. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue2.js.map +1 -1
  109. package/dist/esm/components/CnObjectSidebar/CnFlowTasksPanel.vue3.js.map +1 -1
  110. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue.js +1 -1
  111. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +2 -1
  112. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  113. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  114. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue.js +1 -1
  115. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue2.js +2 -1
  116. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue2.js.map +1 -1
  117. package/dist/esm/components/CnObjectSidebar/CnTasksTab.vue3.js.map +1 -1
  118. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue.js +1 -1
  119. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue2.js +2 -1
  120. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue2.js.map +1 -1
  121. package/dist/esm/components/CnPropertySourceField/CnPropertySourceField.vue3.js.map +1 -1
  122. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue.js +1 -1
  123. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue2.js +2 -1
  124. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue2.js.map +1 -1
  125. package/dist/esm/components/CnRoadmapItem/CnRoadmapItem.vue3.js.map +1 -1
  126. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue2.js +2 -0
  127. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue2.js.map +1 -1
  128. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue3.js +2 -2
  129. package/dist/esm/components/CnSchemaFormDialog/CnSchemaFormDialog.vue3.js.map +1 -1
  130. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue.js +1 -1
  131. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +2 -1
  132. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  133. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  134. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue.js +1 -1
  135. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue2.js +2 -1
  136. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue2.js.map +1 -1
  137. package/dist/esm/components/CnSummaryAggregates/CnSummaryAggregates.vue3.js.map +1 -1
  138. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  139. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +36 -0
  140. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  141. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +55 -40
  142. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  143. package/dist/esm/components/CnTasksCard/CnTasksCard.vue.js +1 -1
  144. package/dist/esm/components/CnTasksCard/CnTasksCard.vue2.js +2 -1
  145. package/dist/esm/components/CnTasksCard/CnTasksCard.vue2.js.map +1 -1
  146. package/dist/esm/components/CnTasksCard/CnTasksCard.vue3.js.map +1 -1
  147. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  148. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +2 -1
  149. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  150. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  151. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue.js +1 -1
  152. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue2.js +2 -1
  153. package/dist/esm/components/CnTranslatedBadge/CnTranslatedBadge.vue2.js.map +1 -1
  154. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue.js +1 -1
  155. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue2.js +2 -1
  156. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue2.js.map +1 -1
  157. package/dist/esm/components/CnVersionHistory/CnVersionHistory.vue3.js.map +1 -1
  158. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  159. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +3 -26
  160. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  161. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  162. package/dist/esm/dialogs/CnNoteHistoryDialog.vue.js +1 -1
  163. package/dist/esm/dialogs/CnNoteHistoryDialog.vue2.js +2 -1
  164. package/dist/esm/dialogs/CnNoteHistoryDialog.vue2.js.map +1 -1
  165. package/dist/esm/dialogs/CnNoteHistoryDialog.vue3.js.map +1 -1
  166. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue.js +1 -1
  167. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue2.js +3 -2
  168. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue2.js.map +1 -1
  169. package/dist/esm/integrations/builtin/activity/CnActivityCard.vue3.js.map +1 -1
  170. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue.js +1 -1
  171. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue2.js +4 -3
  172. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue2.js.map +1 -1
  173. package/dist/esm/integrations/builtin/activity/CnActivityTab.vue3.js.map +1 -1
  174. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue.js +1 -1
  175. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js +3 -2
  176. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue2.js.map +1 -1
  177. package/dist/esm/integrations/builtin/analytics/CnAnalyticsTab.vue3.js.map +1 -1
  178. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
  179. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +2 -1
  180. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
  181. package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
  182. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
  183. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +6 -5
  184. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
  185. package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
  186. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue.js +1 -1
  187. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue2.js +2 -1
  188. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue2.js.map +1 -1
  189. package/dist/esm/integrations/builtin/contactmoment/CnContactmomentCard.vue3.js.map +1 -1
  190. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue.js +1 -1
  191. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js +2 -1
  192. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue2.js.map +1 -1
  193. package/dist/esm/integrations/builtin/cospend/CnCospendTab.vue3.js.map +1 -1
  194. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
  195. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +2 -1
  196. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
  197. package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
  198. package/dist/esm/integrations/builtin/email/CnEmailCard.vue.js +1 -1
  199. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js +2 -1
  200. package/dist/esm/integrations/builtin/email/CnEmailCard.vue2.js.map +1 -1
  201. package/dist/esm/integrations/builtin/email/CnEmailCard.vue3.js.map +1 -1
  202. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue.js +1 -1
  203. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +2 -1
  204. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  205. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  206. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue.js +1 -1
  207. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue2.js +2 -1
  208. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue2.js.map +1 -1
  209. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionTab.vue3.js.map +1 -1
  210. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue.js +1 -1
  211. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js +2 -1
  212. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue2.js.map +1 -1
  213. package/dist/esm/integrations/builtin/photos/CnPhotosTab.vue3.js.map +1 -1
  214. package/dist/esm/l10n/en.json.js +35 -2
  215. package/dist/esm/l10n/en.json.js.map +1 -1
  216. package/dist/esm/l10n/nl.json.js +38 -2
  217. package/dist/esm/l10n/nl.json.js.map +1 -1
  218. package/dist/esm/library-style.js +1 -1
  219. package/dist/esm/nextcloud-vue.css +1068 -962
  220. package/dist/esm/store/useObjectStore.js +7 -2
  221. package/dist/esm/store/useObjectStore.js.map +1 -1
  222. package/dist/esm/utils/boardColumns.js +50 -1
  223. package/dist/esm/utils/boardColumns.js.map +1 -1
  224. package/dist/esm/utils/boardReferenceColumns.js +115 -4
  225. package/dist/esm/utils/boardReferenceColumns.js.map +1 -1
  226. package/dist/esm/utils/contentHeaderLink.js +48 -1
  227. package/dist/esm/utils/contentHeaderLink.js.map +1 -1
  228. package/dist/esm/utils/schema.js +5 -4
  229. package/dist/esm/utils/schema.js.map +1 -1
  230. package/dist/esm/utils/userLocale.js +30 -0
  231. package/dist/esm/utils/userLocale.js.map +1 -0
  232. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  233. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  234. package/dist/nextcloud-vue.cjs.js +1088 -476
  235. package/dist/nextcloud-vue.cjs.js.map +2 -2
  236. package/dist/nextcloud-vue.css +1068 -962
  237. package/l10n/en.json +35 -2
  238. package/l10n/nl.json +38 -2
  239. package/package.json +1 -1
  240. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +3 -2
  241. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +5 -4
  242. package/src/components/CnAnalyticsReportPicker/CnAnalyticsReportPicker.vue +2 -1
  243. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +2 -1
  244. package/src/components/CnBoardView/CnBoardView.vue +189 -24
  245. package/src/components/CnCalendarEventPicker/CnCalendarEventPicker.vue +2 -1
  246. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +2 -1
  247. package/src/components/CnChartWidget/CnChartWidget.vue +6 -5
  248. package/src/components/CnCollectivePagePicker/CnCollectivePagePicker.vue +2 -1
  249. package/src/components/CnConversationThread/CnConversationThread.vue +2 -1
  250. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +2 -1
  251. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -1
  252. package/src/components/CnDataMatrix/CnDataMatrix.vue +2 -1
  253. package/src/components/CnDetailPage/CnDetailHeaderChips.vue +2 -1
  254. package/src/components/CnEmailPicker/CnEmailPicker.vue +2 -1
  255. package/src/components/CnFederationStatus/CnFederationStatus.vue +3 -1
  256. package/src/components/CnFileManager/CnFileManager.vue +2 -1
  257. package/src/components/CnFilePreview/CnFilePreview.vue +2 -1
  258. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +54 -6
  259. package/src/components/CnFollowToggle/CnFollowToggle.vue +2 -1
  260. package/src/components/CnIndexPage/CnIndexPage.vue +30 -4
  261. package/src/components/CnNotesCard/CnNotesCard.vue +2 -1
  262. package/src/components/CnObjectCalendar/CnObjectCalendar.vue +3 -2
  263. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +36 -0
  264. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +5 -4
  265. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +2 -1
  266. package/src/components/CnObjectSidebar/CnFlowTasksPanel.vue +2 -1
  267. package/src/components/CnObjectSidebar/CnNotesTab.vue +2 -1
  268. package/src/components/CnObjectSidebar/CnTasksTab.vue +2 -1
  269. package/src/components/CnPropertySourceField/CnPropertySourceField.vue +2 -1
  270. package/src/components/CnRoadmapItem/CnRoadmapItem.vue +2 -1
  271. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +4 -2
  272. package/src/components/CnStatsBlock/CnStatsBlock.vue +2 -1
  273. package/src/components/CnSummaryAggregates/CnSummaryAggregates.vue +2 -1
  274. package/src/components/CnTabsWidget/CnTabsWidget.vue +71 -0
  275. package/src/components/CnTasksCard/CnTasksCard.vue +2 -1
  276. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +2 -1
  277. package/src/components/CnTranslatedBadge/CnTranslatedBadge.vue +2 -1
  278. package/src/components/CnVersionHistory/CnVersionHistory.vue +2 -1
  279. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +3 -26
  280. package/src/css/board.css +40 -0
  281. package/src/dialogs/CnNoteHistoryDialog.vue +2 -1
  282. package/src/integrations/builtin/activity/CnActivityCard.vue +3 -2
  283. package/src/integrations/builtin/activity/CnActivityTab.vue +4 -3
  284. package/src/integrations/builtin/analytics/CnAnalyticsTab.vue +3 -2
  285. package/src/integrations/builtin/calendar/CnCalendarCard.vue +2 -1
  286. package/src/integrations/builtin/calendar/CnCalendarTab.vue +6 -5
  287. package/src/integrations/builtin/contactmoment/CnContactmomentCard.vue +2 -1
  288. package/src/integrations/builtin/cospend/CnCospendTab.vue +2 -1
  289. package/src/integrations/builtin/deck/CnDeckTab.vue +2 -1
  290. package/src/integrations/builtin/email/CnEmailCard.vue +2 -1
  291. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +2 -1
  292. package/src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue +2 -1
  293. package/src/integrations/builtin/photos/CnPhotosTab.vue +2 -1
  294. package/src/schemas/app-manifest-v2.schema.json +4 -1
  295. package/src/store/useObjectStore.js +7 -2
  296. package/src/utils/boardColumns.js +49 -0
  297. package/src/utils/boardReferenceColumns.js +114 -3
  298. package/src/utils/contentHeaderLink.js +47 -0
  299. package/src/utils/schema.js +5 -4
  300. package/src/utils/userLocale.js +26 -0
  301. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnFieldInspectionCard.vue3.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue?vue&type=template&id=5ce4a8f8&lang.js"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionCard — surface-aware widget for the `field-inspection`\n - integration leaf (offline field data-collection).\n -\n - Drives the generic offline core (src/integrations/offline): a\n - \"Synchronise day\" action fetches today's planned items via the standard OR\n - object API (planningFetch) into the IndexedDB cache, the planning list lets\n - the user open and complete a checklist offline, and saved answers are queued\n - as OR object mutations that replay on reconnect. The sync-state indicator\n - (online / all-synced / queued / conflict) reflects the pending queue.\n -\n - Generic by construction: the checklist items, the planned-items query and\n - the schema names all come from the integration's `offlineConfig` (resolved\n - from the registry by id) — never from app-specific code in this widget.\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<div class=\"cn-field-inspection\" data-testid=\"cn-field-inspection-card\">\n\t\t\t<div class=\"cn-field-inspection__sync\" :class=\"`cn-field-inspection__sync--${indicator.tone}`\" data-testid=\"cn-fi-sync-indicator\">\n\t\t\t\t<span class=\"cn-field-inspection__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span>{{ indicator.text }}</span>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-field-inspection__actions\">\n\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\tdata-testid=\"cn-fi-sync-day\"\n\t\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t\t@click=\"syncDay\">\n\t\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\t\tdata-testid=\"cn-fi-drain-queue\"\n\t\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t\t@click=\"drain\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 || stuckCount > 0\"\n\t\t\t\t\tdata-testid=\"cn-fi-open-queue\"\n\t\t\t\t\t@click=\"showQueue = !showQueue\">\n\t\t\t\t\t{{ showQueue\n\t\t\t\t\t\t? t('nextcloud-vue', 'Hide what is waiting')\n\t\t\t\t\t\t: t('nextcloud-vue', 'See what is waiting') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- 🔴 AN EXPIRED PLANNING USED TO READ AS A CURRENT ONE. This\n\t\t\t\t line only ever said \"Ready offline until\", in the past tense\n\t\t\t\t or not, so a planning downloaded three days ago looked the\n\t\t\t\t same as one downloaded an hour ago. In the field there is\n\t\t\t\t nothing else to check it against. -->\n\t\t\t\t<span v-if=\"planningMeta\" class=\"cn-field-inspection__meta\" data-testid=\"cn-fi-planning-meta\">\n\t\t\t\t\t{{ planningIsStale\n\t\t\t\t\t\t? t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: formatTime(planningMeta.syncedAt) })\n\t\t\t\t\t\t: t('nextcloud-vue', 'Ready offline until {time}', { time: formatTime(planningMeta.expiresAt) }) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\n\t\t\t<!-- The card links to the queue rather than owning a second copy\n\t\t\t of it: one component, one set of statuses, one set of words. -->\n\t\t\t<CnOfflineQueue v-if=\"showQueue\" :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\n\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t\t<NcEmptyContent v-else-if=\"!activeItem && plannedItems.length === 0\"\n\t\t\t\tdata-testid=\"cn-fi-empty\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'No items planned')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.')\" />\n\n\t\t\t<!-- Planning list -->\n\t\t\t<ul v-else-if=\"!activeItem\" class=\"cn-field-inspection__list\" data-testid=\"cn-fi-list\">\n\t\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection__row\">\n\t\t\t\t\t<button type=\"button\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__row-btn\"\n\t\t\t\t\t\t:data-testid=\"`cn-fi-open-${itemId(item)}`\"\n\t\t\t\t\t\t@click=\"openItem(item)\">\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t\t\t</button>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<!-- Checklist completion for one item -->\n\t\t\t<div v-else class=\"cn-field-inspection__checklist\" data-testid=\"cn-fi-checklist\">\n\t\t\t\t<div class=\"cn-field-inspection__checklist-head\">\n\t\t\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-fi-back\" @click=\"closeItem\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<strong>{{ itemTitle(activeItem) }}</strong>\n\t\t\t\t</div>\n\n\t\t\t\t<p v-if=\"activeTemplate\" class=\"cn-field-inspection__progress\" data-testid=\"cn-fi-progress\">\n\t\t\t\t\t{{ t('nextcloud-vue', '{done} of {total} questions completed', { done: progress.done, total: progress.total }) }}\n\t\t\t\t</p>\n\n\t\t\t\t<form v-if=\"activeTemplate\" @submit.prevent=\"saveChecklist\">\n\t\t\t\t\t<fieldset v-for=\"q in activeTemplate.items\" :key=\"q.questionId\" class=\"cn-field-inspection__question\">\n\t\t\t\t\t\t<legend>\n\t\t\t\t\t\t\t{{ q.text }}<span v-if=\"q.required\" class=\"cn-field-inspection__required\" aria-hidden=\"true\"> *</span>\n\t\t\t\t\t\t</legend>\n\t\t\t\t\t\t<select v-if=\"q.type === 'yes_no'\"\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\">\n\t\t\t\t\t\t\t<option value=\"\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '— choose —') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"yes\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Yes') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"no\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'No') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"na\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'N/A') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<textarea v-else\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\"\n\t\t\t\t\t\t\trows=\"2\" />\n\t\t\t\t\t\t<span v-if=\"errorFor(q.questionId)\" class=\"cn-field-inspection__error\" :data-testid=\"`cn-fi-error-${q.questionId}`\">\n\t\t\t\t\t\t\t{{ errorFor(q.questionId) }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</fieldset>\n\n\t\t\t\t\t<!-- `type=\"submit\"` (native HTML type) + `variant=\"primary\"` (style) —\n\t\t\t\t\t @nextcloud/vue 9 split what Vue-2-era code conflated: `type` is now\n\t\t\t\t\t the native button type (`ButtonType`), and the old style-variant\n\t\t\t\t\t prop is `variant`. This line used to pass BOTH wrong: `native-type`\n\t\t\t\t\t (not a real prop — the button silently defaulted to `type=\"button\"`\n\t\t\t\t\t and never fired the form's native submit) and `variant=\"primary\"`\n\t\t\t\t\t (an invalid `ButtonType`, so the visual primary style never applied\n\t\t\t\t\t either). See CnFormPage.vue's inline comment for the submit-button\n\t\t\t\t\t half of this bug in detail. -->\n\t\t\t\t\t<p v-if=\"saveError\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__error\"\n\t\t\t\t\t\trole=\"alert\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save-error\">\n\t\t\t\t\t\t{{ saveError }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<NcButton type=\"submit\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save\"\n\t\t\t\t\t\t:disabled=\"saving\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Save answers offline') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</form>\n\n\t\t\t\t<NcEmptyContent v-else\n\t\t\t\t\tdata-testid=\"cn-fi-no-template\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'Checklist not available offline')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'Synchronise the day while online to download this checklist.')\" />\n\t\t\t</div>\n\t\t</div>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { buildChecklistSubmission } from '../../offline/checklistSubmission.js'\nimport { checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnFieldInspectionCard — surface-aware offline field data-collection widget.\n *\n * Renders the offline planning list and inline checklist completion across the\n * dashboard / detail surfaces, driven entirely by the generic offline core and\n * the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionCard',\n\n\tcomponents: { CnDetailCard, CnOfflineQueue, NcButton, NcLoadingIcon, NcEmptyContent },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/* eslint-disable vue/no-unused-properties -- every integration surface is handed `surface` by its host; this card renders one layout and does not branch on it */\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/** Object context `{ register, schema, objectId }` forwarded by the host page. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** OpenRegister register id (slug or uuid) — falls back to integrationContext. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid) — falls back to integrationContext. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Field inspections') },\n\t\t/** Optional Material Design Icon component for the card header. */\n\t\ticon: { type: [Object, Function], default: () => ClipboardCheckOutline },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tplanningMeta: null,\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** Whether the queue list is open under the card. */\n\t\t\tshowQueue: false,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\tsaving: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t\tactiveItem: null,\n\t\t\tactiveTemplate: null,\n\t\t\tanswers: {},\n\t\t\terrors: [],\n\t\t\t/** Why the last save could not be queued, in words. */\n\t\t\tsaveError: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry, falling back to the canonical defaults. A consuming app's\n\t\t * override supplies its own schema names / filter fields here. Read from\n\t\t * the global registry (not a static import) to avoid an import cycle.\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective register (prop wins, else integrationContext).\n\t\t *\n\t\t * @return {string} The register id.\n\t\t */\n\t\teffectiveRegister() {\n\t\t\treturn this.register || this.integrationContext.register || ''\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else context/prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || this.integrationContext.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.effectiveRegister,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\n\t\tprogress() {\n\t\t\treturn checklistProgress(this.activeTemplate, this.answers)\n\t\t},\n\t},\n\n\twatch: {\n\t\tintegrationContext: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners (auto-drain on reconnect) + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\tformatTime(iso) {\n\t\t\tif (!iso) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn new Date(iso).toLocaleString()\n\t\t},\n\n\t\terrorFor(questionId) {\n\t\t\tconst e = this.errors.find((x) => x.questionId === questionId)\n\t\t\treturn e ? e.message : ''\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.effectiveRegister || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning (planned items + referenced templates) via\n\t\t * the standard OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.effectiveRegister, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open one planned item and load its checklist template from cache.\n\t\t *\n\t\t * @param {object} item The planned item.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync openItem(item) {\n\t\t\tthis.activeItem = item\n\t\t\tthis.errors = []\n\t\t\tconst cfg = this.config\n\t\t\tconst templateRef = item?.[cfg.templateRefField]\n\t\t\t// The app's template shape (sections, its own item keys and type\n\t\t\t// words) is read into the leaf's flat item list here, once.\n\t\t\tthis.activeTemplate = templateRef\n\t\t\t\t? normaliseChecklistTemplate(\n\t\t\t\t\t\tawait getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef)),\n\t\t\t\t\t\tcfg,\n\t\t\t\t\t)\n\t\t\t\t: null\n\t\t\tconst initial = {}\n\t\t\tfor (const q of (this.activeTemplate?.items || [])) {\n\t\t\t\tinitial[q.questionId] = { answer: '', evidenceRefs: [] }\n\t\t\t}\n\t\t\tthis.answers = initial\n\t\t},\n\n\t\tcloseItem() {\n\t\t\tthis.activeItem = null\n\t\t\tthis.activeTemplate = null\n\t\t\tthis.answers = {}\n\t\t\tthis.errors = []\n\t\t\tthis.saveError = ''\n\t\t},\n\n\t\t/**\n\t\t * Capture the current GPS fix (tagged on every answer).\n\t\t *\n\t\t * @return {Promise<object|null>} The GPS reading or null.\n\t\t */\n\t\tasync captureGps() {\n\t\t\tif (typeof navigator === 'undefined' || !navigator.geolocation) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn await new Promise((resolve) => {\n\t\t\t\tnavigator.geolocation.getCurrentPosition(\n\t\t\t\t\t(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude, accuracy: pos.coords.accuracy }),\n\t\t\t\t\t() => resolve(null),\n\t\t\t\t\t{ timeout: 8000 },\n\t\t\t\t)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Validate answers, then queue the run: an object create on\n\t\t * `resultSchema`, or, when the config names a `resultEndpoint`, one\n\t\t * `submit` to that endpoint.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync saveChecklist() {\n\t\t\tconst result = validateChecklistAnswers(this.activeTemplate, this.answers)\n\t\t\tthis.errors = result.errors\n\t\t\tif (result.valid === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst gps = await this.captureGps()\n\t\t\t\tconst submission = buildChecklistSubmission(cfg, {\n\t\t\t\t\tplannedItem: this.activeItem,\n\t\t\t\t\ttemplate: this.activeTemplate,\n\t\t\t\t\tanswers: this.answers,\n\t\t\t\t\tgps,\n\t\t\t\t\tgpsSource: classifyGps(gps).source,\n\t\t\t\t\tcapturedAt: new Date().toISOString(),\n\t\t\t\t\tcapturedOffline: this.offline,\n\t\t\t\t})\n\t\t\t\tawait enqueueMutation({\n\t\t\t\t\tdeviceId: this.deviceId,\n\t\t\t\t\toperationType: submission.operationType,\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: submission.schema,\n\t\t\t\t\tendpoint: submission.endpoint,\n\t\t\t\t\tpayload: submission.payload,\n\t\t\t\t})\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t\tthis.closeItem()\n\t\t\t\tif (this.offline === false) {\n\t\t\t\t\tawait this.drain()\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tthis.saveError = e?.message || t('nextcloud-vue', 'The answers could not be saved on this device.')\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] saveChecklist failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 4px 0;\n}\n\n.cn-field-inspection__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection__sync--success .cn-field-inspection__dot { background: var(--color-success); }\n\n.cn-field-inspection__sync--warning .cn-field-inspection__dot { background: var(--color-warning); }\n\n.cn-field-inspection__sync--error .cn-field-inspection__dot { background: var(--color-error); }\n\n.cn-field-inspection__actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection__meta {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-field-inspection__row-btn {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\twidth: 100%;\n\tpadding: 12px 14px;\n\tmin-height: 44px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n\tcolor: var(--color-main-text);\n}\n\n.cn-field-inspection__row-btn:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-field-inspection__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__checklist-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-field-inspection__progress {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__question {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tmargin-bottom: 12px;\n}\n\n.cn-field-inspection__input {\n\twidth: 100%;\n\tmin-height: 44px;\n\tmargin-top: 6px;\n}\n\n.cn-field-inspection__required { color: var(--color-error); }\n\n.cn-field-inspection__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.85em;\n\tdisplay: block;\n\tmargin-top: 4px;\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_normalizeClass","_toDisplayString","_createVNode","_createTextVNode","_createElementBlock","_openBlock","_Fragment","_renderList","_vModelSelect","_vModelText"],"mappings":"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"}
1
+ {"version":3,"file":"CnFieldInspectionCard.vue3.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue?vue&type=template&id=09716ad5&lang.js"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionCard — surface-aware widget for the `field-inspection`\n - integration leaf (offline field data-collection).\n -\n - Drives the generic offline core (src/integrations/offline): a\n - \"Synchronise day\" action fetches today's planned items via the standard OR\n - object API (planningFetch) into the IndexedDB cache, the planning list lets\n - the user open and complete a checklist offline, and saved answers are queued\n - as OR object mutations that replay on reconnect. The sync-state indicator\n - (online / all-synced / queued / conflict) reflects the pending queue.\n -\n - Generic by construction: the checklist items, the planned-items query and\n - the schema names all come from the integration's `offlineConfig` (resolved\n - from the registry by id) — never from app-specific code in this widget.\n-->\n<template>\n\t<CnDetailCard :title=\"cardTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<div class=\"cn-field-inspection\" data-testid=\"cn-field-inspection-card\">\n\t\t\t<div class=\"cn-field-inspection__sync\" :class=\"`cn-field-inspection__sync--${indicator.tone}`\" data-testid=\"cn-fi-sync-indicator\">\n\t\t\t\t<span class=\"cn-field-inspection__dot\" aria-hidden=\"true\" />\n\t\t\t\t<span>{{ indicator.text }}</span>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-field-inspection__actions\">\n\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\tdata-testid=\"cn-fi-sync-day\"\n\t\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t\t@click=\"syncDay\">\n\t\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\t\tdata-testid=\"cn-fi-drain-queue\"\n\t\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t\t@click=\"drain\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton v-if=\"pendingCount > 0 || stuckCount > 0\"\n\t\t\t\t\tdata-testid=\"cn-fi-open-queue\"\n\t\t\t\t\t@click=\"showQueue = !showQueue\">\n\t\t\t\t\t{{ showQueue\n\t\t\t\t\t\t? t('nextcloud-vue', 'Hide what is waiting')\n\t\t\t\t\t\t: t('nextcloud-vue', 'See what is waiting') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- 🔴 AN EXPIRED PLANNING USED TO READ AS A CURRENT ONE. This\n\t\t\t\t line only ever said \"Ready offline until\", in the past tense\n\t\t\t\t or not, so a planning downloaded three days ago looked the\n\t\t\t\t same as one downloaded an hour ago. In the field there is\n\t\t\t\t nothing else to check it against. -->\n\t\t\t\t<span v-if=\"planningMeta\" class=\"cn-field-inspection__meta\" data-testid=\"cn-fi-planning-meta\">\n\t\t\t\t\t{{ planningIsStale\n\t\t\t\t\t\t? t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: formatTime(planningMeta.syncedAt) })\n\t\t\t\t\t\t: t('nextcloud-vue', 'Ready offline until {time}', { time: formatTime(planningMeta.expiresAt) }) }}\n\t\t\t\t</span>\n\t\t\t</div>\n\n\t\t\t<!-- The card links to the queue rather than owning a second copy\n\t\t\t of it: one component, one set of statuses, one set of words. -->\n\t\t\t<CnOfflineQueue v-if=\"showQueue\" :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\n\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t\t<NcEmptyContent v-else-if=\"!activeItem && plannedItems.length === 0\"\n\t\t\t\tdata-testid=\"cn-fi-empty\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'No items planned')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.')\" />\n\n\t\t\t<!-- Planning list -->\n\t\t\t<ul v-else-if=\"!activeItem\" class=\"cn-field-inspection__list\" data-testid=\"cn-fi-list\">\n\t\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection__row\">\n\t\t\t\t\t<button type=\"button\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__row-btn\"\n\t\t\t\t\t\t:data-testid=\"`cn-fi-open-${itemId(item)}`\"\n\t\t\t\t\t\t@click=\"openItem(item)\">\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t\t\t<span class=\"cn-field-inspection__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t\t\t</button>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<!-- Checklist completion for one item -->\n\t\t\t<div v-else class=\"cn-field-inspection__checklist\" data-testid=\"cn-fi-checklist\">\n\t\t\t\t<div class=\"cn-field-inspection__checklist-head\">\n\t\t\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-fi-back\" @click=\"closeItem\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Back') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<strong>{{ itemTitle(activeItem) }}</strong>\n\t\t\t\t</div>\n\n\t\t\t\t<p v-if=\"activeTemplate\" class=\"cn-field-inspection__progress\" data-testid=\"cn-fi-progress\">\n\t\t\t\t\t{{ t('nextcloud-vue', '{done} of {total} questions completed', { done: progress.done, total: progress.total }) }}\n\t\t\t\t</p>\n\n\t\t\t\t<form v-if=\"activeTemplate\" @submit.prevent=\"saveChecklist\">\n\t\t\t\t\t<fieldset v-for=\"q in activeTemplate.items\" :key=\"q.questionId\" class=\"cn-field-inspection__question\">\n\t\t\t\t\t\t<legend>\n\t\t\t\t\t\t\t{{ q.text }}<span v-if=\"q.required\" class=\"cn-field-inspection__required\" aria-hidden=\"true\"> *</span>\n\t\t\t\t\t\t</legend>\n\t\t\t\t\t\t<select v-if=\"q.type === 'yes_no'\"\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\">\n\t\t\t\t\t\t\t<option value=\"\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', '— choose —') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"yes\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Yes') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"no\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'No') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t<option value=\"na\">\n\t\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'N/A') }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<textarea v-else\n\t\t\t\t\t\t\tv-model=\"answers[q.questionId].answer\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-fi-answer-${q.questionId}`\"\n\t\t\t\t\t\t\tclass=\"cn-field-inspection__input\"\n\t\t\t\t\t\t\trows=\"2\" />\n\t\t\t\t\t\t<span v-if=\"errorFor(q.questionId)\" class=\"cn-field-inspection__error\" :data-testid=\"`cn-fi-error-${q.questionId}`\">\n\t\t\t\t\t\t\t{{ errorFor(q.questionId) }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</fieldset>\n\n\t\t\t\t\t<!-- `type=\"submit\"` (native HTML type) + `variant=\"primary\"` (style) —\n\t\t\t\t\t @nextcloud/vue 9 split what Vue-2-era code conflated: `type` is now\n\t\t\t\t\t the native button type (`ButtonType`), and the old style-variant\n\t\t\t\t\t prop is `variant`. This line used to pass BOTH wrong: `native-type`\n\t\t\t\t\t (not a real prop — the button silently defaulted to `type=\"button\"`\n\t\t\t\t\t and never fired the form's native submit) and `variant=\"primary\"`\n\t\t\t\t\t (an invalid `ButtonType`, so the visual primary style never applied\n\t\t\t\t\t either). See CnFormPage.vue's inline comment for the submit-button\n\t\t\t\t\t half of this bug in detail. -->\n\t\t\t\t\t<p v-if=\"saveError\"\n\t\t\t\t\t\tclass=\"cn-field-inspection__error\"\n\t\t\t\t\t\trole=\"alert\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save-error\">\n\t\t\t\t\t\t{{ saveError }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<NcButton type=\"submit\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\tdata-testid=\"cn-fi-save\"\n\t\t\t\t\t\t:disabled=\"saving\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Save answers offline') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</form>\n\n\t\t\t\t<NcEmptyContent v-else\n\t\t\t\t\tdata-testid=\"cn-fi-no-template\"\n\t\t\t\t\t:name=\"t('nextcloud-vue', 'Checklist not available offline')\"\n\t\t\t\t\t:description=\"t('nextcloud-vue', 'Synchronise the day while online to download this checklist.')\" />\n\t\t\t</div>\n\t\t</div>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { userLocale } from '../../../utils/userLocale.js'\nimport { buildChecklistSubmission } from '../../offline/checklistSubmission.js'\nimport { checklistProgress, classifyGps, normaliseChecklistTemplate, syncIndicator, validateChecklistAnswers } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tenqueueMutation,\n\tgetCachedObject,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnFieldInspectionCard — surface-aware offline field data-collection widget.\n *\n * Renders the offline planning list and inline checklist completion across the\n * dashboard / detail surfaces, driven entirely by the generic offline core and\n * the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionCard',\n\n\tcomponents: { CnDetailCard, CnOfflineQueue, NcButton, NcLoadingIcon, NcEmptyContent },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/* eslint-disable vue/no-unused-properties -- every integration surface is handed `surface` by its host; this card renders one layout and does not branch on it */\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\t\t/* eslint-enable vue/no-unused-properties */\n\n\t\t/** Object context `{ register, schema, objectId }` forwarded by the host page. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** OpenRegister register id (slug or uuid) — falls back to integrationContext. */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid) — falls back to integrationContext. */\n\t\tschema: { type: String, default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Field inspections') },\n\t\t/** Optional Material Design Icon component for the card header. */\n\t\ticon: { type: [Object, Function], default: () => ClipboardCheckOutline },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: false },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tplanningMeta: null,\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** Whether the queue list is open under the card. */\n\t\t\tshowQueue: false,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\tsaving: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t\tactiveItem: null,\n\t\t\tactiveTemplate: null,\n\t\t\tanswers: {},\n\t\t\terrors: [],\n\t\t\t/** Why the last save could not be queued, in words. */\n\t\t\tsaveError: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry, falling back to the canonical defaults. A consuming app's\n\t\t * override supplies its own schema names / filter fields here. Read from\n\t\t * the global registry (not a static import) to avoid an import cycle.\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective register (prop wins, else integrationContext).\n\t\t *\n\t\t * @return {string} The register id.\n\t\t */\n\t\teffectiveRegister() {\n\t\t\treturn this.register || this.integrationContext.register || ''\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else context/prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || this.integrationContext.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.effectiveRegister,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\n\t\tprogress() {\n\t\t\treturn checklistProgress(this.activeTemplate, this.answers)\n\t\t},\n\t},\n\n\twatch: {\n\t\tintegrationContext: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners (auto-drain on reconnect) + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\tformatTime(iso) {\n\t\t\tif (!iso) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn new Date(iso).toLocaleString(userLocale())\n\t\t},\n\n\t\terrorFor(questionId) {\n\t\t\tconst e = this.errors.find((x) => x.questionId === questionId)\n\t\t\treturn e ? e.message : ''\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.effectiveRegister || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.effectiveRegister, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning (planned items + referenced templates) via\n\t\t * the standard OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.effectiveRegister, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Open one planned item and load its checklist template from cache.\n\t\t *\n\t\t * @param {object} item The planned item.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync openItem(item) {\n\t\t\tthis.activeItem = item\n\t\t\tthis.errors = []\n\t\t\tconst cfg = this.config\n\t\t\tconst templateRef = item?.[cfg.templateRefField]\n\t\t\t// The app's template shape (sections, its own item keys and type\n\t\t\t// words) is read into the leaf's flat item list here, once.\n\t\t\tthis.activeTemplate = templateRef\n\t\t\t\t? normaliseChecklistTemplate(\n\t\t\t\t\t\tawait getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef)),\n\t\t\t\t\t\tcfg,\n\t\t\t\t\t)\n\t\t\t\t: null\n\t\t\tconst initial = {}\n\t\t\tfor (const q of (this.activeTemplate?.items || [])) {\n\t\t\t\tinitial[q.questionId] = { answer: '', evidenceRefs: [] }\n\t\t\t}\n\t\t\tthis.answers = initial\n\t\t},\n\n\t\tcloseItem() {\n\t\t\tthis.activeItem = null\n\t\t\tthis.activeTemplate = null\n\t\t\tthis.answers = {}\n\t\t\tthis.errors = []\n\t\t\tthis.saveError = ''\n\t\t},\n\n\t\t/**\n\t\t * Capture the current GPS fix (tagged on every answer).\n\t\t *\n\t\t * @return {Promise<object|null>} The GPS reading or null.\n\t\t */\n\t\tasync captureGps() {\n\t\t\tif (typeof navigator === 'undefined' || !navigator.geolocation) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn await new Promise((resolve) => {\n\t\t\t\tnavigator.geolocation.getCurrentPosition(\n\t\t\t\t\t(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude, accuracy: pos.coords.accuracy }),\n\t\t\t\t\t() => resolve(null),\n\t\t\t\t\t{ timeout: 8000 },\n\t\t\t\t)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Validate answers, then queue the run: an object create on\n\t\t * `resultSchema`, or, when the config names a `resultEndpoint`, one\n\t\t * `submit` to that endpoint.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync saveChecklist() {\n\t\t\tconst result = validateChecklistAnswers(this.activeTemplate, this.answers)\n\t\t\tthis.errors = result.errors\n\t\t\tif (result.valid === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst gps = await this.captureGps()\n\t\t\t\tconst submission = buildChecklistSubmission(cfg, {\n\t\t\t\t\tplannedItem: this.activeItem,\n\t\t\t\t\ttemplate: this.activeTemplate,\n\t\t\t\t\tanswers: this.answers,\n\t\t\t\t\tgps,\n\t\t\t\t\tgpsSource: classifyGps(gps).source,\n\t\t\t\t\tcapturedAt: new Date().toISOString(),\n\t\t\t\t\tcapturedOffline: this.offline,\n\t\t\t\t})\n\t\t\t\tawait enqueueMutation({\n\t\t\t\t\tdeviceId: this.deviceId,\n\t\t\t\t\toperationType: submission.operationType,\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: submission.schema,\n\t\t\t\t\tendpoint: submission.endpoint,\n\t\t\t\t\tpayload: submission.payload,\n\t\t\t\t})\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t\tthis.closeItem()\n\t\t\t\tif (this.offline === false) {\n\t\t\t\t\tawait this.drain()\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tthis.saveError = e?.message || t('nextcloud-vue', 'The answers could not be saved on this device.')\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] saveChecklist failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionCard] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 4px 0;\n}\n\n.cn-field-inspection__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection__sync--success .cn-field-inspection__dot { background: var(--color-success); }\n\n.cn-field-inspection__sync--warning .cn-field-inspection__dot { background: var(--color-warning); }\n\n.cn-field-inspection__sync--error .cn-field-inspection__dot { background: var(--color-error); }\n\n.cn-field-inspection__actions {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection__meta {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-field-inspection__row-btn {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\twidth: 100%;\n\tpadding: 12px 14px;\n\tmin-height: 44px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n\tcolor: var(--color-main-text);\n}\n\n.cn-field-inspection__row-btn:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-field-inspection__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__checklist-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-field-inspection__progress {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-inspection__question {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 12px;\n\tmargin-bottom: 12px;\n}\n\n.cn-field-inspection__input {\n\twidth: 100%;\n\tmin-height: 44px;\n\tmargin-top: 6px;\n}\n\n.cn-field-inspection__required { color: var(--color-error); }\n\n.cn-field-inspection__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.85em;\n\tdisplay: block;\n\tmargin-top: 4px;\n}\n</style>\n"],"names":["_createBlock","_createElementVNode","_normalizeClass","_toDisplayString","_createVNode","_createTextVNode","_createElementBlock","_openBlock","_Fragment","_renderList","_vModelSelect","_vModelText"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnFieldInspectionTab.vue2.js';
2
2
  import { render } from './CnFieldInspectionTab.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-037d0da6";
5
+ script.__scopeId = "data-v-3352c3a0";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -3,6 +3,7 @@ import { NcButton, NcLoadingIcon } from '@nextcloud/vue';
3
3
  import ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue';
4
4
  import Sync from 'vue-material-design-icons/Sync.vue';
5
5
  import __sfc_main from '../../../components/CnOfflineQueue/CnOfflineQueue.vue.js';
6
+ import { userLocale } from '../../../utils/userLocale.js';
6
7
  import { syncIndicator } from '../../offline/fieldCollectionHelpers.js';
7
8
  import { resolveDeviceId, getPlannedItems, getPlanningMeta, countPending, countStuck, storePlanning } from '../../offline/offlineDb.js';
8
9
  import { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js';
@@ -96,7 +97,7 @@ var script = {
96
97
  return ''
97
98
  }
98
99
  const moment = new Date(syncedAt);
99
- return Number.isNaN(moment.getTime()) ? '' : moment.toLocaleString()
100
+ return Number.isNaN(moment.getTime()) ? '' : moment.toLocaleString(userLocale())
100
101
  },
101
102
 
102
103
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"CnFieldInspectionTab.vue2.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionTab — sidebar tab for the `field-inspection` integration\n - leaf (offline field data-collection).\n -\n - The per-object sidebar surface for the offline planning workflow: it shows\n - the sync-state indicator, a \"Synchronise day\" action, and the offline\n - planning list. Tapping an item routes the user into the full checklist\n - completion surface rendered by CnFieldInspectionCard. Generic by\n - construction — schema names and the planning filter come from the\n - integration's `offlineConfig`, never from app-specific code here.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-field-inspection-tab\" data-testid=\"cn-field-inspection-tab\">\n\t\t<div class=\"cn-field-inspection-tab__sync\" :class=\"`cn-field-inspection-tab__sync--${indicator.tone}`\" data-testid=\"cn-fi-tab-sync\">\n\t\t\t<span class=\"cn-field-inspection-tab__dot\" aria-hidden=\"true\" />\n\t\t\t<span>{{ indicator.text }}</span>\n\t\t</div>\n\n\t\t<!-- 🔴 AN EXPIRED PLANNING SAID NOTHING. `getPlanningMeta()` was read and\n\t\t its result thrown away, so a planning downloaded on Monday rendered\n\t\t on Thursday exactly like one downloaded an hour ago. In a cellar\n\t\t with no signal there is nothing else to check it against, so the\n\t\t inspector works a list that may no longer be theirs. It names the\n\t\t download time, because \"out of date\" without a time cannot be\n\t\t judged. -->\n\t\t<p v-if=\"planningIsStale\" class=\"cn-field-inspection-tab__stale\" data-testid=\"cn-fi-tab-stale\">\n\t\t\t{{ t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: downloadedAtLabel }) }}\n\t\t</p>\n\n\t\t<div class=\"cn-field-inspection-tab__actions\">\n\t\t\t<NcButton variant=\"primary\"\n\t\t\t\tdata-testid=\"cn-fi-tab-sync-day\"\n\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t@click=\"syncDay\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Sync :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\tdata-testid=\"cn-fi-tab-drain\"\n\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t@click=\"drain\">\n\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<div v-else-if=\"plannedItems.length === 0\" class=\"cn-sidebar-tab__empty cn-field-inspection-tab__empty\">\n\t\t\t<ClipboardCheckOutline :size=\"32\" />\n\t\t\t<p>{{ t('nextcloud-vue', 'No items planned') }}</p>\n\t\t\t<p class=\"cn-field-inspection-tab__hint\">\n\t\t\t\t{{ t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<ul v-else class=\"cn-field-inspection-tab__list\" data-testid=\"cn-fi-tab-list\">\n\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection-tab__row\">\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- The queue under the planning, not instead of the count. The count\n\t\t above is the summary; this is what it summarises, and it is the only\n\t\t place an inspector can see WHICH capture has not left the device. -->\n\t\t<section class=\"cn-field-inspection-tab__queue\" data-testid=\"cn-fi-tab-queue\">\n\t\t\t<h4 class=\"cn-field-inspection-tab__queue-heading\">\n\t\t\t\t{{ t('nextcloud-vue', 'Waiting to send') }}\n\t\t\t</h4>\n\t\t\t<!-- No `data-testid` here: a fallthrough attribute overrides the\n\t\t\t component's own root attribute, which would rename the queue's\n\t\t\t test hook wherever it is embedded. -->\n\t\t\t<CnOfflineQueue :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport Sync from 'vue-material-design-icons/Sync.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { syncIndicator } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\n/**\n * CnFieldInspectionTab — sidebar tab for the offline field data-collection leaf.\n *\n * Surfaces the offline planning list + sync state for a single object context,\n * driven by the generic offline core and the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionTab',\n\n\tcomponents: { CnOfflineQueue, NcButton, NcLoadingIcon, ClipboardCheckOutline, Sync },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id (sidebar context). */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** The planning meta row, kept rather than discarded (see the stale banner). */\n\t\t\tplanningMeta: null,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry (read from the global, not a static import, to avoid a cycle).\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * When this planning was downloaded, in the reader's locale.\n\t\t *\n\t\t * @return {string} The download moment, or an empty string.\n\t\t */\n\t\tdownloadedAtLabel() {\n\t\t\tconst syncedAt = this.planningMeta?.syncedAt\n\t\t\tif (typeof syncedAt !== 'string' || syncedAt === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst moment = new Date(syncedAt)\n\t\t\treturn Number.isNaN(moment.getTime()) ? '' : moment.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * The register defaults to the one each queued operation already names,\n\t\t * so a consuming app only has to set `conflictSchema`.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.register,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.register || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.register, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.register, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning via the OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.register, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\t// A drain is exactly when work becomes stuck, so the count that\n\t\t\t\t// reports it is re-read here and not only on mount.\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection-tab {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 8px;\n}\n\n.cn-field-inspection-tab__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection-tab__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection-tab__sync--success .cn-field-inspection-tab__dot { background: var(--color-success); }\n\n.cn-field-inspection-tab__sync--warning .cn-field-inspection-tab__dot { background: var(--color-warning); }\n\n.cn-field-inspection-tab__sync--error .cn-field-inspection-tab__dot { background: var(--color-error); }\n\n.cn-field-inspection-tab__stale {\n\tfont-size: 0.85em;\n\tcolor: var(--color-warning-text, var(--color-text-maxcontrast));\n\tmargin: 0;\n}\n\n.cn-field-inspection-tab__queue-heading {\n\tfont-size: 0.9em;\n\tmargin: 0 0 4px;\n}\n\n.cn-field-inspection-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection-tab__empty {\n\ttext-align: center;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 16px 0;\n}\n\n.cn-field-inspection-tab__hint {\n\tfont-size: 0.85em;\n}\n\n.cn-field-inspection-tab__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-field-inspection-tab__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n}\n\n.cn-field-inspection-tab__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["CnOfflineQueue","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnFieldInspectionTab.vue2.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionTab — sidebar tab for the `field-inspection` integration\n - leaf (offline field data-collection).\n -\n - The per-object sidebar surface for the offline planning workflow: it shows\n - the sync-state indicator, a \"Synchronise day\" action, and the offline\n - planning list. Tapping an item routes the user into the full checklist\n - completion surface rendered by CnFieldInspectionCard. Generic by\n - construction — schema names and the planning filter come from the\n - integration's `offlineConfig`, never from app-specific code here.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-field-inspection-tab\" data-testid=\"cn-field-inspection-tab\">\n\t\t<div class=\"cn-field-inspection-tab__sync\" :class=\"`cn-field-inspection-tab__sync--${indicator.tone}`\" data-testid=\"cn-fi-tab-sync\">\n\t\t\t<span class=\"cn-field-inspection-tab__dot\" aria-hidden=\"true\" />\n\t\t\t<span>{{ indicator.text }}</span>\n\t\t</div>\n\n\t\t<!-- 🔴 AN EXPIRED PLANNING SAID NOTHING. `getPlanningMeta()` was read and\n\t\t its result thrown away, so a planning downloaded on Monday rendered\n\t\t on Thursday exactly like one downloaded an hour ago. In a cellar\n\t\t with no signal there is nothing else to check it against, so the\n\t\t inspector works a list that may no longer be theirs. It names the\n\t\t download time, because \"out of date\" without a time cannot be\n\t\t judged. -->\n\t\t<p v-if=\"planningIsStale\" class=\"cn-field-inspection-tab__stale\" data-testid=\"cn-fi-tab-stale\">\n\t\t\t{{ t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: downloadedAtLabel }) }}\n\t\t</p>\n\n\t\t<div class=\"cn-field-inspection-tab__actions\">\n\t\t\t<NcButton variant=\"primary\"\n\t\t\t\tdata-testid=\"cn-fi-tab-sync-day\"\n\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t@click=\"syncDay\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Sync :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\tdata-testid=\"cn-fi-tab-drain\"\n\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t@click=\"drain\">\n\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<div v-else-if=\"plannedItems.length === 0\" class=\"cn-sidebar-tab__empty cn-field-inspection-tab__empty\">\n\t\t\t<ClipboardCheckOutline :size=\"32\" />\n\t\t\t<p>{{ t('nextcloud-vue', 'No items planned') }}</p>\n\t\t\t<p class=\"cn-field-inspection-tab__hint\">\n\t\t\t\t{{ t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<ul v-else class=\"cn-field-inspection-tab__list\" data-testid=\"cn-fi-tab-list\">\n\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection-tab__row\">\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- The queue under the planning, not instead of the count. The count\n\t\t above is the summary; this is what it summarises, and it is the only\n\t\t place an inspector can see WHICH capture has not left the device. -->\n\t\t<section class=\"cn-field-inspection-tab__queue\" data-testid=\"cn-fi-tab-queue\">\n\t\t\t<h4 class=\"cn-field-inspection-tab__queue-heading\">\n\t\t\t\t{{ t('nextcloud-vue', 'Waiting to send') }}\n\t\t\t</h4>\n\t\t\t<!-- No `data-testid` here: a fallthrough attribute overrides the\n\t\t\t component's own root attribute, which would rename the queue's\n\t\t\t test hook wherever it is embedded. -->\n\t\t\t<CnOfflineQueue :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport Sync from 'vue-material-design-icons/Sync.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { userLocale } from '../../../utils/userLocale.js'\nimport { syncIndicator } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\n/**\n * CnFieldInspectionTab — sidebar tab for the offline field data-collection leaf.\n *\n * Surfaces the offline planning list + sync state for a single object context,\n * driven by the generic offline core and the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionTab',\n\n\tcomponents: { CnOfflineQueue, NcButton, NcLoadingIcon, ClipboardCheckOutline, Sync },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id (sidebar context). */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** The planning meta row, kept rather than discarded (see the stale banner). */\n\t\t\tplanningMeta: null,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry (read from the global, not a static import, to avoid a cycle).\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * When this planning was downloaded, in the reader's locale.\n\t\t *\n\t\t * @return {string} The download moment, or an empty string.\n\t\t */\n\t\tdownloadedAtLabel() {\n\t\t\tconst syncedAt = this.planningMeta?.syncedAt\n\t\t\tif (typeof syncedAt !== 'string' || syncedAt === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst moment = new Date(syncedAt)\n\t\t\treturn Number.isNaN(moment.getTime()) ? '' : moment.toLocaleString(userLocale())\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * The register defaults to the one each queued operation already names,\n\t\t * so a consuming app only has to set `conflictSchema`.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.register,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.register || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.register, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.register, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning via the OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.register, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\t// A drain is exactly when work becomes stuck, so the count that\n\t\t\t\t// reports it is re-read here and not only on mount.\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection-tab {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 8px;\n}\n\n.cn-field-inspection-tab__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection-tab__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection-tab__sync--success .cn-field-inspection-tab__dot { background: var(--color-success); }\n\n.cn-field-inspection-tab__sync--warning .cn-field-inspection-tab__dot { background: var(--color-warning); }\n\n.cn-field-inspection-tab__sync--error .cn-field-inspection-tab__dot { background: var(--color-error); }\n\n.cn-field-inspection-tab__stale {\n\tfont-size: 0.85em;\n\tcolor: var(--color-warning-text, var(--color-text-maxcontrast));\n\tmargin: 0;\n}\n\n.cn-field-inspection-tab__queue-heading {\n\tfont-size: 0.9em;\n\tmargin: 0 0 4px;\n}\n\n.cn-field-inspection-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection-tab__empty {\n\ttext-align: center;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 16px 0;\n}\n\n.cn-field-inspection-tab__hint {\n\tfont-size: 0.85em;\n}\n\n.cn-field-inspection-tab__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-field-inspection-tab__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n}\n\n.cn-field-inspection-tab__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["CnOfflineQueue","t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnFieldInspectionTab.vue3.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue?vue&type=template&id=037d0da6&lang.js"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionTab — sidebar tab for the `field-inspection` integration\n - leaf (offline field data-collection).\n -\n - The per-object sidebar surface for the offline planning workflow: it shows\n - the sync-state indicator, a \"Synchronise day\" action, and the offline\n - planning list. Tapping an item routes the user into the full checklist\n - completion surface rendered by CnFieldInspectionCard. Generic by\n - construction — schema names and the planning filter come from the\n - integration's `offlineConfig`, never from app-specific code here.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-field-inspection-tab\" data-testid=\"cn-field-inspection-tab\">\n\t\t<div class=\"cn-field-inspection-tab__sync\" :class=\"`cn-field-inspection-tab__sync--${indicator.tone}`\" data-testid=\"cn-fi-tab-sync\">\n\t\t\t<span class=\"cn-field-inspection-tab__dot\" aria-hidden=\"true\" />\n\t\t\t<span>{{ indicator.text }}</span>\n\t\t</div>\n\n\t\t<!-- 🔴 AN EXPIRED PLANNING SAID NOTHING. `getPlanningMeta()` was read and\n\t\t its result thrown away, so a planning downloaded on Monday rendered\n\t\t on Thursday exactly like one downloaded an hour ago. In a cellar\n\t\t with no signal there is nothing else to check it against, so the\n\t\t inspector works a list that may no longer be theirs. It names the\n\t\t download time, because \"out of date\" without a time cannot be\n\t\t judged. -->\n\t\t<p v-if=\"planningIsStale\" class=\"cn-field-inspection-tab__stale\" data-testid=\"cn-fi-tab-stale\">\n\t\t\t{{ t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: downloadedAtLabel }) }}\n\t\t</p>\n\n\t\t<div class=\"cn-field-inspection-tab__actions\">\n\t\t\t<NcButton variant=\"primary\"\n\t\t\t\tdata-testid=\"cn-fi-tab-sync-day\"\n\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t@click=\"syncDay\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Sync :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\tdata-testid=\"cn-fi-tab-drain\"\n\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t@click=\"drain\">\n\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<div v-else-if=\"plannedItems.length === 0\" class=\"cn-sidebar-tab__empty cn-field-inspection-tab__empty\">\n\t\t\t<ClipboardCheckOutline :size=\"32\" />\n\t\t\t<p>{{ t('nextcloud-vue', 'No items planned') }}</p>\n\t\t\t<p class=\"cn-field-inspection-tab__hint\">\n\t\t\t\t{{ t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<ul v-else class=\"cn-field-inspection-tab__list\" data-testid=\"cn-fi-tab-list\">\n\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection-tab__row\">\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- The queue under the planning, not instead of the count. The count\n\t\t above is the summary; this is what it summarises, and it is the only\n\t\t place an inspector can see WHICH capture has not left the device. -->\n\t\t<section class=\"cn-field-inspection-tab__queue\" data-testid=\"cn-fi-tab-queue\">\n\t\t\t<h4 class=\"cn-field-inspection-tab__queue-heading\">\n\t\t\t\t{{ t('nextcloud-vue', 'Waiting to send') }}\n\t\t\t</h4>\n\t\t\t<!-- No `data-testid` here: a fallthrough attribute overrides the\n\t\t\t component's own root attribute, which would rename the queue's\n\t\t\t test hook wherever it is embedded. -->\n\t\t\t<CnOfflineQueue :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport Sync from 'vue-material-design-icons/Sync.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { syncIndicator } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\n/**\n * CnFieldInspectionTab — sidebar tab for the offline field data-collection leaf.\n *\n * Surfaces the offline planning list + sync state for a single object context,\n * driven by the generic offline core and the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionTab',\n\n\tcomponents: { CnOfflineQueue, NcButton, NcLoadingIcon, ClipboardCheckOutline, Sync },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id (sidebar context). */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** The planning meta row, kept rather than discarded (see the stale banner). */\n\t\t\tplanningMeta: null,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry (read from the global, not a static import, to avoid a cycle).\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * When this planning was downloaded, in the reader's locale.\n\t\t *\n\t\t * @return {string} The download moment, or an empty string.\n\t\t */\n\t\tdownloadedAtLabel() {\n\t\t\tconst syncedAt = this.planningMeta?.syncedAt\n\t\t\tif (typeof syncedAt !== 'string' || syncedAt === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst moment = new Date(syncedAt)\n\t\t\treturn Number.isNaN(moment.getTime()) ? '' : moment.toLocaleString()\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * The register defaults to the one each queued operation already names,\n\t\t * so a consuming app only has to set `conflictSchema`.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.register,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.register || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.register, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.register, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning via the OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.register, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\t// A drain is exactly when work becomes stuck, so the count that\n\t\t\t\t// reports it is re-read here and not only on mount.\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection-tab {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 8px;\n}\n\n.cn-field-inspection-tab__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection-tab__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection-tab__sync--success .cn-field-inspection-tab__dot { background: var(--color-success); }\n\n.cn-field-inspection-tab__sync--warning .cn-field-inspection-tab__dot { background: var(--color-warning); }\n\n.cn-field-inspection-tab__sync--error .cn-field-inspection-tab__dot { background: var(--color-error); }\n\n.cn-field-inspection-tab__stale {\n\tfont-size: 0.85em;\n\tcolor: var(--color-warning-text, var(--color-text-maxcontrast));\n\tmargin: 0;\n}\n\n.cn-field-inspection-tab__queue-heading {\n\tfont-size: 0.9em;\n\tmargin: 0 0 4px;\n}\n\n.cn-field-inspection-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection-tab__empty {\n\ttext-align: center;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 16px 0;\n}\n\n.cn-field-inspection-tab__hint {\n\tfont-size: 0.85em;\n}\n\n.cn-field-inspection-tab__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-field-inspection-tab__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n}\n\n.cn-field-inspection-tab__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_createElementVNode","_normalizeClass","_toDisplayString","_openBlock","_createElementBlock","_createVNode","_createTextVNode","_createBlock","_Fragment","_renderList"],"mappings":"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"}
1
+ {"version":3,"file":"CnFieldInspectionTab.vue3.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionTab.vue?vue&type=template&id=3352c3a0&lang.js"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n -\n - CnFieldInspectionTab — sidebar tab for the `field-inspection` integration\n - leaf (offline field data-collection).\n -\n - The per-object sidebar surface for the offline planning workflow: it shows\n - the sync-state indicator, a \"Synchronise day\" action, and the offline\n - planning list. Tapping an item routes the user into the full checklist\n - completion surface rendered by CnFieldInspectionCard. Generic by\n - construction — schema names and the planning filter come from the\n - integration's `offlineConfig`, never from app-specific code here.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-field-inspection-tab\" data-testid=\"cn-field-inspection-tab\">\n\t\t<div class=\"cn-field-inspection-tab__sync\" :class=\"`cn-field-inspection-tab__sync--${indicator.tone}`\" data-testid=\"cn-fi-tab-sync\">\n\t\t\t<span class=\"cn-field-inspection-tab__dot\" aria-hidden=\"true\" />\n\t\t\t<span>{{ indicator.text }}</span>\n\t\t</div>\n\n\t\t<!-- 🔴 AN EXPIRED PLANNING SAID NOTHING. `getPlanningMeta()` was read and\n\t\t its result thrown away, so a planning downloaded on Monday rendered\n\t\t on Thursday exactly like one downloaded an hour ago. In a cellar\n\t\t with no signal there is nothing else to check it against, so the\n\t\t inspector works a list that may no longer be theirs. It names the\n\t\t download time, because \"out of date\" without a time cannot be\n\t\t judged. -->\n\t\t<p v-if=\"planningIsStale\" class=\"cn-field-inspection-tab__stale\" data-testid=\"cn-fi-tab-stale\">\n\t\t\t{{ t('nextcloud-vue', 'This planning is out of date. It was downloaded on {time}.', { time: downloadedAtLabel }) }}\n\t\t</p>\n\n\t\t<div class=\"cn-field-inspection-tab__actions\">\n\t\t\t<NcButton variant=\"primary\"\n\t\t\t\tdata-testid=\"cn-fi-tab-sync-day\"\n\t\t\t\t:disabled=\"syncing || offline\"\n\t\t\t\t@click=\"syncDay\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Sync :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ syncing ? t('nextcloud-vue', 'Synchronising…') : t('nextcloud-vue', 'Synchronise day') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"pendingCount > 0 && !offline\"\n\t\t\t\tdata-testid=\"cn-fi-tab-drain\"\n\t\t\t\t:disabled=\"syncing\"\n\t\t\t\t@click=\"drain\">\n\t\t\t\t{{ t('nextcloud-vue', 'Sync {n} pending changes', { n: pendingCount }) }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<div v-else-if=\"plannedItems.length === 0\" class=\"cn-sidebar-tab__empty cn-field-inspection-tab__empty\">\n\t\t\t<ClipboardCheckOutline :size=\"32\" />\n\t\t\t<p>{{ t('nextcloud-vue', 'No items planned') }}</p>\n\t\t\t<p class=\"cn-field-inspection-tab__hint\">\n\t\t\t\t{{ t('nextcloud-vue', 'Tap “Synchronise day” while online to download your planning.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<ul v-else class=\"cn-field-inspection-tab__list\" data-testid=\"cn-fi-tab-list\">\n\t\t\t<li v-for=\"item in plannedItems\" :key=\"itemId(item)\" class=\"cn-field-inspection-tab__row\">\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-title\">{{ itemTitle(item) }}</span>\n\t\t\t\t<span class=\"cn-field-inspection-tab__row-status\">{{ itemStatus(item) }}</span>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- The queue under the planning, not instead of the count. The count\n\t\t above is the summary; this is what it summarises, and it is the only\n\t\t place an inspector can see WHICH capture has not left the device. -->\n\t\t<section class=\"cn-field-inspection-tab__queue\" data-testid=\"cn-fi-tab-queue\">\n\t\t\t<h4 class=\"cn-field-inspection-tab__queue-heading\">\n\t\t\t\t{{ t('nextcloud-vue', 'Waiting to send') }}\n\t\t\t</h4>\n\t\t\t<!-- No `data-testid` here: a fallthrough attribute overrides the\n\t\t\t component's own root attribute, which would rename the queue's\n\t\t\t test hook wherever it is embedded. -->\n\t\t\t<CnOfflineQueue :deviceId=\"deviceId\" @requeued=\"loadLocal\" />\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue'\nimport Sync from 'vue-material-design-icons/Sync.vue'\nimport CnOfflineQueue from '../../../components/CnOfflineQueue/index.js'\nimport { userLocale } from '../../../utils/userLocale.js'\nimport { syncIndicator } from '../../offline/fieldCollectionHelpers.js'\nimport {\n\tcountPending,\n\tcountStuck,\n\tgetPlannedItems,\n\tgetPlanningMeta,\n\tresolveDeviceId,\n\tstorePlanning,\n} from '../../offline/offlineDb.js'\nimport { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js'\nimport { drainQueue } from '../../offline/syncReplayService.js'\nimport { DEFAULT_FIELD_INSPECTION_CONFIG } from '../field-inspection.js'\n\n/**\n * CnFieldInspectionTab — sidebar tab for the offline field data-collection leaf.\n *\n * Surfaces the offline planning list + sync state for a single object context,\n * driven by the generic offline core and the integration's `offlineConfig`.\n */\nexport default {\n\tname: 'CnFieldInspectionTab',\n\n\tcomponents: { CnOfflineQueue, NcButton, NcLoadingIcon, ClipboardCheckOutline, Sync },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'field-inspection'`). */\n\t\tintegrationId: { type: String, default: 'field-inspection' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Parent object id (sidebar context). */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** Optional explicit assignee uid for the planning query (default: current user). */\n\t\tassignee: { type: String, default: '' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tplannedItems: [],\n\t\t\tpendingCount: 0,\n\t\t\t/** Operations that will not send again without somebody acting. */\n\t\t\tstuckCount: 0,\n\t\t\t/** The planning meta row, kept rather than discarded (see the stale banner). */\n\t\t\tplanningMeta: null,\n\t\t\tloading: true,\n\t\t\tsyncing: false,\n\t\t\toffline: typeof navigator !== 'undefined' ? navigator.onLine === false : false,\n\t\t\tdeviceId: resolveDeviceId(),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Resolved offline config from the registered descriptor on the shared\n\t\t * registry (read from the global, not a static import, to avoid a cycle).\n\t\t *\n\t\t * @return {object} The offline config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst registry = (typeof window !== 'undefined'\n\t\t\t\t&& window.OCA && window.OCA.OpenRegister && window.OCA.OpenRegister.integrations) || null\n\t\t\tconst entry = (registry && typeof registry.get === 'function') ? registry.get(this.integrationId) : null\n\t\t\treturn (entry && entry.offlineConfig) || DEFAULT_FIELD_INSPECTION_CONFIG\n\t\t},\n\n\t\t/**\n\t\t * Effective planned-items schema (config wins, else prop schema).\n\t\t *\n\t\t * @return {string} The schema id.\n\t\t */\n\t\teffectiveSchema() {\n\t\t\treturn this.config.plannedSchema || this.schema || ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the cached planning has passed its offline lifetime.\n\t\t *\n\t\t * @return {boolean} True when it is past `expiresAt`.\n\t\t */\n\t\tplanningIsStale() {\n\t\t\tconst expiresAt = this.planningMeta?.expiresAt\n\t\t\tif (typeof expiresAt !== 'string' || expiresAt === '') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst expiry = Date.parse(expiresAt)\n\t\t\treturn Number.isNaN(expiry) === false && expiry < Date.now()\n\t\t},\n\n\t\t/**\n\t\t * When this planning was downloaded, in the reader's locale.\n\t\t *\n\t\t * @return {string} The download moment, or an empty string.\n\t\t */\n\t\tdownloadedAtLabel() {\n\t\t\tconst syncedAt = this.planningMeta?.syncedAt\n\t\t\tif (typeof syncedAt !== 'string' || syncedAt === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst moment = new Date(syncedAt)\n\t\t\treturn Number.isNaN(moment.getTime()) ? '' : moment.toLocaleString(userLocale())\n\t\t},\n\n\t\t/**\n\t\t * Where a conflict found during a drain is filed.\n\t\t *\n\t\t * The register defaults to the one each queued operation already names,\n\t\t * so a consuming app only has to set `conflictSchema`.\n\t\t *\n\t\t * @return {object} The drain config.\n\t\t */\n\t\tdrainConfig() {\n\t\t\treturn {\n\t\t\t\tregister: this.config.register || this.register,\n\t\t\t\tconflictSchema: this.config.conflictSchema || '',\n\t\t\t}\n\t\t},\n\n\t\tindicator() {\n\t\t\treturn syncIndicator(this.pendingCount, this.offline === false, this.stuckCount)\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.loadLocal()\n\t\t} },\n\t},\n\n\t/**\n\t * Wire online/offline listeners + load local state.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync mounted() {\n\t\twindow.addEventListener('online', this.onOnline)\n\t\twindow.addEventListener('offline', this.onOffline)\n\t\tawait this.loadLocal()\n\t},\n\n\t/**\n\t * Detach the online/offline listeners.\n\t *\n\t * @return {void}\n\t */\n\tbeforeUnmount() {\n\t\twindow.removeEventListener('online', this.onOnline)\n\t\twindow.removeEventListener('offline', this.onOffline)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\titemId(item) {\n\t\t\treturn String(item?.id ?? item?.['@self']?.id ?? item?.uuid ?? '')\n\t\t},\n\n\t\titemTitle(item) {\n\t\t\treturn item?.[this.config.titleField] || this.itemId(item)\n\t\t},\n\n\t\titemStatus(item) {\n\t\t\tconst status = item?.status || 'planned'\n\t\t\tconst map = {\n\t\t\t\tplanned: t('nextcloud-vue', 'Planned'),\n\t\t\t\tin_progress: t('nextcloud-vue', 'In progress'),\n\t\t\t\tsynced: t('nextcloud-vue', 'Synced'),\n\t\t\t\tconflict: t('nextcloud-vue', 'Conflict'),\n\t\t\t}\n\t\t\treturn map[status] || status\n\t\t},\n\n\t\t/**\n\t\t * Load cached planning + pending count from IndexedDB.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLocal() {\n\t\t\tif (!this.register || !this.effectiveSchema) {\n\t\t\t\tthis.loading = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.plannedItems = await getPlannedItems(this.register, this.effectiveSchema)\n\t\t\t\tthis.planningMeta = await getPlanningMeta(this.register, this.effectiveSchema)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] loadLocal failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Download today's planning via the OR object API and store it offline.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync syncDay() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tconst cfg = this.config\n\t\t\t\tconst items = await fetchPlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\tassigneeField: cfg.assigneeField,\n\t\t\t\t\tassignee: this.assignee || (typeof OC !== 'undefined' && OC.getCurrentUser ? OC.getCurrentUser().uid : ''),\n\t\t\t\t\tdateField: cfg.dateField,\n\t\t\t\t})\n\t\t\t\tconst references = cfg.referenceSchema\n\t\t\t\t\t? await fetchReferences({ register: this.register, referenceSchema: cfg.referenceSchema })\n\t\t\t\t\t: []\n\t\t\t\tawait storePlanning({\n\t\t\t\t\tregister: this.register,\n\t\t\t\t\tschema: this.effectiveSchema,\n\t\t\t\t\titems,\n\t\t\t\t\treferences,\n\t\t\t\t\treferenceSchema: cfg.referenceSchema,\n\t\t\t\t})\n\t\t\t\tawait this.loadLocal()\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] syncDay failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drain the local mutation queue against the OR object API.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync drain() {\n\t\t\tthis.syncing = true\n\t\t\ttry {\n\t\t\t\tawait drainQueue(this.deviceId, this.drainConfig)\n\t\t\t\tthis.pendingCount = await countPending(this.deviceId)\n\t\t\t\t// A drain is exactly when work becomes stuck, so the count that\n\t\t\t\t// reports it is re-read here and not only on mount.\n\t\t\t\tthis.stuckCount = await countStuck(this.deviceId)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnFieldInspectionTab] drain failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.syncing = false\n\t\t\t}\n\t\t},\n\n\t\tonOnline() {\n\t\t\tthis.offline = false\n\t\t\tthis.drain()\n\t\t},\n\n\t\tonOffline() {\n\t\t\tthis.offline = true\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-inspection-tab {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 8px;\n}\n\n.cn-field-inspection-tab__sync {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-field-inspection-tab__dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n}\n\n.cn-field-inspection-tab__sync--success .cn-field-inspection-tab__dot { background: var(--color-success); }\n\n.cn-field-inspection-tab__sync--warning .cn-field-inspection-tab__dot { background: var(--color-warning); }\n\n.cn-field-inspection-tab__sync--error .cn-field-inspection-tab__dot { background: var(--color-error); }\n\n.cn-field-inspection-tab__stale {\n\tfont-size: 0.85em;\n\tcolor: var(--color-warning-text, var(--color-text-maxcontrast));\n\tmargin: 0;\n}\n\n.cn-field-inspection-tab__queue-heading {\n\tfont-size: 0.9em;\n\tmargin: 0 0 4px;\n}\n\n.cn-field-inspection-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-field-inspection-tab__empty {\n\ttext-align: center;\n\tcolor: var(--color-text-maxcontrast);\n\tpadding: 16px 0;\n}\n\n.cn-field-inspection-tab__hint {\n\tfont-size: 0.85em;\n}\n\n.cn-field-inspection-tab__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-field-inspection-tab__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n}\n\n.cn-field-inspection-tab__row-status {\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_createElementVNode","_normalizeClass","_toDisplayString","_openBlock","_createElementBlock","_createVNode","_createTextVNode","_createBlock","_Fragment","_renderList"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnPhotosTab.vue2.js';
2
2
  import { render } from './CnPhotosTab.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-646bb332";
5
+ script.__scopeId = "data-v-1b937a5a";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -8,6 +8,7 @@ import LinkVariant from 'vue-material-design-icons/LinkVariant.vue';
8
8
  import Plus from 'vue-material-design-icons/Plus.vue';
9
9
  import __sfc_main$1 from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue.js';
10
10
  import __sfc_main from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue.js';
11
+ import { userLocale } from '../../../utils/userLocale.js';
11
12
  import { safeHref } from '../../../utils/safeHref.js';
12
13
  import { prefixUrl, buildHeaders } from '../../../utils/headers.js';
13
14
 
@@ -173,7 +174,7 @@ var script = {
173
174
  if (Number.isNaN(d.getTime())) {
174
175
  return String(value)
175
176
  }
176
- return d.toLocaleDateString(undefined, { dateStyle: 'medium' })
177
+ return d.toLocaleDateString(userLocale(), { dateStyle: 'medium' })
177
178
  } catch {
178
179
  return String(value)
179
180
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CnPhotosTab.vue2.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosTab.vue"],"sourcesContent":["<!--\n CnPhotosTab — bespoke sidebar tab for the `photos` integration leaf.\n\n Replaces the generic `CnIntegrationTab` for the `photos` leaf: renders\n the linked NC Photos albums as a card grid (cover thumbnail, album\n name, photo count, last-edited date). Clicking a card deep-links into\n the NC Photos app at `/index.php/apps/photos/albums/{albumId}`.\n\n Tier-2 surface (this commit):\n - \"Link existing album\" → opens CnPhotoAlbumPicker (modal)\n - \"Create new album\" → opens CnPhotoAlbumCreate (modal)\n - Per-tile unlink → DELETE …/photos/{albumId}\n\n Talks to the OpenRegister Tier-2 photo-link endpoints\n GET /api/objects/{r}/{s}/{id}/photos — list\n POST /api/objects/{r}/{s}/{id}/photos — link existing\n POST /api/objects/{r}/{s}/{id}/photos/new — create + link\n DELETE /api/objects/{r}/{s}/{id}/photos/{albumId} — unlink\n GET /api/integrations/photos/available — picker source\n served by `OCA\\OpenRegister\\Controller\\PhotoLinksController`.\n\n The endpoint returns rows shaped:\n { id, title, url, albumId, albumName, coverPhotoUrl, photoCount,\n lastEdited, data: { … } }\n\n Empty + loading + error + 503 unavailable states follow ADR-017 and\n AD-23 graceful degradation. All UI strings are passed through\n `t('nextcloud-vue', ...)` per ADR-007. Styling uses Nextcloud CSS\n variables only so the nldesign overrides apply transparently\n (ADR-010).\n\n Bespoke-vs-generic rationale: the generic tab renders a flat link list\n which loses the cover thumbnail + photo count + recency — the three\n signals users rely on when triaging which album is relevant for a\n case. The bespoke tab surfaces them per card.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-photos-tab\">\n\t\t<!-- 503 unavailable banner (AD-23) -->\n\t\t<div v-if=\"degraded\" class=\"cn-photos-tab__banner\" role=\"alert\">\n\t\t\t<AlertCircleOutline :size=\"18\" />\n\t\t\t<span>{{ degraded }}</span>\n\t\t</div>\n\n\t\t<div class=\"cn-photos-tab__actions\">\n\t\t\t<NcButton variant=\"secondary\" @click=\"openPicker\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Link existing album') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton variant=\"primary\" @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Create new album') }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<div v-else-if=\"error\" class=\"cn-photos-tab__error\" role=\"alert\">\n\t\t\t{{ error }}\n\t\t</div>\n\n\t\t<!-- Empty -->\n\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-sidebar-tab__empty cn-photos-tab__empty\">\n\t\t\t<ImageIcon :size=\"32\" class=\"cn-photos-tab__empty-icon\" />\n\t\t\t<p>{{ emptyLabel }}</p>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\ttarget=\"_blank\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ImageIcon :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ openPhotosLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Photo mosaic grid (mirrors the NC Photos timeline) -->\n\t\t<div v-else class=\"cn-photos-tab__grid\">\n\t\t\t<div\n\t\t\t\tv-for=\"album in albums\"\n\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\tclass=\"cn-photos-tab__tile\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-tab__tile-link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-tab__cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:alt=\"albumName(album)\"\n\t\t\t\t\t\t\tclass=\"cn-photos-tab__cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<span v-else class=\"cn-photos-tab__cover-placeholder\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"36\" class=\"cn-photos-tab__cover-fallback\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Photo-count badge, top-left (mirrors album stack overlay) -->\n\t\t\t\t\t\t<span v-if=\"photoCount(album) !== null\" class=\"cn-photos-tab__badge\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"12\" />\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__badge-count\">{{ photoCountLabel(album) }}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Hover scrim with name + date -->\n\t\t\t\t\t\t<div class=\"cn-photos-tab__meta\">\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__name\">{{ albumName(album) }}</span>\n\t\t\t\t\t\t\t<span v-if=\"lastEdited(album)\" class=\"cn-photos-tab__when\">\n\t\t\t\t\t\t\t\t{{ formatWhen(lastEdited(album)) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Unlink album')\"\n\t\t\t\t\tclass=\"cn-photos-tab__unlink\"\n\t\t\t\t\t@click=\"unlinkAlbum(album)\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<LinkOff :size=\"16\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<CnPhotoAlbumPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLinkPick\" />\n\n\t\t<CnPhotoAlbumCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreatePick\" />\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport LinkOff from 'vue-material-design-icons/LinkOff.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnPhotoAlbumCreate from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue'\nimport CnPhotoAlbumPicker from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\n/**\n * CnPhotosTab — bespoke album grid for the `photos` integration.\n *\n * Renders one tile per linked NC Photos album with cover thumbnail,\n * album name, photo count, and last-edited date. Clicking a tile opens\n * the album in the NC Photos app in a new tab. Tier-2: includes\n * link/create modals and per-tile unlink.\n */\nexport default {\n\tname: 'CnPhotosTab',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tAlertCircleOutline,\n\t\tImageIcon,\n\t\tImageMultiple,\n\t\tLinkOff,\n\t\tLinkVariant,\n\t\tPlus,\n\t\tCnPhotoAlbumPicker,\n\t\tCnPhotoAlbumCreate,\n\t},\n\n\tprops: {\n\t\t/* eslint-disable vue/no-unused-properties -- the integration dispatch binds integrationId on every integration component (see CnIntegrationWidgetGrid), so declaring it keeps it out of $attrs */\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/* eslint-enable vue/no-unused-properties */\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated \"Open Photos\" CTA label. */\n\t\topenPhotosLabel: { type: String, default: () => t('nextcloud-vue', 'Open Photos') },\n\t\t/** Pre-translated 503 unavailable banner copy. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\talbums: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t}\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler(id) {\n\t\t\tif (id) {\n\t\t\t\tthis.fetchAlbums()\n\t\t\t}\n\t\t} },\n\n\t\tregister() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Base for the Tier-2 photos endpoints (list/link/new/destroy).\n\t\t *\n\t\t * @return {string} The endpoint URL.\n\t\t */\n\t\tphotosEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/photos`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.albumId ?? album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * Resolve the displayable album name from the row, stripping any\n\t\t * legacy `[or:{uuid}]` marker (pre-Tier-2 albums embedded the\n\t\t * marker inside `name`).\n\t\t *\n\t\t * @param {object} album Provider row.\n\t\t *\n\t\t * @return {string} The display name.\n\t\t */\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.albumName || album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url && safeHref(album.url) !== '#') {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst id = this.albumKey(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(id)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tphotoCountLabel(album) {\n\t\t\tconst count = this.photoCount(album)\n\t\t\tif (count === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tlastEdited(album) {\n\t\t\treturn album?.lastEdited\n\t\t\t\t|| album?.data?.lastEdited\n\t\t\t\t|| album?.data?.created\n\t\t\t\t|| ''\n\t\t},\n\n\t\tformatWhen(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// NC Photos stores `created` as a unix timestamp in\n\t\t\t\t// seconds; accept ISO strings transparently too.\n\t\t\t\tconst num = Number(value)\n\t\t\t\tconst d = Number.isFinite(num) && num > 0 && String(value).length <= 12\n\t\t\t\t\t? new Date(num * 1000)\n\t\t\t\t\t: new Date(value)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn String(value)\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleDateString(undefined, { dateStyle: 'medium' })\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\n\t\topenPicker() {\n\t\t\tthis.pickerOpen = true\n\t\t},\n\n\t\topenCreate() {\n\t\t\tthis.createOpen = true\n\t\t},\n\n\t\tasync onLinkPick(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'This album is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] link failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t}\n\t\t},\n\n\t\tasync onCreatePick(payload) {\n\t\t\tthis.createOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] create failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t}\n\t\t},\n\n\t\tasync unlinkAlbum(album) {\n\t\t\tconst albumId = this.albumKey(album)\n\t\t\tif (!albumId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/${albumId}`), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] unlink failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAlbums() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503 || response.status === 501) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-photos-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 10px;\n\tflex-wrap: wrap;\n}\n\n.cn-photos-tab__banner {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 10px;\n\tmargin-bottom: 10px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-warning, #e9a40f);\n\tcolor: var(--color-main-background);\n\tfont-size: 0.9em;\n}\n\n.cn-photos-tab__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.9em;\n\tmargin: 4px 0 8px;\n}\n\n.cn-photos-tab__empty {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n\n.cn-photos-tab__empty-icon {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/*\n * Dense edge-to-edge mosaic that mirrors the NC Photos timeline: square\n * thumbnails packed tight with a thin main-background gutter (the same\n * 2px border NC Photos draws between tiles), name + date revealed in a\n * bottom scrim on hover/focus so the imagery dominates at rest.\n */\n.cn-photos-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n\tgap: 2px;\n\tborder-radius: var(--border-radius-large, var(--border-radius));\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile {\n\tposition: relative;\n}\n\n.cn-photos-tab__unlink {\n\tposition: absolute;\n\ttop: 4px;\n\tright: 4px;\n\tz-index: 3;\n\topacity: 0;\n\tbackground: var(--color-main-background);\n\tborder-radius: 50%;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__unlink,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__unlink {\n\topacity: 1;\n}\n\n.cn-photos-tab__tile-link {\n\tdisplay: block;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.cn-photos-tab__cover {\n\tposition: relative;\n\taspect-ratio: 1 / 1;\n\twidth: 100%;\n\t/* Match the NC Photos selection outline on focus. */\n\tborder: 2px solid var(--color-main-background);\n\tbox-sizing: border-box;\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile-link:focus-visible .cn-photos-tab__cover {\n\toutline: var(--color-primary-element) solid 2px;\n\toutline-offset: -2px;\n}\n\n.cn-photos-tab__cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.2s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__cover-img,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__cover-img {\n\ttransform: scale(1.04);\n}\n\n.cn-photos-tab__cover-placeholder {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-photos-tab__cover-fallback {\n\tcolor: var(--color-primary-element);\n}\n\n/* Photo-count badge (mirrors the album stack count overlay). */\n.cn-photos-tab__badge {\n\tposition: absolute;\n\ttop: 4px;\n\tleft: 4px;\n\tz-index: 2;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 1px 6px 1px 4px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: rgba(0, 0, 0, 0.55);\n\tcolor: #fff;\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tline-height: 1.6;\n}\n\n.cn-photos-tab__badge-count {\n\twhite-space: nowrap;\n}\n\n/* Bottom scrim: hidden at rest, revealed on hover/focus. */\n.cn-photos-tab__meta {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1px;\n\tpadding: 16px 8px 6px;\n\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n\tpointer-events: none;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__meta,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__meta {\n\topacity: 1;\n}\n\n.cn-photos-tab__name {\n\tfont-size: 12px;\n\tfont-weight: 600;\n\tcolor: #fff;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-tab__when {\n\tfont-size: 10px;\n\tcolor: rgba(255, 255, 255, 0.85);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":["CnPhotoAlbumPicker","CnPhotoAlbumCreate","t","n"],"mappings":"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"}
1
+ {"version":3,"file":"CnPhotosTab.vue2.js","sources":["../../../../../src/integrations/builtin/photos/CnPhotosTab.vue"],"sourcesContent":["<!--\n CnPhotosTab — bespoke sidebar tab for the `photos` integration leaf.\n\n Replaces the generic `CnIntegrationTab` for the `photos` leaf: renders\n the linked NC Photos albums as a card grid (cover thumbnail, album\n name, photo count, last-edited date). Clicking a card deep-links into\n the NC Photos app at `/index.php/apps/photos/albums/{albumId}`.\n\n Tier-2 surface (this commit):\n - \"Link existing album\" → opens CnPhotoAlbumPicker (modal)\n - \"Create new album\" → opens CnPhotoAlbumCreate (modal)\n - Per-tile unlink → DELETE …/photos/{albumId}\n\n Talks to the OpenRegister Tier-2 photo-link endpoints\n GET /api/objects/{r}/{s}/{id}/photos — list\n POST /api/objects/{r}/{s}/{id}/photos — link existing\n POST /api/objects/{r}/{s}/{id}/photos/new — create + link\n DELETE /api/objects/{r}/{s}/{id}/photos/{albumId} — unlink\n GET /api/integrations/photos/available — picker source\n served by `OCA\\OpenRegister\\Controller\\PhotoLinksController`.\n\n The endpoint returns rows shaped:\n { id, title, url, albumId, albumName, coverPhotoUrl, photoCount,\n lastEdited, data: { … } }\n\n Empty + loading + error + 503 unavailable states follow ADR-017 and\n AD-23 graceful degradation. All UI strings are passed through\n `t('nextcloud-vue', ...)` per ADR-007. Styling uses Nextcloud CSS\n variables only so the nldesign overrides apply transparently\n (ADR-010).\n\n Bespoke-vs-generic rationale: the generic tab renders a flat link list\n which loses the cover thumbnail + photo count + recency — the three\n signals users rely on when triaging which album is relevant for a\n case. The bespoke tab surfaces them per card.\n-->\n<template>\n\t<div class=\"cn-sidebar-tab cn-photos-tab\">\n\t\t<!-- 503 unavailable banner (AD-23) -->\n\t\t<div v-if=\"degraded\" class=\"cn-photos-tab__banner\" role=\"alert\">\n\t\t\t<AlertCircleOutline :size=\"18\" />\n\t\t\t<span>{{ degraded }}</span>\n\t\t</div>\n\n\t\t<div class=\"cn-photos-tab__actions\">\n\t\t\t<NcButton variant=\"secondary\" @click=\"openPicker\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Link existing album') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton variant=\"primary\" @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t</template>\n\t\t\t\t{{ t('nextcloud-vue', 'Create new album') }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<div v-else-if=\"error\" class=\"cn-photos-tab__error\" role=\"alert\">\n\t\t\t{{ error }}\n\t\t</div>\n\n\t\t<!-- Empty -->\n\t\t<div v-else-if=\"albums.length === 0\" class=\"cn-sidebar-tab__empty cn-photos-tab__empty\">\n\t\t\t<ImageIcon :size=\"32\" class=\"cn-photos-tab__empty-icon\" />\n\t\t\t<p>{{ emptyLabel }}</p>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:href=\"photosAppUrl\"\n\t\t\t\ttarget=\"_blank\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ImageIcon :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ openPhotosLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\n\t\t<!-- Photo mosaic grid (mirrors the NC Photos timeline) -->\n\t\t<div v-else class=\"cn-photos-tab__grid\">\n\t\t\t<div\n\t\t\t\tv-for=\"album in albums\"\n\t\t\t\t:key=\"albumKey(album)\"\n\t\t\t\tclass=\"cn-photos-tab__tile\">\n\t\t\t\t<a\n\t\t\t\t\t:href=\"albumUrl(album)\"\n\t\t\t\t\tclass=\"cn-photos-tab__tile-link\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener noreferrer\"\n\t\t\t\t\t:title=\"albumName(album)\">\n\t\t\t\t\t<div class=\"cn-photos-tab__cover\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tv-if=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:src=\"coverUrl(album)\"\n\t\t\t\t\t\t\t:alt=\"albumName(album)\"\n\t\t\t\t\t\t\tclass=\"cn-photos-tab__cover-img\"\n\t\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\t\t@error=\"onCoverError(album)\">\n\t\t\t\t\t\t<span v-else class=\"cn-photos-tab__cover-placeholder\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"36\" class=\"cn-photos-tab__cover-fallback\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Photo-count badge, top-left (mirrors album stack overlay) -->\n\t\t\t\t\t\t<span v-if=\"photoCount(album) !== null\" class=\"cn-photos-tab__badge\">\n\t\t\t\t\t\t\t<ImageMultiple :size=\"12\" />\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__badge-count\">{{ photoCountLabel(album) }}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<!-- Hover scrim with name + date -->\n\t\t\t\t\t\t<div class=\"cn-photos-tab__meta\">\n\t\t\t\t\t\t\t<span class=\"cn-photos-tab__name\">{{ albumName(album) }}</span>\n\t\t\t\t\t\t\t<span v-if=\"lastEdited(album)\" class=\"cn-photos-tab__when\">\n\t\t\t\t\t\t\t\t{{ formatWhen(lastEdited(album)) }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</a>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Unlink album')\"\n\t\t\t\t\tclass=\"cn-photos-tab__unlink\"\n\t\t\t\t\t@click=\"unlinkAlbum(album)\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<LinkOff :size=\"16\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<CnPhotoAlbumPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLinkPick\" />\n\n\t\t<CnPhotoAlbumCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreatePick\" />\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ImageIcon from 'vue-material-design-icons/Image.vue'\nimport ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'\nimport LinkOff from 'vue-material-design-icons/LinkOff.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnPhotoAlbumCreate from '../../../components/CnPhotoAlbumCreate/CnPhotoAlbumCreate.vue'\nimport CnPhotoAlbumPicker from '../../../components/CnPhotoAlbumPicker/CnPhotoAlbumPicker.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\nimport { userLocale } from '../../../utils/userLocale.js'\n\n/**\n * CnPhotosTab — bespoke album grid for the `photos` integration.\n *\n * Renders one tile per linked NC Photos album with cover thumbnail,\n * album name, photo count, and last-edited date. Clicking a tile opens\n * the album in the NC Photos app in a new tab. Tier-2: includes\n * link/create modals and per-tile unlink.\n */\nexport default {\n\tname: 'CnPhotosTab',\n\n\tcomponents: {\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tAlertCircleOutline,\n\t\tImageIcon,\n\t\tImageMultiple,\n\t\tLinkOff,\n\t\tLinkVariant,\n\t\tPlus,\n\t\tCnPhotoAlbumPicker,\n\t\tCnPhotoAlbumCreate,\n\t},\n\n\tprops: {\n\t\t/* eslint-disable vue/no-unused-properties -- the integration dispatch binds integrationId on every integration component (see CnIntegrationWidgetGrid), so declaring it keeps it out of $attrs */\n\t\t/** Stable integration id (forwarded from the registry — always `'photos'`). */\n\t\tintegrationId: { type: String, default: 'photos' },\n\t\t/* eslint-enable vue/no-unused-properties */\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No albums linked yet') },\n\t\t/** Pre-translated \"Open Photos\" CTA label. */\n\t\topenPhotosLabel: { type: String, default: () => t('nextcloud-vue', 'Open Photos') },\n\t\t/** Pre-translated 503 unavailable banner copy. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Photos is currently unavailable.') },\n\t\t/** URL of the NC Photos app entry. */\n\t\tphotosAppUrl: { type: String, default: '/index.php/apps/photos' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\talbums: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tdegraded: '',\n\t\t\tbrokenCovers: {},\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t}\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler(id) {\n\t\t\tif (id) {\n\t\t\t\tthis.fetchAlbums()\n\t\t\t}\n\t\t} },\n\n\t\tregister() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.fetchAlbums()\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Base for the Tier-2 photos endpoints (list/link/new/destroy).\n\t\t *\n\t\t * @return {string} The endpoint URL.\n\t\t */\n\t\tphotosEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/photos`\n\t\t},\n\n\t\talbumKey(album) {\n\t\t\treturn (album && (album.albumId ?? album.id ?? album.data?.album_id)) || ''\n\t\t},\n\n\t\t/**\n\t\t * Resolve the displayable album name from the row, stripping any\n\t\t * legacy `[or:{uuid}]` marker (pre-Tier-2 albums embedded the\n\t\t * marker inside `name`).\n\t\t *\n\t\t * @param {object} album Provider row.\n\t\t *\n\t\t * @return {string} The display name.\n\t\t */\n\t\talbumName(album) {\n\t\t\tconst raw = (album && (album.albumName || album.title || album.name || album.data?.name)) || ''\n\t\t\treturn String(raw).replace(/\\s*\\[or:[^\\]]+\\]\\s*/g, ' ').trim() || t('nextcloud-vue', 'Untitled album')\n\t\t},\n\n\t\talbumUrl(album) {\n\t\t\tif (album && album.url && safeHref(album.url) !== '#') {\n\t\t\t\treturn album.url\n\t\t\t}\n\t\t\tconst id = this.albumKey(album)\n\t\t\treturn `/index.php/apps/photos/albums/${encodeURIComponent(id)}`\n\t\t},\n\n\t\tcoverUrl(album) {\n\t\t\tif (!album || this.brokenCovers[this.albumKey(album)]) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn album.coverPhotoUrl || album.data?.coverPhotoUrl || ''\n\t\t},\n\n\t\tonCoverError(album) {\n\t\t\tthis.brokenCovers[this.albumKey(album)] = true\n\t\t},\n\n\t\tphotoCount(album) {\n\t\t\tconst raw = album?.photoCount ?? album?.data?.photoCount ?? album?.data?.count\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst num = Number(raw)\n\t\t\treturn Number.isFinite(num) ? num : null\n\t\t},\n\n\t\tphotoCountLabel(album) {\n\t\t\tconst count = this.photoCount(album)\n\t\t\tif (count === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{count} photo', '{count} photos', count, { count })\n\t\t},\n\n\t\tlastEdited(album) {\n\t\t\treturn album?.lastEdited\n\t\t\t\t|| album?.data?.lastEdited\n\t\t\t\t|| album?.data?.created\n\t\t\t\t|| ''\n\t\t},\n\n\t\tformatWhen(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// NC Photos stores `created` as a unix timestamp in\n\t\t\t\t// seconds; accept ISO strings transparently too.\n\t\t\t\tconst num = Number(value)\n\t\t\t\tconst d = Number.isFinite(num) && num > 0 && String(value).length <= 12\n\t\t\t\t\t? new Date(num * 1000)\n\t\t\t\t\t: new Date(value)\n\t\t\t\tif (Number.isNaN(d.getTime())) {\n\t\t\t\t\treturn String(value)\n\t\t\t\t}\n\t\t\t\treturn d.toLocaleDateString(userLocale(), { dateStyle: 'medium' })\n\t\t\t} catch {\n\t\t\t\treturn String(value)\n\t\t\t}\n\t\t},\n\n\t\topenPicker() {\n\t\t\tthis.pickerOpen = true\n\t\t},\n\n\t\topenCreate() {\n\t\t\tthis.createOpen = true\n\t\t},\n\n\t\tasync onLinkPick(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'This album is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] link failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not link album.')\n\t\t\t}\n\t\t},\n\n\t\tasync onCreatePick(payload) {\n\t\t\tthis.createOpen = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] create failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not create album.')\n\t\t\t}\n\t\t},\n\n\t\tasync unlinkAlbum(album) {\n\t\t\tconst albumId = this.albumKey(album)\n\t\t\tif (!albumId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.photosEndpoint()}/${albumId}`), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tawait this.fetchAlbums()\n\t\t\t\t} else {\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] unlink failed', err)\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not unlink album.')\n\t\t\t}\n\t\t},\n\n\t\tasync fetchAlbums() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.photosEndpoint()), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.albums = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503 || response.status === 501) {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.albums = []\n\t\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnPhotosTab] failed to fetch albums', err)\n\t\t\t\tthis.albums = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'Could not load albums.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-photos-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-photos-tab__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 10px;\n\tflex-wrap: wrap;\n}\n\n.cn-photos-tab__banner {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 8px 10px;\n\tmargin-bottom: 10px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-warning, #e9a40f);\n\tcolor: var(--color-main-background);\n\tfont-size: 0.9em;\n}\n\n.cn-photos-tab__error {\n\tcolor: var(--color-error);\n\tfont-size: 0.9em;\n\tmargin: 4px 0 8px;\n}\n\n.cn-photos-tab__empty {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 16px 8px;\n\tcolor: var(--color-text-maxcontrast);\n\ttext-align: center;\n}\n\n.cn-photos-tab__empty-icon {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/*\n * Dense edge-to-edge mosaic that mirrors the NC Photos timeline: square\n * thumbnails packed tight with a thin main-background gutter (the same\n * 2px border NC Photos draws between tiles), name + date revealed in a\n * bottom scrim on hover/focus so the imagery dominates at rest.\n */\n.cn-photos-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(104px, 1fr));\n\tgap: 2px;\n\tborder-radius: var(--border-radius-large, var(--border-radius));\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile {\n\tposition: relative;\n}\n\n.cn-photos-tab__unlink {\n\tposition: absolute;\n\ttop: 4px;\n\tright: 4px;\n\tz-index: 3;\n\topacity: 0;\n\tbackground: var(--color-main-background);\n\tborder-radius: 50%;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__unlink,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__unlink {\n\topacity: 1;\n}\n\n.cn-photos-tab__tile-link {\n\tdisplay: block;\n\ttext-decoration: none;\n\tcolor: inherit;\n}\n\n.cn-photos-tab__cover {\n\tposition: relative;\n\taspect-ratio: 1 / 1;\n\twidth: 100%;\n\t/* Match the NC Photos selection outline on focus. */\n\tborder: 2px solid var(--color-main-background);\n\tbox-sizing: border-box;\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\toverflow: hidden;\n}\n\n.cn-photos-tab__tile-link:focus-visible .cn-photos-tab__cover {\n\toutline: var(--color-primary-element) solid 2px;\n\toutline-offset: -2px;\n}\n\n.cn-photos-tab__cover-img {\n\twidth: 100%;\n\theight: 100%;\n\tobject-fit: cover;\n\tdisplay: block;\n\ttransition: transform 0.2s ease;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__cover-img,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__cover-img {\n\ttransform: scale(1.04);\n}\n\n.cn-photos-tab__cover-placeholder {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-photos-tab__cover-fallback {\n\tcolor: var(--color-primary-element);\n}\n\n/* Photo-count badge (mirrors the album stack count overlay). */\n.cn-photos-tab__badge {\n\tposition: absolute;\n\ttop: 4px;\n\tleft: 4px;\n\tz-index: 2;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 1px 6px 1px 4px;\n\tborder-radius: var(--border-radius-pill, 12px);\n\tbackground: rgba(0, 0, 0, 0.55);\n\tcolor: #fff;\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tline-height: 1.6;\n}\n\n.cn-photos-tab__badge-count {\n\twhite-space: nowrap;\n}\n\n/* Bottom scrim: hidden at rest, revealed on hover/focus. */\n.cn-photos-tab__meta {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\tbottom: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1px;\n\tpadding: 16px 8px 6px;\n\tbackground: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n\tpointer-events: none;\n}\n\n.cn-photos-tab__tile:hover .cn-photos-tab__meta,\n.cn-photos-tab__tile:focus-within .cn-photos-tab__meta {\n\topacity: 1;\n}\n\n.cn-photos-tab__name {\n\tfont-size: 12px;\n\tfont-weight: 600;\n\tcolor: #fff;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-photos-tab__when {\n\tfont-size: 10px;\n\tcolor: rgba(255, 255, 255, 0.85);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":["CnPhotoAlbumPicker","CnPhotoAlbumCreate","t","n"],"mappings":"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"}