@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,28 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnFieldListWidget — self-registers the `field-list` widget type (a list
6
+ * stored inside one field of the current object, shown as rows and edited
7
+ * with one PATCH of that field) into the shared dashboardWidgetRegistry at
8
+ * module load. Renderer only: declared in the manifest.
9
+ */
10
+
11
+ import CnFieldListWidget from './CnFieldListWidget.vue'
12
+ import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
13
+
14
+ registerDashboardWidget('field-list', {
15
+ renderer: CnFieldListWidget,
16
+ form: null,
17
+ defaultContent: {
18
+ field: '',
19
+ row: {},
20
+ readOnly: false,
21
+ },
22
+ displayName: 'Field list',
23
+ icon: 'FormatListBulleted',
24
+ surfaces: ['detail-page'],
25
+ })
26
+
27
+ export { CnFieldListWidget }
28
+ export default CnFieldListWidget
@@ -113,7 +113,7 @@ export default {
113
113
  | `allowDraft` | `false` | Offer a **Save draft** button that stores the record with a draft marker instead of validating it. Inert unless the schema declares `draftField`. |
114
114
  | `draftField` | `'isDraft'` | The boolean property that marks a record as a draft. |
115
115
  | `draftAppId` | `''` | The app the draft belongs to, for the storage key. |
116
- | `draftUserId` | `''` | Who is typing, for the storage key. **Pass this on any shared device.** |
116
+ | `draftUserId` | `''` | Who is typing, for the storage key. Empty: the signed-in Nextcloud user, and `anonymous` only when nobody is signed in. |
117
117
 
118
118
  #### What the draft does and does not do
119
119
 
@@ -122,9 +122,15 @@ Restore and Discard; the values go in only when somebody presses Restore. A form
122
122
  that filled itself would be indistinguishable from one the server prefilled, and
123
123
  somebody would submit last week's answers without knowing they were there.
124
124
 
125
- `draftUserId` is part of the storage key. A shared browser profile at a service
125
+ The user is part of the storage key. A shared browser profile at a service
126
126
  desk is the ordinary case in a municipality, and a draft keyed without the user
127
- hands the next person at the counter what the last one typed. Pass it.
127
+ hands the next person at the counter what the last one typed. Without
128
+ `draftUserId` the dialog uses the signed-in Nextcloud user. A draft stored
129
+ under `anonymous` is removed, never offered, once somebody is signed in.
130
+
131
+ A draft is written only after a person changes a field, and only while that
132
+ field differs from the value the form opened with. Opening a dialog, or the
133
+ form filling in values by itself, writes nothing and shows no Saving or Saved.
128
134
 
129
135
  A draft is dropped rather than offered when it is more than seven days old, when
130
136
  the record has been saved by somebody else since it was written, or when the
@@ -186,7 +186,7 @@
186
186
  :error="!!errors[field.key]"
187
187
  :type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
188
188
  :disabled="field.readOnly"
189
- :placeholder="field.description"
189
+ :placeholder="fieldPlaceholder(field)"
190
190
  @update:modelValue="value => updateField(field.key, value)" />
191
191
  <CnFieldHelper
192
192
  :id="helpId(field)"
@@ -208,7 +208,7 @@
208
208
  :error="!!errors[field.key]"
209
209
  type="number"
210
210
  :disabled="field.readOnly"
211
- :placeholder="field.description"
211
+ :placeholder="fieldPlaceholder(field)"
212
212
  @update:modelValue="value => updateField(field.key, value !== '' ? Number(value) : null)" />
213
213
  <CnFieldHelper
214
214
  :id="helpId(field)"
@@ -230,7 +230,7 @@
230
230
  v-bind="fieldAria(field)"
231
231
  :value="formData[field.key] || ''"
232
232
  :disabled="field.readOnly"
233
- :placeholder="field.description"
233
+ :placeholder="fieldPlaceholder(field)"
234
234
  rows="4"
235
235
  @input="updateField(field.key, $event.target.value)" />
236
236
  <CnFieldHelper
@@ -652,7 +652,7 @@
652
652
  :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
653
653
  :error="!!errors[field.key]"
654
654
  :disabled="field.readOnly"
655
- :placeholder="field.description"
655
+ :placeholder="fieldPlaceholder(field)"
656
656
  @update:modelValue="value => updateField(field.key, value)" />
657
657
  <CnFieldHelper
658
658
  :id="helpId(field)"
@@ -787,7 +787,7 @@ import CnPropertySourceField from '../CnPropertySourceField/CnPropertySourceFiel
787
787
  import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
788
788
  import CnSubObjectsField from '../CnSubObjectsField/CnSubObjectsField.vue'
789
789
  import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js'
790
- import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
790
+ import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
791
791
  import { heldFromFailures, splitHeldFiles, uploadHeldFiles } from '../../composables/useHeldFileUpload.js'
792
792
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
793
793
  import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
@@ -1172,16 +1172,16 @@ export default {
1172
1172
  * 🔴 THE USER IS PART OF THE DRAFT KEY. A shared browser profile at a
1173
1173
  * service desk is the ordinary case in a municipality, and a draft
1174
1174
  * keyed without the user hands the next person at the counter what the
1175
- * last one typed. A host that passes nothing gets `anonymous`, which
1176
- * is correct for a single-user context and wrong for a counter — so
1177
- * the prop exists and is documented rather than guessed at.
1175
+ * last one typed. A host that passes nothing gets the signed-in
1176
+ * Nextcloud user (`@nextcloud/auth`), and `anonymous` only when
1177
+ * nobody is signed in.
1178
1178
  */
1179
1179
  draftAppId: {
1180
1180
  type: String,
1181
1181
  default: '',
1182
1182
  },
1183
1183
 
1184
- /** Who is typing, for the draft key. See `draftAppId`. */
1184
+ /** Who is typing, for the draft key. Empty: the signed-in user. See `draftAppId`. */
1185
1185
  draftUserId: {
1186
1186
  type: String,
1187
1187
  default: '',
@@ -1442,10 +1442,20 @@ export default {
1442
1442
  appId: this.draftAppId,
1443
1443
  schema: (this.schema?.slug || this.schema?.title || ''),
1444
1444
  objectId: (this.item?.id || this.item?.uuid || ''),
1445
- userId: this.draftUserId,
1445
+ userId: this.draftUserOwner,
1446
1446
  })
1447
1447
  },
1448
1448
 
1449
+ /**
1450
+ * Who the draft belongs to: `draftUserId`, else the signed-in user.
1451
+ *
1452
+ * @return {string} The user id, or '' when nobody is signed in.
1453
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
1454
+ */
1455
+ draftUserOwner() {
1456
+ return resolveDraftUserId(this.draftUserId)
1457
+ },
1458
+
1449
1459
  /**
1450
1460
  * Whether "Save draft" may be offered.
1451
1461
  *
@@ -1757,17 +1767,6 @@ export default {
1757
1767
  formData: {
1758
1768
  deep: true,
1759
1769
  handler(values) {
1760
- // 🔴 THE FIRST CHANGE IS THE COMPONENT SEEDING THE FORM, NOT
1761
- // SOMEBODY TYPING. Writing a draft for it stores the empty
1762
- // form over a real one, and the indicator announces "Saving
1763
- // draft" about work nobody did — an instrument reporting
1764
- // activity that did not happen, which is the whole class of
1765
- // defect this library keeps paying down.
1766
- if (this.draftSeeded !== true) {
1767
- this.draftSeeded = true
1768
- return
1769
- }
1770
-
1771
1770
  // Nothing is written while a draft is being offered: the user
1772
1771
  // has not chosen yet, and overwriting the stored draft with the
1773
1772
  // empty form they are looking at would destroy the thing they
@@ -1776,6 +1775,19 @@ export default {
1776
1775
  return
1777
1776
  }
1778
1777
 
1778
+ // 🔴 ONLY A PERSON'S CHANGE IS WORTH A DRAFT. The form fills
1779
+ // in values by itself after it opens (an input reporting its
1780
+ // value back, normalised dates, defaults, the organisation),
1781
+ // and each of those used to write a draft and say "Saved just
1782
+ // now" on a dialog nobody had touched. A field counts once a
1783
+ // person edited it AND it differs from what the form opened
1784
+ // with, so undoing the edit drops the draft again.
1785
+ const edited = Object.keys(this.touchedFields || {}).filter((key) => this.touchedFields[key])
1786
+ if (!hasDraftChanges(values, this.draftBaseline, edited)) {
1787
+ this.settleUnchangedDraft(this.formDraftKey)
1788
+ return
1789
+ }
1790
+
1779
1791
  this.scheduleDraftWrite(this.formDraftKey, values)
1780
1792
  },
1781
1793
  },
@@ -1940,6 +1952,12 @@ export default {
1940
1952
  // Silently refilling a form is how somebody submits last week's answers
1941
1953
  // without noticing they were there, and a form that fills itself is
1942
1954
  // indistinguishable from one the server prefilled.
1955
+ // A draft stored under `anonymous` was written by a page that did not
1956
+ // know who was typing. Once somebody is signed in it is removed, never
1957
+ // offered: it may be another person's.
1958
+ if (this.formDraftKey && this.draftUserOwner) {
1959
+ clearAnonymousDrafts()
1960
+ }
1943
1961
  if (this.formDraftKey) {
1944
1962
  this.draftOffer = readDraft(this.formDraftKey, {
1945
1963
  objectUpdated: (this.item?.updated || this.item?.['@self']?.updated || ''),
@@ -2298,6 +2316,10 @@ export default {
2298
2316
  this.runAssignments(null)
2299
2317
  this.loadProfileDefaults()
2300
2318
  }
2319
+ // What the form opened with. A draft is written only once a field
2320
+ // a person edited differs from this (see the `formData` watcher).
2321
+ // Not reactive: nothing renders from it.
2322
+ this.draftBaseline = JSON.parse(JSON.stringify(this.formData || {}))
2301
2323
  },
2302
2324
 
2303
2325
  /**
@@ -3085,6 +3107,27 @@ export default {
3085
3107
  return this.showsOuterLabel(field) ? '' : this.errors[field.key]
3086
3108
  },
3087
3109
 
3110
+ /**
3111
+ * The placeholder of a text, number or textarea control.
3112
+ *
3113
+ * The field's description already shows as the help line under the
3114
+ * control, so the placeholder stays empty rather than repeat it word
3115
+ * for word. The help line is the one kept: it is still there after
3116
+ * somebody typed (a placeholder disappears with the first keystroke),
3117
+ * it is read out through `aria-describedby`, and in the Nextcloud look
3118
+ * a placeholder is hidden until the field has focus anyway. While an
3119
+ * error takes the help line's place, the description returns as the
3120
+ * placeholder so it is not lost.
3121
+ *
3122
+ * @param {object} field The field definition.
3123
+ * @return {string} The placeholder, or '' when the help line says it.
3124
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-field-description-shows-once
3125
+ */
3126
+ fieldPlaceholder(field) {
3127
+ const description = field.description || ''
3128
+ return this.helperError(field) ? description : ''
3129
+ },
3130
+
3088
3131
  /**
3089
3132
  * The accessibility attributes of a text control. In both looks an invalid
3090
3133
  * control is `aria-invalid` and `aria-describedby` lists the error before
@@ -4567,6 +4610,11 @@ export default {
4567
4610
  }
4568
4611
 
4569
4612
  this.formData = { ...this.formData, ...this.draftOffer.values }
4613
+ // What the person typed last time is their edit now: it counts for
4614
+ // the next draft write and survives the record arriving late.
4615
+ for (const key of Object.keys(this.draftOffer.values)) {
4616
+ this.touchedFields[key] = true
4617
+ }
4570
4618
  this.draftOffer = null
4571
4619
  },
4572
4620
 
@@ -397,7 +397,7 @@ import CnFormErrorSummary from '../CnFormErrorSummary/CnFormErrorSummary.vue'
397
397
  import CnFormField from '../CnFormField/CnFormField.vue'
398
398
  import CnStepper from '../CnStepper/CnStepper.vue'
399
399
  import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
400
- import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
400
+ import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
401
401
  import { normalizeLook } from '../../composables/useLook.js'
402
402
  import { cnSelectAria } from '../../directives/cnSelectAria.js'
403
403
  import { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'
@@ -529,7 +529,7 @@ export default {
529
529
  default: '',
530
530
  },
531
531
 
532
- /** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. */
532
+ /** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. Empty: the signed-in Nextcloud user. */
533
533
  draftUserId: {
534
534
  type: String,
535
535
  default: '',
@@ -949,7 +949,17 @@ export default {
949
949
  if (this.recoverDraft !== true) {
950
950
  return ''
951
951
  }
952
- return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserId })
952
+ return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserOwner })
953
+ },
954
+
955
+ /**
956
+ * Who the draft belongs to: `draftUserId`, else the signed-in user.
957
+ *
958
+ * @return {string} The user id, or '' when nobody is signed in.
959
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
960
+ */
961
+ draftUserOwner() {
962
+ return resolveDraftUserId(this.draftUserId)
953
963
  },
