@conduction/nextcloud-vue 2.111.0 → 2.113.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 (153) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +4 -6
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  6. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
  7. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  8. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
  9. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  14. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  19. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  22. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  23. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  24. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  29. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  34. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  35. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  37. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -3
  38. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  40. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -13
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -3
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  46. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  47. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  48. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  49. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +222 -15
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +24 -7
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  59. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  60. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  61. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  63. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  64. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  65. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  66. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  67. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  68. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  69. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  70. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  71. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  72. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  73. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  74. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +29 -12
  75. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  76. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +3 -1
  77. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  79. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  80. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  81. package/dist/esm/composables/useRefLabels.js +26 -6
  82. package/dist/esm/composables/useRefLabels.js.map +1 -1
  83. package/dist/esm/integrations/registry.js +4 -0
  84. package/dist/esm/integrations/registry.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +3 -1
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +3 -1
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/library-style.js +1 -1
  90. package/dist/esm/nextcloud-vue.css +680 -449
  91. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  92. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  93. package/dist/esm/store/plugins/logs.js +2 -1
  94. package/dist/esm/store/plugins/logs.js.map +1 -1
  95. package/dist/esm/utils/actionLink.js +104 -1
  96. package/dist/esm/utils/actionLink.js.map +1 -1
  97. package/dist/esm/utils/detailActionModel.js +28 -1
  98. package/dist/esm/utils/detailActionModel.js.map +1 -1
  99. package/dist/esm/utils/fillTemplate.js +13 -5
  100. package/dist/esm/utils/fillTemplate.js.map +1 -1
  101. package/dist/esm/utils/headerFieldChips.js +21 -2
  102. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  103. package/dist/esm/utils/mapList.js +26 -11
  104. package/dist/esm/utils/mapList.js.map +1 -1
  105. package/dist/esm/utils/savedViewMenu.js +12 -5
  106. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +2800 -1933
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +680 -449
  112. package/l10n/en.json +3 -1
  113. package/l10n/nl.json +3 -1
  114. package/package.json +1 -1
  115. package/src/components/CnActionButtons/CnActionButtons.vue +4 -6
  116. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  117. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  118. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  119. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  120. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  121. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  122. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  123. package/src/components/CnChoiceCards/CnChoiceCards.vue +0 -3
  124. package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -5
  125. package/src/components/CnDetailPage/CnDetailPage.vue +62 -14
  126. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  127. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  128. package/src/components/CnIndexPage/CnIndexPage.vue +233 -18
  129. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  130. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  131. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  132. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  133. package/src/components/CnReportsPage/CnReportsPage.vue +31 -12
  134. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  135. package/src/composables/useRefLabels.js +25 -5
  136. package/src/css/badge.css +15 -13
  137. package/src/css/board.css +0 -1
  138. package/src/css/dialog.css +53 -3
  139. package/src/css/index-page.css +26 -1
  140. package/src/css/look-board-detail.css +36 -15
  141. package/src/css/look-board-index.css +67 -15
  142. package/src/css/look-board.css +32 -18
  143. package/src/integrations/registry.js +4 -0
  144. package/src/schemas/app-manifest-v2.schema.json +61 -13
  145. package/src/store/plugins/liveUpdates.js +3 -1
  146. package/src/store/plugins/logs.js +2 -1
  147. package/src/utils/actionLink.js +103 -0
  148. package/src/utils/detailActionModel.js +27 -0
  149. package/src/utils/fillTemplate.js +13 -5
  150. package/src/utils/headerFieldChips.js +21 -2
  151. package/src/utils/mapList.js +26 -11
  152. package/src/utils/savedViewMenu.js +12 -5
  153. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -3,24 +3,39 @@
3
3
  - SPDX-License-Identifier: EUPL-1.2
4
4
  -->
5
5
  <template>
