@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
@@ -0,0 +1,248 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * A data source for CnFilesBrowser that reads and writes one OpenRegister
6
+ * object's folder through OpenRegister's own files API, not WebDAV.
7
+ *
8
+ * Why: an object's files live in the `openregister` account's home, outside
9
+ * every person's Files app, so a reader has no WebDAV path to them. Every call
10
+ * here goes through the object's access rule on the server, as the signed-in
11
+ * person: a listing needs read on the object, a change needs update. Nothing
12
+ * is shared to make the folder visible.
13
+ *
14
+ * Paths are relative to the object folder and written with a leading slash
15
+ * (`/`, `/Bijlagen`), the same shape the WebDAV browser uses for its own
16
+ * paths, so crumbs and navigation work unchanged.
17
+ *
18
+ * @spec openspec/changes/files-browser-openregister-source/specs/files-browser/spec.md
19
+ */
20
+ import axios from '@nextcloud/axios'
21
+ import { FileType } from '@nextcloud/files'
22
+ import { generateUrl } from '@nextcloud/router'
23
+
24
+ /**
25
+ * A browser path (`/a/b`) as the API's relative path (`a/b`).
26
+ *
27
+ * @param {string} path The browser path.
28
+ * @return {string} The relative path, empty for the root.
29
+ */
30
+ export function toApiPath(path) {
31
+ return String(path || '').replace(/^\/+|\/+$/g, '')
32
+ }
33
+
34
+ /**
35
+ * One API listing entry as a row the browser can render and sort.
36
+ *
37
+ * The shape follows what the browser reads off an `@nextcloud/files` node
38
+ * (basename, type, mime, size, mtime, fileid, path, source, attributes), so
39
+ * the table, the sorter and the icons need no second code path.
40
+ *
41
+ * @param {object} entry The API entry: `{ id, name, type, mimetype, size, mtime, path }`.
42
+ * @return {object} The row.
43
+ */
44
+ export function entryToNode(entry) {
45
+ const isFolder = entry.type === 'folder'
46
+ return {
47
+ source: `openregister:${entry.id}`,
48
+ fileid: entry.id,
49
+ basename: entry.name,
50
+ displayname: entry.name,
51
+ path: `/${toApiPath(entry.path)}`,
52
+ type: isFolder ? FileType.Folder : FileType.File,
53
+ mime: isFolder ? 'httpd/unix-directory' : (entry.mimetype || ''),
54
+ size: typeof entry.size === 'number' ? entry.size : undefined,
55
+ mtime: typeof entry.mtime === 'number' ? new Date(entry.mtime * 1000) : undefined,
56
+ attributes: {},
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The status of a failed request, or null.
62
+ *
63
+ * @param {Error} err The error.
64
+ * @return {number|null} The HTTP status.
65
+ */
66
+ function statusOf(err) {
67
+ return err?.response?.status ?? err?.status ?? null
68
+ }
69
+
70
+ /**
71
+ * The server's own message on a failed request, or null.
72
+ *
73
+ * @param {Error} err The error.
74
+ * @return {string|null} The message.
75
+ */
76
+ function messageOf(err) {
77
+ const data = err?.response?.data
78
+ return (data && typeof data.error === 'string' && data.error !== '') ? data.error : null
79
+ }
80
+
81
+ /**
82
+ * Build the OpenRegister source for one object.
83
+ *
84
+ * @param {object} options The object.
85
+ * @param {string} [options.apiBase] OpenRegister's API base, `/apps/openregister/api`.
86
+ * @param {string} options.register The register slug or id.
87
+ * @param {string} options.schema The schema slug or id.
88
+ * @param {string} options.objectId The object's uuid.
89
+ * @return {object} The source CnFilesBrowser takes as `source`.
90
+ */
91
+ export function createOpenRegisterSource({ apiBase = '/apps/openregister/api', register, schema, objectId }) {
92
+ const objectUrl = (suffix) => generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}${suffix}`, { register, schema, objectId })
93
+
94
+ /**
95
+ * Re-throw a failed request with its status and the server's message.
96
+ *
97
+ * @param {Error} err The error.
98
+ * @return {never}
99
+ */
100
+ const fail = (err) => {
101
+ const error = new Error(messageOf(err) || err?.message || 'Request failed')
102
+ error.status = statusOf(err)
103
+ throw error
104
+ }
105
+
106
+ return {
107
+ kind: 'openregister',
108
+
109
+ /**
110
+ * List a folder.
111
+ *
112
+ * @param {string} path The browser path.
113
+ * @return {Promise<{folder: object, nodes: Array<object>}>} The folder (with `canChange`) and its rows.
114
+ */
115
+ async list(path) {
116
+ try {
117
+ const { data } = await axios.get(objectUrl('/folder'), { params: { path: toApiPath(path) } })
118
+ if (!Array.isArray(data?.entries)) {
119
+ // Not a folder listing: an OpenRegister without these endpoints.
120
+ const error = new Error('This OpenRegister has no folder endpoint')
121
+ error.status = 501
122
+ throw error
123
+ }
124
+ const relative = toApiPath(data?.path ?? path)
125
+ return {
126
+ folder: {
127
+ fileid: data?.folderId ?? null,
128
+ path: `/${relative}`,
129
+ basename: relative.split('/').pop() || '',
130
+ type: FileType.Folder,
131
+ canChange: data?.canChange === true,
132
+ },
133
+ nodes: data.entries.map(entryToNode),
134
+ }
135
+ } catch (err) {
136
+ if (err?.status && !err.response) {
137
+ throw err
138
+ }
139
+ return fail(err)
140
+ }
141
+ },
142
+
143
+ /**
144
+ * Create a folder.
145
+ *
146
+ * @param {string} path Where, as a browser path.
147
+ * @param {string} name The new folder's name.
148
+ * @return {Promise<object>} The new folder's row.
149
+ */
150
+ async createFolder(path, name) {
151
+ try {
152
+ const { data } = await axios.post(objectUrl('/folder'), { path: toApiPath(path), name })
153
+ return entryToNode(data)
154
+ } catch (err) {
155
+ return fail(err)
156
+ }
157
+ },
158
+
159
+ /**
160
+ * Upload one file into a folder.
161
+ *
162
+ * @param {string} path The target folder, as a browser path.
163
+ * @param {File} file The file.
164
+ * @param {(percent: number) => void} [onProgress] Called with 0..100.
165
+ * @return {Promise<object>} The stored file's row.
166
+ */
167
+ async upload(path, file, onProgress) {
168
+ const form = new FormData()
169
+ form.append('path', toApiPath(path))
170
+ form.append('files[]', file, file.name)
171
+ try {
172
+ const { data } = await axios.post(objectUrl('/folder/upload'), form, {
173
+ onUploadProgress: (progress) => {
174
+ if (progress?.total && typeof onProgress === 'function') {
175
+ onProgress(Math.round((progress.loaded / progress.total) * 100))
176
+ }
177
+ },
178
+ })
179
+ const stored = Array.isArray(data?.stored) ? data.stored : []
180
+ if (stored.length === 0) {
181
+ const error = new Error(data?.rejected?.[0]?.error || 'The upload failed')
182
+ error.status = 400
183
+ throw error
184
+ }
185
+ return entryToNode(stored[0])
186
+ } catch (err) {
187
+ if (err?.status && !err.response) {
188
+ throw err
189
+ }
190
+ return fail(err)
191
+ }
192
+ },
193
+
194
+ /**
195
+ * Rename a file or folder.
196
+ *
197
+ * @param {object} node The row.
198
+ * @param {string} name The new name.
199
+ * @return {Promise<object>} The renamed row.
200
+ */
201
+ async rename(node, name) {
202
+ try {
203
+ const { data } = await axios.put(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`), { name })
204
+ return entryToNode(data)
205
+ } catch (err) {
206
+ return fail(err)
207
+ }
208
+ },
209
+
210
+ /**
211
+ * Delete a file or folder.
212
+ *
213
+ * @param {object} node The row.
214
+ * @return {Promise<void>}
215
+ */
216
+ async remove(node) {
217
+ try {
218
+ await axios.delete(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`))
219
+ } catch (err) {
220
+ fail(err)
221
+ }
222
+ },
223
+
224
+ /**
225
+ * Where a file downloads from: the object's own file endpoint, which
226
+ * checks read on the object.
227
+ *
228
+ * @param {object} node The row.
229
+ * @return {string|null} The url, or null for a folder.
230
+ */
231
+ downloadUrl(node) {
232
+ if (!node?.fileid || node.type === FileType.Folder) {
233
+ return null
234
+ }
235
+ return objectUrl(`/files/${encodeURIComponent(node.fileid)}`)
236
+ },
237
+
238
+ /**
239
+ * A small preview of an image, through the object's preview endpoint.
240
+ *
241
+ * @param {object} node The row.
242
+ * @return {string} The url.
243
+ */
244
+ previewUrl(node) {
245
+ return objectUrl(`/files/${encodeURIComponent(node.fileid)}/preview?width=64&height=64`)
246
+ },
247
+ }
248
+ }
@@ -113,7 +113,7 @@ export default {
113
113
  | `allowDraft` | `false` | Offer a **Save draft** button that stores the record with a draft marker instead of validating it. Inert unless the schema declares `draftField`. |
114
114
  | `draftField` | `'isDraft'` | The boolean property that marks a record as a draft. |
115
115
  | `draftAppId` | `''` | The app the draft belongs to, for the storage key. |
116
- | `draftUserId` | `''` | Who is typing, for the storage key. **Pass this on any shared device.** |
116
+ | `draftUserId` | `''` | Who is typing, for the storage key. Empty: the signed-in Nextcloud user, and `anonymous` only when nobody is signed in. |
117
117
 
118
118
  #### What the draft does and does not do
119
119
 
@@ -122,9 +122,15 @@ Restore and Discard; the values go in only when somebody presses Restore. A form
122
122
  that filled itself would be indistinguishable from one the server prefilled, and
123
123
  somebody would submit last week's answers without knowing they were there.
124
124
 
125
- `draftUserId` is part of the storage key. A shared browser profile at a service
125
+ The user is part of the storage key. A shared browser profile at a service
126
126
  desk is the ordinary case in a municipality, and a draft keyed without the user
127
- hands the next person at the counter what the last one typed. Pass it.
127
+ hands the next person at the counter what the last one typed. Without
128
+ `draftUserId` the dialog uses the signed-in Nextcloud user. A draft stored
129
+ under `anonymous` is removed, never offered, once somebody is signed in.
130
+
131
+ A draft is written only after a person changes a field, and only while that
132
+ field differs from the value the form opened with. Opening a dialog, or the
133
+ form filling in values by itself, writes nothing and shows no Saving or Saved.
128
134
 
129
135
  A draft is dropped rather than offered when it is more than seven days old, when
130
136
  the record has been saved by somebody else since it was written, or when the
@@ -186,7 +186,7 @@
186
186
  :error="!!errors[field.key]"
187
187
  :type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
188
188
  :disabled="field.readOnly"
189
- :placeholder="field.description"
189
+ :placeholder="fieldPlaceholder(field)"
190
190
  @update:modelValue="value => updateField(field.key, value)" />
191
191
  <CnFieldHelper
192
192
  :id="helpId(field)"
@@ -208,7 +208,7 @@
208
208
  :error="!!errors[field.key]"
209
209
  type="number"
210
210
  :disabled="field.readOnly"
211
- :placeholder="field.description"
211
+ :placeholder="fieldPlaceholder(field)"
212
212
  @update:modelValue="value => updateField(field.key, value !== '' ? Number(value) : null)" />
213
213
  <CnFieldHelper
214
214
  :id="helpId(field)"
@@ -230,7 +230,7 @@
230
230
  v-bind="fieldAria(field)"
231
231
  :value="formData[field.key] || ''"
232
232
  :disabled="field.readOnly"
233
- :placeholder="field.description"
233
+ :placeholder="fieldPlaceholder(field)"
234
234
  rows="4"
235
235
  @input="updateField(field.key, $event.target.value)" />
236
236
  <CnFieldHelper
@@ -652,7 +652,7 @@
652
652
  :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
653
653
  :error="!!errors[field.key]"
654
654
  :disabled="field.readOnly"
655
- :placeholder="field.description"
655
+ :placeholder="fieldPlaceholder(field)"
656
656
  @update:modelValue="value => updateField(field.key, value)" />
657
657
  <CnFieldHelper
658
658
  :id="helpId(field)"
@@ -787,7 +787,7 @@ import CnPropertySourceField from '../CnPropertySourceField/CnPropertySourceFiel
787
787
  import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
788
788
  import CnSubObjectsField from '../CnSubObjectsField/CnSubObjectsField.vue'
789
789
  import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js'
790
- import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
790
+ import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
791
791
  import { heldFromFailures, splitHeldFiles, uploadHeldFiles } from '../../composables/useHeldFileUpload.js'
792
792
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
793
793
  import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
@@ -1172,16 +1172,16 @@ export default {
1172
1172
  * 🔴 THE USER IS PART OF THE DRAFT KEY. A shared browser profile at a
1173
1173
  * service desk is the ordinary case in a municipality, and a draft
1174
1174
  * keyed without the user hands the next person at the counter what the
1175
- * last one typed. A host that passes nothing gets `anonymous`, which
1176
- * is correct for a single-user context and wrong for a counter — so
1177
- * the prop exists and is documented rather than guessed at.
1175
+ * last one typed. A host that passes nothing gets the signed-in
1176
+ * Nextcloud user (`@nextcloud/auth`), and `anonymous` only when
1177
+ * nobody is signed in.
1178
1178
  */
1179
1179
  draftAppId: {
1180
1180
  type: String,
1181
1181
  default: '',
1182
1182
  },
1183
1183
 
1184
- /** Who is typing, for the draft key. See `draftAppId`. */
1184
+ /** Who is typing, for the draft key. Empty: the signed-in user. See `draftAppId`. */
1185
1185
  draftUserId: {
1186
1186
  type: String,
1187
1187
  default: '',
@@ -1442,10 +1442,20 @@ export default {
1442
1442
  appId: this.draftAppId,
1443
1443
  schema: (this.schema?.slug || this.schema?.title || ''),
1444
1444
  objectId: (this.item?.id || this.item?.uuid || ''),
1445
- userId: this.draftUserId,
1445
+ userId: this.draftUserOwner,
1446
1446
  })
1447
1447
  },
1448
1448
 
1449
+ /**
1450
+ * Who the draft belongs to: `draftUserId`, else the signed-in user.
1451
+ *
1452
+ * @return {string} The user id, or '' when nobody is signed in.
1453
+ * @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
1454
+ */
1455
+ draftUserOwner() {
1456
+ return resolveDraftUserId(this.draftUserId)
1457
+ },
1458
+
1449
1459
  /**
1450
1460
  * Whether "Save draft" may be offered.
1451
1461
  *
@@ -1757,17 +1767,6 @@ export default {
1757
1767
  formData: {
1758
1768
  deep: true,
1759
1769
  handler(values) {
1760
- // 🔴 THE FIRST CHANGE IS THE COMPONENT SEEDING THE FORM, NOT
1761
- // SOMEBODY TYPING. Writing a draft for it stores the empty
1762
- // form over a real one, and the indicator announces "Saving
1763
- // draft" about work nobody did — an instrument reporting
1764
- // activity that did not happen, which is the whole class of
1765
- // defect this library keeps paying down.
1766
- if (this.draftSeeded !== true) {
1767
- this.draftSeeded = true
1768
- return
1769
- }
1770
-
1771
1770
  // Nothing is written while a draft is being offered: the user
1772
1771
  // has not chosen yet, and overwriting the stored draft with the
1773
1772
  // empty form they are looking at would destroy the thing they
@@ -1776,6 +1775,19 @@ export default {
1776
1775
  return
1777
1776
  }
1778
1777
 
1778
+ // 🔴 ONLY A PERSON'S CHANGE IS WORTH A DRAFT. The form fills
1779
+ // in values by itself after it opens (an input reporting its
1780
+ // value back, normalised dates, defaults, the organisation),
1781
+ // and each of those used to write a draft and say "Saved just
1782
+ // now" on a dialog nobody had touched. A field counts once a
1783
+ // person edited it AND it differs from what the form opened
1784
+ // with, so undoing the edit drops the draft again.
1785
+ const edited = Object.keys(this.touchedFields || {}).filter((key) => this.touchedFields[key])
1786
+ if (!hasDraftChanges(values, this.draftBaseline, edited)) {
1787
+ this.settleUnchangedDraft(this.formDraftKey)
1788
+ return
1789
+ }
1790
+
1779
1791
  this.scheduleDraftWrite(this.formDraftKey, values)
1780
1792
  },
1781
1793
  },
@@ -1940,6 +1952,12 @@ export default {
1940
1952
  // Silently refilling a form is how somebody submits last week's answers
1941
1953
  // without noticing they were there, and a form that fills itself is
1942
1954
  // indistinguishable from one the server prefilled.
1955
+ // A draft stored under `anonymous` was written by a page that did not
1956
+ // know who was typing. Once somebody is signed in it is removed, never
1957
+ // offered: it may be another person's.
1958
+ if (this.formDraftKey && this.draftUserOwner) {
1959
+ clearAnonymousDrafts()
1960
+ }
1943
1961
  if (this.formDraftKey) {
1944
1962
  this.draftOffer = readDraft(this.formDraftKey, {
1945
1963
  objectUpdated: (this.item?.updated || this.item?.['@self']?.updated || ''),
@@ -2298,6 +2316,10 @@ export default {
2298
2316
  this.runAssignments(null)
2299
2317
  this.loadProfileDefaults()
2300
2318
  }
2319
+ // What the form opened with. A draft is written only once a field
2320
+ // a person edited differs from this (see the `formData` watcher).
2321
+ // Not reactive: nothing renders from it.
2322
+ this.draftBaseline = JSON.parse(JSON.stringify(this.formData || {}))
2301
2323
  },
2302
2324
 
2303
2325
  /**
@@ -3085,6 +3107,27 @@ export default {
3085
3107
  return this.showsOuterLabel(field) ? '' : this.errors[field.key]
3086
3108
  },
3087
3109
 
3110
+ /**
3111
+ * The placeholder of a text, number or textarea control.
3112
+ *
3113
+ * The field's description already shows as the help line under the
3114
+ * control, so the placeholder stays empty rather than repeat it word
3115
+ * for word. The help line is the one kept: it is still there after
3116
+ * somebody typed (a placeholder disappears with the first keystroke),
3117
+ * it is read out through `aria-describedby`, and in the Nextcloud look
3118
+ * a placeholder is hidden until the field has focus anyway. While an
3119
+ * error takes the help line's place, the description returns as the
3120
+ * placeholder so it is not lost.
3121
+ *
3122
+ * @param {object} field The field definition.
3123
+ * @return {string} The placeholder, or '' when the help line says it.
3124
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-field-description-shows-once
3125
+ */
3126
+ fieldPlaceholder(field) {
3127
+ const description = field.description || ''
3128
+ return this.helperError(field) ? description : ''
3129
+ },
3130
+
3088
3131
  /**
3089
3132
  * The accessibility attributes of a text control. In both looks an invalid
3090
3133
  * control is `aria-invalid` and `aria-describedby` lists the error before
@@ -3891,7 +3934,7 @@ export default {
3891
3934
  const mappedProps = propertiesFromDefinitions(
3892
3935
  Array.isArray(records) ? records : [],
3893
3936
  config,
3894
- { orderFrom: 1000 + Object.keys(properties).length },
3937
+ { orderFrom: 1000 + Object.keys(properties).length, create: this.isCreateMode },
3895
3938
  )
3896
3939
  Object.assign(properties, mappedProps.properties)
3897
3940
  required.push(...mappedProps.required)
@@ -4567,6 +4610,11 @@ export default {
4567
4610
  }
4568
4611
 
4569
4612
  this.formData = { ...this.formData, ...this.draftOffer.values }
4613
+ // What the person typed last time is their edit now: it counts for
4614
+ // the next draft write and survives the record arriving late.
4615
+ for (const key of Object.keys(this.draftOffer.values)) {
4616
+ this.touchedFields[key] = true
4617
+ }
4570
4618
  this.draftOffer = null
4571
4619
  },
4572
4620
 
@@ -397,7 +397,7 @@ import CnFormErrorSummary from '../CnFormErrorSummary/CnFormErrorSummary.vue'
397
397
  import CnFormField from '../CnFormField/CnFormField.vue'
398
398
  import CnStepper from '../CnStepper/CnStepper.vue'
399
399
  import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
400
- import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
400
+ import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
401
401
  import { normalizeLook } from '../../composables/useLook.js'
402
402
  import { cnSelectAria } from '../../directives/cnSelectAria.js'
403
403
  import { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'
@@ -529,7 +529,7 @@ export default {
529
529
  default: '',
530
530
  },
531
531
 
532
- /** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. */
532
+ /** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. Empty: the signed-in Nextcloud user. */
533
533
  draftUserId: {
534
534
  type: String,
535
535
  default: '',
@@ -949,7 +949,17 @@ export default {
949
949
  if (this.recoverDraft !== true) {
950
950
  return ''
951
951
  }
952
- return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserId })
952
+ return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserOwner })
953
+ },
954
+
955
+ /**
956
+ * Who the draft belongs to: `draftUserId`, else the signed-in user.
957
+ *
958
+ * @return {string} The user id, or '' when nobody is signed in.
959
+ * @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
960
+ */
961
+ draftUserOwner() {
962
+ return resolveDraftUserId(this.draftUserId)
953
963
  },
954
964
 
955
965
  /**
@@ -997,6 +1007,11 @@ export default {
997
1007
  this.calcTimers = {}
998
1008
  // Offered, never applied: a form that fills itself is indistinguishable
999
1009
  // from one the server prefilled.
1010
+ // A draft under `anonymous` may be somebody else's: removed once a
1011
+ // signed-in user is known, never offered.
1012
+ if (this.formDraftKey && this.draftUserOwner) {
1013
+ clearAnonymousDrafts()
1014
+ }
1000
1015
  if (this.formDraftKey) {
1001
1016
  this.draftOffer = readDraft(this.formDraftKey)
1002
1017
  }