954
964
 
955
965
  /**
@@ -997,6 +1007,11 @@ export default {
997
1007
  this.calcTimers = {}
998
1008
  // Offered, never applied: a form that fills itself is indistinguishable
999
1009
  // from one the server prefilled.
1010
+ // A draft under `anonymous` may be somebody else's: removed once a
1011
+ // signed-in user is known, never offered.
1012
+ if (this.formDraftKey && this.draftUserOwner) {
1013
+ clearAnonymousDrafts()
1014
+ }
1000
1015
  if (this.formDraftKey) {
1001
1016
  this.draftOffer = readDraft(this.formDraftKey)
1002
1017
  }
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cn-index-page"
3
- :class="lookClass"
3
+ :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel }]"
4
4
  data-testid="cn-index-page"
5
5
  @keydown="onListKeydown">
6
6
  <!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
@@ -76,6 +76,20 @@
76
76
  <slot name="below-header" />
77
77
  </div>
78
78
 
79
+ <!-- KPI tiles above the list (`statRow`) and cards beside it
80
+ (`sidePanel`), as DqTeamwachtrij and PqContracten
81
+ (screens-index-stat-row). -->
82
+ <CnIndexPageWidgets
83
+ v-if="statRow && statRow.length > 0"
84
+ class="cn-index-page__stat-row"
85
+ variant="stat-row"
86
+ :widgets="statRow" />
87
+ <CnIndexPageWidgets
88
+ v-if="hasSidePanel"
89
+ class="cn-index-page__side-panel"
90
+ variant="side-panel"
91
+ :widgets="sidePanel" />
92
+
79
93
  <!-- Actions bar -->
80
94
  <CnActionsBar
81
95
  :layout="isBoardLook ? 'board' : 'nextcloud'"
@@ -97,6 +111,7 @@
97
111
  :viewMode="currentViewMode"
98
112
  :showViewToggle="showViewToggle"
99
113
  :availableViewModes="effectiveToggleModes"
114
+ :disabledViewModes="viewSwitchDisabledModes"
100
115
  :cardsLabel="cardsLabel"
101
116
  :tableLabel="tableLabel"
102
117
  :listLabel="listLabel"
@@ -959,6 +974,7 @@ import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
959
974
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
960
975
  import CnEmptyContent from '../CnEmptyContent/CnEmptyContent.vue'
961
976
  import CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'
977
+ import CnIndexPageWidgets from './CnIndexPageWidgets.vue'
962
978
  import { useContextMenu } from '../../composables/index.js'
963
979
  import { useLook } from '../../composables/useLook.js'
964
980
  import { copyKindsOf } from '../../composables/useObjectCopy.js'
@@ -1243,6 +1259,7 @@ export default {
1243
1259
  name: 'CnIndexPage',
1244
1260
 
1245
1261
  components: {
1262
+ CnIndexPageWidgets,
1246
1263
  CnBuildiqEditButton,
1247
1264
  CnFollowToggle,
1248
1265
  NcLoadingIcon,
@@ -1587,7 +1604,10 @@ export default {
1587
1604
  * `$route.params` just like the `filter` prop. The first tab with
1588
1605
  * `default:true` (else index 0) is active on mount; changing tabs
1589
1606
  * re-fetches at page 1. Omit (or `null`) → no tab strip, behaviour
1590
- * unchanged.
1607
+ * unchanged. Under the board look a tab with `activeLabel` ("Pipeline:
1608
+ * Sales") adds a removable chip with that text to the Active line and
1609
+ * counts on the Filter badge while it is selected; removing the chip
1610
+ * goes back to the default tab.
1591
1611
  */
