@conduction/nextcloud-vue 2.78.0 → 2.80.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 (259) hide show
  1. package/dist/bundled-packages.json +0 -2
  2. package/dist/esm/bundled-packages.json +0 -2
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
  9. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
  19. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  40. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  41. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  42. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  43. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  44. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  46. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  48. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  50. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  51. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
  52. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
  54. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  55. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
  56. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  57. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
  58. package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
  60. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  61. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  62. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  63. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  64. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
  67. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
  69. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  71. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  72. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  73. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  74. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  75. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  81. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  82. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  83. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
  84. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  85. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  92. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  93. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  95. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  96. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  98. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  99. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  100. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  101. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  102. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  103. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  104. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  105. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  106. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  107. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  108. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  110. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
  112. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  113. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  114. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  115. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  116. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  117. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  118. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  119. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  120. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  121. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  122. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  123. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  124. package/dist/esm/composables/useAppManifest.js +8 -3
  125. package/dist/esm/composables/useAppManifest.js.map +1 -1
  126. package/dist/esm/composables/useFormDraft.js +140 -9
  127. package/dist/esm/composables/useFormDraft.js.map +1 -1
  128. package/dist/esm/index.js +5 -0
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  131. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  132. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  133. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  134. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  135. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  136. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  137. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  138. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  139. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  140. package/dist/esm/integrations/offline/index.js +2 -1
  141. package/dist/esm/integrations/offline/index.js.map +1 -1
  142. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  143. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  144. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  145. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  146. package/dist/esm/l10n/en.json.js +72 -1
  147. package/dist/esm/l10n/en.json.js.map +1 -1
  148. package/dist/esm/l10n/nl.json.js +120 -2
  149. package/dist/esm/l10n/nl.json.js.map +1 -1
  150. package/dist/esm/nextcloud-vue.css +1314 -74
  151. package/dist/esm/utils/boardDate.js +193 -0
  152. package/dist/esm/utils/boardDate.js.map +1 -0
  153. package/dist/esm/utils/detailActionModel.js +60 -3
  154. package/dist/esm/utils/detailActionModel.js.map +1 -1
  155. package/dist/esm/utils/dynamicProperties.js +5 -1
  156. package/dist/esm/utils/dynamicProperties.js.map +1 -1
  157. package/dist/esm/utils/fillTemplate.js +114 -0
  158. package/dist/esm/utils/fillTemplate.js.map +1 -0
  159. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  160. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  161. package/dist/esm/utils/queryPresets.js +96 -0
  162. package/dist/esm/utils/queryPresets.js.map +1 -0
  163. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  164. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  165. package/dist/esm/utils/schema.js +21 -0
  166. package/dist/esm/utils/schema.js.map +1 -1
  167. package/dist/esm/utils/sentinelTokens.js +3 -3
  168. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  169. package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
  170. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  171. package/dist/esm/utils/widgetDispatch.js +7 -1
  172. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  173. package/dist/nextcloud-vue.cjs.js +14278 -8169
  174. package/dist/nextcloud-vue.cjs.js.map +2 -2
  175. package/dist/nextcloud-vue.css +1314 -74
  176. package/l10n/en.json +72 -1
  177. package/l10n/nl.json +120 -2
  178. package/package.json +1 -1
  179. package/scripts/build-validators.js +26 -0
  180. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  181. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  182. package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
  183. package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
  184. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  185. package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
  186. package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
  187. package/src/components/CnDataTable/CnDataTable.md +2 -0
  188. package/src/components/CnDataTable/CnDataTable.vue +82 -2
  189. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  190. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  191. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
  192. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  193. package/src/components/CnFactsWidget/index.js +29 -0
  194. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  195. package/src/components/CnFieldListWidget/index.js +28 -0
  196. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
  197. package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
  198. package/src/components/CnFilesBrowser/index.js +1 -0
  199. package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
  200. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  201. package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
  202. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  203. package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
  204. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  205. package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
  206. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  207. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
  208. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  209. package/src/components/CnRecordListWidget/index.js +30 -0
  210. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
  211. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  212. package/src/components/CnSectionsWidget/index.js +34 -0
  213. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  214. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  215. package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
  216. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  217. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  218. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  219. package/src/components/index.js +5 -1
  220. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  221. package/src/composables/useAppManifest.js +8 -3
  222. package/src/composables/useFormDraft.js +139 -8
  223. package/src/css/form-field.css +14 -0
  224. package/src/css/index-page.css +71 -0
  225. package/src/css/kpi-card.css +32 -0
  226. package/src/css/look-board-detail.css +87 -0
  227. package/src/css/look-board-index.css +143 -14
  228. package/src/css/look-board.css +114 -2
  229. package/src/css/table.css +56 -0
  230. package/src/index.js +8 -0
  231. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  232. package/src/integrations/builtin/field-inspection.js +21 -0
  233. package/src/integrations/offline/checklistSubmission.js +137 -0
  234. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  235. package/src/integrations/offline/index.js +3 -0
  236. package/src/integrations/offline/offlineDb.js +6 -1
  237. package/src/integrations/offline/syncReplayService.js +8 -2
  238. package/src/schemas/app-manifest-v2.schema.json +149 -5
  239. package/src/utils/boardDate.js +189 -0
  240. package/src/utils/detailActionModel.js +59 -2
  241. package/src/utils/dynamicProperties.js +5 -1
  242. package/src/utils/fillTemplate.js +110 -0
  243. package/src/utils/libraryWidgetKeys.js +4 -0
  244. package/src/utils/queryPresets.js +93 -0
  245. package/src/utils/resolveFilterTokens.js +11 -0
  246. package/src/utils/schema.js +21 -0
  247. package/src/utils/sentinelTokens.js +3 -3
  248. package/src/utils/validateManifestV2.compiled.js +57 -2
  249. package/src/utils/widgetDispatch.js +7 -1
  250. package/dist/esm/_virtual/equal.js +0 -4
  251. package/dist/esm/_virtual/equal.js.map +0 -1
  252. package/dist/esm/_virtual/ucs2length.js +0 -4
  253. package/dist/esm/_virtual/ucs2length.js.map +0 -1
  254. package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
  255. package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
  256. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
  257. package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
  258. package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
  259. package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { getCurrentUser } from '@nextcloud/auth';
1
2
  import { translate } from '@nextcloud/l10n';
2
3
 
3
4
  /**
@@ -35,6 +36,9 @@ const DRAFT_DEBOUNCE_MS = 500;
35
36
  /** The prefix every draft key carries, so a sweep can find them. */
36
37
  const DRAFT_KEY_PREFIX = 'cn-form-draft';
37
38
 
