@conduction/nextcloud-vue 2.140.0 → 2.142.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 (123) hide show
  1. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  2. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
  3. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +24 -3
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +12 -0
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +57 -40
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -3
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +43 -3
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +1 -1
  31. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +21 -1
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
  37. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  38. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
  39. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  40. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
  41. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  42. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  43. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
  44. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  51. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
  52. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  54. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  55. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
  56. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
  57. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  58. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +2 -0
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
  66. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
  68. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  69. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
  70. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
  71. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
  72. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
  73. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
  74. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  75. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
  76. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  77. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
  78. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  79. package/dist/esm/l10n/en.json.js +11 -1
  80. package/dist/esm/l10n/en.json.js.map +1 -1
  81. package/dist/esm/l10n/nl.json.js +12 -2
  82. package/dist/esm/l10n/nl.json.js.map +1 -1
  83. package/dist/esm/library-style.js +1 -1
  84. package/dist/esm/nextcloud-vue.css +1116 -686
  85. package/dist/esm/utils/buildViewTree.js +23 -8
  86. package/dist/esm/utils/buildViewTree.js.map +1 -1
  87. package/dist/esm/utils/groupSublines.js +120 -0
  88. package/dist/esm/utils/groupSublines.js.map +1 -0
  89. package/dist/nextcloud-vue.cjs.js +2023 -1239
  90. package/dist/nextcloud-vue.cjs.js.map +2 -2
  91. package/dist/nextcloud-vue.css +1116 -686
  92. package/l10n/en.json +11 -1
  93. package/l10n/nl.json +12 -2
  94. package/package.json +1 -1
  95. package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
  96. package/src/components/CnCellRenderer/CnCellRenderer.vue +26 -4
  97. package/src/components/CnChartWidget/CnChartWidget.md +1 -0
  98. package/src/components/CnChartWidget/CnChartWidget.vue +49 -4
  99. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  100. package/src/components/CnDataTable/CnDataTable.md +1 -0
  101. package/src/components/CnDataTable/CnDataTable.vue +28 -4
  102. package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
  103. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +44 -4
  104. package/src/components/CnIndexPage/CnIndexPage.vue +22 -1
  105. package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
  106. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +101 -3
  107. package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
  108. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
  109. package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
  110. package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
  111. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
  112. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +33 -9
  113. package/src/css/dialog.css +13 -1
  114. package/src/css/look-board-detail.css +107 -9
  115. package/src/css/look-board-index.css +143 -7
  116. package/src/css/look-board.css +109 -0
  117. package/src/css/table.css +12 -0
  118. package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
  119. package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
  120. package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
  121. package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
  122. package/src/utils/buildViewTree.js +23 -8
  123. package/src/utils/groupSublines.js +125 -0
@@ -103,7 +103,7 @@
103
103
  </p>
104
104
  </div>
105
105
 
106
- <div v-if="hasActiveFilters" class="cn-index-sidebar__section">
106
+ <div v-if="hasActiveFilters && !isBoardLook" class="cn-index-sidebar__section">
107
107
  <NcButton variant="tertiary" @click="onClearAll">
108
108
  <template #icon>
109
109
  <FilterRemoveOutline :size="20" />
@@ -113,7 +113,20 @@
113
113
  </div>
114
114
 
115
115
  <div v-if="schemaFilters.length > 0" class="cn-index-sidebar__section">
116
- <h3>{{ filtersLabel }}</h3>
116
+ <!-- The board look: "Alles wissen" beside the heading (DqZakenSorteren). -->
117
+ <div v-if="isBoardLook" class="cn-index-sidebar__filters-head" data-testid="cn-index-sidebar-filters-head">
118
+ <h3>{{ filtersLabel }}</h3>
119
+ <button
120
+ v-if="hasActiveFilters"
121
+ type="button"
122
+ class="cn-index-sidebar__filters-clear"
123
+ @click="onClearAll">
124
+ {{ clearAllLabel }}
125
+ </button>
126
+ </div>
127
+ <h3 v-else>
128
+ {{ filtersLabel }}
129
+ </h3>
117
130
  <div
