@conduction/nextcloud-vue 2.110.0 → 2.112.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 (142) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +2 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  23. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  29. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -8
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +98 -63
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +203 -14
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +19 -4
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -1
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  53. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  54. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  61. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  62. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  63. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  64. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  65. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  66. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  67. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  68. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  69. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  70. package/dist/esm/composables/useRefLabels.js +26 -6
  71. package/dist/esm/composables/useRefLabels.js.map +1 -1
  72. package/dist/esm/integrations/registry.js +4 -0
  73. package/dist/esm/integrations/registry.js.map +1 -1
  74. package/dist/esm/l10n/en.json.js +3 -1
  75. package/dist/esm/l10n/en.json.js.map +1 -1
  76. package/dist/esm/l10n/nl.json.js +3 -1
  77. package/dist/esm/l10n/nl.json.js.map +1 -1
  78. package/dist/esm/library-style.js +1 -1
  79. package/dist/esm/nextcloud-vue.css +630 -303
  80. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  81. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  82. package/dist/esm/store/plugins/logs.js +2 -1
  83. package/dist/esm/store/plugins/logs.js.map +1 -1
  84. package/dist/esm/utils/detailActionModel.js +28 -1
  85. package/dist/esm/utils/detailActionModel.js.map +1 -1
  86. package/dist/esm/utils/fillTemplate.js +13 -5
  87. package/dist/esm/utils/fillTemplate.js.map +1 -1
  88. package/dist/esm/utils/headerFieldChips.js +21 -2
  89. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  90. package/dist/esm/utils/mapList.js +26 -11
  91. package/dist/esm/utils/mapList.js.map +1 -1
  92. package/dist/esm/utils/savedViewHelpers.js +40 -3
  93. package/dist/esm/utils/savedViewHelpers.js.map +1 -1
  94. package/dist/esm/utils/savedViewMenu.js +12 -5
  95. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  96. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  97. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  98. package/dist/nextcloud-vue.cjs.js +2760 -1995
  99. package/dist/nextcloud-vue.cjs.js.map +2 -2
  100. package/dist/nextcloud-vue.css +630 -303
  101. package/l10n/en.json +3 -1
  102. package/l10n/nl.json +3 -1
  103. package/package.json +1 -1
  104. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  105. package/src/components/CnAppRoot/CnAppRoot.vue +2 -1
  106. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  107. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  108. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  109. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  110. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  111. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  112. package/src/components/CnDetailPage/CnDetailPage.vue +77 -20
  113. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  114. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  115. package/src/components/CnIndexPage/CnIndexPage.md +1 -1
  116. package/src/components/CnIndexPage/CnIndexPage.vue +211 -16
  117. package/src/components/CnIndexSidebar/CnIndexSidebar.md +1 -1
  118. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +2 -2
  119. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  120. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  121. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  122. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  123. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  124. package/src/composables/useRefLabels.js +25 -5
  125. package/src/css/board.css +5 -2
  126. package/src/css/dialog.css +53 -3
  127. package/src/css/index-page.css +26 -1
  128. package/src/css/look-board-detail.css +55 -22
  129. package/src/css/look-board-index.css +82 -12
  130. package/src/css/look-board-reports.css +1 -1
  131. package/src/css/look-board.css +84 -11
  132. package/src/integrations/registry.js +4 -0
  133. package/src/schemas/app-manifest-v2.schema.json +11 -5
  134. package/src/store/plugins/liveUpdates.js +3 -1
  135. package/src/store/plugins/logs.js +2 -1
  136. package/src/utils/detailActionModel.js +27 -0
  137. package/src/utils/fillTemplate.js +13 -5
  138. package/src/utils/headerFieldChips.js +21 -2
  139. package/src/utils/mapList.js +26 -11
  140. package/src/utils/savedViewHelpers.js +39 -2
  141. package/src/utils/savedViewMenu.js +12 -5
  142. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -24,7 +24,11 @@
24
24
  class="cn-object-card__record-pill"
25
25
  :label="recordHeadPill.label"
26
26
  :variant="recordHeadPill.variant"
27
- data-testid="cn-object-card-head-pill" />
27
+ data-testid="cn-object-card-head-pill">
28
+ <template v-if="recordHeadPill.icon" #icon>
29
+ <CnIcon :name="recordHeadPill.icon" :size="14" />
30
+ </template>
31
+ </CnStatusBadge>
28
32
  <span v-if="recordNumber" class="cn-object-card__record-number" data-testid="cn-object-card-number">{{ recordNumber }}</span>
29
33
  <div
30
34
  v-if="$slots.actions"
@@ -63,7 +67,11 @@
63
67
  :key="chip.field"
64
68
  class="cn-object-card__record-pill"
65
69
  :label="chip.label"
66
- :variant="chip.variant" />
70
+ :variant="chip.variant">
71
+ <template v-if="chip.icon" #icon>
72
+ <CnIcon :name="chip.icon" :size="14" />
73
+ </template>
74
+ </CnStatusBadge>
67
75
  </div>
