@conduction/nextcloud-vue 2.82.0 → 2.84.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 (178) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +2 -2
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  13. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  14. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  15. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  17. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  18. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  21. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
  22. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  24. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +14 -0
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  30. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  31. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  42. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  43. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  50. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
  53. package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
  55. package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
  56. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  57. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
  58. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
  61. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
  62. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
  64. package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  70. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
  71. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  72. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
  73. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  74. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
  75. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
  76. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
  78. package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  83. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  84. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +4 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  88. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  89. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
  90. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
  92. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
  94. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
  95. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
  97. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  99. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  106. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  107. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  108. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  109. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +16 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +41 -4
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +900 -537
  116. package/dist/esm/utils/buildManifest.js +37 -3
  117. package/dist/esm/utils/buildManifest.js.map +1 -1
  118. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  119. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  120. package/dist/esm/utils/fetchAggregate.js +25 -2
  121. package/dist/esm/utils/fetchAggregate.js.map +1 -1
  122. package/dist/esm/utils/formatMetric.js +39 -3
  123. package/dist/esm/utils/formatMetric.js.map +1 -1
  124. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  125. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  126. package/dist/esm/utils/sentinelTokens.js +9 -0
  127. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  128. package/dist/esm/utils/switchValues.js +62 -0
  129. package/dist/esm/utils/switchValues.js.map +1 -0
  130. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  131. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  132. package/dist/nextcloud-vue.cjs.js +36711 -34909
  133. package/dist/nextcloud-vue.cjs.js.map +2 -2
  134. package/dist/nextcloud-vue.css +900 -537
  135. package/l10n/en.json +16 -1
  136. package/l10n/nl.json +41 -4
  137. package/package.json +1 -1
  138. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
  139. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  140. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  141. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  142. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
  143. package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
  144. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  145. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  146. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  147. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  148. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  149. package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
  150. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  151. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  152. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
  153. package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
  154. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  155. package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
  156. package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
  157. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  158. package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
  159. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
  160. package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
  161. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  162. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  163. package/src/css/form-field.css +14 -0
  164. package/src/css/form-widget.css +11 -0
  165. package/src/css/index-page.css +11 -0
  166. package/src/css/index.css +1 -0
  167. package/src/css/look-board-reports.css +175 -0
  168. package/src/css/look-board.css +44 -1
  169. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  170. package/src/schemas/app-manifest-v2.schema.json +24 -3
  171. package/src/utils/buildManifest.js +36 -2
  172. package/src/utils/detailWidgetVisibility.js +145 -0
  173. package/src/utils/fetchAggregate.js +24 -1
  174. package/src/utils/formatMetric.js +38 -2
  175. package/src/utils/resolveManifestTokens.js +1 -0
  176. package/src/utils/sentinelTokens.js +9 -0
  177. package/src/utils/switchValues.js +59 -0
  178. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"CnSettingsFormWidget.vue3.js","sources":["../../../../src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue?vue&type=template&id=66086856&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<form class=\"cn-settings-form\"\n\t\tdata-testid=\"cn-settings-form\"\n\t\tnovalidate\n\t\t@submit.prevent=\"save\">\n\t\t<div class=\"cn-settings-form__grid\" :class=\"`cn-settings-form__grid--${columns}`\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in fields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-settings-form__field\"\n\t\t\t\t:class=\"{ 'cn-settings-form__field--wide': field.wide }\"\n\t\t\t\t:data-testid=\"`cn-settings-form-field-${field.key}`\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-if=\"field.type === 'switch'\"\n\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t:disabled=\"field.readOnly\">\n\t\t\t\t\t{{ field.label }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<CnChipsFieldWidget\n\t\t\t\t\tv-else-if=\"field.type === 'chips'\"\n\t\t\t\t\t:modelValue=\"form[field.key]\"\n\t\t\t\t\t:content=\"chipsContent(field)\"\n\t\t\t\t\t@update:modelValue=\"form[field.key] = $event\" />\n\t\t\t\t<template v-else>\n\t\t\t\t\t<label class=\"cn-settings-form__label\" :for=\"inputId(field)\">{{ field.label }}</label>\n\t\t\t\t\t<div v-if=\"field.type === 'file'\" class=\"cn-settings-form__file\" :data-testid=\"`cn-settings-form-file-${field.key}`\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\t\tclass=\"cn-settings-form__control cn-settings-form__control--file\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-settings-form__control--placeholder': !form[field.key] }\">\n\t\t\t\t\t\t\t{{ form[field.key] ? fileName(form[field.key]) : field.placeholder }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"!field.readOnly\"\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t:aria-describedby=\"inputId(field)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-choose-${field.key}`\"\n\t\t\t\t\t\t\t@click=\"chooseFile(field)\">\n\t\t\t\t\t\t\t{{ field.chooseLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"!field.readOnly && form[field.key]\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-clear-${field.key}`\"\n\t\t\t\t\t\t\t@click=\"form[field.key] = ''\">\n\t\t\t\t\t\t\t{{ field.clearLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t\t<select\n\t\t\t\t\t\tv-else-if=\"field.type === 'reference'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:disabled=\"field.readOnly\"\n\t\t\t\t\t\t:aria-busy=\"referenceState[field.key] === 'loading' ? 'true' : null\"\n\t\t\t\t\t\t:data-testid=\"`cn-settings-form-reference-${field.key}`\">\n\t\t\t\t\t\t<option v-for=\"option in referenceChoices(field)\" :key=\"option.value\" :value=\"option.value\">\n\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t</select>\n\t\t\t\t\t<select\n\t\t\t\t\t\tv-else-if=\"field.type === 'select'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:disabled=\"field.readOnly\">\n\t\t\t\t\t\t<option v-for=\"option in field.options\" :key=\"String(option.value)\" :value=\"option.value\">\n\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t</select>\n\t\t\t\t\t<textarea\n\t\t\t\t\t\tv-else-if=\"field.type === 'textarea'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control cn-settings-form__control--area\"\n\t\t\t\t\t\t:rows=\"field.rows\"\n\t\t\t\t\t\t:placeholder=\"field.placeholder\"\n\t\t\t\t\t\t:readonly=\"field.readOnly\" />\n\t\t\t\t\t<input\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:type=\"field.type === 'number' ? 'number' : 'text'\"\n\t\t\t\t\t\t:min=\"field.min\"\n\t\t\t\t\t\t:max=\"field.max\"\n\t\t\t\t\t\t:placeholder=\"field.placeholder\"\n\t\t\t\t\t\t:readonly=\"field.readOnly\"\n\t\t\t\t\t\t:aria-invalid=\"errors[field.key] ? 'true' : null\">\n\t\t\t\t</template>\n\t\t\t\t<p v-if=\"errors[field.key]\" class=\"cn-settings-form__error\" role=\"alert\">\n\t\t\t\t\t{{ errors[field.key] }}\n\t\t\t\t</p>\n\t\t\t\t<p v-else-if=\"field.hint\" class=\"cn-settings-form__hint\">\n\t\t\t\t\t{{ field.hint }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcNoteCard v-if=\"notice\" :type=\"noticeType\" data-testid=\"cn-settings-form-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\n\t\t<div v-if=\"!readOnly\" class=\"cn-settings-form__actions\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\ttype=\"submit\"\n\t\t\t\t:disabled=\"saving\"\n\t\t\t\tdata-testid=\"cn-settings-form-save\">\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t</form>\n</template>\n\n<script>\nimport { FilePickerClosed, getFilePickerBuilder } from '@nextcloud/dialogs'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'\nimport CnChipsFieldWidget from '../CnChipsFieldWidget/CnChipsFieldWidget.vue'\nimport { pickRefLabel } from '../../composables/useRefLabels.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { changedFields, objectUrlOf, writeFields } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\n// The native file-picker modal ships its chrome styles here; without this the\n// spawned picker renders unstyled. Rules are scoped to the picker/dialog classes.\nimport '@nextcloud/dialogs/style.css'\n\nconst TYPES = ['text', 'textarea', 'select', 'switch', 'number', 'file', 'chips', 'reference']\n\nlet uid = 0\n\n/**\n * CnSettingsFormWidget — the `settings-form` widget: fields of the CURRENT\n * object as a one- or two-column grid of framed inputs with light labels and\n * hints, saved by ONE PATCH of the fields that changed since the last save.\n *\n * \"Last save\" is the baseline this widget keeps: a save moves it forward, so\n * editing a field back to the value the page loaded with, after a save, still\n * counts as a change (comparing against the loaded object skipped that PATCH).\n *\n * @spec openspec/changes/abstract-settings-form/specs/settings-form/spec.md\n */\nexport default {\n\tname: 'CnSettingsFormWidget',\n\n\tcomponents: { CnChipsFieldWidget, NcButton, NcCheckboxRadioSwitch, NcNoteCard },\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 * - `fields` (required) — `[{ key, label, type?: 'text'|'textarea'|'select'|'switch'|'number'|'file'|'chips'|'reference', register?, schema?, labelField?, valueTemplate?, filter?, limit?, emptyLabel?, default?, minItems?, required?, minItemsMessage?, lowercase?, addLabel?, confirmLabel?, removeLabel?, picker?: { title?, mimeTypes?, startAt?, valueTemplate? }, chooseLabel?, clearLabel?, hint?, placeholder?, options?: [{ value, label }], required?, pattern?, patternMessage?, min?, max?, rows?, wide?, readOnly? }]`; `key` is a field of the object, a top-level name or a dot-path.\n\t\t * - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).\n\t\t * - `columns` — 1 or 2 (default 2).\n\t\t * - `saveLabel`, `savedText`, `readOnly`.\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\tuid += 1\n\t\treturn {\n\t\t\tuid,\n\t\t\tform: {},\n\t\t\tbaseline: {},\n\t\t\tloaded: {},\n\t\t\t// `reference` fields: the objects to choose from and the read's state (`loading`, `ready`, `error`), by field key.\n\t\t\treferenceOptions: {},\n\t\t\treferenceState: {},\n\t\t\terrors: {},\n\t\t\tsaving: false,\n\t\t\tnotice: '',\n\t\t\tnoticeType: 'success',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The configured fields, normalised and translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tfields() {\n\t\t\tconst list = Array.isArray(this.content.fields) ? this.content.fields : []\n\t\t\treturn list\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\ttype: TYPES.includes(field.type) ? field.type : 'text',\n\t\t\t\t\tlabel: this.tr(field.label || field.key),\n\t\t\t\t\thint: this.tr(field.hint || ''),\n\t\t\t\t\tplaceholder: this.tr(field.placeholder || ''),\n\t\t\t\t\tchooseLabel: this.tr(field.chooseLabel || 'Choose a file'),\n\t\t\t\t\tclearLabel: this.tr(field.clearLabel || 'Remove'),\n\t\t\t\t\trows: field.rows || 3,\n\t\t\t\t\twide: field.wide === true || field.type === 'textarea',\n\t\t\t\t\treadOnly: this.readOnly || field.readOnly === true,\n\t\t\t\t\toptions: (Array.isArray(field.options) ? field.options : []).map((option) => (\n\t\t\t\t\t\ttypeof option === 'object' && option !== null\n\t\t\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)) }\n\t\t\t\t\t\t\t: { value: option, label: this.tr(String(option)) }\n\t\t\t\t\t)),\n\t\t\t\t}))\n\t\t},\n\n\t\tcolumns() {\n\t\t\treturn this.content.columns === 1 ? 1 : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\tsaveLabel() {\n\t\t\treturn this.tr(this.content.saveLabel || 'Save')\n\t\t},\n\n\t\t/** The object URL the form is saved to, or '' when it is not known. */\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt(true)\n\t\tthis.loadReferences()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the objects each `reference` field chooses from: one list read\n\t\t * per field of its `register` and `schema` (`filter` as query\n\t\t * parameters, at most `limit`, 100 by default). A failed read leaves the\n\t\t * field with its stored value only, and says so in the hint's place.\n\t\t *\n\t\t * @spec openspec/changes/screens-settings-form-reference/specs/settings-form/spec.md#requirement-a-settings-form-field-can-pick-an-object\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadReferences() {\n\t\t\tconst fields = this.fields.filter((field) => field.type === 'reference' && typeof field.register === 'string' && field.register !== '' && typeof field.schema === 'string' && field.schema !== '')\n\t\t\tif (fields.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\tawait Promise.all(fields.map(async (field) => {\n\t\t\t\tthis.referenceState = { ...this.referenceState, [field.key]: 'loading' }\n\t\t\t\ttry {\n\t\t\t\t\tconst url = `/apps/openregister/api/objects/${encodeURIComponent(field.register)}/${encodeURIComponent(field.schema)}`\n\t\t\t\t\tconst params = { ...(field.filter && typeof field.filter === 'object' ? field.filter : {}), _limit: Number(field.limit) > 0 ? Number(field.limit) : 100 }\n\t\t\t\t\tconst { data } = await axios.get(prefixUrl(url), { params })\n\t\t\t\t\tconst rows = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : [])\n\t\t\t\t\tconst options = []\n\t\t\t\t\tfor (const row of rows) {\n\t\t\t\t\t\tconst id = row && (row.id ?? row.uuid ?? (row['@self'] && row['@self'].id))\n\t\t\t\t\t\tif (id === undefined || id === null || id === '') {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst scope = { ...row, id, register: field.register, schema: field.schema }\n\t\t\t\t\t\tconst value = fillTemplate(typeof field.valueTemplate === 'string' && field.valueTemplate !== '' ? field.valueTemplate : '{id}', scope)\n\t\t\t\t\t\tif (value !== '') {\n\t\t\t\t\t\t\toptions.push({ value, label: pickRefLabel(row, field.labelField) || String(id) })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.referenceOptions = { ...this.referenceOptions, [field.key]: options }\n\t\t\t\t\tthis.referenceState = { ...this.referenceState, [field.key]: 'ready' }\n\t\t\t\t} catch {\n\t\t\t\t\tthis.referenceOptions = { ...this.referenceOptions, [field.key]: [] }\n\t\t\t\t\tthis.referenceState = { ...this.referenceState, [field.key]: 'error' }\n\t\t\t\t\tthis.errors = { ...this.errors, [field.key]: this.tr('The list to choose from could not be loaded.') }\n\t\t\t\t}\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The choices of a `reference` field: \"none\" (unless required), the\n\t\t * objects read, and the value the form holds when it is not one of\n\t\t * them (a free string such as a URL stays selectable, as it is stored).\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\treferenceChoices(field) {\n\t\t\tconst options = this.referenceOptions[field.key] || []\n\t\t\tconst current = this.form[field.key]\n\t\t\tconst choices = field.required ? [] : [{ value: '', label: this.tr(field.emptyLabel || 'None') }]\n\t\t\tif (typeof current === 'string' && current !== '' && !options.some((option) => option.value === current)) {\n\t\t\t\tchoices.push({ value: current, label: current })\n\t\t\t}\n\t\t\treturn [...choices, ...options]\n\t\t},\n\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\tinputId(field) {\n\t\t\treturn `cn-settings-form-${this.uid}-${field.key}`\n\t\t},\n\n\t\t/**\n\t\t * The name shown for a stored file value: its last path segment.\n\t\t *\n\t\t * @param {string} value The stored value.\n\t\t * @return {string}\n\t\t */\n\t\tfileName(value) {\n\t\t\tconst text = String(value)\n\t\t\treturn text.split('/').filter(Boolean).pop() || text\n\t\t},\n\n\t\t/**\n\t\t * Open the Nextcloud file picker and take the chosen file as the value.\n\t\t * `field.picker` = `{ title?, mimeTypes?, startAt?, valueTemplate? }`; the\n\t\t * template's `{path}`, `{fileid}` and `{name}` are filled from the chosen\n\t\t * node (default `{path}`).\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-settings-form-file/specs/settings-form/spec.md#requirement-a-file-field-takes-a-file-from-the-nextcloud-picker\n\t\t */\n\t\tasync chooseFile(field) {\n\t\t\tconst picker = field.picker || {}\n\t\t\tlet builder = getFilePickerBuilder(this.tr(picker.title || field.label))\n\t\t\t\t.setMultiSelect(false)\n\t\t\t\t.allowDirectories(false)\n\t\t\t\t.startAt(picker.startAt || '/')\n\t\t\t\t.addButton({ label: t('nextcloud-vue', 'Choose'), type: 'primary', callback: () => {} })\n\t\t\tif (Array.isArray(picker.mimeTypes) && picker.mimeTypes.length > 0) {\n\t\t\t\tbuilder = builder.setMimeTypeFilter(picker.mimeTypes)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst nodes = await builder.build().pickNodes()\n\t\t\t\tconst node = Array.isArray(nodes) ? nodes[0] : nodes\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.form[field.key] = String(picker.valueTemplate || '{path}').replace(/\\{(path|fileid|name)\\}/g, (match, part) => {\n\t\t\t\t\tconst value = part === 'name' ? (node.basename || this.fileName(node.path)) : node[part]\n\t\t\t\t\treturn value === undefined || value === null ? '' : String(value)\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof FilePickerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = this.tr('The file picker could not be opened.')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The stored value of a field as the input holds it.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\tstored(field) {\n\t\t\tconst value = readPath(this.objectData || {}, field.key)\n\t\t\tif (field.type === 'switch') {\n\t\t\t\treturn value === true\n\t\t\t}\n\t\t\tif (field.type === 'chips') {\n\t\t\t\treturn Array.isArray(value) ? value.map((item) => String(item)) : []\n\t\t\t}\n\t\t\treturn value === undefined || value === null ? '' : value\n\t\t},\n\n\t\t/**\n\t\t * The value the input shows for a field: the stored one, else the field's\n\t\t * `default` when nothing is stored. The baseline stays the stored value, so\n\t\t * a default that was never saved counts as a change and the next Save writes it.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\tshown(field) {\n\t\t\tconst value = this.stored(field)\n\t\t\tconst absent = readPath(this.objectData || {}, field.key) === undefined || readPath(this.objectData || {}, field.key) === null || value === '' || (field.type === 'chips' && value.length === 0)\n\t\t\tif (absent && field.default !== undefined) {\n\t\t\t\treturn field.type === 'chips' && Array.isArray(field.default) ? [...field.default] : field.default\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * Take the object's values as the new baseline; a field the person has\n\t\t * not touched follows it, a touched one keeps the typed value.\n\t\t *\n\t\t * @param {boolean} [first] Whether this is the first read.\n\t\t */\n\t\tadopt(first = false) {\n\t\t\tconst baseline = {}\n\t\t\tconst loaded = {}\n\t\t\tconst form = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tbaseline[field.key] = this.stored(field)\n\t\t\t\tloaded[field.key] = this.shown(field)\n\t\t\t\tconst untouched = first || JSON.stringify(this.form[field.key]) === JSON.stringify(this.loaded[field.key])\n\t\t\t\tform[field.key] = untouched ? loaded[field.key] : this.form[field.key]\n\t\t\t}\n\t\t\tthis.baseline = baseline\n\t\t\tthis.loaded = loaded\n\t\t\tthis.form = form\n\t\t},\n\n\t\t/**\n\t\t * The configuration of the embedded chips widget for a `chips` field.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {object}\n\t\t */\n\t\tchipsContent(field) {\n\t\t\treturn {\n\t\t\t\tlabel: field.label,\n\t\t\t\thint: field.hint,\n\t\t\t\tplaceholder: field.placeholder,\n\t\t\t\taddLabel: field.addLabel,\n\t\t\t\tconfirmLabel: field.confirmLabel,\n\t\t\t\tremoveLabel: field.removeLabel,\n\t\t\t\tpattern: field.pattern,\n\t\t\t\tpatternMessage: field.patternMessage,\n\t\t\t\tlowercase: field.lowercase,\n\t\t\t\toptions: field.options,\n\t\t\t\tminItems: field.minItems,\n\t\t\t\trequired: field.required,\n\t\t\t\tminItemsMessage: field.minItemsMessage,\n\t\t\t\treadOnly: field.readOnly,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The value to send for a field: numbers as numbers, an empty value as null.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\toutgoing(field) {\n\t\t\tconst value = this.form[field.key]\n\t\t\tif (field.type === 'number') {\n\t\t\t\treturn value === '' || value === null || value === undefined ? null : Number(value)\n\t\t\t}\n\t\t\tif (field.type === 'switch') {\n\t\t\t\treturn value === true\n\t\t\t}\n\t\t\tif (field.type === 'chips') {\n\t\t\t\treturn Array.isArray(value) ? [...value] : []\n\t\t\t}\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn value.trim() === '' ? null : value.trim()\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * Check required, number range and pattern before any write.\n\t\t *\n\t\t * @return {object} Error text by field key.\n\t\t */\n\t\tvalidate() {\n\t\t\tconst errors = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (field.type === 'switch' || field.readOnly) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst value = this.outgoing(field)\n\t\t\t\tif (field.type === 'chips') {\n\t\t\t\t\tconst least = Number(field.minItems) > 0 ? Number(field.minItems) : (field.required ? 1 : 0)\n\t\t\t\t\tif (value.length < least) {\n\t\t\t\t\t\terrors[field.key] = this.tr(field.minItemsMessage || 'Keep at least {count}.').replace('{count}', String(least))\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst empty = value === null || value === undefined || value === ''\n\t\t\t\tif (empty) {\n\t\t\t\t\tif (field.required) {\n\t\t\t\t\t\terrors[field.key] = this.tr('Fill this in.')\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (field.type === 'number') {\n\t\t\t\t\tif (Number.isNaN(value) || (field.min !== undefined && value < field.min) || (field.max !== undefined && value > field.max)) {\n\t\t\t\t\t\terrors[field.key] = this.tr('This number is not valid.')\n\t\t\t\t\t}\n\t\t\t\t} else if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\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\treturn errors\n\t\t},\n\n\t\t/**\n\t\t * Save: PATCH the fields that differ from the last saved values.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-settings-form/specs/settings-form/spec.md#requirement-a-settings-form-saves-only-what-changed\n\t\t */\n\t\tasync save() {\n\t\t\tthis.errors = this.validate()\n\t\t\tthis.notice = ''\n\t\t\tif (Object.keys(this.errors).length > 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.objectUrl && !(this.content.save && this.content.save.url)) {\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = this.tr('This form cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = {}\n\t\t\tconst compare = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (!field.readOnly) {\n\t\t\t\t\tnext[field.key] = this.outgoing(field)\n\t\t\t\t\tcompare[field.key] = this.baseline[field.key] === '' ? null : this.baseline[field.key]\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst patch = changedFields(compare, next)\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tif (Object.keys(patch).length > 0) {\n\t\t\t\t\tconst data = await writeFields(this.objectUrl, patch, { save: this.content.save, object: this.objectData })\n\t\t\t\t\tconst baseline = { ...this.baseline }\n\t\t\t\t\tconst loaded = { ...this.loaded }\n\t\t\t\t\tconst form = { ...this.form }\n\t\t\t\t\tfor (const key of Object.keys(patch)) {\n\t\t\t\t\t\tconst field = this.fields.find((candidate) => candidate.key === key)\n\t\t\t\t\t\tconst answered = readPath(data, key)\n\t\t\t\t\t\tconst written = answered !== undefined ? answered : patch[key]\n\t\t\t\t\t\tbaseline[key] = field.type === 'switch' ? written === true : (written === null ? (field.type === 'chips' ? [] : '') : written)\n\t\t\t\t\t\tform[key] = baseline[key]\n\t\t\t\t\t\tloaded[key] = baseline[key]\n\t\t\t\t\t}\n\t\t\t\t\tthis.baseline = baseline\n\t\t\t\t\tthis.loaded = loaded\n\t\t\t\t\tthis.form = form\n\t\t\t\t}\n\t\t\t\tthis.noticeType = 'success'\n\t\t\t\tthis.notice = this.tr(this.content.savedText || 'Saved.')\n\t\t\t\t/**\n\t\t\t\t * @event saved The changed fields were written.\n\t\t\t\t * @type {{patch: object}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { patch })\n\t\t\t} catch (e) {\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-settings-form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tmin-width: 0;\n}\n\n.cn-settings-form__grid {\n\tdisplay: grid;\n\tgap: 14px 16px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-settings-form__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-settings-form__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tmin-width: 0;\n}\n\n.cn-settings-form__field--wide {\n\tgrid-column: 1 / -1;\n}\n\n.cn-settings-form__label {\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-settings-form__control {\n\twidth: 100%;\n\tbox-sizing: border-box;\n\theight: var(--cn-board-control-height, 40px);\n\tpadding: 0 12px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont: inherit;\n}\n\n.cn-settings-form__file {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-settings-form__control--file {\n\tdisplay: inline-flex;\n\talign-items: center;\n\twidth: auto;\n\tmin-width: 120px;\n\tflex: 0 1 auto;\n\toverflow-wrap: anywhere;\n}\n\n.cn-settings-form__control--placeholder {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__control--area {\n\theight: auto;\n\tpadding: 10px 12px;\n\tresize: vertical;\n}\n\n.cn-settings-form__control:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 1px;\n}\n\n.cn-settings-form__hint,\n.cn-settings-form__error {\n\tmargin: 0;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__error {\n\tcolor: var(--color-error-text);\n}\n\n.cn-settings-form__actions {\n\tdisplay: flex;\n\tjustify-content: flex-start;\n}\n\n@media (max-width: 640px) {\n\t.cn-settings-form__grid--2 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_Fragment","_renderList","_createBlock","_createTextVNode","_toDisplayString","_createElementVNode","_openBlock"],"mappings":"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"}
1
+ {"version":3,"file":"CnSettingsFormWidget.vue3.js","sources":["../../../../src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue?vue&type=template&id=7325a0ee&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<form class=\"cn-settings-form\"\n\t\tdata-testid=\"cn-settings-form\"\n\t\tnovalidate\n\t\t@submit.prevent=\"save\">\n\t\t<div class=\"cn-settings-form__grid\" :class=\"`cn-settings-form__grid--${columns}`\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in fields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-settings-form__field\"\n\t\t\t\t:class=\"{ 'cn-settings-form__field--wide': field.wide }\"\n\t\t\t\t:data-testid=\"`cn-settings-form-field-${field.key}`\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-if=\"field.type === 'switch'\"\n\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t:disabled=\"field.readOnly\">\n\t\t\t\t\t{{ field.label }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<CnChipsFieldWidget\n\t\t\t\t\tv-else-if=\"field.type === 'chips'\"\n\t\t\t\t\t:modelValue=\"form[field.key]\"\n\t\t\t\t\t:content=\"chipsContent(field)\"\n\t\t\t\t\t@update:modelValue=\"form[field.key] = $event\" />\n\t\t\t\t<template v-else>\n\t\t\t\t\t<label class=\"cn-settings-form__label\" :for=\"inputId(field)\">{{ field.label }}</label>\n\t\t\t\t\t<div v-if=\"field.type === 'file'\" class=\"cn-settings-form__file\" :data-testid=\"`cn-settings-form-file-${field.key}`\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\t\tclass=\"cn-settings-form__control cn-settings-form__control--file\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-settings-form__control--placeholder': !form[field.key] }\">\n\t\t\t\t\t\t\t{{ form[field.key] ? fileName(form[field.key]) : field.placeholder }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"!field.readOnly\"\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t:aria-describedby=\"inputId(field)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-choose-${field.key}`\"\n\t\t\t\t\t\t\t@click=\"chooseFile(field)\">\n\t\t\t\t\t\t\t{{ field.chooseLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"!field.readOnly && form[field.key]\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-clear-${field.key}`\"\n\t\t\t\t\t\t\t@click=\"form[field.key] = ''\">\n\t\t\t\t\t\t\t{{ field.clearLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div v-else-if=\"field.type === 'reference'\" class=\"cn-settings-form__reference\">\n\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t\t:disabled=\"field.readOnly\"\n\t\t\t\t\t\t\t:aria-busy=\"referenceState[field.key] === 'loading' ? 'true' : null\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-reference-${field.key}`\">\n\t\t\t\t\t\t\t<option v-for=\"option in referenceChoices(field)\" :key=\"option.value\" :value=\"option.value\">\n\t\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t\t<!-- `free`: a typed value (a URL) besides picking an object. -->\n\t\t\t\t\t\t<template v-if=\"field.free && !field.readOnly\">\n\t\t\t\t\t\t\t<label class=\"cn-settings-form__sublabel\" :for=\"`${inputId(field)}-free`\">{{ field.freeLabel }}</label>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t:id=\"`${inputId(field)}-free`\"\n\t\t\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tinputmode=\"url\"\n\t\t\t\t\t\t\t\t:value=\"freeValue(field)\"\n\t\t\t\t\t\t\t\t:placeholder=\"field.freePlaceholder\"\n\t\t\t\t\t\t\t\t:aria-invalid=\"errors[field.key] ? 'true' : null\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-reference-free-${field.key}`\"\n\t\t\t\t\t\t\t\t@input=\"form[field.key] = $event.target.value.trim()\">\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</div>\n\t\t\t\t\t<select\n\t\t\t\t\t\tv-else-if=\"field.type === 'select'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:disabled=\"field.readOnly\">\n\t\t\t\t\t\t<option v-for=\"option in field.options\" :key=\"String(option.value)\" :value=\"option.value\">\n\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t</select>\n\t\t\t\t\t<textarea\n\t\t\t\t\t\tv-else-if=\"field.type === 'textarea'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control cn-settings-form__control--area\"\n\t\t\t\t\t\t:rows=\"field.rows\"\n\t\t\t\t\t\t:placeholder=\"field.placeholder\"\n\t\t\t\t\t\t:readonly=\"field.readOnly\" />\n\t\t\t\t\t<input\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:type=\"field.type === 'number' ? 'number' : 'text'\"\n\t\t\t\t\t\t:min=\"field.min\"\n\t\t\t\t\t\t:max=\"field.max\"\n\t\t\t\t\t\t:placeholder=\"field.placeholder\"\n\t\t\t\t\t\t:readonly=\"field.readOnly\"\n\t\t\t\t\t\t:aria-invalid=\"errors[field.key] ? 'true' : null\">\n\t\t\t\t</template>\n\t\t\t\t<p v-if=\"errors[field.key]\" class=\"cn-settings-form__error\" role=\"alert\">\n\t\t\t\t\t{{ errors[field.key] }}\n\t\t\t\t</p>\n\t\t\t\t<!-- A chips field prints its own hint (it gets it in chipsContent). -->\n\t\t\t\t<p v-else-if=\"field.hint && field.type !== 'chips'\" class=\"cn-settings-form__hint\">\n\t\t\t\t\t{{ field.hint }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcNoteCard v-if=\"notice\" :type=\"noticeType\" data-testid=\"cn-settings-form-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\n\t\t<div v-if=\"!readOnly\" class=\"cn-settings-form__actions\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\ttype=\"submit\"\n\t\t\t\t:disabled=\"saving\"\n\t\t\t\tdata-testid=\"cn-settings-form-save\">\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t</form>\n</template>\n\n<script>\nimport { FilePickerClosed, getFilePickerBuilder } from '@nextcloud/dialogs'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'\nimport CnChipsFieldWidget from '../CnChipsFieldWidget/CnChipsFieldWidget.vue'\nimport { pickRefLabel } from '../../composables/useRefLabels.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { changedFields, objectUrlOf, writeFields } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\nimport { switchIsOn, switchStoredValue } from '../../utils/switchValues.js'\n\n// The native file-picker modal ships its chrome styles here; without this the\n// spawned picker renders unstyled. Rules are scoped to the picker/dialog classes.\nimport '@nextcloud/dialogs/style.css'\n\nconst TYPES = ['text', 'textarea', 'select', 'switch', 'number', 'file', 'chips', 'reference']\n\nlet uid = 0\n\n/**\n * CnSettingsFormWidget — the `settings-form` widget: fields of the CURRENT\n * object as a one- or two-column grid of framed inputs with light labels and\n * hints, saved by ONE PATCH of the fields that changed since the last save.\n *\n * \"Last save\" is the baseline this widget keeps: a save moves it forward, so\n * editing a field back to the value the page loaded with, after a save, still\n * counts as a change (comparing against the loaded object skipped that PATCH).\n *\n * @spec openspec/changes/abstract-settings-form/specs/settings-form/spec.md\n */\nexport default {\n\tname: 'CnSettingsFormWidget',\n\n\tcomponents: { CnChipsFieldWidget, NcButton, NcCheckboxRadioSwitch, NcNoteCard },\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 * - `fields` (required) — `[{ key, label, type?: 'text'|'textarea'|'select'|'switch'|'number'|'file'|'chips'|'reference', register?, schema?, labelField?, valueTemplate?, filter?, limit?, emptyLabel?, free?, freeLabel?, freePlaceholder?, freePattern?, freePatternMessage?, default?, minItems?, required?, minItemsMessage?, values?: { on, off }, lowercase?, addLabel?, confirmLabel?, removeLabel?, picker?: { title?, mimeTypes?, startAt?, valueTemplate? }, chooseLabel?, clearLabel?, hint?, placeholder?, options?: [{ value, label }], required?, pattern?, patternMessage?, min?, max?, rows?, wide?, readOnly? }]`; `key` is a field of the object, a top-level name or a dot-path. A `reference` field's `filter` values may be `@objectId` and `@object.<field>` (the record being edited). A `switch` field's `values: { on, off }` reads and writes those values instead of true/false.\n\t\t * - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).\n\t\t * - `columns` — 1 or 2 (default 2).\n\t\t * - `saveLabel`, `savedText`, `readOnly`.\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\tuid += 1\n\t\treturn {\n\t\t\tuid,\n\t\t\tform: {},\n\t\t\tbaseline: {},\n\t\t\tloaded: {},\n\t\t\t// `reference` fields: the objects to choose from and the read's state (`loading`, `ready`, `error`), by field key.\n\t\t\treferenceOptions: {},\n\t\t\treferenceState: {},\n\t\t\t// The query each `reference` field was last read with, by field key, so a\n\t\t\t// new object re-reads only the fields whose filter changes with it.\n\t\t\treferenceQuery: {},\n\t\t\terrors: {},\n\t\t\tsaving: false,\n\t\t\tnotice: '',\n\t\t\tnoticeType: 'success',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The configured fields, normalised and translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tfields() {\n\t\t\tconst list = Array.isArray(this.content.fields) ? this.content.fields : []\n\t\t\treturn list\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\ttype: TYPES.includes(field.type) ? field.type : 'text',\n\t\t\t\t\tlabel: this.tr(field.label || field.key),\n\t\t\t\t\thint: this.tr(field.hint || ''),\n\t\t\t\t\tplaceholder: this.tr(field.placeholder || ''),\n\t\t\t\t\tchooseLabel: this.tr(field.chooseLabel || 'Choose a file'),\n\t\t\t\t\tfree: field.type === 'reference' && field.free === true,\n\t\t\t\t\tfreeLabel: this.tr(field.freeLabel || 'Or type a value'),\n\t\t\t\t\tfreePlaceholder: this.tr(field.freePlaceholder || ''),\n\t\t\t\t\tclearLabel: this.tr(field.clearLabel || 'Remove'),\n\t\t\t\t\trows: field.rows || 3,\n\t\t\t\t\twide: field.wide === true || field.type === 'textarea',\n\t\t\t\t\treadOnly: this.readOnly || field.readOnly === true,\n\t\t\t\t\toptions: (Array.isArray(field.options) ? field.options : []).map((option) => (\n\t\t\t\t\t\ttypeof option === 'object' && option !== null\n\t\t\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)) }\n\t\t\t\t\t\t\t: { value: option, label: this.tr(String(option)) }\n\t\t\t\t\t)),\n\t\t\t\t}))\n\t\t},\n\n\t\tcolumns() {\n\t\t\treturn this.content.columns === 1 ? 1 : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\tsaveLabel() {\n\t\t\treturn this.tr(this.content.saveLabel || 'Save')\n\t\t},\n\n\t\t/** The object URL the form is saved to, or '' when it is not known. */\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t\tthis.loadReferences()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt(true)\n\t\tthis.loadReferences()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the objects each `reference` field chooses from: one list read\n\t\t * per field of its `register` and `schema` (`filter` as query\n\t\t * parameters, at most `limit`, 100 by default). A failed read leaves the\n\t\t * field with its stored value only, and says so in the hint's place.\n\t\t *\n\t\t * `filter` values may be `@objectId` and `@object.<field>`: the record\n\t\t * the form edits, so a list can be narrowed to it (the logos of this\n\t\t * portal). Until the record has that value the field is not read; it is\n\t\t * read again when the record changes the resolved query, never twice\n\t\t * for the same query.\n\t\t *\n\t\t * @spec openspec/changes/screens-settings-form-reference/specs/settings-form/spec.md#requirement-a-settings-form-field-can-pick-an-object\n\t\t * @spec openspec/changes/screens-settings-form-reference-object-filter/specs/settings-form/spec.md#requirement-a-reference-fields-filter-can-name-the-record-being-edited\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadReferences() {\n\t\t\tconst fields = this.fields.filter((field) => field.type === 'reference' && typeof field.register === 'string' && field.register !== '' && typeof field.schema === 'string' && field.schema !== '')\n\t\t\tif (fields.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { objectId: this.objectId || (this.objectData && this.objectData.id) || undefined, object: this.objectData || undefined }\n\t\t\tconst pending = []\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst filter = resolveFilterTokens(field.filter && typeof field.filter === 'object' ? field.filter : {}, ctx)\n\t\t\t\tif (hasUnresolvedTokens(filter)) {\n\t\t\t\t\t// The record has no value for the token yet: nothing to read.\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst params = { ...filter, _limit: Number(field.limit) > 0 ? Number(field.limit) : 100 }\n\t\t\t\tconst query = JSON.stringify(params)\n\t\t\t\tif (this.referenceQuery[field.key] === query) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tthis.referenceQuery = { ...this.referenceQuery, [field.key]: query }\n\t\t\t\tpending.push({ field, params, query })\n\t\t\t}\n\t\t\tif (pending.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\tawait Promise.all(pending.map(async ({ field, params, query }) => {\n\t\t\t\tthis.referenceState = { ...this.referenceState, [field.key]: 'loading' }\n\t\t\t\ttry {\n\t\t\t\t\tconst url = `/apps/openregister/api/objects/${encodeURIComponent(field.register)}/${encodeURIComponent(field.schema)}`\n\t\t\t\t\tconst { data } = await axios.get(prefixUrl(url), { params })\n\t\t\t\t\tif (this.referenceQuery[field.key] !== query) {\n\t\t\t\t\t\t// A newer record asked for another list meanwhile.\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst rows = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : [])\n\t\t\t\t\tconst options = []\n\t\t\t\t\tfor (const row of rows) {\n\t\t\t\t\t\tconst id = row && (row.id ?? row.uuid ?? (row['@self'] && row['@self'].id))\n\t\t\t\t\t\tif (id === undefined || id === null || id === '') {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst scope = { ...row, id, register: field.register, schema: field.schema }\n\t\t\t\t\t\tconst value = fillTemplate(typeof field.valueTemplate === 'string' && field.valueTemplate !== '' ? field.valueTemplate : '{id}', scope)\n\t\t\t\t\t\tif (value !== '') {\n\t\t\t\t\t\t\toptions.push({ value, label: pickRefLabel(row, field.labelField) || String(id) })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.referenceOptions = { ...this.referenceOptions, [field.key]: options }\n\t\t\t\t\tthis.referenceState = { ...this.referenceState, [field.key]: 'ready' }\n\t\t\t\t} catch {\n\t\t\t\t\tthis.referenceOptions = { ...this.referenceOptions, [field.key]: [] }\n\t\t\t\t\tthis.referenceState = { ...this.referenceState, [field.key]: 'error' }\n\t\t\t\t\tthis.errors = { ...this.errors, [field.key]: this.tr('The list to choose from could not be loaded.') }\n\t\t\t\t}\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The choices of a `reference` field: \"none\" (unless required), the\n\t\t * objects read, and the value the form holds when it is not one of\n\t\t * them (a free string such as a URL stays selectable, as it is stored).\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\treferenceChoices(field) {\n\t\t\tconst options = this.referenceOptions[field.key] || []\n\t\t\tconst current = this.form[field.key]\n\t\t\tconst choices = field.required ? [] : [{ value: '', label: this.tr(field.emptyLabel || 'None') }]\n\t\t\tif (typeof current === 'string' && current !== '' && !options.some((option) => option.value === current)) {\n\t\t\t\tchoices.push({ value: current, label: current })\n\t\t\t}\n\t\t\treturn [...choices, ...options]\n\t\t},\n\n\t\t/**\n\t\t * The typed value of a `reference` field with `free`: the value the form\n\t\t * holds when it is not one of the objects to pick, else empty (picking\n\t\t * an object clears the typed value).\n\t\t *\n\t\t * @spec openspec/changes/screens-settings-form-reference-free-value/specs/settings-form/spec.md#requirement-a-reference-field-can-also-take-a-typed-value\n\t\t * @param {object} field The field.\n\t\t * @return {string}\n\t\t */\n\t\tfreeValue(field) {\n\t\t\tconst current = this.form[field.key]\n\t\t\tif (typeof current !== 'string' || current === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst options = this.referenceOptions[field.key] || []\n\t\t\treturn options.some((option) => option.value === current) ? '' : current\n\t\t},\n\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\tinputId(field) {\n\t\t\treturn `cn-settings-form-${this.uid}-${field.key}`\n\t\t},\n\n\t\t/**\n\t\t * The name shown for a stored file value: its last path segment.\n\t\t *\n\t\t * @param {string} value The stored value.\n\t\t * @return {string}\n\t\t */\n\t\tfileName(value) {\n\t\t\tconst text = String(value)\n\t\t\treturn text.split('/').filter(Boolean).pop() || text\n\t\t},\n\n\t\t/**\n\t\t * Open the Nextcloud file picker and take the chosen file as the value.\n\t\t * `field.picker` = `{ title?, mimeTypes?, startAt?, valueTemplate? }`; the\n\t\t * template's `{path}`, `{fileid}` and `{name}` are filled from the chosen\n\t\t * node (default `{path}`).\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-settings-form-file/specs/settings-form/spec.md#requirement-a-file-field-takes-a-file-from-the-nextcloud-picker\n\t\t */\n\t\tasync chooseFile(field) {\n\t\t\tconst picker = field.picker || {}\n\t\t\tlet builder = getFilePickerBuilder(this.tr(picker.title || field.label))\n\t\t\t\t.setMultiSelect(false)\n\t\t\t\t.allowDirectories(false)\n\t\t\t\t.startAt(picker.startAt || '/')\n\t\t\t\t.addButton({ label: t('nextcloud-vue', 'Choose'), type: 'primary', callback: () => {} })\n\t\t\tif (Array.isArray(picker.mimeTypes) && picker.mimeTypes.length > 0) {\n\t\t\t\tbuilder = builder.setMimeTypeFilter(picker.mimeTypes)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst nodes = await builder.build().pickNodes()\n\t\t\t\tconst node = Array.isArray(nodes) ? nodes[0] : nodes\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.form[field.key] = String(picker.valueTemplate || '{path}').replace(/\\{(path|fileid|name)\\}/g, (match, part) => {\n\t\t\t\t\tconst value = part === 'name' ? (node.basename || this.fileName(node.path)) : node[part]\n\t\t\t\t\treturn value === undefined || value === null ? '' : String(value)\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof FilePickerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = this.tr('The file picker could not be opened.')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The stored value of a field as the input holds it.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\tstored(field) {\n\t\t\tconst value = readPath(this.objectData || {}, field.key)\n\t\t\tif (field.type === 'switch') {\n\t\t\t\treturn switchIsOn(value, field.values)\n\t\t\t}\n\t\t\tif (field.type === 'chips') {\n\t\t\t\treturn Array.isArray(value) ? value.map((item) => String(item)) : []\n\t\t\t}\n\t\t\treturn value === undefined || value === null ? '' : value\n\t\t},\n\n\t\t/**\n\t\t * The value the input shows for a field: the stored one, else the field's\n\t\t * `default` when nothing is stored. The baseline stays the stored value, so\n\t\t * a default that was never saved counts as a change and the next Save writes it.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\tshown(field) {\n\t\t\tconst value = this.stored(field)\n\t\t\tconst absent = readPath(this.objectData || {}, field.key) === undefined || readPath(this.objectData || {}, field.key) === null || value === '' || (field.type === 'chips' && value.length === 0)\n\t\t\tif (absent && field.default !== undefined) {\n\t\t\t\treturn field.type === 'chips' && Array.isArray(field.default) ? [...field.default] : field.default\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * Take the object's values as the new baseline; a field the person has\n\t\t * not touched follows it, a touched one keeps the typed value.\n\t\t *\n\t\t * @param {boolean} [first] Whether this is the first read.\n\t\t */\n\t\tadopt(first = false) {\n\t\t\tconst baseline = {}\n\t\t\tconst loaded = {}\n\t\t\tconst form = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tbaseline[field.key] = this.stored(field)\n\t\t\t\tloaded[field.key] = this.shown(field)\n\t\t\t\tconst untouched = first || JSON.stringify(this.form[field.key]) === JSON.stringify(this.loaded[field.key])\n\t\t\t\tform[field.key] = untouched ? loaded[field.key] : this.form[field.key]\n\t\t\t}\n\t\t\tthis.baseline = baseline\n\t\t\tthis.loaded = loaded\n\t\t\tthis.form = form\n\t\t},\n\n\t\t/**\n\t\t * The configuration of the embedded chips widget for a `chips` field.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {object}\n\t\t */\n\t\tchipsContent(field) {\n\t\t\treturn {\n\t\t\t\tlabel: field.label,\n\t\t\t\thint: field.hint,\n\t\t\t\tplaceholder: field.placeholder,\n\t\t\t\taddLabel: field.addLabel,\n\t\t\t\tconfirmLabel: field.confirmLabel,\n\t\t\t\tremoveLabel: field.removeLabel,\n\t\t\t\tpattern: field.pattern,\n\t\t\t\tpatternMessage: field.patternMessage,\n\t\t\t\tlowercase: field.lowercase,\n\t\t\t\toptions: field.options,\n\t\t\t\tminItems: field.minItems,\n\t\t\t\trequired: field.required,\n\t\t\t\tminItemsMessage: field.minItemsMessage,\n\t\t\t\treadOnly: field.readOnly,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The value to send for a field: numbers as numbers, an empty value as null.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\toutgoing(field) {\n\t\t\tconst value = this.form[field.key]\n\t\t\tif (field.type === 'number') {\n\t\t\t\treturn value === '' || value === null || value === undefined ? null : Number(value)\n\t\t\t}\n\t\t\tif (field.type === 'switch') {\n\t\t\t\treturn value === true\n\t\t\t}\n\t\t\tif (field.type === 'chips') {\n\t\t\t\treturn Array.isArray(value) ? [...value] : []\n\t\t\t}\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn value.trim() === '' ? null : value.trim()\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * Check required, number range and pattern before any write.\n\t\t *\n\t\t * @return {object} Error text by field key.\n\t\t */\n\t\tvalidate() {\n\t\t\tconst errors = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (field.type === 'switch' || field.readOnly) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst value = this.outgoing(field)\n\t\t\t\tif (field.type === 'chips') {\n\t\t\t\t\tconst least = Number(field.minItems) > 0 ? Number(field.minItems) : (field.required ? 1 : 0)\n\t\t\t\t\tif (value.length < least) {\n\t\t\t\t\t\terrors[field.key] = this.tr(field.minItemsMessage || 'Keep at least {count}.').replace('{count}', String(least))\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst empty = value === null || value === undefined || value === ''\n\t\t\t\tif (empty) {\n\t\t\t\t\tif (field.required) {\n\t\t\t\t\t\terrors[field.key] = this.tr('Fill this in.')\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (field.type === 'number') {\n\t\t\t\t\tif (Number.isNaN(value) || (field.min !== undefined && value < field.min) || (field.max !== undefined && value > field.max)) {\n\t\t\t\t\t\terrors[field.key] = this.tr('This number is not valid.')\n\t\t\t\t\t}\n\t\t\t\t} else if (field.free && field.freePattern && this.freeValue(field) !== '' && !new RegExp(field.freePattern).test(String(value))) {\n\t\t\t\t\t// A typed value (not one of the objects) answers to `freePattern`.\n\t\t\t\t\terrors[field.key] = this.tr(field.freePatternMessage || 'This value is not valid.')\n\t\t\t\t} else if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\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\treturn errors\n\t\t},\n\n\t\t/**\n\t\t * Save: PATCH the fields that differ from the last saved values.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-settings-form/specs/settings-form/spec.md#requirement-a-settings-form-saves-only-what-changed\n\t\t */\n\t\tasync save() {\n\t\t\tthis.errors = this.validate()\n\t\t\tthis.notice = ''\n\t\t\tif (Object.keys(this.errors).length > 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.objectUrl && !(this.content.save && this.content.save.url)) {\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = this.tr('This form cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = {}\n\t\t\tconst compare = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (!field.readOnly) {\n\t\t\t\t\tnext[field.key] = this.outgoing(field)\n\t\t\t\t\tcompare[field.key] = this.baseline[field.key] === '' ? null : this.baseline[field.key]\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst patch = changedFields(compare, next)\n\t\t\t// A switch with a value map (`values: { on, off }`) writes those values.\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (field.type === 'switch' && Object.hasOwn(patch, field.key)) {\n\t\t\t\t\tpatch[field.key] = switchStoredValue(patch[field.key], field.values)\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tif (Object.keys(patch).length > 0) {\n\t\t\t\t\tconst data = await writeFields(this.objectUrl, patch, { save: this.content.save, object: this.objectData })\n\t\t\t\t\tconst baseline = { ...this.baseline }\n\t\t\t\t\tconst loaded = { ...this.loaded }\n\t\t\t\t\tconst form = { ...this.form }\n\t\t\t\t\tfor (const key of Object.keys(patch)) {\n\t\t\t\t\t\tconst field = this.fields.find((candidate) => candidate.key === key)\n\t\t\t\t\t\tconst answered = readPath(data, key)\n\t\t\t\t\t\tconst written = answered !== undefined ? answered : patch[key]\n\t\t\t\t\t\tbaseline[key] = field.type === 'switch' ? switchIsOn(written, field.values) : (written === null ? (field.type === 'chips' ? [] : '') : written)\n\t\t\t\t\t\tform[key] = baseline[key]\n\t\t\t\t\t\tloaded[key] = baseline[key]\n\t\t\t\t\t}\n\t\t\t\t\tthis.baseline = baseline\n\t\t\t\t\tthis.loaded = loaded\n\t\t\t\t\tthis.form = form\n\t\t\t\t}\n\t\t\t\tthis.noticeType = 'success'\n\t\t\t\tthis.notice = this.tr(this.content.savedText || 'Saved.')\n\t\t\t\t/**\n\t\t\t\t * @event saved The changed fields were written.\n\t\t\t\t * @type {{patch: object}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { patch })\n\t\t\t} catch (e) {\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-settings-form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tmin-width: 0;\n}\n\n.cn-settings-form__grid {\n\tdisplay: grid;\n\tgap: 14px 16px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-settings-form__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-settings-form__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tmin-width: 0;\n}\n\n.cn-settings-form__field--wide {\n\tgrid-column: 1 / -1;\n}\n\n.cn-settings-form__label {\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-settings-form__reference {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-settings-form__sublabel {\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__control {\n\twidth: 100%;\n\tbox-sizing: border-box;\n\theight: var(--cn-board-control-height, 40px);\n\tpadding: 0 12px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont: inherit;\n}\n\n.cn-settings-form__file {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-settings-form__control--file {\n\tdisplay: inline-flex;\n\talign-items: center;\n\twidth: auto;\n\tmin-width: 120px;\n\tflex: 0 1 auto;\n\toverflow-wrap: anywhere;\n}\n\n.cn-settings-form__control--placeholder {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__control--area {\n\theight: auto;\n\tpadding: 10px 12px;\n\tresize: vertical;\n}\n\n.cn-settings-form__control:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 1px;\n}\n\n.cn-settings-form__hint,\n.cn-settings-form__error {\n\tmargin: 0;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__error {\n\tcolor: var(--color-error-text);\n}\n\n.cn-settings-form__actions {\n\tdisplay: flex;\n\tjustify-content: flex-start;\n}\n\n@media (max-width: 640px) {\n\t.cn-settings-form__grid--2 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_Fragment","_renderList","_createBlock","_createTextVNode","_toDisplayString","_createElementVNode","_openBlock"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnStagesWidget.vue2.js';
2
2
  import { render } from './CnStagesWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-a394760a";
5
+ script.__scopeId = "data-v-3946def5";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -5,7 +5,7 @@ import { inject, ref } from 'vue';
5
5
  import __sfc_main$1 from '../../dialogs/CnTransitionInputDialog.vue.js';
6
6
  import __sfc_main from '../CnTimelineStages/CnTimelineStages.vue.js';
7
7
  import { useEndpointSource, getByPath } from '../../composables/useEndpointSource.js';
8
- import { actionsByTarget, isBlockedAction, actionNote, declaresInputs, performTransition, transitionError, readAvailableActions } from '../../composables/useLifecycleTransitions.js';
8
+ import { actionsByTarget, isBlockedAction, actionNote, declaresInputs, performTransition, transitionError, transitionFieldErrors, readAvailableActions } from '../../composables/useLifecycleTransitions.js';
9
9
  import { normalizeLook } from '../../composables/useLook.js';
10
10
  import { useObjectStore } from '../../store/useObjectStore.js';
11
11
  import { resolveObjectOpType } from '../../utils/actionsDispatcher.js';
@@ -353,6 +353,17 @@ var script = {
353
353
  pendingAction: null,
354
354
  /** @type {boolean} Whether a move is running. */
355
355
  busy: false,
356
+ /**
357
+ * @type {string} The stage a running move leads to, or ''. Drives
358
+ * the loading indicator on that one stage.
359
+ */
360
+ movingTo: '',
361
+ /** @type {string} A refusal shown inside the open input dialog. */
362
+ inputError: '',
363
+ /** @type {string[]} The input keys that refusal named. */
364
+ inputFieldErrors: [],
365
+ /** @type {string[]} The input keys the last refused move named. */
366
+ lastFieldErrors: [],
356
367
  /** @type {string} Why the last move failed, or ''. */
357
368
  moveError: '',
358
369
  /**
@@ -585,6 +596,20 @@ var script = {
585
596
  return this.interactive && !this.busy
586
597
  },
587
598
 
599
+ /**
600
+ * What the stage a move is running to says while it runs: the
601
+ * loading indicator's name and the live region's sentence.
602
+ *
603
+ * @return {string} The sentence, or '' when nothing is moving.
604
+ */
605
+ movingMessage() {
606
+ if (!this.movingTo) {
607
+ return ''
608
+ }
609
+ const stage = this.stages.find((s) => s.id === this.movingTo);
610
+ return this.tr('Moving to {stage}', { stage: stage ? stage.label : this.movingTo })
611
+ },
612
+
588
613
  /**
589
614
  * The allowed moves by the stage they lead to, or null while they are
590
615
  * unknown.
@@ -1044,13 +1069,44 @@ var script = {
1044
1069
  * @param {object} data The collected input values.
1045
1070
  * @return {void}
1046
1071
  */
1047
- onInputConfirm(data) {
1072
+ async onInputConfirm(data) {
1048
1073
  const pending = this.pendingAction;
1049
- this.pendingAction = null;
1050
1074
  if (!pending || !pending.__request) {
1075
+ this.closeInputDialog();
1076
+ return
1077
+ }
1078
+ this.inputError = '';
1079
+ this.inputFieldErrors = [];
1080
+ // THE DIALOG STAYS OPEN WHILE THE MOVE RUNS, with its confirm
1081
+ // button busy. Closing it first left the person looking at an
1082
+ // unchanged strip for as long as the transition took, which on a
1083
+ // live case was long enough to reload the page thinking it had
1084
+ // not been sent.
1085
+ const moved = await this.performMove(pending.__request, data);
1086
+ if (this.pendingAction !== pending) {
1087
+ // Closed while the move ran; nothing left to update.
1051
1088
  return
1052
1089
  }
1053
- this.performMove(pending.__request, data);
1090
+ if (moved) {
1091
+ this.closeInputDialog();
1092
+ return
1093
+ }
1094
+ // Refused. The sentence goes into the dialog the person is looking
1095
+ // at, with what they typed still in it.
1096
+ this.inputError = this.moveError;
1097
+ this.inputFieldErrors = this.lastFieldErrors;
1098
+ this.moveError = '';
1099
+ },
1100
+
1101
+ /**
1102
+ * Close the input dialog and forget the refusal it was showing.
1103
+ *
1104
+ * @return {void}
1105
+ */
1106
+ closeInputDialog() {
1107
+ this.pendingAction = null;
1108
+ this.inputError = '';
1109
+ this.inputFieldErrors = [];
1054
1110
  },
1055
1111
 
1056
1112
  /**
@@ -1058,11 +1114,17 @@ var script = {
1058
1114
  *
1059
1115
  * @param {{stage: {id: string, label: string}, action: string}} request The move.
1060
1116
  * @param {object} [data] The collected transition inputs, when any.
1061
- * @return {Promise<void>}
1117
+ * @return {Promise<boolean>} Whether the move was made.
1062
1118
  */
1063
1119
  async performMove(request, data) {
1064
1120
  this.busy = true;
1121
+ this.movingTo = request.stage.id;
1065
1122
  this.moveError = '';
1123
+ this.lastFieldErrors = [];
1124
+ // Said as the move starts, not only when it lands: a screen reader
1125
+ // user otherwise hears nothing for as long as the server takes.
1126
+ this.statusMessage = this.movingMessage;
1127
+ let moved = false;
1066
1128
  try {
1067
1129
  if (this.lifecycleMode) {
1068
1130
  await performTransition(this.recordId, request.action, data);
@@ -1089,11 +1151,16 @@ var script = {
1089
1151
  if (this.lifecycleMode) {
1090
1152
  await this.loadActions();
1091
1153
  }
1154
+ moved = true;
1092
1155
  } catch (error) {
1093
1156
  this.moveError = transitionError(error, this.tr('The move could not be made'));
1157
+ this.lastFieldErrors = transitionFieldErrors(error);
1158
+ this.statusMessage = this.moveError;
1094
1159
  } finally {
1095
1160
  this.busy = false;
1161
+ this.movingTo = '';
1096
1162
  }
1163
+ return moved
1097
1164
  },
1098
1165
 
1099
1166
  /**