118
131
  v-for="filter in schemaFilters"
119
132
  :key="filter.key"
@@ -155,6 +168,27 @@
155
168
  :label="filter.label"
156
169
  :placeholder="filter.label"
157
170
  @update:modelValue="onTextChange(filter.key, $event)" />
171
+ <!-- The board look: a short field as checkbox rows with counts. -->
172
+ <fieldset
173
+ v-else-if="checklistOptions(filter)"
174
+ class="cn-index-sidebar__checklist"
175
+ :data-testid="`cn-index-sidebar-checklist-${filter.key}`">
176
+ <legend class="hidden-visually">
177
+ {{ filter.label }}
178
+ </legend>
179
+ <label
180
+ v-for="option in checklistOptions(filter)"
181
+ :key="`${filter.key}-${option.id}`"
182
+ class="cn-index-sidebar__checklist-row"
183
+ data-testid="cn-index-sidebar-checklist-row">
184
+ <input
185
+ type="checkbox"
186
+ :checked="isChecked(filter, option.id)"
187
+ @change="onChecklistToggle(filter, option.id, $event.target.checked)">
188
+ <span class="cn-index-sidebar__checklist-label">{{ option.label }}</span>
189
+ <span v-if="option.count !== null" class="cn-index-sidebar__checklist-count">{{ option.count }}</span>
190
+ </label>
191
+ </fieldset>
158
192
  <NcSelect
159
193
  v-else
160
194
  class="cn-index-sidebar__select"
@@ -369,7 +403,7 @@ import CnSavedViewsPanel from '../CnSavedViewsPanel/CnSavedViewsPanel.vue'
369
403
  import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
370
404
  import { normalizeLook } from '../../composables/useLook.js'
371
405
  import { METADATA_COLUMNS } from '../../constants/metadata.js'
372
- import { facetOptions } from '../../utils/facets.js'
406
+ import { facetOptions, MISSING_VALUE } from '../../utils/facets.js'
373
407
  import { labelLang } from '../../utils/manifestTranslate.js'
374
408
  import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