68
76
  <div v-if="$slots.badges" class="cn-object-card__badges">
69
77
  <slot name="badges" :object="object" />
@@ -74,7 +82,11 @@
74
82
  class="cn-object-card__record-pill"
75
83
  :label="recordFootPill.label"
76
84
  :variant="recordFootPill.variant"
77
- data-testid="cn-object-card-foot-pill" />
85
+ data-testid="cn-object-card-foot-pill">
86
+ <template v-if="recordFootPill.icon" #icon>
87
+ <CnIcon :name="recordFootPill.icon" :size="14" />
88
+ </template>
89
+ </CnStatusBadge>
78
90
  <span
79
91
  v-if="recordDate"
80
92
  class="cn-object-card__record-date"
@@ -207,6 +219,7 @@ import { normalizeLook } from '../../composables/useLook.js'
207
219
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
208
220
  import { formatBoardDate } from '../../utils/boardDate.js'
209
221
  import { resolveDateVariant } from '../../utils/dateVariant.js'
222
+ import { pillLook } from '../../utils/detailActionModel.js'
210
223
  import { resolveDue } from '../../utils/dueText.js'
211
224
  import { readPath } from '../../utils/readPath.js'
212
225
  import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
@@ -375,6 +388,20 @@ export default {
375
388
  default: null,
376
389
  },
377
390
 
391
+ /**
392
+ * The referenced records' colour and icon names, by field and id
393
+ * (`{ [field]: { [id]: { color, icon } } }`, CnIndexPage reads them).
394
+ * A record layout pill with `resolve: { colorField?, iconField? }` takes
395
+ * its record's tone and icon from here.
396
+ *
397
+ * @type {object}
398
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-card-pill-takes-its-tone-and-icon-from-the-referenced-record
399
+ */
400
+ pillLooks: {
401
+ type: Object,
402
+ default: () => ({}),
403
+ },
404
+
378
405
  /**
379
406
  * The property keys the facts list shows, in order (manifest key
380
407
  * `config.cardFields`). Without it the card shows the first
@@ -588,7 +615,7 @@ export default {
588
615
  const labels = def.labels && typeof def.labels === 'object' ? def.labels : {}
589
616
  const shown = typeof labels[String(raw)] === 'string' ? this.cnTranslate(labels[String(raw)]) : label
590
617
  const variant = mapped || def.variant
591
- return { field: def.field, label: shown, variant: isBadgeVariant(variant) ? variant : 'default' }
618
+ return this.withLook({ field: def.field, label: shown, variant: isBadgeVariant(variant) ? variant : 'default' }, def)
592
619
  })
593
620
  .filter(Boolean)
594
621
  },
@@ -838,7 +865,26 @@ export default {
838
865
  const colorMap = def.colorMap && typeof def.colorMap === 'object' ? def.colorMap : {}
839
866
  const key = Object.keys(colorMap).find((k) => k.toLowerCase() === value.toLowerCase())
840
867
  const variant = key !== undefined ? colorMap[key] : def.variant
841
- return { label, variant: isBadgeVariant(variant) ? variant : 'default' }
868
+ return this.withLook({ label, variant: isBadgeVariant(variant) ? variant : 'default' }, def)
869
+ },
870
+
871
+ /**
872
+ * Lay the referenced record's tone and icon over a layout pill that
873
+ * declares `resolve` (see `pillLooks`).
874
+ *
875
+ * @param {{label: string, variant: string}} pill The pill as declared.
876
+ * @param {object} def The pill's definition.
877
+ * @return {{label: string, variant: string, icon?: string}} The pill.
878
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-card-pill-takes-its-tone-and-icon-from-the-referenced-record
879
+ */
880
+ withLook(pill, def) {
881
+ if (!def || !def.resolve || !this.pillLooks || typeof this.pillLooks !== 'object') {
882
+ return pill
883
+ }
884
+ const looks = this.pillLooks[def.field]
885
+ const raw = readPath(this.object, def.field)
886
+ const look = pillLook(looks && raw !== null && raw !== undefined ? looks[String(raw)] : null)
887
+ return { ...pill, ...(look.variant ? { variant: look.variant } : {}), ...(look.icon ? { icon: look.icon } : {}) }
842
888
  },
843
889
 
844
890
  /**
@@ -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>
@@ -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/board.css CHANGED
@@ -55,10 +55,13 @@
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));
58
59
  background-color: var(--color-main-background);
59
- color: var(--color-main-text);
60
+ color: var(--cn-board-text-soft, var(--color-main-text));
60
61
  font-size: 12px;
61
- font-weight: 400;
62
+ font-weight: 700;
63
+ /* 16px tall on its 1px pad (DqWerkbord's column counts). */
64
+ line-height: 14px;
62
65
  }
63
66
 
64
67
  /* A column at its limit: the count in the error colour; "full" is said in words too. */
@@ -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;