@conduction/nextcloud-vue 2.79.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 (197) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -8
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +133 -96
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  32. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  34. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  40. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  46. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +305 -72
  49. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +31 -13
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  53. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  54. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  55. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  56. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  57. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  58. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  59. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  60. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  67. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  68. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  69. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  70. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  71. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  72. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  73. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  74. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  75. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  76. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  77. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  78. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +122 -7
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  86. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  92. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  93. package/dist/esm/composables/useFormDraft.js +140 -9
  94. package/dist/esm/composables/useFormDraft.js.map +1 -1
  95. package/dist/esm/index.js +4 -0
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  98. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  99. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  100. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  101. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  102. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  103. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  104. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  105. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  106. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  107. package/dist/esm/integrations/offline/index.js +2 -1
  108. package/dist/esm/integrations/offline/index.js.map +1 -1
  109. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  110. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  111. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  112. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  113. package/dist/esm/l10n/en.json.js +12 -1
  114. package/dist/esm/l10n/en.json.js.map +1 -1
  115. package/dist/esm/l10n/nl.json.js +16 -2
  116. package/dist/esm/l10n/nl.json.js.map +1 -1
  117. package/dist/esm/nextcloud-vue.css +1000 -59
  118. package/dist/esm/utils/detailActionModel.js +60 -3
  119. package/dist/esm/utils/detailActionModel.js.map +1 -1
  120. package/dist/esm/utils/fillTemplate.js +114 -0
  121. package/dist/esm/utils/fillTemplate.js.map +1 -0
  122. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  123. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  124. package/dist/esm/utils/queryPresets.js +96 -0
  125. package/dist/esm/utils/queryPresets.js.map +1 -0
  126. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  127. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  128. package/dist/esm/utils/schema.js +21 -0
  129. package/dist/esm/utils/schema.js.map +1 -1
  130. package/dist/esm/utils/sentinelTokens.js +3 -3
  131. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  132. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  133. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  134. package/dist/esm/utils/widgetDispatch.js +7 -1
  135. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  136. package/dist/nextcloud-vue.cjs.js +12368 -7717
  137. package/dist/nextcloud-vue.cjs.js.map +2 -2
  138. package/dist/nextcloud-vue.css +1000 -59
  139. package/l10n/en.json +12 -1
  140. package/l10n/nl.json +16 -2
  141. package/package.json +1 -1
  142. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  143. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  144. package/src/components/CnActionsBar/CnActionsBar.vue +42 -10
  145. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  146. package/src/components/CnCellRenderer/CnCellRenderer.vue +188 -1
  147. package/src/components/CnDataTable/CnDataTable.vue +1 -0
  148. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  149. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  150. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
  151. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  152. package/src/components/CnFactsWidget/index.js +29 -0
  153. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  154. package/src/components/CnFieldListWidget/index.js +28 -0
  155. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  156. package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
  157. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  158. package/src/components/CnIndexPage/CnIndexPage.vue +254 -6
  159. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  160. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  161. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  162. package/src/components/CnRecordListWidget/index.js +30 -0
  163. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  164. package/src/components/CnSectionsWidget/index.js +34 -0
  165. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  166. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  167. package/src/components/CnTabsWidget/CnTabsWidget.vue +184 -8
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  169. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  170. package/src/components/index.js +4 -0
  171. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  172. package/src/composables/useFormDraft.js +139 -8
  173. package/src/css/form-field.css +14 -0
  174. package/src/css/index-page.css +71 -0
  175. package/src/css/kpi-card.css +32 -0
  176. package/src/css/look-board-detail.css +77 -0
  177. package/src/css/look-board-index.css +51 -0
  178. package/src/css/look-board.css +37 -2
  179. package/src/css/table.css +19 -0
  180. package/src/index.js +4 -0
  181. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  182. package/src/integrations/builtin/field-inspection.js +21 -0
  183. package/src/integrations/offline/checklistSubmission.js +137 -0
  184. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  185. package/src/integrations/offline/index.js +3 -0
  186. package/src/integrations/offline/offlineDb.js +6 -1
  187. package/src/integrations/offline/syncReplayService.js +8 -2
  188. package/src/schemas/app-manifest-v2.schema.json +104 -5
  189. package/src/utils/detailActionModel.js +59 -2
  190. package/src/utils/fillTemplate.js +110 -0
  191. package/src/utils/libraryWidgetKeys.js +4 -0
  192. package/src/utils/queryPresets.js +93 -0
  193. package/src/utils/resolveFilterTokens.js +11 -0
  194. package/src/utils/schema.js +21 -0
  195. package/src/utils/sentinelTokens.js +3 -3
  196. package/src/utils/validateManifestV2.compiled.js +3 -3
  197. package/src/utils/widgetDispatch.js +7 -1