39
+ /** The user part of a draft key when nobody is signed in. */
40
+ const DRAFT_ANONYMOUS_USER = 'anonymous';
41
+
38
42
  /**
39
43
  * The storage key one form's draft lives under.
40
44
  *
@@ -55,10 +59,124 @@ function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {})
55
59
  String(appId || 'app'),
56
60
  String(schema || 'schema'),
57
61
  String(objectId || 'new'),
58
- String(userId || 'anonymous'),
62
+ String(userId || DRAFT_ANONYMOUS_USER),
59
63
  ].join(':')
60
64
  }
61
65
 
66
+ /**
67
+ * Who a draft belongs to: the id the host passed, else the signed-in
68
+ * Nextcloud user, else '' (which {@link draftKey} stores as `anonymous`).
69
+ *
70
+ * 🔴 A HOST THAT PASSES NOTHING STILL GETS PER-USER DRAFTS. Leaving the user
71
+ * to every host meant no host passed it, and every draft on a shared device
72
+ * was keyed `anonymous`: the next person at the counter was offered what the
73
+ * last one typed.
74
+ *
75
+ * @param {string} [explicit] The id the host passed (`draftUserId`).
76
+ * @return {string} The user id, or '' when nobody is signed in.
77
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
78
+ */
79
+ function resolveDraftUserId(explicit = '') {
80
+ if (explicit) {
81
+ return String(explicit)
82
+ }
83
+ try {
84
+ const uid = getCurrentUser()?.uid;
85
+ return uid ? String(uid) : ''
86
+ } catch {
87
+ // No Nextcloud page around us (a test, an embed): nobody is signed in.
88
+ return ''
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Remove every draft stored under the anonymous user.
94
+ *
95
+ * Called once a signed-in user is known. Such a draft was written by a page
96
+ * that did not know who was typing, so it may be somebody else's, and it is
97
+ * deleted rather than moved: moving it would hand it to whoever opened the
98
+ * form next.
99
+ *
100
+ * @return {number} How many drafts were removed.
101
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
102
+ */
103
+ function clearAnonymousDrafts() {
104
+ let removed = 0;
105
+ try {
106
+ const storage = window.localStorage;
107
+ const suffix = ':' + DRAFT_ANONYMOUS_USER;
108
+ const doomed = [];
109
+ for (let i = 0; i < storage.length; i++) {
110
+ const key = storage.key(i);
111
+ if (key && key.startsWith(DRAFT_KEY_PREFIX + ':') && key.endsWith(suffix)) {
112
+ doomed.push(key);
113
+ }
114
+ }
115
+ for (const key of doomed) {
116
+ storage.removeItem(key);
117
+ removed++;
118
+ }
119
+ } catch {
120
+ // Blocked storage holds no drafts to leak.
121
+ }
122
+ return removed
123
+ }
124
+
125
+ /**
126
+ * Whether a value counts as empty for a draft: null, undefined, '' and [].
127
+ *
128
+ * @param {unknown} value The value.
129
+ * @return {boolean} True when empty.
130
+ */
131
+ function isEmptyDraftValue(value) {
132
+ return value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)
133
+ }
134
+
135
+ /**
136
+ * Whether two field values are the same for the purpose of a draft.
137
+ *
138
+ * Empty values are equal to each other: an input reports a missing value back
139
+ * as '', and that is the control normalising, not a person typing.
140
+ *
141
+ * @param {unknown} a One value.
142
+ * @param {unknown} b The other.
143
+ * @return {boolean} True when they are the same.
144
+ */
145
+ function sameDraftValue(a, b) {
146
+ if (isEmptyDraftValue(a) && isEmptyDraftValue(b)) {
147
+ return true
148
+ }
149
+ if (a === b) {
150
+ return true
151
+ }
152
+ try {
153
+ return JSON.stringify(a) === JSON.stringify(b)
154
+ } catch {
155
+ return false
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Whether a person changed the form: one of the fields they edited holds a
161
+ * value different from the one the form opened with.
162
+ *
163
+ * 🔴 ONLY EDITED FIELDS COUNT. A form fills in values by itself after it
164
+ * opens (defaults, normalised dates, the active organisation, options that
165
+ * arrive later). Counting those wrote a draft and said "Saved just now" on a
166
+ * dialog nobody had touched.
167
+ *
168
+ * @param {object} values The current values.
169
+ * @param {object} baseline The values the form opened with.
170
+ * @param {string[]} editedKeys The fields a person edited.
171
+ * @return {boolean} True when there is something worth keeping.
172
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
173
+ */
174
+ function hasDraftChanges(values, baseline, editedKeys) {
175
+ const now = values || {};
176
+ const base = baseline || {};
177
+ return (editedKeys || []).some((key) => !sameDraftValue(now[key], base[key]))
178
+ }
179
+
62
180
  /**
63
181
  * Read one draft back, or null when there is nothing usable.
64
182
  *
@@ -219,13 +337,6 @@ function formDraftMixin() {
219
337
  draftSavedAt: 0,
220
338
  /** Non-reactive handle for the debounce. */
221
339
  draftTimeout: null,
222
- /**
223
- * Whether the form's own initial population has happened.
224
- *
225
- * The first change to the values is the component building
226
- * them, not a person typing. See the watcher that reads this.
227
- */
228
- draftSeeded: false,
229
340
  }
230
341
  },
231
342
 
@@ -266,6 +377,26 @@ function formDraftMixin() {
266
377
  }, DRAFT_DEBOUNCE_MS);
267
378
  },
268
379
 