6
- <CnSearchSelect
7
- class="cn-party-picker"
8
- :modelValue="selectedOption"
9
- :options="options"
10
- :inputLabel="inputLabel"
11
- :labelOutside="labelOutside"
12
- :placeholder="placeholder ? cnTranslate(placeholder) : ''"
13
- :loading="searching"
14
- :filterable="false"
15
- :clearable="clearable"
16
- :disabled="disabled"
17
- data-testid="cn-party-picker"
18
- @search="onSearch"
19
- @update:modelValue="onPick" />
6
+ <div class="cn-party-picker">
7
+ <CnSearchSelect
8
+ :modelValue="selectedOption"
9
+ :options="options"
10
+ :inputLabel="inputLabel"
11
+ :labelOutside="labelOutside"
12
+ :placeholder="placeholder ? cnTranslate(placeholder) : ''"
13
+ :loading="searching"
14
+ :filterable="false"
15
+ :clearable="clearable"
16
+ :disabled="disabled"
17
+ data-testid="cn-party-picker"
18
+ @search="onSearch"
19
+ @update:modelValue="onPick" />
20
+ <p v-if="refusal"
21
+ class="cn-party-picker__refusal"
22
+ role="alert"
23
+ data-testid="cn-party-picker-refusal">
24
+ {{ t('nextcloud-vue', 'This search could not be read.') }} {{ refusal }}
25
+ </p>
26
+ <p v-for="name in unavailable"
27
+ :key="name"
28
+ class="cn-party-picker__unavailable"
29
+ role="status"
30
+ data-testid="cn-party-picker-unavailable">
31
+ {{ t('nextcloud-vue', '{source} could not be searched, so this list may be incomplete.', { source: name }) }}
32
+ </p>
33
+ </div>
20
34
  </template>
21
35
 
22
36
  <script>
23
37
  import axios from '@nextcloud/axios'
38
+ import { translate as t } from '@nextcloud/l10n'
24
39
  import { generateUrl } from '@nextcloud/router'
25
40
  import { fillTemplate } from '../../utils/fillTemplate.js'
26
41
  import { CnSearchSelect } from '../CnSearchSelect/index.js'
@@ -142,6 +157,8 @@ export default {
142
157
  known: null,
143
158
  timer: null,
144
159
  searchSeq: 0,
160
+ refusal: '',
161
+ unavailable: [],
145
162
  }
146
163
  },
147
164
 
@@ -185,6 +202,8 @@ export default {
185
202
  },
186
203
 