@@ -0,0 +1,183 @@
1
+ import { resolveComponent, openBlock, createElementBlock, toDisplayString, Fragment, renderList, createElementVNode, createCommentVNode, createBlock, withCtx, createTextVNode, withModifiers, createVNode } from 'vue';
2
+
3
+ const _hoisted_1 = {
4
+ class: "cn-field-list-widget",
5
+ "data-testid": "cn-field-list-widget"
6
+ };
7
+ const _hoisted_2 = {
8
+ key: 0,
9
+ class: "cn-field-list-widget__empty",
10
+ "data-testid": "cn-field-list-empty"
11
+ };
12
+ const _hoisted_3 = {
13
+ key: 1,
14
+ class: "cn-field-list-widget__rows"
15
+ };
16
+ const _hoisted_4 = ["data-testid"];
17
+ const _hoisted_5 = { class: "cn-field-list-widget__line" };
18
+ const _hoisted_6 = {
19
+ key: 0,
20
+ class: "cn-field-list-widget__avatar",
21
+ "aria-hidden": "true"
22
+ };
23
+ const _hoisted_7 = { class: "cn-field-list-widget__main" };
24
+ const _hoisted_8 = { class: "cn-field-list-widget__title" };
25
+ const _hoisted_9 = {
26
+ key: 0,
27
+ class: "cn-field-list-widget__meta"
28
+ };
29
+ const _hoisted_10 = {
30
+ key: 2,
31
+ class: "cn-field-list-widget__aside"
32
+ };
33
+ const _hoisted_11 = {
34
+ key: 0,
35
+ class: "cn-field-list-widget__detail"
36
+ };
37
+ const _hoisted_12 = {
38
+ key: 2,
39
+ class: "cn-field-list-widget__hint"
40
+ };
41
+ const _hoisted_13 = ["data-testid"];
42
+
43
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
44
+ const _component_CnStatusBadge = resolveComponent("CnStatusBadge");
45
+ const _component_NcButton = resolveComponent("NcButton");
46
+ const _component_NcSelect = resolveComponent("NcSelect");
47
+ const _component_NcTextField = resolveComponent("NcTextField");
48
+ const _component_NcNoteCard = resolveComponent("NcNoteCard");
49
+
50
+ return (openBlock(), createElementBlock("div", _hoisted_1, [
51
+ ($options.rows.length === 0)
52
+ ? (openBlock(), createElementBlock("p", _hoisted_2, toDisplayString($options.emptyLabel), 1 /* TEXT */))
53
+ : (openBlock(), createElementBlock("ul", _hoisted_3, [
54
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.rows, (row) => {
55
+ return (openBlock(), createElementBlock("li", {
56
+ key: row.key,
57
+ class: "cn-field-list-widget__row",
58
+ "data-testid": `cn-field-list-row-${row.key}`
59
+ }, [
60
+ createElementVNode("div", _hoisted_5, [
61
+ (row.initials)
62
+ ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(row.initials), 1 /* TEXT */))
63
+ : createCommentVNode("v-if", true),
64
+ createElementVNode("span", _hoisted_7, [
65
+ createElementVNode("span", _hoisted_8, toDisplayString(row.title), 1 /* TEXT */),
66
+ (row.meta)
67
+ ? (openBlock(), createElementBlock("span", _hoisted_9, toDisplayString(row.meta), 1 /* TEXT */))
68
+ : createCommentVNode("v-if", true)
69
+ ]),
70
+ (row.pill)
71
+ ? (openBlock(), createBlock(_component_CnStatusBadge, {
72
+ key: 1,
73
+ label: row.pill.label,
74
+ variant: row.pill.variant,
75
+ size: "small"
76
+ }, null, 8 /* PROPS */, ["label", "variant"]))
77
+ : createCommentVNode("v-if", true),
78
+ (row.aside)
79
+ ? (openBlock(), createElementBlock("span", _hoisted_10, toDisplayString(row.aside), 1 /* TEXT */))
80
+ : createCommentVNode("v-if", true),
81
+ (openBlock(true), createElementBlock(Fragment, null, renderList(row.actions, (action) => {
82
+ return (openBlock(), createBlock(_component_NcButton, {
83
+ key: action.id,
84
+ variant: "tertiary",
85
+ disabled: $data.busy !== '',
86
+ "data-testid": `cn-field-list-action-${action.id}-${row.key}`,
87
+ onClick: $event => ($options.runAction(action, row))
88
+ }, {
89
+ default: withCtx(() => [
90
+ createTextVNode(toDisplayString(action.label), 1 /* TEXT */)
91
+ ]),
92
+ _: 2 /* DYNAMIC */
93
+ }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["disabled", "data-testid", "onClick"]))
94
+ }), 128 /* KEYED_FRAGMENT */)),
95
+ ($options.canRemove)
96
+ ? (openBlock(), createBlock(_component_NcButton, {
97
+ key: 3,
98
+ variant: "tertiary",
99
+ disabled: $data.busy !== '',
100
+ "aria-label": $options.removeAriaLabel(row),
101
+ "data-testid": `cn-field-list-remove-${row.key}`,
102
+ onClick: $event => ($options.remove(row))
103
+ }, {
104
+ default: withCtx(() => [
105
+ createTextVNode(toDisplayString($options.removeLabel), 1 /* TEXT */)
106
+ ]),
107
+ _: 1 /* STABLE */
108
+ }, 8 /* PROPS */, ["disabled", "aria-label", "data-testid", "onClick"]))
109
+ : createCommentVNode("v-if", true)
110
+ ]),
111
+ (row.detail)
112
+ ? (openBlock(), createElementBlock("p", _hoisted_11, toDisplayString(row.detail), 1 /* TEXT */))
113
+ : createCommentVNode("v-if", true)
114
+ ], 8 /* PROPS */, _hoisted_4))
115
+ }), 128 /* KEYED_FRAGMENT */))
116
+ ])),
117
+ ($options.hint)
118
+ ? (openBlock(), createElementBlock("p", _hoisted_12, toDisplayString($options.hint), 1 /* TEXT */))
119
+ : createCommentVNode("v-if", true),
120
+ ($options.canAdd)
121
+ ? (openBlock(), createElementBlock("form", {
122
+ key: 3,
123
+ class: "cn-field-list-widget__add",
124
+ "data-testid": "cn-field-list-add",
125
+ onSubmit: _cache[0] || (_cache[0] = withModifiers((...args) => ($options.add && $options.add(...args)), ["prevent"]))
126
+ }, [
127
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.addFields, (field) => {
128
+ return (openBlock(), createElementBlock("div", {
129
+ key: field.key,
130
+ class: "cn-field-list-widget__field",
131
+ "data-testid": `cn-field-list-input-${field.key}`
132
+ }, [
133
+ (field.type === 'select')
134
+ ? (openBlock(), createBlock(_component_NcSelect, {
135
+ key: 0,
136
+ modelValue: $data.draft[field.key],
137
+ "onUpdate:modelValue": $event => (($data.draft[field.key]) = $event),
138
+ inputLabel: field.label,
139
+ options: field.options,
140
+ reduce: (option) => option.value,
141
+ clearable: false,
142
+ label: "label"
143
+ }, null, 8 /* PROPS */, ["modelValue", "onUpdate:modelValue", "inputLabel", "options", "reduce"]))
144
+ : (openBlock(), createBlock(_component_NcTextField, {
145
+ key: 1,
146
+ modelValue: $data.draft[field.key],
147
+ "onUpdate:modelValue": $event => (($data.draft[field.key]) = $event),
148
+ label: field.label,
149
+ error: Boolean($data.errors[field.key]),
150
+ helperText: $data.errors[field.key] || ''
151
+ }, null, 8 /* PROPS */, ["modelValue", "onUpdate:modelValue", "label", "error", "helperText"]))
152
+ ], 8 /* PROPS */, _hoisted_13))
153
+ }), 128 /* KEYED_FRAGMENT */)),
154
+ createVNode(_component_NcButton, {
155
+ type: "submit",
156
+ variant: "secondary",
157
+ disabled: $data.busy !== '',
158
+ "data-testid": "cn-field-list-submit"
159
+ }, {
160
+ default: withCtx(() => [
161
+ createTextVNode(toDisplayString($options.addLabel), 1 /* TEXT */)
162
+ ]),
163
+ _: 1 /* STABLE */
164
+ }, 8 /* PROPS */, ["disabled"])
165
+ ], 32 /* NEED_HYDRATION */))
166
+ : createCommentVNode("v-if", true),
167
+ ($data.notice)
168
+ ? (openBlock(), createBlock(_component_NcNoteCard, {
169
+ key: 4,
170
+ type: $data.noticeType,
171
+ "data-testid": "cn-field-list-notice"
172
+ }, {
173
+ default: withCtx(() => [
174
+ createTextVNode(toDisplayString($data.notice), 1 /* TEXT */)
175
+ ]),
176
+ _: 1 /* STABLE */
177
+ }, 8 /* PROPS */, ["type"]))
178
+ : createCommentVNode("v-if", true)
179
+ ]))
180
+ }
181
+
182
+ export { render };
183
+ //# sourceMappingURL=CnFieldListWidget.vue3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnFieldListWidget.vue3.js","sources":["../../../../src/components/CnFieldListWidget/CnFieldListWidget.vue?vue&type=template&id=0f29a833&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-field-list-widget\" data-testid=\"cn-field-list-widget\">\n\t\t<p v-if=\"rows.length === 0\" class=\"cn-field-list-widget__empty\" data-testid=\"cn-field-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-field-list-widget__rows\">\n\t\t\t<li\n\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-field-list-widget__row\"\n\t\t\t\t:data-testid=\"`cn-field-list-row-${row.key}`\">\n\t\t\t\t<div class=\"cn-field-list-widget__line\">\n\t\t\t\t\t<span v-if=\"row.initials\" class=\"cn-field-list-widget__avatar\" aria-hidden=\"true\">{{ row.initials }}</span>\n\t\t\t\t\t<span class=\"cn-field-list-widget__main\">\n\t\t\t\t\t\t<span class=\"cn-field-list-widget__title\">{{ row.title }}</span>\n\t\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-field-list-widget__meta\">{{ row.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-if=\"row.pill\"\n\t\t\t\t\t\t:label=\"row.pill.label\"\n\t\t\t\t\t\t:variant=\"row.pill.variant\"\n\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t<span v-if=\"row.aside\" class=\"cn-field-list-widget__aside\">{{ row.aside }}</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-for=\"action in row.actions\"\n\t\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-action-${action.id}-${row.key}`\"\n\t\t\t\t\t\t@click=\"runAction(action, row)\">\n\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"canRemove\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t:aria-label=\"removeAriaLabel(row)\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-remove-${row.key}`\"\n\t\t\t\t\t\t@click=\"remove(row)\">\n\t\t\t\t\t\t{{ removeLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<p v-if=\"row.detail\" class=\"cn-field-list-widget__detail\">\n\t\t\t\t\t{{ row.detail }}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<p v-if=\"hint\" class=\"cn-field-list-widget__hint\">\n\t\t\t{{ hint }}\n\t\t</p>\n\n\t\t<form\n\t\t\tv-if=\"canAdd\"\n\t\t\tclass=\"cn-field-list-widget__add\"\n\t\t\tdata-testid=\"cn-field-list-add\"\n\t\t\t@submit.prevent=\"add\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in addFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-field-list-widget__field\"\n\t\t\t\t:data-testid=\"`cn-field-list-input-${field.key}`\">\n\t\t\t\t<NcSelect\n\t\t\t\t\tv-if=\"field.type === 'select'\"\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:inputLabel=\"field.label\"\n\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t:reduce=\"(option) => option.value\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\tlabel=\"label\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:label=\"field.label\"\n\t\t\t\t\t:error=\"Boolean(errors[field.key])\"\n\t\t\t\t\t:helperText=\"errors[field.key] || ''\" />\n\t\t\t</div>\n\t\t\t<NcButton type=\"submit\"\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-submit\">\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</form>\n\n\t\t<NcNoteCard v-if=\"notice\" :type=\"noticeType\" data-testid=\"cn-field-list-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { NcButton, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\n/**\n * CnFieldListWidget — the `field-list` widget: a list stored INSIDE one field\n * of the current object (a portal's `domains[]`, its `editors[]`), shown as\n * rows and edited in place.\n *\n * Rows are drawn from configuration (title, meta line, pill, aside, a detail\n * line); a small form adds an entry, a button removes one, and each change is\n * saved as ONE PATCH of that field on the object, after re-reading the stored\n * value so a concurrent change to the same list is not overwritten with a\n * stale copy. Optional row actions call a configured endpoint (a \"check\n * again\"), and an optional `rowSource` merges live per-row state read from an\n * endpoint by key without writing it back.\n *\n * The server decides who may write: a refused PATCH shows the server's words\n * and leaves the list as stored.\n *\n * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md\n */\nexport default {\n\tname: 'CnFieldListWidget',\n\n\tcomponents: { CnStatusBadge, NcButton, NcNoteCard, NcSelect, NcTextField },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the object field holding the array.\n\t\t * - `key` — the item field that identifies an entry (default: the item itself for strings).\n\t\t * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix? }, detail?: { template, when? } }`.\n\t\t * - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.\n\t\t * - `remove` — `{ label? }`; omitted = no remove.\n\t\t * - `rowActions` — `[{ id, label, when?, request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.\n\t\t * - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.\n\t\t * - `readOnly`, `hint`, `emptyText`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The list as last saved by this widget; null = read it off objectData. */\n\t\t\tsaved: null,\n\t\t\t/** Live per-row state from `rowSource`, by key. */\n\t\t\tlive: {},\n\t\t\tdraft: {},\n\t\t\terrors: {},\n\t\t\tbusy: '',\n\t\t\tnotice: '',\n\t\t\tnoticeType: 'error',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The stored list.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\titems() {\n\t\t\tconst list = this.saved !== null ? this.saved : readPath(this.objectData || {}, this.content.field)\n\t\t\treturn Array.isArray(list) ? list : []\n\t\t},\n\n\t\t/**\n\t\t * The rows to draw.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-draws-the-entries-of-one-object-field\n\t\t */\n\t\trows() {\n\t\t\tconst spec = this.content.row || {}\n\t\t\treturn this.items.map((item, index) => {\n\t\t\t\tconst key = this.keyOf(item, index)\n\t\t\t\tconst entry = item && typeof item === 'object'\n\t\t\t\t\t? { ...item, ...(this.live[key] || {}) }\n\t\t\t\t\t: { value: item, ...(this.live[key] || {}) }\n\t\t\t\tconst title = this.text(entry, spec.title) || this.text(entry, spec.titleFallback) || (typeof item === 'string' ? item : key)\n\t\t\t\tconst meta = (Array.isArray(spec.meta) ? spec.meta : [])\n\t\t\t\t\t.map((part) => this.text(entry, part))\n\t\t\t\t\t.filter((part) => part !== '')\n\t\t\t\t\t.join(' · ')\n\t\t\t\treturn {\n\t\t\t\t\tkey,\n\t\t\t\t\titem,\n\t\t\t\t\tentry,\n\t\t\t\t\ttitle,\n\t\t\t\t\tinitials: spec.avatar ? this.initials(title) : '',\n\t\t\t\t\tmeta,\n\t\t\t\t\tpill: this.pillOf(entry, spec.pill),\n\t\t\t\t\taside: spec.aside ? this.asideOf(entry, spec.aside) : '',\n\t\t\t\t\tdetail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',\n\t\t\t\t\tactions: this.actionsFor(entry),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** Whether the list is read-only. */\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether entries can be added. */\n\t\tcanAdd() {\n\t\t\treturn !this.readOnly && this.addFields.length > 0 && Boolean(this.content.field)\n\t\t},\n\n\t\t/** Whether entries can be removed. */\n\t\tcanRemove() {\n\t\t\treturn !this.readOnly && Boolean(this.content.remove) && Boolean(this.content.field)\n\t\t},\n\n\t\t/**\n\t\t * The add form's fields, labels translated, select options translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\taddFields() {\n\t\t\tconst fields = this.content.add && Array.isArray(this.content.add.fields) ? this.content.add.fields : []\n\t\t\treturn fields\n\t\t\t\t.filter((field) => field && field.key)\n\t\t\t\t.map((field) => ({\n\t\t\t\t\t...field,\n\t\t\t\t\tlabel: this.tr(field.label || field.key),\n\t\t\t\t\toptions: (Array.isArray(field.options) ? field.options : []).map((option) => (\n\t\t\t\t\t\ttypeof option === 'object' ? { value: option.value, label: this.tr(option.label || String(option.value)) } : { value: option, label: this.tr(String(option)) }\n\t\t\t\t\t)),\n\t\t\t\t}))\n\t\t},\n\n\t\taddLabel() {\n\t\t\treturn this.tr((this.content.add && this.content.add.label) || 'Add')\n\t\t},\n\n\t\tremoveLabel() {\n\t\t\treturn this.tr((this.content.remove && this.content.remove.label) || 'Remove')\n\t\t},\n\n\t\thint() {\n\t\t\treturn this.tr(this.content.hint || '')\n\t\t},\n\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content.emptyText || 'Nothing listed yet')\n\t\t},\n\n\t\t/** The object URL the list is saved to, or '' when it is not known. */\n\t\tobjectUrl() {\n\t\t\tconst id = this.objectId || (this.objectData && this.objectData.id)\n\t\t\tif (!this.register || !this.schema || !id || typeof this.register !== 'string' || typeof this.schema !== 'string') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(this.register)}/${encodeURIComponent(this.schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.saved = null\n\t\t\tthis.loadLive()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.resetDraft()\n\t\tthis.loadLive()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The identity of an entry.\n\t\t *\n\t\t * @param {unknown} item The entry.\n\t\t * @param {number} index Its position.\n\t\t * @return {string}\n\t\t */\n\t\tkeyOf(item, index) {\n\t\t\tif (item === null || typeof item !== 'object') {\n\t\t\t\treturn String(item)\n\t\t\t}\n\t\t\tconst key = this.content.key ? readPath(item, this.content.key) : undefined\n\t\t\treturn key === undefined || key === null || key === '' ? `item-${index}` : String(key)\n\t\t},\n\n\t\t/**\n\t\t * A display text off an entry: a path, or `{ path, map?, format? }`.\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {string|object} spec The spec.\n\t\t * @return {string}\n\t\t */\n\t\ttext(entry, spec) {\n\t\t\tif (!spec) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst def = typeof spec === 'string' ? { path: spec } : spec\n\t\t\tconst raw = readPath(entry, def.path)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (def.format === 'date' || def.format === 'date-long') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\tif (date === null) {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\treturn def.format === 'date-long' ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1)) : formatBoardDate(date)\n\t\t\t}\n\t\t\tif (def.map && Object.hasOwn(def.map, String(raw))) {\n\t\t\t\treturn this.tr(def.map[String(raw)])\n\t\t\t}\n\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t},\n\n\t\tpillOf(entry, spec) {\n\t\t\tif (!spec || !spec.path) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(entry, spec.path)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.text(entry, spec)\n\t\t\tconst variant = spec.variantMap && spec.variantMap[String(raw)]\n\t\t\treturn { label, variant: isBadgeVariant(variant) ? variant : 'default' }\n\t\t},\n\n\t\tasideOf(entry, spec) {\n\t\t\tconst value = this.text(entry, spec)\n\t\t\tif (value === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn spec.prefix ? `${this.tr(spec.prefix)} ${value}` : value\n\t\t},\n\n\t\t/**\n\t\t * Whether an entry matches a `{ field: value }` condition (every field equal).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {object|undefined} when The condition.\n\t\t * @return {boolean}\n\t\t */\n\t\tmatches(entry, when) {\n\t\t\tif (!when || typeof when !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Object.entries(when).every(([field, expected]) => {\n\t\t\t\tconst actual = readPath(entry, field)\n\t\t\t\tif (expected === false) {\n\t\t\t\t\treturn actual === false || actual === undefined || actual === null || actual === ''\n\t\t\t\t}\n\t\t\t\treturn String(actual) === String(expected)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Fill `{path}` placeholders from an entry; '' when one stays empty.\n\t\t *\n\t\t * @param {string} template The template, translated first.\n\t\t * @param {object} entry The entry.\n\t\t * @return {string}\n\t\t */\n\t\tfill(template, entry) {\n\t\t\tif (typeof template !== 'string') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tlet missing = false\n\t\t\tconst out = this.tr(template).replace(/\\{([\\w.@]+)\\}/g, (match, path) => {\n\t\t\t\tconst value = readPath(entry, path)\n\t\t\t\tif (value === undefined || value === null || value === '') {\n\t\t\t\t\tmissing = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(value)\n\t\t\t})\n\t\t\treturn missing ? '' : out\n\t\t},\n\n\t\tactionsFor(entry) {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst actions = Array.isArray(this.content.rowActions) ? this.content.rowActions : []\n\t\t\treturn actions\n\t\t\t\t.filter((action) => action && action.id && action.request && action.request.url && this.matches(entry, action.when))\n\t\t\t\t.map((action) => ({ ...action, label: this.tr(action.label || action.id) }))\n\t\t},\n\n\t\tinitials(name) {\n\t\t\treturn String(name || '').split(/[\\s@._-]+/).filter(Boolean).slice(0, 2).map((part) => part.charAt(0).toUpperCase()).join('')\n\t\t},\n\n\t\tremoveAriaLabel(row) {\n\t\t\treturn `${this.removeLabel}: ${row.title}`\n\t\t},\n\n\t\tresetDraft() {\n\t\t\tconst draft = {}\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst first = field.type === 'select' && field.options.length > 0 ? field.options[0].value : ''\n\t\t\t\tdraft[field.key] = field.default !== undefined ? field.default : first\n\t\t\t}\n\t\t\tthis.draft = draft\n\t\t\tthis.errors = {}\n\t\t},\n\n\t\t/**\n\t\t * Resolve `@objectId`, `@object.<field>` and `@row.<field>` inside a string or a payload.\n\t\t *\n\t\t * @param {unknown} value The value.\n\t\t * @param {object} entry The row entry.\n\t\t * @return {unknown}\n\t\t */\n\t\tresolve(value, entry) {\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\treturn value.map((v) => this.resolve(v, entry))\n\t\t\t}\n\t\t\tif (value && typeof value === 'object') {\n\t\t\t\treturn Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.resolve(v, entry)]))\n\t\t\t}\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst object = this.objectData || {}\n\t\t\tconst read = (source, path) => {\n\t\t\t\tconst v = readPath(source || {}, path)\n\t\t\t\treturn v === undefined || v === null ? '' : v\n\t\t\t}\n\t\t\tconst whole = value.match(/^@(row|object)\\.([\\w.@]+)$/)\n\t\t\tif (whole) {\n\t\t\t\treturn read(whole[1] === 'row' ? entry : object, whole[2])\n\t\t\t}\n\t\t\treturn value\n\t\t\t\t.replace(/@objectId/g, () => encodeURIComponent(String(this.objectId || object.id || '')))\n\t\t\t\t.replace(/@(row|object)\\.([\\w.@]+)/g, (m, scope, path) => encodeURIComponent(String(read(scope === 'row' ? entry : object, path))))\n\t\t},\n\n\t\tasync http() {\n\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\treturn { axios }\n\t\t},\n\n\t\t/**\n\t\t * Read live per-row state from `rowSource` and keep it by key.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLive() {\n\t\t\tconst source = this.content.rowSource\n\t\t\tif (!source || !source.url || !source.key || !this.objectData) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst response = await axios.get(prefixUrl(this.resolve(source.url, {})))\n\t\t\t\tconst data = response && response.data\n\t\t\t\tconst list = source.path ? readPath(data, source.path) : data\n\t\t\t\tconst live = {}\n\t\t\t\tfor (const item of Array.isArray(list) ? list : []) {\n\t\t\t\t\tconst key = item && readPath(item, source.key)\n\t\t\t\t\tif (key !== undefined && key !== null) {\n\t\t\t\t\t\tlive[String(key)] = item\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.live = live\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFieldListWidget] rowSource could not be read:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save a new list: re-read the stored value, apply the change to it, and\n\t\t * PATCH that one field.\n\t\t *\n\t\t * @param {(stored: Array) => Array} change Builds the next list from the stored one, or throws an Error with a `userMessage`.\n\t\t * @param {string} busyKey What is busy.\n\t\t * @return {Promise<boolean>} Whether it saved.\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-saves-one-field-with-one-patch\n\t\t */\n\t\tasync save(change, busyKey) {\n\t\t\tif (!this.objectUrl) {\n\t\t\t\tthis.notify(this.tr('This list cannot be saved here.'))\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthis.busy = busyKey\n\t\t\tthis.notice = ''\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst { data: stored } = await axios.get(prefixUrl(this.objectUrl))\n\t\t\t\tconst current = readPath(stored || {}, this.content.field)\n\t\t\t\tconst next = change(Array.isArray(current) ? current : [])\n\t\t\t\tconst { data } = await axios.patch(prefixUrl(this.objectUrl), { [this.content.field]: next })\n\t\t\t\tconst written = readPath(data || {}, this.content.field)\n\t\t\t\tthis.saved = Array.isArray(written) ? written : next\n\t\t\t\t/**\n\t\t\t\t * @event saved The list was saved: one PATCH of the field succeeded.\n\t\t\t\t * @type {{field: string, value: Array}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: this.saved })\n\t\t\t\treturn true\n\t\t\t} catch (e) {\n\t\t\t\tthis.notify(e && e.userMessage ? e.userMessage : serverErrorMessage(e))\n\t\t\t\treturn false\n\t\t\t} finally {\n\t\t\t\tthis.busy = ''\n\t\t\t}\n\t\t},\n\n\t\tnotify(message, type = 'error') {\n\t\t\tthis.notice = message\n\t\t\tthis.noticeType = type\n\t\t},\n\n\t\t/**\n\t\t * Validate the draft and add it.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-adds-and-removes-entries\n\t\t */\n\t\tasync add() {\n\t\t\tconst errors = {}\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst value = this.draft[field.key]\n\t\t\t\tconst empty = value === undefined || value === null || String(value).trim() === ''\n\t\t\t\tif (field.required && empty) {\n\t\t\t\t\terrors[field.key] = this.tr('Fill this in.')\n\t\t\t\t} else if (!empty && field.pattern && !new RegExp(field.pattern).test(String(value).trim())) {\n\t\t\t\t\terrors[field.key] = this.tr(field.patternMessage || 'This value is not valid.')\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.errors = errors\n\t\t\tif (Object.keys(errors).length > 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst entry = { ...((this.content.add && this.content.add.defaults) || {}) }\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst value = this.draft[field.key]\n\t\t\t\tif (value !== undefined && value !== null && String(value).trim() !== '') {\n\t\t\t\t\tentry[field.key] = typeof value === 'string' ? value.trim() : value\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst unique = this.content.add && this.content.add.unique\n\t\t\tconst saved = await this.save((stored) => {\n\t\t\t\tif (unique && stored.some((item) => item && String(readPath(item, unique)) === String(entry[unique]))) {\n\t\t\t\t\tconst error = new Error('duplicate')\n\t\t\t\t\terror.userMessage = this.tr('This is already in the list.')\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\treturn [...stored, entry]\n\t\t\t}, '*')\n\t\t\tif (saved) {\n\t\t\t\tthis.resetDraft()\n\t\t\t\tthis.loadLive()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an entry by its key.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync remove(row) {\n\t\t\tawait this.save((stored) => stored.filter((item, index) => this.keyOf(item, index) !== row.key), row.key)\n\t\t},\n\n\t\t/**\n\t\t * Run a row action's request, then re-read the live state.\n\t\t *\n\t\t * @param {object} action The action.\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-row-can-run-a-configured-request\n\t\t */\n\t\tasync runAction(action, row) {\n\t\t\tthis.busy = row.key\n\t\t\tthis.notice = ''\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst method = String(action.request.method || 'POST').toLowerCase()\n\t\t\t\tconst payload = this.resolve(action.request.payload || {}, row.entry)\n\t\t\t\tconst response = method === 'get'\n\t\t\t\t\t? await axios.get(prefixUrl(this.resolve(action.request.url, row.entry)))\n\t\t\t\t\t: await axios[method](prefixUrl(this.resolve(action.request.url, row.entry)), payload)\n\t\t\t\tconst message = response && response.data && typeof response.data.message === 'string' ? response.data.message : ''\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.notify(message, 'info')\n\t\t\t\t}\n\t\t\t\tawait this.loadLive()\n\t\t\t} catch (e) {\n\t\t\t\tthis.notify(serverErrorMessage(e))\n\t\t\t} finally {\n\t\t\t\tthis.busy = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-list-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-field-list-widget__row {\n\tpadding: 10px 0;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-field-list-widget__row:first-child {\n\tborder-top: none;\n\tpadding-top: 0;\n}\n\n.cn-field-list-widget__line {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__avatar {\n\tflex: 0 0 auto;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: 50%;\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text, var(--color-main-text));\n\tfont-size: 12px;\n\tfont-weight: 700;\n}\n\n.cn-field-list-widget__main {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__title {\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__meta,\n.cn-field-list-widget__aside,\n.cn-field-list-widget__detail,\n.cn-field-list-widget__hint,\n.cn-field-list-widget__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-list-widget__meta,\n.cn-field-list-widget__aside,\n.cn-field-list-widget__detail {\n\tfont-size: 13px;\n}\n\n.cn-field-list-widget__aside {\n\twhite-space: nowrap;\n}\n\n.cn-field-list-widget__detail,\n.cn-field-list-widget__hint,\n.cn-field-list-widget__empty {\n\tmargin: 4px 0 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__add {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tgap: 8px 12px;\n}\n\n.cn-field-list-widget__add > * {\n\tflex: 1 1 180px;\n}\n\n.cn-field-list-widget__add > button {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_Fragment","_renderList","_createElementVNode","_createBlock","_createTextVNode","_createVNode"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import __sfc_main from './CnFieldListWidget.vue.js';
2
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js';
3
+
4
+ /**
5
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
6
+ * SPDX-License-Identifier: EUPL-1.2
7
+ *
8
+ * CnFieldListWidget — self-registers the `field-list` widget type (a list
9
+ * stored inside one field of the current object, shown as rows and edited
10
+ * with one PATCH of that field) into the shared dashboardWidgetRegistry at
11
+ * module load. Renderer only: declared in the manifest.
12
+ */
13
+
14
+
15
+ registerDashboardWidget('field-list', {
16
+ renderer: __sfc_main,
17
+ form: null,
18
+ defaultContent: {
19
+ field: '',
20
+ row: {},
21
+ readOnly: false,
22
+ },
23
+ displayName: 'Field list',
24
+ icon: 'FormatListBulleted',
25
+ surfaces: ['detail-page'],
26
+ });
27
+
28
+ export { __sfc_main as CnFieldListWidget, __sfc_main as default };
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../../src/components/CnFieldListWidget/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * CnFieldListWidget — self-registers the `field-list` widget type (a list\n * stored inside one field of the current object, shown as rows and edited\n * with one PATCH of that field) into the shared dashboardWidgetRegistry at\n * module load. Renderer only: declared in the manifest.\n */\n\nimport CnFieldListWidget from './CnFieldListWidget.vue'\nimport { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'\n\nregisterDashboardWidget('field-list', {\n\trenderer: CnFieldListWidget,\n\tform: null,\n\tdefaultContent: {\n\t\tfield: '',\n\t\trow: {},\n\t\treadOnly: false,\n\t},\n\tdisplayName: 'Field list',\n\ticon: 'FormatListBulleted',\n\tsurfaces: ['detail-page'],\n})\n\nexport { CnFieldListWidget }\nexport default CnFieldListWidget\n"],"names":["CnFieldListWidget"],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA,uBAAuB,CAAC,YAAY,EAAE;AACtC,CAAC,QAAQ,EAAEA,UAAiB;AAC5B,CAAC,IAAI,EAAE,IAAI;AACX,CAAC,cAAc,EAAE;AACjB,EAAE,KAAK,EAAE,EAAE;AACX,EAAE,GAAG,EAAE,EAAE;AACT,EAAE,QAAQ,EAAE,KAAK;AACjB,EAAE;AACF,CAAC,WAAW,EAAE,YAAY;AAC1B,CAAC,IAAI,EAAE,oBAAoB;AAC3B,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC;AAC1B,CAAC;;;;"}
@@ -17,7 +17,7 @@ import __sfc_main$2 from '../CnPropertySourceField/CnPropertySourceField.vue.js'
17
17
  import __sfc_main$a from '../CnResourceSelect/CnResourceSelect.vue.js';
18
18
  import __sfc_main$4 from '../CnSubObjectsField/CnSubObjectsField.vue.js';
19
19
  import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js';
20
- import { formDraftMixin, draftKey, draftIndicatorText, readDraft } from '../../composables/useFormDraft.js';
20
+ import { formDraftMixin, draftKey, resolveDraftUserId, draftIndicatorText, hasDraftChanges, clearAnonymousDrafts, readDraft } from '../../composables/useFormDraft.js';
21
21
  import { splitHeldFiles, uploadHeldFiles, heldFromFailures } from '../../composables/useHeldFileUpload.js';
22
22
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js';
23
23
  import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js';
@@ -393,16 +393,16 @@ var script = {
393
393
  * 🔴 THE USER IS PART OF THE DRAFT KEY. A shared browser profile at a
394
394
  * service desk is the ordinary case in a municipality, and a draft
395
395
  * keyed without the user hands the next person at the counter what the
396
- * last one typed. A host that passes nothing gets `anonymous`, which
397
- * is correct for a single-user context and wrong for a counter — so
398
- * the prop exists and is documented rather than guessed at.
396
+ * last one typed. A host that passes nothing gets the signed-in
397
+ * Nextcloud user (`@nextcloud/auth`), and `anonymous` only when
398
+ * nobody is signed in.
399
399
  */
400
400
  draftAppId: {
401
401
  type: String,
402
402
  default: '',
403
403
  },
404
404
 
405
- /** Who is typing, for the draft key. See `draftAppId`. */
405
+ /** Who is typing, for the draft key. Empty: the signed-in user. See `draftAppId`. */
406
406
  draftUserId: {
407
407
  type: String,
408
408
  default: '',
@@ -663,10 +663,20 @@ var script = {
663
663
  appId: this.draftAppId,
664
664
  schema: (this.schema?.slug || this.schema?.title || ''),
665
665
  objectId: (this.item?.id || this.item?.uuid || ''),
666
- userId: this.draftUserId,
666
+ userId: this.draftUserOwner,
667
667
  })
668
668
  },
669
669
 
670
+ /**
671
+ * Who the draft belongs to: `draftUserId`, else the signed-in user.
672
+ *
673
+ * @return {string} The user id, or '' when nobody is signed in.
674
+ * @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
675
+ */
676
+ draftUserOwner() {
677
+ return resolveDraftUserId(this.draftUserId)
678
+ },
679
+
670
680
  /**
671
681
  * Whether "Save draft" may be offered.
672
682
  *
@@ -978,17 +988,6 @@ var script = {
978
988
  formData: {
979
989
  deep: true,
980
990
  handler(values) {
981
- // 🔴 THE FIRST CHANGE IS THE COMPONENT SEEDING THE FORM, NOT
982
- // SOMEBODY TYPING. Writing a draft for it stores the empty
983
- // form over a real one, and the indicator announces "Saving
984
- // draft" about work nobody did — an instrument reporting
985
- // activity that did not happen, which is the whole class of
986
- // defect this library keeps paying down.
987
- if (this.draftSeeded !== true) {
988
- this.draftSeeded = true;
989
- return
990
- }
991
-
992
991
  // Nothing is written while a draft is being offered: the user
993
992
  // has not chosen yet, and overwriting the stored draft with the
994
993
  // empty form they are looking at would destroy the thing they
@@ -997,6 +996,19 @@ var script = {
997
996
  return
998
997
  }
999
998
 
999
+ // 🔴 ONLY A PERSON'S CHANGE IS WORTH A DRAFT. The form fills
1000
+ // in values by itself after it opens (an input reporting its
1001
+ // value back, normalised dates, defaults, the organisation),
1002
+ // and each of those used to write a draft and say "Saved just
1003
+ // now" on a dialog nobody had touched. A field counts once a
1004
+ // person edited it AND it differs from what the form opened
1005
+ // with, so undoing the edit drops the draft again.
1006
+ const edited = Object.keys(this.touchedFields || {}).filter((key) => this.touchedFields[key]);
1007
+ if (!hasDraftChanges(values, this.draftBaseline, edited)) {
1008
+ this.settleUnchangedDraft(this.formDraftKey);
1009
+ return
1010
+ }
1011
+
1000
1012
  this.scheduleDraftWrite(this.formDraftKey, values);
1001
1013
  },
1002
1014
  },
@@ -1161,6 +1173,12 @@ var script = {
1161
1173
  // Silently refilling a form is how somebody submits last week's answers
1162
1174
  // without noticing they were there, and a form that fills itself is
1163
1175
  // indistinguishable from one the server prefilled.
1176
+ // A draft stored under `anonymous` was written by a page that did not
1177
+ // know who was typing. Once somebody is signed in it is removed, never
1178
+ // offered: it may be another person's.
1179
+ if (this.formDraftKey && this.draftUserOwner) {
1180
+ clearAnonymousDrafts();
1181
+ }
1164
1182
  if (this.formDraftKey) {
1165
1183
  this.draftOffer = readDraft(this.formDraftKey, {
1166
1184
  objectUpdated: (this.item?.updated || this.item?.['@self']?.updated || ''),
@@ -1519,6 +1537,10 @@ var script = {
1519
1537
  this.runAssignments(null);
1520
1538
  this.loadProfileDefaults();
1521
1539
  }
1540
+ // What the form opened with. A draft is written only once a field
1541
+ // a person edited differs from this (see the `formData` watcher).
1542
+ // Not reactive: nothing renders from it.
1543
+ this.draftBaseline = JSON.parse(JSON.stringify(this.formData || {}));
1522
1544
  },
1523
1545
 
1524
1546
  /**
@@ -2306,6 +2328,27 @@ var script = {
2306
2328
  return this.showsOuterLabel(field) ? '' : this.errors[field.key]
2307
2329
  },
2308
2330
 
2331
+ /**
2332
+ * The placeholder of a text, number or textarea control.
2333
+ *
2334
+ * The field's description already shows as the help line under the
2335
+ * control, so the placeholder stays empty rather than repeat it word
2336
+ * for word. The help line is the one kept: it is still there after
2337
+ * somebody typed (a placeholder disappears with the first keystroke),
2338
+ * it is read out through `aria-describedby`, and in the Nextcloud look
2339
+ * a placeholder is hidden until the field has focus anyway. While an
2340
+ * error takes the help line's place, the description returns as the
2341
+ * placeholder so it is not lost.
2342
+ *
2343
+ * @param {object} field The field definition.
2344
+ * @return {string} The placeholder, or '' when the help line says it.
2345
+ * @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
2346
+ */
2347
+ fieldPlaceholder(field) {
2348
+ const description = field.description || '';
2349
+ return this.helperError(field) ? description : ''
2350
+ },
2351
+
2309
2352
  /**
2310
2353
  * The accessibility attributes of a text control. In both looks an invalid
2311
2354
  * control is `aria-invalid` and `aria-describedby` lists the error before
@@ -3788,6 +3831,11 @@ var script = {
3788
3831
  }
3789
3832
 
3790
3833
  this.formData = { ...this.formData, ...this.draftOffer.values };
3834
+ // What the person typed last time is their edit now: it counts for
3835
+ // the next draft write and survives the record arriving late.
3836
+ for (const key of Object.keys(this.draftOffer.values)) {
3837
+ this.touchedFields[key] = true;
3838
+ }
3791
3839
  this.draftOffer = null;
3792
3840
  },
3793
3841