380
+ /**
381
+ * The form is back to what it opened with: cancel a pending write,
382
+ * remove a draft written earlier and stop announcing anything.
383
+ *
384
+ * @param {string} key The storage key.
385
+ * @return {void}
386
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
387
+ */
388
+ settleUnchangedDraft(key) {
389
+ if (this.draftTimeout) {
390
+ clearTimeout(this.draftTimeout);
391
+ this.draftTimeout = null;
392
+ }
393
+ if (this.draftState !== 'idle') {
394
+ clearDraft(key);
395
+ this.draftState = 'idle';
396
+ this.draftSavedAt = 0;
397
+ }
398
+ },
399
+
269
400
  /**
270
401
  * Drop the draft and stop announcing anything about it.
271
402
  *
@@ -286,5 +417,5 @@ function formDraftMixin() {
286
417
  }
287
418
  }
288
419
 
289
- export { DRAFT_DEBOUNCE_MS, DRAFT_KEY_PREFIX, DRAFT_MAX_AGE_MS, clearDraft, draftIndicatorText, draftKey, formDraftMixin, readDraft, writeDraft };
420
+ export { DRAFT_ANONYMOUS_USER, DRAFT_DEBOUNCE_MS, DRAFT_KEY_PREFIX, DRAFT_MAX_AGE_MS, clearAnonymousDrafts, clearDraft, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId, sameDraftValue, writeDraft };
290
421
  //# sourceMappingURL=useFormDraft.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFormDraft.js","sources":["../../../src/composables/useFormDraft.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: AGPL-3.0-or-later\n *\n * useFormDraft — keep what somebody typed when the dialog closes on them.\n *\n * A handler filling a 27-field case type form and losing the tab starts over.\n * This writes the values to `localStorage` as they type and offers them back\n * the next time the same form opens.\n *\n * 🔴 IT NEVER DECIDES TO RESTORE. The draft is offered and the person chooses.\n * Silently refilling a form is how somebody submits last week's answers without\n * noticing they were there, and a form that fills itself is indistinguishable\n * from a form the server prefilled.\n *\n * 🔴 EVERY READ AND WRITE IS WRAPPED. `localStorage` throws in a private\n * window, with site data blocked, and inside some embedded webviews. A draft\n * that cannot be saved is a small loss; a dialog that will not open because\n * saving the draft threw is a large one.\n *\n * WHAT IT DELIBERATELY DOES NOT DO. It does not touch the server. A keystroke\n * autosave against the object API writes an audit row for every half-typed\n * value, and the audit trail is read by people answering \"who changed this\".\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** How long a draft stays interesting, in milliseconds (7 days). */\nexport const DRAFT_MAX_AGE_MS = (7 * 24 * 60 * 60 * 1000)\n\n/** How long to wait after the last keystroke before writing, in milliseconds. */\nexport const DRAFT_DEBOUNCE_MS = 500\n\n/** The prefix every draft key carries, so a sweep can find them. */\nexport const DRAFT_KEY_PREFIX = 'cn-form-draft'\n\n/**\n * The storage key one form's draft lives under.\n *\n * 🔴 THE USER ID IS PART OF THE KEY. A shared browser profile at a service\n * desk is the ordinary case in a municipality, and a draft keyed without the\n * user hands the next person at the counter what the last one typed.\n *\n * @param {object} parts The identifying parts.\n * @param {string} [parts.appId] The app the form belongs to.\n * @param {string} [parts.schema] The schema being filled in.\n * @param {string} [parts.objectId] The object being edited, or '' when new.\n * @param {string} [parts.userId] Who is typing.\n * @return {string} The storage key.\n */\nexport function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {}) {\n\treturn [\n\t\tDRAFT_KEY_PREFIX,\n\t\tString(appId || 'app'),\n\t\tString(schema || 'schema'),\n\t\tString(objectId || 'new'),\n\t\tString(userId || 'anonymous'),\n\t].join(':')\n}\n\n/**\n * Read one draft back, or null when there is nothing usable.\n *\n * Returns null for an absent key, unreadable storage, unparseable JSON, a\n * draft older than {@link DRAFT_MAX_AGE_MS}, and a draft older than the object\n * it belongs to. That last one matters: a colleague who saved the record after\n * you started typing has said something more recent than your draft, and\n * offering yours on top would invite you to overwrite them without being told.\n *\n * @param {string} key The storage key.\n * @param {object} [options] Reading options.\n * @param {string} [options.objectUpdated] The object's `updated` timestamp, when it has one.\n * @param {number} [options.now] The current epoch ms, for testing.\n * @return {{values: object, savedAt: number}|null} The draft, or null.\n */\nexport function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {\n\tlet raw\n\ttry {\n\t\traw = window.localStorage.getItem(key)\n\t} catch {\n\t\t// Blocked or unavailable storage reads as \"no draft\", never as a throw.\n\t\treturn null\n\t}\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tlet parsed\n\ttry {\n\t\tparsed = JSON.parse(raw)\n\t} catch {\n\t\t// A corrupted entry is dropped rather than offered. Nothing here can\n\t\t// repair it, and offering half a form is worse than offering none.\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif ((now - parsed.savedAt) > DRAFT_MAX_AGE_MS) {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (objectUpdated) {\n\t\tconst updatedAt = Date.parse(String(objectUpdated))\n\t\tif (!Number.isNaN(updatedAt) && updatedAt > parsed.savedAt) {\n\t\t\t// The record moved on after this draft was written.\n\t\t\tclearDraft(key)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tif (!parsed.values || typeof parsed.values !== 'object') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\treturn { values: parsed.values, savedAt: parsed.savedAt }\n}\n\n/**\n * What the draft indicator says: \"Saving\" while a local write is pending,\n * \"Saved just now\" / \"Saved 3 minutes ago\" once it landed, nothing before that.\n *\n * After a failed save (`options.saveFailed`) it never says Saving or Saved:\n * next to a server error those words read as if the record had been saved.\n * It then says \"Draft kept on this device\" when a local copy is stored, and\n * nothing otherwise.\n *\n * @param {string} state `idle`, `saving` or `saved`.\n * @param {number} [savedAt] When the last write landed, in epoch ms.\n * @param {number} [now] The current epoch ms, for testing.\n * @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.\n * @return {string} The text, or '' when it says nothing.\n * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save\n */\nexport function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {\n\tif (options && options.saveFailed === true) {\n\t\treturn state === 'saved' ? t('nextcloud-vue', 'Draft kept on this device') : ''\n\t}\n\tif (state === 'saving') {\n\t\treturn t('nextcloud-vue', 'Saving')\n\t}\n\tif (state !== 'saved') {\n\t\treturn ''\n\t}\n\tconst minutes = Math.floor(Math.max(0, now - savedAt) / 60000)\n\tif (!savedAt || minutes < 1) {\n\t\treturn t('nextcloud-vue', 'Saved just now')\n\t}\n\ttry {\n\t\tconst relative = new Intl.RelativeTimeFormat(document.documentElement.lang || 'en', { numeric: 'auto' }).format(-minutes, 'minute')\n\t\treturn t('nextcloud-vue', 'Saved {time}', { time: relative })\n\t} catch {\n\t\treturn t('nextcloud-vue', 'Saved')\n\t}\n}\n\n/**\n * Write one draft.\n *\n * @param {string} key The storage key.\n * @param {object} values The form values.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {boolean} True when it was stored.\n */\nexport function writeDraft(key, values, now = Date.now()) {\n\ttry {\n\t\twindow.localStorage.setItem(\n\t\t\tkey,\n\t\t\tJSON.stringify({ savedAt: now, values: (values || {}) }),\n\t\t)\n\t\treturn true\n\t} catch {\n\t\t// Quota exceeded, or storage blocked. The form keeps working; the\n\t\t// recovery does not exist for this session. Reported to the caller so\n\t\t// the indicator can stay quiet rather than claim \"Saved\".\n\t\treturn false\n\t}\n}\n\n/**\n * Forget one draft.\n *\n * @param {string} key The storage key.\n * @return {void}\n */\nexport function clearDraft(key) {\n\ttry {\n\t\twindow.localStorage.removeItem(key)\n\t} catch {\n\t\t// Nothing to do and nothing worth failing a dialog over.\n\t}\n}\n\n/**\n * The Options API mixin the library's components use.\n *\n * A mixin rather than a Vue 3 composable, because these components are Options\n * API and the library builds for both Vue 2.7 and Vue 3.\n *\n * @return {object} The mixin.\n */\nexport function formDraftMixin() {\n\treturn {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\t/** The draft offered on open, or null when there is none. */\n\t\t\t\tdraftOffer: null,\n\t\t\t\t/** 'idle' | 'saving' | 'saved' — what the indicator announces. */\n\t\t\t\tdraftState: 'idle',\n\t\t\t\t/** When the last local write landed, in epoch ms. */\n\t\t\t\tdraftSavedAt: 0,\n\t\t\t\t/** Non-reactive handle for the debounce. */\n\t\t\t\tdraftTimeout: null,\n\t\t\t\t/**\n\t\t\t\t * Whether the form's own initial population has happened.\n\t\t\t\t *\n\t\t\t\t * The first change to the values is the component building\n\t\t\t\t * them, not a person typing. See the watcher that reads this.\n\t\t\t\t */\n\t\t\t\tdraftSeeded: false,\n\t\t\t}\n\t\t},\n\n\t\tbeforeUnmount() {\n\t\t\tif (this.draftTimeout) {\n\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\tthis.draftTimeout = null\n\t\t\t}\n\t\t},\n\n\t\tmethods: {\n\t\t\t/**\n\t\t\t * Schedule a draft write, debounced.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @param {object} values The current form values.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tscheduleDraftWrite(key, values) {\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.draftState = 'saving'\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t}\n\n\t\t\t\tthis.draftTimeout = setTimeout(() => {\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t\t// 'idle' and not 'saved' when the write failed: an\n\t\t\t\t\t// indicator that says Saved about a draft that is not\n\t\t\t\t\t// stored is the whole class of defect this library has\n\t\t\t\t\t// been paying down, in the one place a user is trusting it.\n\t\t\t\t\tconst stored = writeDraft(key, values)\n\t\t\t\t\tthis.draftState = stored ? 'saved' : 'idle'\n\t\t\t\t\tthis.draftSavedAt = stored ? Date.now() : 0\n\t\t\t\t}, DRAFT_DEBOUNCE_MS)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop the draft and stop announcing anything about it.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tforgetDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\n\t\t\t\tclearDraft(key)\n\t\t\t\tthis.draftOffer = null\n\t\t\t\tthis.draftState = 'idle'\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"useFormDraft.js","sources":["../../../src/composables/useFormDraft.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: AGPL-3.0-or-later\n *\n * useFormDraft — keep what somebody typed when the dialog closes on them.\n *\n * A handler filling a 27-field case type form and losing the tab starts over.\n * This writes the values to `localStorage` as they type and offers them back\n * the next time the same form opens.\n *\n * 🔴 IT NEVER DECIDES TO RESTORE. The draft is offered and the person chooses.\n * Silently refilling a form is how somebody submits last week's answers without\n * noticing they were there, and a form that fills itself is indistinguishable\n * from a form the server prefilled.\n *\n * 🔴 EVERY READ AND WRITE IS WRAPPED. `localStorage` throws in a private\n * window, with site data blocked, and inside some embedded webviews. A draft\n * that cannot be saved is a small loss; a dialog that will not open because\n * saving the draft threw is a large one.\n *\n * WHAT IT DELIBERATELY DOES NOT DO. It does not touch the server. A keystroke\n * autosave against the object API writes an audit row for every half-typed\n * value, and the audit trail is read by people answering \"who changed this\".\n */\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { translate as t } from '@nextcloud/l10n'\n\n/** How long a draft stays interesting, in milliseconds (7 days). */\nexport const DRAFT_MAX_AGE_MS = (7 * 24 * 60 * 60 * 1000)\n\n/** How long to wait after the last keystroke before writing, in milliseconds. */\nexport const DRAFT_DEBOUNCE_MS = 500\n\n/** The prefix every draft key carries, so a sweep can find them. */\nexport const DRAFT_KEY_PREFIX = 'cn-form-draft'\n\n/** The user part of a draft key when nobody is signed in. */\nexport const DRAFT_ANONYMOUS_USER = 'anonymous'\n\n/**\n * The storage key one form's draft lives under.\n *\n * 🔴 THE USER ID IS PART OF THE KEY. A shared browser profile at a service\n * desk is the ordinary case in a municipality, and a draft keyed without the\n * user hands the next person at the counter what the last one typed.\n *\n * @param {object} parts The identifying parts.\n * @param {string} [parts.appId] The app the form belongs to.\n * @param {string} [parts.schema] The schema being filled in.\n * @param {string} [parts.objectId] The object being edited, or '' when new.\n * @param {string} [parts.userId] Who is typing.\n * @return {string} The storage key.\n */\nexport function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {}) {\n\treturn [\n\t\tDRAFT_KEY_PREFIX,\n\t\tString(appId || 'app'),\n\t\tString(schema || 'schema'),\n\t\tString(objectId || 'new'),\n\t\tString(userId || DRAFT_ANONYMOUS_USER),\n\t].join(':')\n}\n\n/**\n * Who a draft belongs to: the id the host passed, else the signed-in\n * Nextcloud user, else '' (which {@link draftKey} stores as `anonymous`).\n *\n * 🔴 A HOST THAT PASSES NOTHING STILL GETS PER-USER DRAFTS. Leaving the user\n * to every host meant no host passed it, and every draft on a shared device\n * was keyed `anonymous`: the next person at the counter was offered what the\n * last one typed.\n *\n * @param {string} [explicit] The id the host passed (`draftUserId`).\n * @return {string} The user id, or '' when nobody is signed in.\n * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user\n */\nexport function resolveDraftUserId(explicit = '') {\n\tif (explicit) {\n\t\treturn String(explicit)\n\t}\n\ttry {\n\t\tconst uid = getCurrentUser()?.uid\n\t\treturn uid ? String(uid) : ''\n\t} catch {\n\t\t// No Nextcloud page around us (a test, an embed): nobody is signed in.\n\t\treturn ''\n\t}\n}\n\n/**\n * Remove every draft stored under the anonymous user.\n *\n * Called once a signed-in user is known. Such a draft was written by a page\n * that did not know who was typing, so it may be somebody else's, and it is\n * deleted rather than moved: moving it would hand it to whoever opened the\n * form next.\n *\n * @return {number} How many drafts were removed.\n * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user\n */\nexport function clearAnonymousDrafts() {\n\tlet removed = 0\n\ttry {\n\t\tconst storage = window.localStorage\n\t\tconst suffix = ':' + DRAFT_ANONYMOUS_USER\n\t\tconst doomed = []\n\t\tfor (let i = 0; i < storage.length; i++) {\n\t\t\tconst key = storage.key(i)\n\t\t\tif (key && key.startsWith(DRAFT_KEY_PREFIX + ':') && key.endsWith(suffix)) {\n\t\t\t\tdoomed.push(key)\n\t\t\t}\n\t\t}\n\t\tfor (const key of doomed) {\n\t\t\tstorage.removeItem(key)\n\t\t\tremoved++\n\t\t}\n\t} catch {\n\t\t// Blocked storage holds no drafts to leak.\n\t}\n\treturn removed\n}\n\n/**\n * Whether a value counts as empty for a draft: null, undefined, '' and [].\n *\n * @param {unknown} value The value.\n * @return {boolean} True when empty.\n */\nfunction isEmptyDraftValue(value) {\n\treturn value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)\n}\n\n/**\n * Whether two field values are the same for the purpose of a draft.\n *\n * Empty values are equal to each other: an input reports a missing value back\n * as '', and that is the control normalising, not a person typing.\n *\n * @param {unknown} a One value.\n * @param {unknown} b The other.\n * @return {boolean} True when they are the same.\n */\nexport function sameDraftValue(a, b) {\n\tif (isEmptyDraftValue(a) && isEmptyDraftValue(b)) {\n\t\treturn true\n\t}\n\tif (a === b) {\n\t\treturn true\n\t}\n\ttry {\n\t\treturn JSON.stringify(a) === JSON.stringify(b)\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Whether a person changed the form: one of the fields they edited holds a\n * value different from the one the form opened with.\n *\n * 🔴 ONLY EDITED FIELDS COUNT. A form fills in values by itself after it\n * opens (defaults, normalised dates, the active organisation, options that\n * arrive later). Counting those wrote a draft and said \"Saved just now\" on a\n * dialog nobody had touched.\n *\n * @param {object} values The current values.\n * @param {object} baseline The values the form opened with.\n * @param {string[]} editedKeys The fields a person edited.\n * @return {boolean} True when there is something worth keeping.\n * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field\n */\nexport function hasDraftChanges(values, baseline, editedKeys) {\n\tconst now = values || {}\n\tconst base = baseline || {}\n\treturn (editedKeys || []).some((key) => !sameDraftValue(now[key], base[key]))\n}\n\n/**\n * Read one draft back, or null when there is nothing usable.\n *\n * Returns null for an absent key, unreadable storage, unparseable JSON, a\n * draft older than {@link DRAFT_MAX_AGE_MS}, and a draft older than the object\n * it belongs to. That last one matters: a colleague who saved the record after\n * you started typing has said something more recent than your draft, and\n * offering yours on top would invite you to overwrite them without being told.\n *\n * @param {string} key The storage key.\n * @param {object} [options] Reading options.\n * @param {string} [options.objectUpdated] The object's `updated` timestamp, when it has one.\n * @param {number} [options.now] The current epoch ms, for testing.\n * @return {{values: object, savedAt: number}|null} The draft, or null.\n */\nexport function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {\n\tlet raw\n\ttry {\n\t\traw = window.localStorage.getItem(key)\n\t} catch {\n\t\t// Blocked or unavailable storage reads as \"no draft\", never as a throw.\n\t\treturn null\n\t}\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tlet parsed\n\ttry {\n\t\tparsed = JSON.parse(raw)\n\t} catch {\n\t\t// A corrupted entry is dropped rather than offered. Nothing here can\n\t\t// repair it, and offering half a form is worse than offering none.\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif ((now - parsed.savedAt) > DRAFT_MAX_AGE_MS) {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (objectUpdated) {\n\t\tconst updatedAt = Date.parse(String(objectUpdated))\n\t\tif (!Number.isNaN(updatedAt) && updatedAt > parsed.savedAt) {\n\t\t\t// The record moved on after this draft was written.\n\t\t\tclearDraft(key)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tif (!parsed.values || typeof parsed.values !== 'object') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\treturn { values: parsed.values, savedAt: parsed.savedAt }\n}\n\n/**\n * What the draft indicator says: \"Saving\" while a local write is pending,\n * \"Saved just now\" / \"Saved 3 minutes ago\" once it landed, nothing before that.\n *\n * After a failed save (`options.saveFailed`) it never says Saving or Saved:\n * next to a server error those words read as if the record had been saved.\n * It then says \"Draft kept on this device\" when a local copy is stored, and\n * nothing otherwise.\n *\n * @param {string} state `idle`, `saving` or `saved`.\n * @param {number} [savedAt] When the last write landed, in epoch ms.\n * @param {number} [now] The current epoch ms, for testing.\n * @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.\n * @return {string} The text, or '' when it says nothing.\n * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save\n */\nexport function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {\n\tif (options && options.saveFailed === true) {\n\t\treturn state === 'saved' ? t('nextcloud-vue', 'Draft kept on this device') : ''\n\t}\n\tif (state === 'saving') {\n\t\treturn t('nextcloud-vue', 'Saving')\n\t}\n\tif (state !== 'saved') {\n\t\treturn ''\n\t}\n\tconst minutes = Math.floor(Math.max(0, now - savedAt) / 60000)\n\tif (!savedAt || minutes < 1) {\n\t\treturn t('nextcloud-vue', 'Saved just now')\n\t}\n\ttry {\n\t\tconst relative = new Intl.RelativeTimeFormat(document.documentElement.lang || 'en', { numeric: 'auto' }).format(-minutes, 'minute')\n\t\treturn t('nextcloud-vue', 'Saved {time}', { time: relative })\n\t} catch {\n\t\treturn t('nextcloud-vue', 'Saved')\n\t}\n}\n\n/**\n * Write one draft.\n *\n * @param {string} key The storage key.\n * @param {object} values The form values.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {boolean} True when it was stored.\n */\nexport function writeDraft(key, values, now = Date.now()) {\n\ttry {\n\t\twindow.localStorage.setItem(\n\t\t\tkey,\n\t\t\tJSON.stringify({ savedAt: now, values: (values || {}) }),\n\t\t)\n\t\treturn true\n\t} catch {\n\t\t// Quota exceeded, or storage blocked. The form keeps working; the\n\t\t// recovery does not exist for this session. Reported to the caller so\n\t\t// the indicator can stay quiet rather than claim \"Saved\".\n\t\treturn false\n\t}\n}\n\n/**\n * Forget one draft.\n *\n * @param {string} key The storage key.\n * @return {void}\n */\nexport function clearDraft(key) {\n\ttry {\n\t\twindow.localStorage.removeItem(key)\n\t} catch {\n\t\t// Nothing to do and nothing worth failing a dialog over.\n\t}\n}\n\n/**\n * The Options API mixin the library's components use.\n *\n * A mixin rather than a Vue 3 composable, because these components are Options\n * API and the library builds for both Vue 2.7 and Vue 3.\n *\n * @return {object} The mixin.\n */\nexport function formDraftMixin() {\n\treturn {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\t/** The draft offered on open, or null when there is none. */\n\t\t\t\tdraftOffer: null,\n\t\t\t\t/** 'idle' | 'saving' | 'saved' — what the indicator announces. */\n\t\t\t\tdraftState: 'idle',\n\t\t\t\t/** When the last local write landed, in epoch ms. */\n\t\t\t\tdraftSavedAt: 0,\n\t\t\t\t/** Non-reactive handle for the debounce. */\n\t\t\t\tdraftTimeout: null,\n\t\t\t}\n\t\t},\n\n\t\tbeforeUnmount() {\n\t\t\tif (this.draftTimeout) {\n\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\tthis.draftTimeout = null\n\t\t\t}\n\t\t},\n\n\t\tmethods: {\n\t\t\t/**\n\t\t\t * Schedule a draft write, debounced.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @param {object} values The current form values.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tscheduleDraftWrite(key, values) {\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.draftState = 'saving'\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t}\n\n\t\t\t\tthis.draftTimeout = setTimeout(() => {\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t\t// 'idle' and not 'saved' when the write failed: an\n\t\t\t\t\t// indicator that says Saved about a draft that is not\n\t\t\t\t\t// stored is the whole class of defect this library has\n\t\t\t\t\t// been paying down, in the one place a user is trusting it.\n\t\t\t\t\tconst stored = writeDraft(key, values)\n\t\t\t\t\tthis.draftState = stored ? 'saved' : 'idle'\n\t\t\t\t\tthis.draftSavedAt = stored ? Date.now() : 0\n\t\t\t\t}, DRAFT_DEBOUNCE_MS)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * The form is back to what it opened with: cancel a pending write,\n\t\t\t * remove a draft written earlier and stop announcing anything.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field\n\t\t\t */\n\t\t\tsettleUnchangedDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\t\t\t\tif (this.draftState !== 'idle') {\n\t\t\t\t\tclearDraft(key)\n\t\t\t\t\tthis.draftState = 'idle'\n\t\t\t\t\tthis.draftSavedAt = 0\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop the draft and stop announcing anything about it.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tforgetDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\n\t\t\t\tclearDraft(key)\n\t\t\t\tthis.draftOffer = null\n\t\t\t\tthis.draftState = 'idle'\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":["t"],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA;AACY,MAAC,gBAAgB,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAC;AACzD;AACA;AACY,MAAC,iBAAiB,GAAG,IAAG;AACpC;AACA;AACY,MAAC,gBAAgB,GAAG,gBAAe;AAC/C;AACA;AACY,MAAC,oBAAoB,GAAG,YAAW;AAC/C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,QAAQ,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,EAAE,QAAQ,GAAG,EAAE,EAAE,MAAM,GAAG,EAAE,EAAE,GAAG,EAAE,EAAE;AACvF,CAAC,OAAO;AACR,EAAE,gBAAgB;AAClB,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;AACxB,EAAE,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC;AAC5B,EAAE,MAAM,CAAC,QAAQ,IAAI,KAAK,CAAC;AAC3B,EAAE,MAAM,CAAC,MAAM,IAAI,oBAAoB,CAAC;AACxC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;AACZ,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,kBAAkB,CAAC,QAAQ,GAAG,EAAE,EAAE;AAClD,CAAC,IAAI,QAAQ,EAAE;AACf,EAAE,OAAO,MAAM,CAAC,QAAQ,CAAC;AACzB,EAAE;AACF,CAAC,IAAI;AACL,EAAE,MAAM,GAAG,GAAG,cAAc,EAAE,EAAE,IAAG;AACnC,EAAE,OAAO,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE;AAC/B,EAAE,CAAC,MAAM;AACT;AACA,EAAE,OAAO,EAAE;AACX,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,oBAAoB,GAAG;AACvC,CAAC,IAAI,OAAO,GAAG,EAAC;AAChB,CAAC,IAAI;AACL,EAAE,MAAM,OAAO,GAAG,MAAM,CAAC,aAAY;AACrC,EAAE,MAAM,MAAM,GAAG,GAAG,GAAG,qBAAoB;AAC3C,EAAE,MAAM,MAAM,GAAG,GAAE;AACnB,EAAE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAC3C,GAAG,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAC;AAC7B,GAAG,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,gBAAgB,GAAG,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;AAC9E,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,EAAC;AACpB,IAAI;AACJ,GAAG;AACH,EAAE,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE;AAC5B,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,EAAC;AAC1B,GAAG,OAAO,GAAE;AACZ,GAAG;AACH,EAAE,CAAC,MAAM;AACT;AACA,EAAE;AACF,CAAC,OAAO,OAAO;AACf,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,iBAAiB,CAAC,KAAK,EAAE;AAClC,CAAC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;AAC7G,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE;AACrC,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,EAAE;AACnD,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;AACd,EAAE,OAAO,IAAI;AACb,EAAE;AACF,CAAC,IAAI;AACL,EAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;AAChD,EAAE,CAAC,MAAM;AACT,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE;AAC9D,CAAC,MAAM,GAAG,GAAG,MAAM,IAAI,GAAE;AACzB,CAAC,MAAM,IAAI,GAAG,QAAQ,IAAI,GAAE;AAC5B,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9E,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,SAAS,CAAC,GAAG,EAAE,EAAE,aAAa,GAAG,EAAE,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,EAAE;AAC9E,CAAC,IAAI,IAAG;AACR,CAAC,IAAI;AACL,EAAE,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAC;AACxC,EAAE,CAAC,MAAM;AACT;AACA,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,CAAC,GAAG,EAAE;AACX,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,OAAM;AACX,CAAC,IAAI;AACL,EAAE,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAC;AAC1B,EAAE,CAAC,MAAM;AACT;AACA;AACA,EAAE,UAAU,CAAC,GAAG,EAAC;AACjB,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE;AAClF,EAAE,UAAU,CAAC,GAAG,EAAC;AACjB,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,IAAI,gBAAgB,EAAE;AAChD,EAAE,UAAU,CAAC,GAAG,EAAC;AACjB,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,IAAI,aAAa,EAAE;AACpB,EAAE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,EAAC;AACrD,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,MAAM,CAAC,OAAO,EAAE;AAC9D;AACA,GAAG,UAAU,CAAC,GAAG,EAAC;AAClB,GAAG,OAAO,IAAI;AACd,GAAG;AACH,EAAE;AACF;AACA,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE;AAC1D,EAAE,UAAU,CAAC,GAAG,EAAC;AACjB,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;AAC1D,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE;AACvF,CAAC,IAAI,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI,EAAE;AAC7C,EAAE,OAAO,KAAK,KAAK,OAAO,GAAGA,SAAC,CAAC,eAAe,EAAE,2BAA2B,CAAC,GAAG,EAAE;AACjF,EAAE;AACF,CAAC,IAAI,KAAK,KAAK,QAAQ,EAAE;AACzB,EAAE,OAAOA,SAAC,CAAC,eAAe,EAAE,QAAQ,CAAC;AACrC,EAAE;AACF,CAAC,IAAI,KAAK,KAAK,OAAO,EAAE;AACxB,EAAE,OAAO,EAAE;AACX,EAAE;AACF,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,KAAK,EAAC;AAC/D,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,GAAG,CAAC,EAAE;AAC9B,EAAE,OAAOA,SAAC,CAAC,eAAe,EAAE,gBAAgB,CAAC;AAC7C,EAAE;AACF,CAAC,IAAI;AACL,EAAE,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAC;AACrI,EAAE,OAAOA,SAAC,CAAC,eAAe,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC/D,EAAE,CAAC,MAAM;AACT,EAAE,OAAOA,SAAC,CAAC,eAAe,EAAE,OAAO,CAAC;AACpC,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE;AAC1D,CAAC,IAAI;AACL,EAAE,MAAM,CAAC,YAAY,CAAC,OAAO;AAC7B,GAAG,GAAG;AACN,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;AAC3D,IAAG;AACH,EAAE,OAAO,IAAI;AACb,EAAE,CAAC,MAAM;AACT;AACA;AACA;AACA,EAAE,OAAO,KAAK;AACd,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,UAAU,CAAC,GAAG,EAAE;AAChC,CAAC,IAAI;AACL,EAAE,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,EAAC;AACrC,EAAE,CAAC,MAAM;AACT;AACA,EAAE;AACF,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,cAAc,GAAG;AACjC,CAAC,OAAO;AACR,EAAE,IAAI,GAAG;AACT,GAAG,OAAO;AACV;AACA,IAAI,UAAU,EAAE,IAAI;AACpB;AACA,IAAI,UAAU,EAAE,MAAM;AACtB;AACA,IAAI,YAAY,EAAE,CAAC;AACnB;AACA,IAAI,YAAY,EAAE,IAAI;AACtB,IAAI;AACJ,GAAG;AACH;AACA,EAAE,aAAa,GAAG;AAClB,GAAG,IAAI,IAAI,CAAC,YAAY,EAAE;AAC1B,IAAI,YAAY,CAAC,IAAI,CAAC,YAAY,EAAC;AACnC,IAAI,IAAI,CAAC,YAAY,GAAG,KAAI;AAC5B,IAAI;AACJ,GAAG;AACH;AACA,EAAE,OAAO,EAAE;AACX;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE;AACnC,IAAI,IAAI,CAAC,GAAG,EAAE;AACd,KAAK,MAAM;AACX,KAAK;AACL;AACA,IAAI,IAAI,CAAC,UAAU,GAAG,SAAQ;AAC9B,IAAI,IAAI,IAAI,CAAC,YAAY,EAAE;AAC3B,KAAK,YAAY,CAAC,IAAI,CAAC,YAAY,EAAC;AACpC,KAAK;AACL;AACA,IAAI,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,MAAM;AACzC,KAAK,IAAI,CAAC,YAAY,GAAG,KAAI;AAC7B;AACA;AACA;AACA;AACA,KAAK,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAC;AAC3C,KAAK,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,OAAM;AAChD,KAAK,IAAI,CAAC,YAAY,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAC;AAChD,KAAK,EAAE,iBAAiB,EAAC;AACzB,IAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG,oBAAoB,CAAC,GAAG,EAAE;AAC7B,IAAI,IAAI,IAAI,CAAC,YAAY,EAAE;AAC3B,KAAK,YAAY,CAAC,IAAI,CAAC,YAAY,EAAC;AACpC,KAAK,IAAI,CAAC,YAAY,GAAG,KAAI;AAC7B,KAAK;AACL,IAAI,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,EAAE;AACpC,KAAK,UAAU,CAAC,GAAG,EAAC;AACpB,KAAK,IAAI,CAAC,UAAU,GAAG,OAAM;AAC7B,KAAK,IAAI,CAAC,YAAY,GAAG,EAAC;AAC1B,KAAK;AACL,IAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG,WAAW,CAAC,GAAG,EAAE;AACpB,IAAI,IAAI,IAAI,CAAC,YAAY,EAAE;AAC3B,KAAK,YAAY,CAAC,IAAI,CAAC,YAAY,EAAC;AACpC,KAAK,IAAI,CAAC,YAAY,GAAG,KAAI;AAC7B,KAAK;AACL;AACA,IAAI,UAAU,CAAC,GAAG,EAAC;AACnB,IAAI,IAAI,CAAC,UAAU,GAAG,KAAI;AAC1B,IAAI,IAAI,CAAC,UAAU,GAAG,OAAM;AAC5B,IAAI;AACJ,GAAG;AACH,EAAE;AACF;;;;"}
package/dist/esm/index.js CHANGED
@@ -129,6 +129,7 @@ export { default as CnEmailTab } from './integrations/builtin/email/CnEmailTab.v
129
129
  export { default as CnEnvironmentBanner } from './components/CnEnvironmentBanner/CnEnvironmentBanner.vue.js';
130
130
  export { default as CnExportWizard } from './components/CnExportWizard/CnExportWizard.vue.js';
131
131
  export { default as CnFacetSidebar } from './components/CnFacetSidebar/CnFacetSidebar.vue.js';
132
+ export { default as CnFactsWidget } from './components/CnFactsWidget/CnFactsWidget.vue.js';
132
133
  export { default as CnFavouriteToggle } from './components/CnFavouriteToggle/CnFavouriteToggle.vue.js';
133
134
  export { default as CnFeaturesAndRoadmapLink } from './components/CnFeaturesAndRoadmapLink/CnFeaturesAndRoadmapLink.vue.js';
134
135
  export { default as CnFeaturesAndRoadmapPage } from './components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue.js';
@@ -137,6 +138,7 @@ export { default as CnFeaturesAndRoadmapView } from './components/CnFeaturesAndR
137
138
  export { default as CnFeaturesTab } from './components/CnFeaturesTab/CnFeaturesTab.vue.js';
138
139
  export { default as CnFederationStatus } from './components/CnFederationStatus/CnFederationStatus.vue.js';
139
140
  export { default as CnFieldHelper } from './components/CnFieldHelper/CnFieldHelper.vue.js';
141
+ export { default as CnFieldListWidget } from './components/CnFieldListWidget/CnFieldListWidget.vue.js';
140
142
  export { default as CnFileField } from './components/CnFileField/CnFileField.vue.js';
141
143
  export { default as CnFileManager } from './components/CnFileManager/CnFileManager.vue.js';
142
144
  export { default as CnFilePreview } from './components/CnFilePreview/CnFilePreview.vue.js';
@@ -253,6 +255,7 @@ export { default as CnPropertyValueCell } from './components/CnAdvancedFormDialo
253
255
  export { default as CnQuickFilterBar } from './components/CnQuickFilterBar/CnQuickFilterBar.vue.js';
254
256
  export { default as CnQuicklinksWidget } from './components/CnQuicklinksWidget/CnQuicklinksWidget.vue.js';
255
257
  export { default as CnQuicklinksWidgetForm } from './components/CnQuicklinksWidgetForm/CnQuicklinksWidgetForm.vue.js';
258
+ export { default as CnRecordListWidget } from './components/CnRecordListWidget/CnRecordListWidget.vue.js';
256
259
  export { default as CnReferencePreview } from './components/CnReferencePreview/CnReferencePreview.vue.js';
257
260
  export { default as CnRegisterMapping } from './components/CnRegisterMapping/CnRegisterMapping.vue.js';
258
261
  export { default as CnRegisterSchemaSelect } from './components/CnRegisterSchemaSelect/CnRegisterSchemaSelect.vue.js';
@@ -275,6 +278,7 @@ export { default as CnSavedViewWidgetForm } from './components/CnSavedViewWidget
275
278
  export { default as CnSaveViewDialog } from './components/CnSaveViewDialog/CnSaveViewDialog.vue.js';
276
279
  export { default as CnSchemaFormDialog } from './components/CnSchemaFormDialog/CnSchemaFormDialog.vue.js';
277
280
  export { default as CnSearchPage } from './components/CnSearchPage/CnSearchPage.vue.js';
281
+ export { default as CnSectionsWidget } from './components/CnSectionsWidget/CnSectionsWidget.vue.js';
278
282
  export { default as CnSettingsCard } from './components/CnSettingsCard/CnSettingsCard.vue.js';
279
283
  export { default as CnSettingsPage } from './components/CnSettingsPage/CnSettingsPage.vue.js';
280
284
  export { default as CnSettingsSection } from './components/CnSettingsSection/CnSettingsSection.vue.js';
@@ -342,6 +346,7 @@ export { defaultPageTypes } from './components/CnPageRenderer/pageTypes.js';
342
346
  export { fetchWebdavFolderTree } from './components/CnFolderSidebar/webdavFolders.js';
343
347
  export { mdiCatalogue, vmdiCatalogue } from './components/CnIconBrowser/iconCatalogue.js';
344
348
  export { registerIcons } from './components/CnIcon/CnIcon.vue2.js';
349
+ export { createOpenRegisterSource } from './components/CnFilesBrowser/openRegisterSource.js';
345
350
  export { default as CnBrandStripe } from './components/CnBrandStripe/CnBrandStripe.vue.js';
346
351
  export { default as CnSegmentedControl } from './components/CnSegmentedControl/CnSegmentedControl.vue.js';
347
352
  export { default as CnStackedBarWidget } from './components/CnStackedBarWidget/CnStackedBarWidget.vue.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -3,7 +3,8 @@ import { NcButton, NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue';
3
3
  import ClipboardCheckOutline from 'vue-material-design-icons/ClipboardCheckOutline.vue';
4
4
  import __sfc_main from '../../../components/CnDetailCard/CnDetailCard.vue.js';
5
5
  import __sfc_main$1 from '../../../components/CnOfflineQueue/CnOfflineQueue.vue.js';
6
- import { syncIndicator, checklistProgress, validateChecklistAnswers, classifyGps } from '../../offline/fieldCollectionHelpers.js';
6
+ import { buildChecklistSubmission } from '../../offline/checklistSubmission.js';
7
+ import { syncIndicator, checklistProgress, normaliseChecklistTemplate, validateChecklistAnswers, classifyGps } from '../../offline/fieldCollectionHelpers.js';
7
8
  import { resolveDeviceId, getPlannedItems, getPlanningMeta, countPending, countStuck, storePlanning, getCachedObject, enqueueMutation } from '../../offline/offlineDb.js';
8
9
  import { fetchPlanning, fetchReferences } from '../../offline/planningFetch.js';
9
10
  import { drainQueue } from '../../offline/syncReplayService.js';
@@ -73,6 +74,8 @@ var script = {
73
74
  activeTemplate: null,
74
75
  answers: {},
75
76
  errors: [],
77
+ /** Why the last save could not be queued, in words. */
78
+ saveError: '',
76
79
  }
77
80
  },
78
81
 
@@ -286,8 +289,13 @@ var script = {
286
289
  this.errors = [];
287
290
  const cfg = this.config;
288
291
  const templateRef = item?.[cfg.templateRefField];
292
+ // The app's template shape (sections, its own item keys and type
293
+ // words) is read into the leaf's flat item list here, once.
289
294
  this.activeTemplate = templateRef
290
- ? await getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef))
295
+ ? normaliseChecklistTemplate(
296
+ await getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef)),
297
+ cfg,
298
+ )
291
299
  : null;