1592
1612
  quickFilters: {
1593
1613
  type: Array,
@@ -1958,6 +1978,22 @@ export default {
1958
1978
  default: null,
1959
1979
  },
1960
1980
 
1981
+ /**
1982
+ * Board look: the segments the view switch draws, e.g. `['table',
1983
+ * 'cards', 'board', 'map']` as the boards do. A listed mode the page
1984
+ * can open is an ordinary segment; one it cannot (no board columns, no
1985
+ * map configured) is drawn disabled, named "<mode>: not available on
1986
+ * this list". Fed from `pages[].config.viewSwitch`. Ignored without the
1987
+ * board look, and absent the switch is as before.
1988
+ *
1989
+ * @type {Array<'table' | 'cards' | 'board' | 'map'>}
1990
+ * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
1991
+ */
1992
+ viewSwitch: {
1993
+ type: Array,
1994
+ default: null,
1995
+ },
1996
+
1961
1997
  /**
1962
1998
  * The board's configuration, mirroring the manifest `config.board`
1963
1999
  * block: `{ statusField, cardFields, swimlaneField }`. The board is
@@ -2644,6 +2680,35 @@ export default {
2644
2680
  default: null,
2645
2681
  },
2646
2682
 
2683
+ /**
2684
+ * KPI tiles above the list (manifest `config.statRow`): dashboard
2685
+ * widget definitions `{ id, type, title?, content }`, usually `stat`,
2686
+ * drawn in an auto-fit row (each at least 200px) in the listed order.
2687
+ * Types resolve like dashboard widgets: the app registry, then the
2688
+ * dashboard catalog. Empty by default.
2689
+ *
2690
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-stat-row-above-the-list
2691
+ * @type {Array<object>}
2692
+ */
2693
+ statRow: {
2694
+ type: Array,
2695
+ default: () => [],
2696
+ },
2697
+
2698
+ /**
2699
+ * Cards beside the list (manifest `config.sidePanel`): dashboard
2700
+ * widget definitions `{ id, type, title?, content, headerLink? }`
2701
+ * drawn in a 300px column right of the toolbar and the list. Empty
2702
+ * by default; below 1024px the column moves above the list.
2703
+ *
2704
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-side-panel-beside-the-list
2705
+ * @type {Array<object>}
2706
+ */
2707
+ sidePanel: {
2708
+ type: Array,
2709
+ default: () => [],
2710
+ },
2711
+
2647
2712
  /**
2648
2713
  * Where a row carries the actions the server says this caller may run
2649
2714
  * on it: a dotted path, read off the rows the list already fetched, so
@@ -3323,6 +3388,16 @@ export default {
3323
3388
  },
3324
3389
 
3325
3390
  computed: {
3391
+ /**
3392
+ * Whether the page draws cards beside its list (`sidePanel`).
3393
+ *
3394
+ * @spec openspec/changes/screens-index-stat-row/specs/index-page/spec.md#requirement-an-index-page-can-draw-a-side-panel-beside-the-list
3395
+ * @return {boolean}
3396
+ */
3397
+ hasSidePanel() {
3398
+ return Array.isArray(this.sidePanel) && this.sidePanel.length > 0
3399
+ },
3400
+
3326
3401
  /**
3327
3402
  * The empty-state copy run through the host translate function
3328
3403
  * (the injected `cnTranslate`, identity by default). Only this
@@ -3695,6 +3770,21 @@ export default {
3695
3770
  return Object.keys(this.mapConfig || {}).length > 0
3696
3771
  },
3697
3772
 
3773
+ /**
3774
+ * Board look: the modes `viewSwitch` lists that this page cannot open,
3775
+ * drawn as disabled segments.
3776
+ *
3777
+ * @return {Array<string>} The disabled modes.
3778
+ * @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
3779
+ */
3780
+ viewSwitchDisabledModes() {
3781
+ if (!this.isBoardLook || !Array.isArray(this.viewSwitch)) {
3782
+ return []
3783
+ }
3784
+ const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
3785
+ return this.viewSwitch.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
3786
+ },
3787
+
3698
3788
  /**
3699
3789
  * The base view-toggle segments (cards/table/list) passed to CnActionsBar.
3700
3790
  * Beta's explicit `viewModes` whitelist wins when set; otherwise the
@@ -5264,11 +5354,11 @@ export default {
5264
5354
  return []
5265
5355
  }
5266
5356
  const props = this.effectiveSchema?.properties || {}
5267
- const chips = []
5357
+ const chips = [...this.quickFilterActiveChips]
5268
5358
  for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
5269
5359
  const values = (Array.isArray(raw) ? raw : [raw])
5270
5360
  .filter((value) => value !== null && value !== undefined && value !== '')
5271
- .map((value) => String(value))
5361
+ .map((value) => this.activeFilterValueLabel(key, value))
5272
5362
  if (values.length === 0) {
5273
5363
  continue
5274
5364
  }
@@ -5278,6 +5368,87 @@ export default {
5278
5368
  return chips
5279
5369
  },
5280
5370
 
5371
+ /**
5372
+ * The Active line's chips for the selected quick filters: one per
5373
+ * selected tab that declares an `activeLabel` ("Pipeline: Sales"), run
5374
+ * through the app's translate. A tab without one (an "All" tab) adds no
5375
+ * chip. The chip carries the tab's index so removing it can deselect it.
5376
+ *
5377
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-a-quick-filter-with-an-active-label-joins-the-active-line
5378
+ * @return {Array<{key: string, label: string, quickFilterIndex: number}>}
5379
+ */
5380
+ quickFilterActiveChips() {
5381
+ const tabs = this.tabStripEntries || []
5382
+ if (tabs.length === 0) {
5383
+ return []
5384
+ }
5385
+ const indices = this.quickFilterMultiple
5386
+ ? (this.selectedQuickFilterIndices || [])
5387
+ : [this.activeQuickFilterIndex]
5388
+ return indices
5389
+ .filter((index) => typeof index === 'number' && tabs[index] && typeof tabs[index].activeLabel === 'string' && tabs[index].activeLabel !== '')
5390
+ .map((index) => ({ key: `quick-filter:${index}`, label: this.cnTranslate(tabs[index].activeLabel), quickFilterIndex: index }))
5391
+ },
5392
+
5393
+ /**
5394
+ * Filter keys over a reference property that the Active line must name,
5395
+ * and that no reference column resolves already: one spec each, with the
5396
+ * referenced register and schema off the page schema's `$ref`.
5397
+ *
5398
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-an-active-chip-names-the-value-not-its-id
5399
+ * @return {Array<{key: string, labelField: string, register: string, schema: string}>}
5400
+ */
5401
+ activeFilterRefSpecs() {
5402
+ if (!this.isBoardLook || !this.effectiveSchema) {
5403
+ return []
5404
+ }
5405
+ const keys = Object.keys(this.effectiveActiveFilters || {})
5406
+ if (keys.length === 0) {
5407
+ return []
5408
+ }
5409
+ const covered = new Set(this.refLabelSpecs.map((spec) => spec.key))
5410
+ const fields = fieldsFromSchema(this.effectiveSchema, { includeReadOnly: true })
5411
+ const specs = []
5412
+ for (const key of keys) {
5413
+ if (covered.has(key)) {
5414
+ continue
5415
+ }
5416
+ const field = fields.find((f) => f.key === key)
5417
+ const ref = field && field.reference
5418
+ if (!ref || ref.schema === undefined || ref.schema === null) {
5419
+ continue
5420
+ }
5421
+ specs.push({
5422
+ key,
5423
+ labelField: '',
5424
+ register: ref.register || (typeof this.register === 'string' ? this.register : ''),
5425
+ schema: String(ref.schema),
5426
+ })
5427
+ }
5428
+ return specs
5429
+ },
5430
+
5431
+ /**
5432
+ * The ids each active reference filter holds, per filter key, for the
5433
+ * label lookup of the Active line.
5434
+ *
5435
+ * @return {{[key: string]: string[]}} Distinct ids per filter key.
5436
+ */
5437
+ activeFilterRefIds() {
5438
+ const out = {}
5439
+ const keys = new Set([...this.refLabelSpecs, ...this.activeFilterRefSpecs].map((spec) => spec.key))
5440
+ for (const [key, raw] of Object.entries(this.effectiveActiveFilters || {})) {
5441
+ if (!keys.has(key)) {
5442
+ continue
5443
+ }
5444
+ out[key] = (Array.isArray(raw) ? raw : [raw])
5445
+ .filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')
5446
+ .map((id) => String(id))
5447
+ .sort()
5448
+ }
5449
+ return out
5450
+ },
5451
+
5281
5452
  /**
5282
5453
  * The plural the board bulk band names: "With the selected cases".
5283
5454
  * The schema's `titlePlural` (or `plural`), else the lower-cased
@@ -5558,6 +5729,14 @@ export default {
5558
5729
  },
5559
5730
  },
5560
5731
 
5732
+ // The Active line names a referenced value, not its id.
5733
+ activeFilterRefIds: {
5734
+ deep: true,
5735
+ handler() {
5736
+ this.loadRefLabels()
5737
+ },
5738
+ },
5739
+
5561
5740
  countRequestKey: {
5562
5741
  immediate: true,
5563
5742
  /** @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views */
@@ -5826,7 +6005,7 @@ export default {
5826
6005
  * @spec openspec/changes/index-ref-column-labels/tasks.md#task-2
5827
6006
  */
5828
6007
  async loadRefLabels() {
5829
- const specs = this.refLabelSpecs
6008
+ const specs = [...this.refLabelSpecs, ...this.activeFilterRefSpecs]
5830
6009
  if (specs.length === 0) {
5831
6010
  return
5832
6011
  }
@@ -5841,7 +6020,8 @@ export default {
5841
6020
  }
5842
6021
  await Promise.all(specs.map(async (spec) => {
5843
6022
  const known = this.refLabels[spec.key] || {}
5844
- const ids = (this.refLabelIds[spec.key] || []).filter((id) => !Object.hasOwn(known, id))
6023
+ const wanted = new Set([...(this.refLabelIds[spec.key] || []), ...(this.activeFilterRefIds[spec.key] || [])])
6024
+ const ids = [...wanted].filter((id) => !Object.hasOwn(known, id))
5845
6025
  if (ids.length === 0) {
5846
6026
  return
5847
6027
  }
@@ -6525,6 +6705,11 @@ export default {
6525
6705
  * @return {void}
6526
6706
  */
6527
6707
  onClearFilters() {
6708
+ // "Clear all" on the Active line also undoes the quick filters it
6709
+ // listed (a tab without an `activeLabel` is not a filter there).
6710
+ for (const chip of this.quickFilterActiveChips) {
6711
+ this.deselectQuickFilterChip(chip.quickFilterIndex)
6712
+ }
6528
6713
  // Clearing the view brings the person's own columns back.
6529
6714
  this.appliedViewColumns = null
6530
6715
  this.appliedSavedViewId = ''
@@ -6615,11 +6800,74 @@ export default {
6615
6800
  * @spec openspec/changes/screens-index-list-parity/specs/index-list-board-look/spec.md#requirement-the-toolbar-sits-on-the-ground-in-two-rows
6616
6801
  */
6617
6802
  onRemoveActiveFilter(chip) {
6803
+ if (chip && typeof chip.quickFilterIndex === 'number') {
6804
+ this.deselectQuickFilterChip(chip.quickFilterIndex)
6805
+ return
6806
+ }
6618
6807
  if (chip && typeof chip.key === 'string') {
6619
6808
  this.onFilterEvent({ key: chip.key, values: [] })
6620
6809
  }
6621
6810
  },
6622
6811
 
6812
+ /**
6813
+ * Undo a quick filter from the Active line. With several tabs selectable
6814
+ * the tab leaves the selection; with one, the page goes back to its
6815
+ * default tab when that tab names no active filter itself, and else to no
6816
+ * tab at all, so the chip does not come straight back.
6817
+ *
6818
+ * @param {number} index The tab index the chip stands for.
6819
+ * @return {void}
6820
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-a-quick-filter-with-an-active-label-joins-the-active-line
6821
+ */
6822
+ deselectQuickFilterChip(index) {
6823
+ if (this.quickFilterMultiple) {
6824
+ this.onQuickFilterMultiChange((this.selectedQuickFilterIndices || []).filter((i) => i !== index))
6825
+ return
6826
+ }
6827
+ if (this.activeQuickFilterIndex !== index) {
6828
+ return
6829
+ }
6830
+ const tabs = this.tabStripEntries || []
6831
+ let fallback = tabs.findIndex((tab) => tab && tab.default === true)
6832
+ if (fallback < 0 && tabs.length > 0) {
6833
+ fallback = 0
6834
+ }
6835
+ const fallbackNamed = fallback >= 0 && typeof tabs[fallback].activeLabel === 'string' && tabs[fallback].activeLabel !== ''
6836
+ this.onQuickFilterChange(fallback >= 0 && fallback !== index && !fallbackNamed ? fallback : null)
6837
+ },
6838
+
6839
+ /**
6840
+ * The text an Active chip shows for one filter value: the referenced
6841
+ * object's label, the facet bucket's label, the schema's `oneOf` title,
6842
+ * else the value itself.
6843
+ *
6844
+ * @param {string} key The filter key.
6845
+ * @param {unknown} value One value of that filter.
6846
+ * @return {string} The label.
6847
+ * @spec openspec/changes/screens-active-filter-line-parity/specs/active-filter-line-board-look/spec.md#requirement-an-active-chip-names-the-value-not-its-id
6848
+ */
6849
+ activeFilterValueLabel(key, value) {
6850
+ const id = String(value)
6851
+ const refs = this.refLabels[key]
6852
+ if (refs && typeof refs[id] === 'string' && refs[id] !== '') {
6853
+ return refs[id]
6854
+ }
6855
+ const facet = this.effectiveFacetData && this.effectiveFacetData[key]
6856
+ const bucket = facet && Array.isArray(facet.values)
6857
+ ? facet.values.find((fv) => fv && typeof fv === 'object' && String(fv.value) === id)
6858
+ : null
6859
+ if (bucket && typeof bucket.label === 'string' && bucket.label !== '') {
6860
+ return this.cnTranslate(bucket.label)
6861
+ }
6862
+ const prop = this.effectiveSchema?.properties?.[key]
6863
+ const options = prop && Array.isArray(prop.oneOf) ? prop.oneOf : []
6864
+ const option = options.find((o) => o && String(o.const) === id && typeof o.title === 'string')
6865
+ if (option) {
6866
+ return this.cnTranslate(option.title)
6867
+ }
6868
+ return id
6869
+ },
6870
+
6623
6871
  /**
6624
6872
  * A header filter was applied or cleared. One column can own several
6625
6873
  * query parameters (a range is `key[gte]` and `key[lte]`), so they are