187
204
  methods: {
205
+ t,
206
+
188
207
  /**
189
208
  * A source's row as a picker option.
190
209
  *
@@ -244,7 +263,9 @@ export default {
244
263
  },
245
264
 
246
265
  /**
247
- * Search every source at once; a source that fails adds nothing.
266
+ * Search every source at once. A term a source refused (400) is said
267
+ * under the field in its words; a source that could not be searched at
268
+ * all is named, so an empty list never hides a missing source.
248
269
  *
249
270
  * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party
250
271
  * @param {string} term The search term.
@@ -253,6 +274,8 @@ export default {
253
274
  async search(term) {
254
275
  const seq = ++this.searchSeq
255
276
  this.searching = true
277
+ let refusal = ''
278
+ const unavailable = []
256
279
  const sets = await Promise.all((this.sources || []).map(async (source) => {
257
280
  try {
258
281
  if (source.type === 'contacts') {
@@ -264,7 +287,16 @@ export default {
264
287
  }
265
288
  const { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })
266
289
  return (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))
267
- } catch {
290
+ } catch (failure) {
291
+ // OpenRegister refuses a term it cannot parse with a 400 rather
292
+ // than answering nothing, so that refusal is said, not hidden.
293
+ const response = failure && failure.response
294
+ if (response && response.status === 400) {
295
+ const body = response.data
296
+ refusal = String((body && (body.message || body.error)) || (typeof body === 'string' ? body : '') || refusal)
297
+ } else {
298
+ unavailable.push(source.label ? this.cnTranslate(source.label) : String(source.id || ''))
299
+ }
268
300
  return []
269
301
  }
270
302
  }))
@@ -272,6 +304,8 @@ export default {
272
304
  return
273
305
  }
274
306
  this.options = sets.flat().filter(Boolean)
307
+ this.refusal = refusal
308
+ this.unavailable = unavailable.filter(Boolean)
275
309
  this.searching = false
276
310
  },
277
311
 
@@ -315,3 +349,15 @@ export default {
315
349
  },
316
350
  }
317
351
  </script>
352
+
353
+ <style scoped>
354
+ .cn-party-picker__refusal {
355
+ margin: 4px 0 0;
356
+ color: var(--color-error-text, var(--color-error));
357
+ }
358
+
359
+ .cn-party-picker__unavailable {
360
+ margin: 4px 0 0;
361
+ color: var(--color-text-maxcontrast);
362
+ }
363
+ </style>
@@ -48,6 +48,8 @@
48
48
  <NcButton
49
49
  v-else
50
50
  :variant="button.variant"
51
+ :href="button.link ? button.link.href : undefined"
52
+ :target="button.link ? button.link.target : undefined"
51
53
  :data-testid="`cn-reports-header-button-${button.key}`"
52
54
  @click="onHeaderButton(button)">
53
55
  <template v-if="button.icon" #icon>
@@ -163,6 +165,7 @@ import { defineAsyncComponent } from 'vue'
163
165
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
164
166
  import CnIcon from '../CnIcon/CnIcon.vue'
165
167
  import { useLook } from '../../composables/useLook.js'
168
+ import { headerActionAsLink } from '../../utils/actionLink.js'
166
169
  import { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
167
170
  import { followLinkClick } from '../../utils/linkNavigation.js'
168
171
 
@@ -325,7 +328,11 @@ export default {
325
328
  * function, `navigate` (with `route`, and `params`), `emit`, `none`,
326
329
  * or a registry name; `type: "navigate"` with a `target` URL opens
327
330
  * that URL in a new tab. Every click also emits `header-action`
328
- * `{ action, id }`, except for `handler: "none"`.
331
+ * `{ action, id }`, except for `handler: "none"`. An entry with `href`
332
+ * makes its button a real link that runs and emits nothing: an
333
+ * absolute `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written,
334
+ * an instance path (leading `/`) through `generateUrl()`, in a new tab with
335
+ * `target: '_blank'`.
329
336
  *
330
337
  * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
331
338
  * @type {Array<object>|null}
@@ -451,24 +458,31 @@ export default {
451
458
 
452
459
  /**
453
460
  * The declared header buttons that have a label and an action,
454
- * translated, each with a stable key and a variant.
461
+ * translated, each with a stable key and a variant. A button naming a
462
+ * header action with `href` carries that `link`.
455
463
  *
456
464
  * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
457
- * @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.
465
+ * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
466
+ * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, link: ({href: string, target: string}|null)}>} The buttons.
458
467
  */
459
468
  resolvedHeaderButtons() {
460
469
  const declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []
470
+ const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []
461
471
  return declared
462
472
  .filter((button) => button && typeof button === 'object'
463
473
  && typeof button.action === 'string' && button.action !== ''
464
474
  && typeof button.label === 'string' && button.label !== '')
465
- .map((button) => ({
466
- key: String(button.id || button.action),
467
- label: this.tr(button.label),
468
- action: button.action,
469
- variant: button.variant === 'primary' ? 'primary' : 'secondary',
470
- icon: typeof button.icon === 'string' ? button.icon : '',
471
- }))
475
+ .map((button) => {
476
+ const entry = headerActionAsLink(actions.find((action) => action && action.id === button.action))
477
+ return {
478
+ key: String(button.id || button.action),
479
+ label: this.tr(button.label),
480
+ action: button.action,
481
+ variant: button.variant === 'primary' ? 'primary' : 'secondary',
482
+ icon: typeof button.icon === 'string' ? button.icon : '',
483
+ link: entry && typeof entry.href === 'string' && entry.href !== '' ? { href: entry.href, target: entry.target } : null,
484
+ }
485
+ })
472
486
  },
473
487
 
474
488
  /**
@@ -685,13 +699,18 @@ export default {
685
699
 
686
700
  /**
687
701
  * A header button was clicked: run the `headerActions` entry it names
688
- * and emit `header-action`, as an index page's header button does.
702
+ * and emit `header-action`, as an index page's header button does. A
703
+ * button that is a link is left to the browser.
689
704
  *
690
705
  * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
691
- * @param {{action: string}} button The resolved button.
706
+ * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
707
+ * @param {{action: string, link?: object}} button The resolved button.
692
708
  * @return {void}
693
709
  */
694
710
  onHeaderButton(button) {
711
+ if (button.link) {
712
+ return
713
+ }
695
714
  const id = button.action
696
715
  const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []
697
716
  const entry = actions.find((action) => action && action.id === id) || null
@@ -214,6 +214,13 @@ export default {
214
214
  position: relative;
215
215
  }
216
216
 
217
+ /* The field takes the row: NcSelect is an inline block of at least 260px,
218
+ which left the magnifier, placed at the row's end, outside the field
219
+ (DqOverdragen "Nieuw team"). */
220
+ .cn-search-select :deep(.v-select) {
221
+ width: 100%;
222
+ }
223
+
217
224
  /* The magnifier sits where the chevron was: in the field, at its end. */
218
225
  .cn-search-select__magnifier {
219
226
  position: absolute;
@@ -222,6 +229,7 @@ export default {
222
229
  height: var(--default-clickable-area, 34px);
223
230
  display: flex;
224
231
  align-items: center;
232
+ z-index: 1;
225
233
  color: var(--color-text-maxcontrast);
226
234
  pointer-events: none;
227
235
  }
@@ -65,6 +65,22 @@ export function pickRefLabel(obj, labelField) {
65
65
  return null
66
66
  }
67
67
 
68
+ /**
69
+ * Read one field off a referenced object, with no fallback: a colour or an
70
+ * icon name must not fall back to the title.
71
+ *
72
+ * @param {object|null} obj The referenced object.
73
+ * @param {string} field The (possibly nested) property.
74
+ * @return {string|null} The value as text, or null when it is empty.
75
+ */
76
+ export function pickRefField(obj, field) {
77
+ if (!obj || typeof obj !== 'object' || !field) {
78
+ return null
79
+ }
80
+ const text = asText(readPath(obj, field))
81
+ return text !== '' ? text : null
82
+ }
83
+
68
84
  /**
69
85
  * Create a resolver bound to an object store. Labels are cached per register,
70
86
  * schema and label field for the lifetime of the resolver.
@@ -91,9 +107,13 @@ export function createRefLabelResolver(getStore) {
91
107
  * @param {string} schema Schema slug of the referenced objects.
92
108
  * @param {string[]} ids Ids to resolve (duplicates are dropped).
93
109
  * @param {string} labelField Property holding the label (dotted paths allowed).
110
+ * @param {{strict?: boolean}} [options] `strict`: read exactly that field, with no
111
+ * fallback to the title or name (a colour or an icon name).
94
112
  * @return {Promise<Object<string, string|null>>} Label by id.
95
113
  */
96
- async resolve(register, schema, ids, labelField) {
114
+ async resolve(register, schema, ids, labelField, options = {}) {
115
+ const strict = options.strict === true
116
+ const pick = strict ? pickRefField : pickRefLabel
97
117
  const distinct = [...new Set((ids || []).filter((id) => id !== null && id !== undefined && id !== '').map(String))]
98
118
  const out = {}
99
119
  if (distinct.length === 0) {
@@ -115,14 +135,14 @@ export function createRefLabelResolver(getStore) {
115
135
  })
116
136
  return out
117
137
  }
118
- const hit = bucket(`${type}::${labelField || ''}`)
138
+ const hit = bucket(`${type}::${labelField || ''}::${strict ? 'field' : 'label'}`)
119
139
  const missing = []
120
140
  for (const id of distinct) {
121
141
  const stored = store.objects && store.objects[type] && store.objects[type][id]
122
142
  if (hit.has(id)) {
123
143
  continue
124
144
  }
125
- const label = stored ? pickRefLabel(stored, labelField) : null
145
+ const label = stored ? pick(stored, labelField) : null
126
146
  if (label !== null) {
127
147
  hit.set(id, label)
128
148
  } else {
@@ -136,7 +156,7 @@ export function createRefLabelResolver(getStore) {
136
156
  rows = await store.fetchCollectionForOptions(type, {
137
157
  _ids: missing.join(','),
138
158
  _limit: missing.length,
139
- _fields: [...new Set([top, 'title', 'name'])].join(','),
159
+ _fields: (strict ? [top] : [...new Set([top, 'title', 'name'])]).join(','),
140
160
  })
141
161
  } catch {
142
162
  rows = []
@@ -148,7 +168,7 @@ export function createRefLabelResolver(getStore) {
148
168
  byId.set(String(id), row)
149
169
  }
150
170
  })
151
- missing.forEach((id) => hit.set(id, pickRefLabel(byId.get(id), labelField)))
171
+ missing.forEach((id) => hit.set(id, pick(byId.get(id), labelField)))
152
172
  }
153
173
  distinct.forEach((id) => {
154
174
  out[id] = hit.has(id) ? hit.get(id) : null
package/src/css/badge.css CHANGED
@@ -3,12 +3,14 @@
3
3
  ======================================== */
4
4
 
5
5
  /* The two board tones without a Nextcloud colour role (screens-cell-pill-parity).
6
- A theme may redefine them; the six role variants below use Nextcloud's own. */
6
+ A theme may redefine them, or name the NL Design System hue tint and pill
7
+ text (`--nl-color-<hue>-light`, `--nl-color-<hue>-text`), which the tones
8
+ and the four role variants below read first (status-badge-hue-tokens). */
7
9
  :root {
8
- --cn-status-purple-bg: #e8e6f6;
9
- --cn-status-purple-text: #4a3f8f;
10
- --cn-status-teal-bg: #e3f1f3;
11
- --cn-status-teal-text: #1d5e66;
10
+ --cn-status-purple-bg: var(--nl-color-purple-light, #e8e6f6);
11
+ --cn-status-purple-text: var(--nl-color-purple-text, #4a3f8f);
12
+ --cn-status-teal-bg: var(--nl-color-teal-light, #e3f1f3);
13
+ --cn-status-teal-text: var(--nl-color-teal-text, #1d5e66);
12
14
  }
13
15
 
14
16
  .cn-status-badge {
@@ -43,26 +45,26 @@
43
45
  }
44
46
 
45
47
  .cn-status-badge--success {
46
- --cn-status-badge-ink: var(--color-success-text, var(--color-success));
47
- background-color: var(--color-success-light, rgba(70, 186, 97, 0.15));
48
+ --cn-status-badge-ink: var(--nl-color-green-text, var(--color-success-text, var(--color-success)));
49
+ background-color: var(--nl-color-green-light, var(--color-success-light, rgba(70, 186, 97, 0.15)));
48
50
  color: var(--cn-status-badge-ink);
49
51
  }
50
52
 
51
53
  .cn-status-badge--warning {
52
- --cn-status-badge-ink: var(--color-warning-text, var(--color-warning));
53
- background-color: var(--color-warning-light, rgba(232, 163, 39, 0.15));
54
+ --cn-status-badge-ink: var(--nl-color-orange-text, var(--color-warning-text, var(--color-warning)));
55
+ background-color: var(--nl-color-orange-light, var(--color-warning-light, rgba(232, 163, 39, 0.15)));
54
56
  color: var(--cn-status-badge-ink);
55
57
  }
56
58
 
57
59
  .cn-status-badge--error {
58
- --cn-status-badge-ink: var(--color-error-text, var(--color-error));
59
- background-color: var(--color-error-light, rgba(224, 68, 68, 0.15));
60
+ --cn-status-badge-ink: var(--nl-color-red-text, var(--color-error-text, var(--color-error)));
61
+ background-color: var(--nl-color-red-light, var(--color-error-light, rgba(224, 68, 68, 0.15)));
60
62
  color: var(--cn-status-badge-ink);
61
63
  }
62
64
 
63
65
  .cn-status-badge--info {
64
- --cn-status-badge-ink: var(--color-info-text, var(--color-info, #0082c9));
65
- background-color: var(--color-info-light, rgba(0, 130, 201, 0.15));
66
+ --cn-status-badge-ink: var(--nl-color-blue-text, var(--color-info-text, var(--color-info, #0082c9)));
67
+ background-color: var(--nl-color-blue-light, var(--color-info-light, rgba(0, 130, 201, 0.15)));
66
68
  color: var(--cn-status-badge-ink);
67
69
  }
68
70
 
package/src/css/board.css CHANGED
@@ -55,7 +55,6 @@
55
55
  margin-inline-start: auto;
56
56
  padding: 1px 8px;
57
57
  border-radius: 10px;
58
- --nldesign-color-on-surface: var(--cn-board-text-soft, var(--color-main-text));
59
58
  background-color: var(--color-main-background);
60
59
  color: var(--cn-board-text-soft, var(--color-main-text));
61
60
  font-size: 12px;
@@ -20,7 +20,7 @@
20
20
  --nldesign-component-dialog-border-radius: var(--cn-board-card-radius, 12px);
21
21
  --cn-dialog-width: 640px;
22
22
  --cn-dialog-danger: var(--color-error);
23
- --cn-dialog-eyebrow-color: var(--color-primary-element-text, var(--color-main-text));
23
+ --cn-dialog-eyebrow-color: var(--color-primary-element-light-text, var(--color-main-text));
24
24
  --cn-dialog-eyebrow-transform: none;
25
25
  --cn-dialog-eyebrow-tracking: 0;
26
26
  }
@@ -86,6 +86,7 @@
86
86
  }
87
87
 
88
88
  .cn-dialog-header__eyebrow {
89
+ --nldesign-color-on-surface: var(--cn-dialog-eyebrow-color, var(--color-main-text));
89
90
  margin-bottom: 4px;
90
91
  font-size: 13px;
91
92
  font-weight: 700;
@@ -101,13 +102,24 @@
101
102
  color: var(--color-main-text);
102
103
  }
103
104
 
105
+ /* The subtitle is a <p>, which the nldesign theme sizes and inks with
106
+ `!important` through its paragraph tokens: the board's 14px muted line
107
+ goes to those tokens too (DqOverdragen, DqDocumentGenereren). */
104
108
  .cn-dialog-header__subtitle {
109
+ --nldesign-component-paragraph-font-size: 14px;
110
+ --nldesign-component-paragraph-color: var(--color-text-maxcontrast);
111
+ --nldesign-color-on-surface: var(--color-text-maxcontrast);
105
112
  margin: 4px 0 0;
106
113
  font-size: 14px;
107
114
  color: var(--color-text-maxcontrast);
108
115
  }
109
116
 
110
- .cn-dialog-header__close {
117
+ /* The close button is a 36px circle. NcButton sizes an icon-only button by
118
+ --button-size with `!important`, so the size goes through that; the radius
119
+ also goes to the nldesign theme's button radius token. */
120
+ .cn-dialog-header .cn-dialog-header__close.button-vue {
121
+ --button-size: 36px;
122
+ --nldesign-component-button-border-radius: 50%;
111
123
  flex: 0 0 auto;
112
124
  width: 36px;
113
125
  min-width: 36px;
@@ -147,6 +159,28 @@
147
159
  border-radius: var(--cn-board-control-radius, 8px);
148
160
  }
149
161
 
162
+ /* The footer buttons set 14px text, 16px in at both sides, and 18px when
163
+ they carry an icon (DqOverdragen "Annuleren" and "Overdragen"). The padding
164
+ goes through NcButton's --button-padding. The mask class and the doubled
165
+ class outrank NcButton's text-only rule, `.button-vue[data-v-…]:has(…:empty)`
166
+ (four classes, in a stylesheet a spawned dialog loads later), and the board
167
+ look's general icon-and-label rule.
168
+ @spec openspec/changes/board-dialog-controls/specs/board-dialog-controls/spec.md#requirement-a-board-dialog-draws-its-controls-as-the-boards-do */
169
+ .modal-mask.cn-dialog-board .dialog__actions .button-vue.button-vue {
170
+ --button-padding: 16px;
171
+ font-size: 14px;
172
+ }
173
+
174
+ .modal-mask.cn-dialog-board .dialog__actions .button-vue.button-vue:has(> .button-vue__wrapper > .button-vue__icon:not(:empty)):has(> .button-vue__wrapper > .button-vue__text:not(:empty)) {
175
+ --button-padding: 18px;
176
+ }
177
+
178
+ /* NcButton pads a text-only label 4px a side inside the button's own padding;
179
+ the board's 16px is the whole inset. */
180
+ .modal-mask.cn-dialog-board .dialog__actions .button-vue .button-vue__text.button-vue__text {
181
+ padding-inline: 0;
182
+ }
183
+
150
184
  /* Cancel is an outlined secondary, never a text link. */
151
185
  .cn-dialog-board .dialog__actions .button-vue--vue-secondary,
152
186
  .cn-dialog-board .dialog__actions .button-vue--secondary {
@@ -223,15 +257,31 @@
223
257
  height: calc(var(--cn-field-height, 40px) + 2 * var(--border-width-input-focused, 2px));
224
258
  }
225
259
 
260
+ /* The board draws the control's edge as a 1px border (DqOverdragen "Waarom de
261
+ zaak verhuist", DqBesluit); NcInputField and NcTextArea draw it as a shadow.
262
+ The colour is the one that meets WCAG's 3:1 for a control's edge, as the
263
+ footer's secondary button. Focus keeps NcInputField's own ring, which
264
+ carries `!important`. */
226
265
  .cn-dialog-board .dialog__content .input-field__input,
227
266
  .cn-dialog-board .dialog__content .input-field__input:hover:not([disabled]),
228
267
  .cn-dialog-board .dialog__content .textarea__input,
229
268
  .cn-dialog-board .dialog__content .textarea__input:hover:not([disabled]) {
269
+ border: 1px solid var(--color-border-maxcontrast);
230
270
  border-radius: 8px;
231
- box-shadow: 0 0 0 1px var(--color-border-dark);
271
+ box-shadow: none;
232
272
  font-size: var(--cn-field-text-size, 15px);
233
273
  }
234
274
 
275
+ /* A plain input an app puts in a board dialog (a date, a number) is the same
276
+ 40px control; Nextcloud's core stylesheet makes it 34px. */
277
+ .cn-dialog-board .dialog__content input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=range]):not([type=color]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image]):not(.input-field__input) {
278
+ box-sizing: border-box;
279
+ height: var(--cn-field-height, 40px);
280
+ min-height: var(--cn-field-height, 40px);
281
+ border: 1px solid var(--color-border-maxcontrast);
282
+ border-radius: 8px;
283
+ }
284
+
235
285
  .cn-dialog-board .dialog__content .input-field:has(.input-field__label) .input-field__input:not(:focus)::placeholder,
236
286
  .cn-dialog-board .dialog__content .textarea__input:not(:focus)::placeholder {
237
287
  opacity: 1 !important;
@@ -102,11 +102,17 @@
102
102
  overflow-y: auto;
103
103
  }
104
104
 
105
+ /* The list scrolls inside the side column and the card for the rows without
106
+ a place stays under it, as the board draws it (DqZakenKaart). Scrolling the
107
+ column as a whole pushed that card below a page's worth of rows. */
105
108
  .cn-index-page__map-list {
106
109
  display: flex;
110
+ flex: 0 1 auto;
107
111
  flex-direction: column;
112
+ min-height: 0;
108
113
  margin: 0;
109
114
  padding: 0;
115
+ overflow-y: auto;
110
116
  border: 1px solid var(--color-border);
111
117
  border-radius: 10px;
112
118
  list-style: none;
@@ -114,6 +120,7 @@
114
120
 
115
121
  .cn-index-page__map-item {
116
122
  display: flex;
123
+ flex: none;
117
124
  gap: 12px;
118
125
  padding: 12px 14px;
119
126
  }
@@ -152,11 +159,22 @@
152
159
  min-width: 0;
153
160
  }
154
161
 
155
- .cn-index-page__map-title {
162
+ /* The title is a button for the keyboard but reads as the row's title text.
163
+ Nextcloud styles every `button:not(.button-vue, ...)` (specificity 0,2,1)
164
+ as a 34px tinted pill with a 3px margin, which outranked a single class and
165
+ turned each title into a blue block (DqZakenKaart rows 111px against 89).
166
+ Three classes outrank it, and the box properties it sets are reset.
167
+ @spec openspec/changes/map-list-title-button/specs/map-list-title-button/spec.md#requirement-a-map-list-title-reads-as-text-not-as-a-button */
168
+ .cn-index-page__map-list .cn-index-page__map-item .cn-index-page__map-title {
169
+ width: auto;
170
+ height: auto;
171
+ min-height: 0;
156
172
  margin: 0;
157
173
  padding: 0;
158
174
  border: 0;
175
+ border-radius: 0;
159
176
  background: none;
177
+ box-shadow: none;
160
178
  color: var(--color-main-text);
161
179
  font: inherit;
162
180
  font-size: 15px;
@@ -166,6 +184,12 @@
166
184
  cursor: pointer;
167
185
  }
168
186
 
187
+ .cn-index-page__map-list .cn-index-page__map-item .cn-index-page__map-title:hover,
188
+ .cn-index-page__map-list .cn-index-page__map-item .cn-index-page__map-title:focus {
189
+ background: none;
190
+ color: var(--color-main-text);
191
+ }
192
+
169
193
  .cn-index-page__map-sub,
170
194
  .cn-index-page__map-due {
171
195
  color: var(--color-text-maxcontrast);
@@ -181,6 +205,7 @@
181
205
 
182
206
  .cn-index-page__map-missing {
183
207
  display: flex;
208
+ flex: none;
184
209
  flex-direction: column;
185
210
  align-items: flex-start;
186
211
  gap: 8px;