292
300
  const initial = {};
293
301
  for (const q of (this.activeTemplate?.items || [])) {
@@ -301,6 +309,7 @@ var script = {
301
309
  this.activeTemplate = null;
302
310
  this.answers = {};
303
311
  this.errors = [];
312
+ this.saveError = '';
304
313
  },
305
314
 
306
315
  /**
@@ -322,7 +331,9 @@ var script = {
322
331
  },
323
332
 
324
333
  /**
325
- * Validate answers, then queue a `create` mutation for the result.
334
+ * Validate answers, then queue the run: an object create on
335
+ * `resultSchema`, or, when the config names a `resultEndpoint`, one
336
+ * `submit` to that endpoint.
326
337
  *
327
338
  * @return {Promise<void>}
328
339
  */
@@ -336,26 +347,22 @@ var script = {
336
347
  try {
337
348
  const cfg = this.config;
338
349
  const gps = await this.captureGps();
339
- const gpsClass = classifyGps(gps);
340
- const now = new Date().toISOString();
341
- const items = this.activeTemplate.items.map((q) => ({
342
- questionId: q.questionId,
343
- answer: this.answers[q.questionId].answer,
344
- evidenceRefs: this.answers[q.questionId].evidenceRefs,
345
- answeredAt: now,
346
- gpsAtAnswer: gps ? { ...gps, source: gpsClass.source } : { source: 'sensorless' },
347
- }));
348
- const payload = {
349
- inspectionRef: this.itemId(this.activeItem),
350
- checklistTemplateRef: this.activeTemplate.id ?? this.activeTemplate['@self']?.id,
351
- items,
352
- };
350
+ const submission = buildChecklistSubmission(cfg, {
351
+ plannedItem: this.activeItem,
352
+ template: this.activeTemplate,
353
+ answers: this.answers,
354
+ gps,
355
+ gpsSource: classifyGps(gps).source,
356
+ capturedAt: new Date().toISOString(),
357
+ capturedOffline: this.offline,
358
+ });
353
359
  await enqueueMutation({
354
360
  deviceId: this.deviceId,
355
- operationType: 'create',
361
+ operationType: submission.operationType,
356
362
  register: this.effectiveRegister,
357
- schema: cfg.resultSchema,
358
- payload,
363
+ schema: submission.schema,
364
+ endpoint: submission.endpoint,
365
+ payload: submission.payload,
359
366
  });
360
367
  this.pendingCount = await countPending(this.deviceId);
361
368
  this.stuckCount = await countStuck(this.deviceId);
@@ -364,6 +371,7 @@ var script = {
364
371
  await this.drain();
365
372
  }
366
373
  } catch (e) {
374
+ this.saveError = e?.message || translate('nextcloud-vue', 'The answers could not be saved on this device.');
367
375
  // eslint-disable-next-line no-console
368
376
  console.error('[CnFieldInspectionCard] saveChecklist failed', e);
369
377
  } finally {
@@ -1 +1 @@
1
- {"version":3,"file":"CnFieldInspectionCard.vue2.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue"],"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<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 { checklistProgress, classifyGps, 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}\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\tthis.activeTemplate = templateRef\n\t\t\t\t? await getCachedObject(this.effectiveRegister, cfg.referenceSchema, 'references', String(templateRef))\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},\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 a `create` mutation for the result.\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 gpsClass = classifyGps(gps)\n\t\t\t\tconst now = new Date().toISOString()\n\t\t\t\tconst items = this.activeTemplate.items.map((q) => ({\n\t\t\t\t\tquestionId: q.questionId,\n\t\t\t\t\tanswer: this.answers[q.questionId].answer,\n\t\t\t\t\tevidenceRefs: this.answers[q.questionId].evidenceRefs,\n\t\t\t\t\tansweredAt: now,\n\t\t\t\t\tgpsAtAnswer: gps ? { ...gps, source: gpsClass.source } : { source: 'sensorless' },\n\t\t\t\t}))\n\t\t\t\tconst payload = {\n\t\t\t\t\tinspectionRef: this.itemId(this.activeItem),\n\t\t\t\t\tchecklistTemplateRef: this.activeTemplate.id ?? this.activeTemplate['@self']?.id,\n\t\t\t\t\titems,\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: 'create',\n\t\t\t\t\tregister: this.effectiveRegister,\n\t\t\t\t\tschema: cfg.resultSchema,\n\t\t\t\t\tpayload,\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\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":["CnDetailCard","CnOfflineQueue","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnFieldInspectionCard.vue2.js","sources":["../../../../../src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue"],"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":["CnDetailCard","CnOfflineQueue","t"],"mappings":"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"}
@@ -41,6 +41,12 @@ const _hoisted_15 = { value: "no" };
41
41
  const _hoisted_16 = { value: "na" };
42
42
  const _hoisted_17 = ["onUpdate:modelValue", "data-testid"];
43
43
  const _hoisted_18 = ["data-testid"];
44
+ const _hoisted_19 = {
45
+ key: 0,
46
+ class: "cn-field-inspection__error",
47
+ role: "alert",
48
+ "data-testid": "cn-fi-save-error"
49
+ };
44
50
 
45
51
  function render(_ctx, _cache, $props, $setup, $data, $options) {
46
52
  const _component_NcButton = resolveComponent("NcButton");
@@ -214,6 +220,9 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
214
220
  : createCommentVNode("v-if", true)
215
221
  ]))
216
222
  }), 128 /* KEYED_FRAGMENT */)),
223
+ ($data.saveError)
224
+ ? (openBlock(), createElementBlock("p", _hoisted_19, toDisplayString($data.saveError), 1 /* TEXT */))
225
+ : createCommentVNode("v-if", true),
217
226
  createVNode(_component_NcButton, {
218
227
  type: "submit",
219
228
  variant: "primary",