375
409
  /**
@@ -389,6 +423,9 @@ import { labelSidePanelClose } from '../../utils/sidePanelCloseLabel.js'
389
423
  import { reverseSortKeys, sameSortKeys } from '../../utils/sortOrder.js'
390
424
  import { CnDateRangePicker, DEFAULT_DATE_RANGE_PRESETS } from '../CnDateRangePicker/index.js'
391
425
  import { CnIcon } from '../CnIcon/index.js'
426
+ /** The most values a filter shows as checkbox rows in the board look; a longer field keeps its picker. */
427
+ const CHECKLIST_MAX = 12
428
+
392
429
  export default {
393
430
  name: 'CnIndexSidebar',
394
431
 
@@ -1411,6 +1448,67 @@ export default {
1411
1448
  return values.map((v) => options.find((o) => o.id === v) || { id: v, label: String(v) })
1412
1449
  },
1413
1450
 
1451
+ /**
1452
+ * A field's values as checkbox rows, in the board look and for a short
1453
+ * list only: label and count apart, from the facet when there is one,
1454
+ * else the field's own options. Null where the picker stays (the
1455
+ * Nextcloud look, a single choice, a reference, more than
1456
+ * CHECKLIST_MAX values).
1457
+ *
1458
+ * @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts
1459
+ * @param {object} filter The filter definition.
1460
+ * @return {Array<{id: (string|number|boolean), label: string, count: (number|null)}>|null} The rows, or null.
1461
+ */
1462
+ checklistOptions(filter) {
1463
+ if (!this.isBoardLook || filter.multiple === false || filter.type === 'reference') {
1464
+ return null
1465
+ }
1466
+ const facet = this.facetData[filter.key]
1467
+ let rows
1468
+ if (facet && Array.isArray(facet.values) && facet.values.length > 0) {
1469
+ rows = facet.values.map((bucket) => ({
1470
+ id: bucket.value,
1471
+ label: String(bucket.label !== undefined && bucket.label !== null ? bucket.label : bucket.value),
1472
+ count: typeof bucket.count === 'number' ? bucket.count : null,
1473
+ }))
1474
+ if (typeof facet.missing === 'number' && facet.missing > 0) {
1475
+ rows.push({ id: MISSING_VALUE, label: t('nextcloud-vue', 'No value'), count: facet.missing })
1476
+ }
1477
+ } else {
1478
+ rows = (filter.options || []).map((option) => ({ id: option.id, label: String(option.label), count: null }))
1479
+ }
1480
+ return rows.length > 0 && rows.length <= CHECKLIST_MAX ? rows : null
1481
+ },
1482
+
1483
+ /**
1484
+ * Whether a value of a field is among its active filter values.
1485
+ *
1486
+ * @param {object} filter The filter definition.
1487
+ * @param {string|number|boolean} id The value.
1488
+ * @return {boolean}
1489
+ */
1490
+ isChecked(filter, id) {
1491
+ const value = this.activeFilters[filter.key]
1492
+ const values = Array.isArray(value) ? value : (value === undefined || value === null || value === '' ? [] : [value])
1493
+ return values.map(String).includes(String(id))
1494
+ },
1495
+
1496
+ /**
1497
+ * A checkbox row was toggled: the field's values with it added or taken out.
1498
+ *
1499
+ * @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts
1500
+ * @param {object} filter The filter definition.
1501
+ * @param {string|number|boolean} id The value.
1502
+ * @param {boolean} checked Whether it is now ticked.
1503
+ * @return {void}
1504
+ */
1505
+ onChecklistToggle(filter, id, checked) {
1506
+ const value = this.activeFilters[filter.key]
1507
+ const current = Array.isArray(value) ? [...value] : (value === undefined || value === null || value === '' ? [] : [value])
1508
+ const rest = current.filter((v) => String(v) !== String(id))
1509
+ this.$emit('filter-change', { key: filter.key, values: checked ? [...rest, id] : rest })
1510
+ },
1511
+
1414
1512
  /**
1415
1513
  * Handle filter select change
1416
1514
  *
@@ -226,6 +226,25 @@ function controlIcon(path) {
226
226
  * @slot popup Per-marker popup body override. Scoped props: `{ feature, properties }`. When omitted, the widget renders the value of `markers.popupField` (or no popup at all when unset).
227
227
  * @slot fallback Replaces the default "map unavailable" message when Leaflet fails to load (test environments, CSP-blocked builds, offline dev). No scoped props.
228
228
  */
229
+ /**
230
+ * The count line a header spec gives (`{ one, other }` or one string, with
231
+ * `{total}`), once the marker source answered with markers.
232
+ *
233
+ * @param {object|string|undefined} spec The declared line.
234
+ * @param {object} vm The map: `markersLoaded`, `markersFailed`, `markerCount`, `cnTranslate`.
235
+ * @return {string} The line, or ''.
236
+ */
237
+ function countLine(spec, vm) {
238
+ if (!spec || !vm.markersLoaded || vm.markersFailed || vm.markerCount === 0) {
239
+ return ''
240
+ }
241
+ const text = typeof spec === 'string' ? spec : (vm.markerCount === 1 ? spec.one : spec.other)
242
+ if (typeof text !== 'string' || text === '') {
243
+ return ''
244
+ }
245
+ return vm.cnTranslate(text).replace(/\{total\}/g, String(vm.markerCount))
246
+ }
247
+
229
248
  export default {
230
249
  name: 'CnMapWidget',
231
250
 
@@ -577,15 +596,19 @@ export default {
577
596
  * @spec openspec/changes/map-widget-header-count/specs/map-widget-header-count/spec.md#requirement-a-map-card-can-say-how-many-places-it-shows
578
597
  */
579
598
  headerSubtitleText() {
580
- const spec = this.content?.headerSubtitle
581
- if (!spec || !this.markersLoaded || this.markersFailed || this.markerCount === 0) {
582
- return ''
583
- }
584
- const text = typeof spec === 'string' ? spec : (this.markerCount === 1 ? spec.one : spec.other)
585
- if (typeof text !== 'string' || text === '') {
586
- return ''
587
- }
588
- return this.cnTranslate(text).replace(/\{total\}/g, String(this.markerCount))
599
+ return countLine(this.content?.headerSubtitle, this)
600
+ },
601
+
602
+ /**
603
+ * The same count as a note at the right of the card's title row
604
+ * (`content.headerNote`, the same shape as `headerSubtitle`), as the
605
+ * case boards draw "Locaties ... 1 locatie".
606
+ *
607
+ * @return {string}
608
+ * @spec openspec/changes/map-count-note/specs/map-widget/spec.md
609
+ */
610
+ headerNoteText() {
611
+ return countLine(this.content?.headerNote, this)
589
612
  },
590
613
 
591
614
  markerStatusText() {
@@ -672,6 +695,21 @@ export default {
672
695
  }
673
696
  },
674
697
 
698
+ /**
699
+ * Report the count note to the card around the map.
700
+ *
701
+ * @param {string} note The note, or ''.
702
+ */
703
+ headerNoteText(note) {
704
+ if (this.content?.headerNote) {
705
+ /**
706
+ * @event header The card header's note at the right of the title.
707
+ * @type {{note: string}}
708
+ */
709
+ this.$emit('header', { note })
710
+ }
711
+ },
712
+
675
713
  // Keyed, never `deep: true` — see `layersKey`.
676
714
  layersKey() {
677
715
  if (this.map) {
@@ -669,14 +669,14 @@ export default {
669
669
  },
670
670
 
671
671
  /**
672
- * How the table draws its title column (`content.rowTitle`: link, plain
673
- * or text), link when unset or unknown.
672
+ * How the table draws its title column (`content.rowTitle`: link, plain,
673
+ * text or strong), link when unset or unknown.
674
674
  *
675
675
  * @return {string}
676
676
  * @spec openspec/changes/board-log-table-plain-cells/specs/board-log-table-plain-cells/spec.md#requirement-a-tables-title-column-can-read-as-plain-text
677
677
  */
678
678
  rowTitle() {
679
- return ['plain', 'text'].includes(this.content.rowTitle) ? this.content.rowTitle : 'link'
679
+ return ['plain', 'text', 'strong'].includes(this.content.rowTitle) ? this.content.rowTitle : 'link'
680
680
  },
681
681
 
682
682
  /**
@@ -16,6 +16,7 @@
16
16
  :dropOverlay="dropOverlay"
17
17
  :dropLabel="dropLabel"
18
18
  :dropDetail="dropDetail"
19
+ :dropDetailValues="dropDetailValues"
19
20
  :dropHint="dropHint"
20
21
  :rowActions="rowActions"
21
22
  :newActions="newActions"
@@ -360,7 +361,9 @@ export default {
360
361
  /** Forwarded to CnFilesBrowser: the drop state's line ("Laat los om toe te voegen"); the library's own without it. */
361
362
  dropLabel: { type: String, default: undefined },
362
363
  /** Forwarded to CnFilesBrowser: the drop state's second line, `{count}` the files dragged; it also makes the overlay fill the panel. */
363
- dropDetail: { type: String, default: '' },
364
+ dropDetail: { type: [String, Array], default: '' },
365
+ /** Forwarded to CnFilesBrowser: named values filled into `dropDetail`. */
366
+ dropDetailValues: { type: Object, default: null },
364
367
  /** Forwarded to CnFilesBrowser: "Or drag files onto this list." under the list. */
365
368
  dropHint: { type: Boolean, default: false },
366
369
  /**
@@ -52,7 +52,9 @@ const SEARCH_DELAY = 250
52
52
  * Organisatie · KvK …"). Built on CnSearchSelect.
53
53
  *
54
54
  * `sources`: each `{ id, register, schema, label?, title?, secondary?,
55
- * limit?, icon? }` searches OpenRegister with `_search`; `title` and
55
+ * limit?, icon?, lookup? }` searches OpenRegister with `_search`; `lookup`
56
+ * (`{ url, when }`) sends a term matching `when` to the app's exact lookup
57
+ * instead (a POST of `{ term }` answering `{ results }`); `title` and
56
58
  * `secondary` are templates filled from the row (default `{name}` / none),
57
59
  * `label` names the kind ("Resident") and starts the second line; `label`
58
60
  * and `secondary` go through the host translate, so a manifest can write
@@ -284,6 +286,10 @@ export default {
284
286
  const { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })
285
287
  return (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))
286
288
  }
289
+ if (this.isExactLookup(source, term) === true) {
290
+ const { data } = await axios.post(generateUrl(source.lookup.url), { term })
291
+ return (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))
292
+ }
287
293
  if (!source.register || !source.schema) {
288
294
  return []
289
295
  }
@@ -311,6 +317,30 @@ export default {
311
317
  this.searching = false
312
318
  },
313
319
 
320
+ /**
321
+ * Whether a typed term goes to the source's exact lookup instead of the
322
+ * free-text search: `source.lookup` is `{ url, when }`, `when` a regular
323
+ * expression the whole term must match ("^[0-9]{9}$" for a BSN). The
324
+ * lookup is a POST, so the term never sits in a URL or a server log line,
325
+ * and the app behind it may check and record who looked whom up.
326
+ *
327
+ * @param {object} source The source.
328
+ * @param {string} term The typed term.
329
+ * @return {boolean}
330
+ * @spec openspec/changes/party-picker-exact-lookup/specs/party-picker/spec.md#requirement-a-party-source-can-look-a-term-up-exactly
331
+ */
332
+ isExactLookup(source, term) {
333
+ const lookup = source && source.lookup
334
+ if (!lookup || typeof lookup.url !== 'string' || lookup.url === '' || typeof lookup.when !== 'string') {
335
+ return false
336
+ }
337
+ try {
338
+ return new RegExp(lookup.when).test(term)
339
+ } catch {
340
+ return false
341
+ }
342
+ },
343
+
314
344
  /**
315
345
  * Name a value the list does not hold: the first OpenRegister source
316
346
  * that has an object with that id.
@@ -6,7 +6,7 @@
6
6
  <template>
7
7
  <div
8
8
  class="cn-record-list"
9
- :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact', 'cn-record-list--tiles': presentation === 'rows' && content.tiles === true, 'cn-record-list--rule-above': content.ruleAbove === true, 'cn-record-list--data-table': presentation === 'table' && content.tableLook === 'data' }]"
9
+ :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact', 'cn-record-list--stacked': content.density === 'stacked', 'cn-record-list--tiles': presentation === 'rows' && content.tiles === true, 'cn-record-list--rule-above': content.ruleAbove === true, 'cn-record-list--data-table': presentation === 'table' && content.tableLook === 'data' }]"
10
10
  data-testid="cn-record-list">
11
11
  <p v-if="intro && !absent" class="cn-record-list__intro" data-testid="cn-record-list-intro">
12
12
  {{ intro }}
@@ -713,6 +713,8 @@ export default {
713
713
  * without a value.
714
714
  * `density: "compact"` draws the rows 10px above and below, at least
715
715
  * 56px tall, with the title at 500 (DqProcesanalyse).
716
+ * `density: "stacked"` stands the rows 10px apart with no rule and no
717
+ * row padding (DqDagAfsluiten "Morgen eerst").
716
718
  * In `rows`, `text` is a line under the meta line ("12 dagen stil"),
717
719
  * and `chevron: false` leaves the chevron off a linked row
718
720
  * (DqDashboard).
@@ -3103,6 +3105,23 @@ export default {
3103
3105
  font-weight: 500;
3104
3106
  }
3105
3107
 
3108
+ /* `density: "stacked"`: the rows stand 10px apart with no rule and no row
3109
+ padding, a short side list (DqDagAfsluiten "Morgen eerst"). */
3110
+ .cn-record-list--stacked .cn-record-list__list {
3111
+ display: flex;
3112
+ flex-direction: column;
3113
+ gap: 10px;
3114
+ }
3115
+
3116
+ .cn-record-list--stacked .cn-record-list__row {
3117
+ min-height: 0;
3118
+ padding: 0;
3119
+ }
3120
+
3121
+ .cn-record-list--stacked .cn-record-list__item + .cn-record-list__item {
3122
+ border-top: 0;
3123
+ }
3124
+
3106
3125
  .cn-record-list__meta {
3107
3126
  color: var(--color-text-maxcontrast);
3108
3127
  font-size: 13px;
@@ -37,15 +37,20 @@
37
37
  data-testid="cn-record-list-due">{{ item.due.text }}</span>
38
38
  </div>
39
39
  <div class="cn-record-notes__fields">
40
- <NcTextField
41
- class="cn-record-notes__field"
42
- :modelValue="drafts[item.key] || ''"
43
- :label="fieldLabel"
44
- :labelOutside="false"
45
- :placeholder="placeholder"
46
- :disabled="busy"
47
- :data-testid="`cn-record-list-note-${item.key}`"
48
- @update:modelValue="setDraft(item.key, $event)" />
40
+ <!-- The label stands above the field, as the leaf's own beside it
41
+ (humaniq "Uren"), so both fields share one line. -->
42
+ <div class="cn-record-notes__field">
43
+ <label :for="fieldId(item.key)" class="cn-record-notes__field-label">{{ fieldLabel }}</label>
44
+ <NcTextField
45
+ :id="fieldId(item.key)"
46
+ :modelValue="drafts[item.key] || ''"
47
+ :label="fieldLabel"
48
+ labelOutside
49
+ :placeholder="placeholder"
50
+ :disabled="busy"
51
+ :data-testid="`cn-record-list-note-${item.key}`"
52
+ @update:modelValue="setDraft(item.key, $event)" />
53
+ </div>
49
54
  <div v-if="leaf && leafProvider" class="cn-record-notes__leaf" :data-testid="`cn-record-list-leaf-${item.key}`">
50
55
  <CnLeafMountHost
51
56
  v-if="leafIsMount"
@@ -301,6 +306,16 @@ export default {
301
306
  this.drafts = { ...this.drafts, [key]: String(value ?? '') }
302
307
  },
303
308
 
309
+ /**
310
+ * The id of a row's note field, for its label.
311
+ *
312
+ * @param {string} key The row key.
313
+ * @return {string} The id.
314
+ */
315
+ fieldId(key) {
316
+ return `cn-record-notes-${this.$.uid}-${String(key).replace(/[^A-Za-z0-9_-]/g, '-')}`
317
+ },
318
+
304
319
  /**
305
320
  * @param {string} key The row key.
306
321
  * @return {boolean} Whether the row has a note typed.
@@ -472,7 +487,16 @@ export default {
472
487
  }
473
488
 
474
489
  .cn-record-notes__field {
490
+ display: flex;
475
491
  flex: 1 1 260px;
492
+ flex-direction: column;
493
+ gap: 6px;
494
+ }
495
+
496
+ .cn-record-notes__field-label {
497
+ color: var(--color-main-text);
498
+ font-size: 14px;
499
+ font-weight: 600;
476
500
  }
477
501
 
478
502
  .cn-record-notes__leaf {
@@ -238,8 +238,11 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
238
238
  padding-inline-end: 24px;
239
239
  }
240
240
 
241
+ /* The body starts 22px under the header, as most dialog boards draw it
242
+ (header padding-bottom 4px + form padding-top 18px; DqBerichtVersturen,
243
+ DqOverdragen, DqNieuweZaak, DqBesluit): the 18px gap plus 4px. */
241
244
  .cn-dialog-board .dialog__content > .cn-dialog-header {
242
- margin-bottom: -2px;
245
+ margin-bottom: 4px;
243
246
  }
244
247
 
245
248
  /* Footer: 16px 24px, hairline on top, right aligned, 40px buttons, 10px gap. */
@@ -344,6 +347,15 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
344
347
  margin-block-start: 26px;
345
348
  }
346
349
 
350
+ /* A select in a form dialog sits under its label and over its help line
351
+ as a text field does (DqNieuweZaak "Zaaktype", "Intakekanaal"): the
352
+ form's label stands outside the select, so NcSelect's own 6px/4px margins
353
+ only pushed the control 6px below the text fields beside it and its help
354
+ line 4px further down. */
355
+ .cn-dialog-board .dialog__content .cn-form-dialog__select-wrapper > .v-select.select:not(:has(.input-field__label)) {
356
+ margin-block: 0;
357
+ }
358
+
347
359
  /* A multiple select is the board's 40px field too (DqFilterDelen "Delen
348
360
  met"). The edge goes on the toggle, which holds the chips and the search,
349
361
  so the chosen people and groups sit inside the field; the search inside it
@@ -721,6 +721,13 @@
721
721
  padding-top: calc(14px - var(--cn-board-first-line-leading));
722
722
  }
723
723
 
724
+ /* A table (an object list) starts 12px below the title's box: its head has
725
+ a fill and no leading, so the glyph-to-glyph gap above left it 2px short
726
+ (DqZaaktypeStatussen "Statussen", "Resultaten"). */
727
+ .cn-look-board .cn-detail-page .cn-widget-wrapper__content:has(> .cn-object-list-widget):not(.cn-detail-page__side *) {
728
+ padding-top: 12px;
729
+ }
730
+
724
731
  /* Running text (DqZaak "Het verzoek") sits 12px below the title's box and
725
732
  keeps its own half-leading, at 1.6 as drawn.
726
733
  @spec openspec/changes/board-card-title-gap-variants/specs/board-card-title-gap-variants/spec.md#requirement-a-board-card-starts-its-body-as-drawn-for-its-kind-of-body */
@@ -1527,12 +1534,12 @@
1527
1534
  }
1528
1535
 
1529
1536
  /* ---- A card's Add ------------------------------------------------------ */
1530
- /* The board draws a list card's Add as the neutral secondary at the 40px
1531
- control height, right-aligned in the title row (DqZaakGerelateerd
1532
- "Deelzaak aanmaken", DqZaakTaken "Toevoegen"). CnDetailWidgetHost renders
1537
+ /* The board draws a list card's Add as the neutral secondary right-aligned
1538
+ in the title row, at the title row's 34px (DqZaaktypeStatussen "Status
1539
+ toevoegen", DqZaaktype "Taak toevoegen", DqCatalogus "Objecttype
1540
+ toevoegen": 13 of the 15 boards that draw one). CnDetailWidgetHost renders
1533
1541
  that button in the board look, so the list's footer Add bar, which ADR-062
1534
- draws in the Nextcloud look, is hidden in the card. The size outranks a
1535
- card title row's 34px buttons.
1542
+ draws in the Nextcloud look, is hidden in the card.
1536
1543
  @spec openspec/changes/board-object-list-add-button/specs/board-object-list-add-button/spec.md#requirement-the-board-look-draws-a-cards-add-as-a-title-row-button */
1537
1544
  .cn-look-board .cn-detail-page__catalog-card:not(.cn-look-nextcloud *) .cn-object-list-widget__add {
1538
1545
  display: none;
@@ -1544,10 +1551,6 @@
1544
1551
  display: none;
1545
1552
  }
1546
1553
 
1547
- .cn-look-board .cn-widget-wrapper__actions .button-vue.cn-detail-page__catalog-add:has(> .button-vue__wrapper > .button-vue__text:not(:empty)):where(:not(.cn-look-nextcloud *)) {
1548
- --button-size: var(--cn-board-control-height);
1549
- }
1550
-
1551
1554
  /* A map in a board card has the board's control corners (DqZaak, DqZaakMeer
1552
1555
  "Locaties"); overflow keeps the tiles inside them. */
1553
1556
  .cn-look-board .cn-map-widget:not(.cn-map-widget--fullscreen) {
@@ -1582,6 +1585,49 @@
1582
1585
  line-height: 1.2;
1583
1586
  }
1584
1587
 
1588
+ /* The contacts tab's rows as DqZaakBetrokkenen "Partijen" draws them: the
1589
+ role heading 15px bold ink in sentence case, each person on a row 12px
1590
+ high-padded under a hairline, the details and the period 13px muted.
1591
+ @spec openspec/changes/board-contacts-rows/specs/contacts/spec.md */
1592
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__group-title {
1593
+ margin: 6px 0 0;
1594
+ font-size: 15px;
1595
+ font-weight: 700;
1596
+ letter-spacing: 0;
1597
+ text-transform: none;
1598
+ color: var(--color-main-text);
1599
+ }
1600
+
1601
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__item {
1602
+ align-items: center;
1603
+ padding: 12px 0;
1604
+ border-top: 1px solid var(--cn-board-hairline);
1605
+ border-radius: 0;
1606
+ }
1607
+
1608
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__item:first-child {
1609
+ margin-top: 8px;
1610
+ }
1611
+
1612
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__name {
1613
+ font-size: 15px;
1614
+ font-weight: 600;
1615
+ }
1616
+
1617
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__meta,
1618
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__period {
1619
+ font-size: 13px;
1620
+ color: var(--color-text-maxcontrast);
1621
+ }
1622
+
1623
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__period {
1624
+ flex: none;
1625
+ }
1626
+
1627
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__group {
1628
+ margin-bottom: 8px;
1629
+ }
1630
+
1585
1631
  /* The contacts tab inside a detail card (DqZaakBetrokkenen "Partijen") takes
1586
1632
  the card's padding instead of its own, and its buttons start at the card's
1587
1633
  edge under the subtitle.
@@ -1593,3 +1639,55 @@
1593
1639
  .cn-look-board .cn-detail-page__leaf-card .cn-contacts-tab .cn-contacts-tab__header {
1594
1640
  justify-content: flex-start;
1595
1641
  }
1642
+
1643
+ /* A body card's subtitle as the Dq boards draw it: 14px muted at line
1644
+ height 1.2, 3px under the title, and the content 12px under it.
1645
+ @spec openspec/changes/board-card-subtitle-gaps/specs/board-card/spec.md */
1646
+ .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *) .cn-widget-wrapper__title-stack:has(> .cn-widget-wrapper__subtitle) {
1647
+ gap: 3px;
1648
+ }
1649
+
1650
+ .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *) .cn-widget-wrapper__subtitle {
1651
+ font-size: 14px;
1652
+ line-height: 1.2;
1653
+ }
1654
+
1655
+ .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *):has(> .cn-widget-wrapper__header .cn-widget-wrapper__subtitle) > .cn-widget-wrapper__content {
1656
+ padding-top: 12px;
1657
+ }
1658
+
1659
+ /* A checklist (DqTaak): each item an 18-20px box at the card's own inset, the
1660
+ label 10px after it in weight 600, 12px between items, the list 2px below
1661
+ the title block. Nextcloud's checkbox pads its box 7px in and makes every row
1662
+ 34px tall, which put the list 7px right and 2px apart too far per item.
1663
+ @spec openspec/changes/board-checklist-rows/specs/checklist-widget/spec.md#requirement-a-board-checklist-sits-at-the-card-inset */
1664
+ .cn-look-board .cn-checklist-widget__items {
1665
+ display: flex;
1666
+ flex-direction: column;
1667
+ gap: 12px;
1668
+ margin-top: 2px;
1669
+ }
1670
+
1671
+ .cn-look-board .cn-checklist-widget__item .checkbox-content {
1672
+ padding: 0;
1673
+ gap: 10px;
1674
+ min-height: 20px;
1675
+ }
1676
+
1677
+ .cn-look-board .cn-checklist-widget__item .checkbox-content.checkbox-content .checkbox-content__icon {
1678
+ margin: 0;
1679
+ }
1680
+
1681
+ .cn-look-board .cn-checklist-widget__item .checkbox-radio-switch__input {
1682
+ min-height: 20px;
1683
+ }
1684
+
1685
+ .cn-look-board .cn-checklist-widget__item .checkbox-content__wrapper,
1686
+ .cn-look-board .cn-checklist-widget__item .checkbox-content__text {
1687
+ line-height: 18px;
1688
+ min-height: 0;
1689
+ }
1690
+
1691
+ .cn-look-board .cn-checklist-widget__item .checkbox-content__text {
1692
+ font-weight: 600;
1693
+ }