@conduction/nextcloud-vue 2.138.0 → 2.139.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 (46) hide show
  1. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  2. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  3. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  4. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  5. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  6. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  7. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  8. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  9. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  10. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +99 -4
  11. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +2 -0
  15. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  16. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +13 -2
  17. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  18. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  19. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  20. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  22. package/dist/esm/composables/indexSources.js +43 -4
  23. package/dist/esm/composables/indexSources.js.map +1 -1
  24. package/dist/esm/l10n/en.json.js +3 -1
  25. package/dist/esm/l10n/en.json.js.map +1 -1
  26. package/dist/esm/l10n/nl.json.js +3 -1
  27. package/dist/esm/l10n/nl.json.js.map +1 -1
  28. package/dist/esm/library-style.js +1 -1
  29. package/dist/esm/nextcloud-vue.css +340 -238
  30. package/dist/nextcloud-vue.cjs.js +166 -19
  31. package/dist/nextcloud-vue.cjs.js.map +2 -2
  32. package/dist/nextcloud-vue.css +340 -238
  33. package/l10n/en.json +3 -1
  34. package/l10n/nl.json +3 -1
  35. package/package.json +1 -1
  36. package/src/components/CnChartWidget/CnChartWidget.vue +4 -0
  37. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  38. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +11 -0
  39. package/src/components/CnIndexPage/CnIndexPage.vue +101 -6
  40. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +10 -2
  41. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  42. package/src/composables/indexSources.js +43 -4
  43. package/src/css/dialog.css +18 -0
  44. package/src/css/look-board-detail.css +24 -0
  45. package/src/css/look-board-index.css +21 -0
  46. package/src/css/look-board.css +21 -0
package/l10n/en.json CHANGED
@@ -2378,7 +2378,9 @@
2378
2378
  "Search a person or group": "Search a person or group",
2379
2379
  "Now: by {order}.": "Now: by {order}.",
2380
2380
  "You save the order along with a view.": "You save the order along with a view.",
2381
- "me": "me"
2381
+ "me": "me",
2382
+ "Open profile": "Open profile",
2383
+ "Link a contact from your address book.": "Link a contact from your address book."
2382
2384
  },
2383
2385
  "plurals": {
2384
2386
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2628,7 +2628,9 @@
2628
2628
  "Search a person or group": "Zoek een persoon of groep",
2629
2629
  "Now: by {order}.": "Nu: op {order}.",
2630
2630
  "You save the order along with a view.": "De sortering bewaart u mee in een weergave.",
2631
- "me": "ik"
2631
+ "me": "ik",
2632
+ "Open profile": "Profiel openen",
2633
+ "Link a contact from your address book.": "Koppel een contact uit je adresboek."
2632
2634
  },
2633
2635
  "plurals": {
2634
2636
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.138.0",
3
+ "version": "2.139.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -2964,10 +2964,14 @@ export default {
2964
2964
  display: contents;
2965
2965
  }
2966
2966
 
2967
+ /* A column takes its share of the width (minmax(0, 1fr)), so a long phase
2968
+ name breaks inside its own column instead of running into the next. */
2967
2969
  .cn-chart-widget__matrix-head {
2968
2970
  --nldesign-color-on-surface: var(--color-text-maxcontrast);
2969
2971
  color: var(--color-text-maxcontrast);
2970
2972
  padding: 6px;
2973
+ hyphens: auto;
2974
+ overflow-wrap: anywhere;
2971
2975
  }
2972
2976
 
2973
2977
  .cn-chart-widget__matrix-label {
@@ -1845,11 +1845,13 @@ export default {
1845
1845
  display: contents;
1846
1846
  }
1847
1847
 
1848
- /* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which
1849
- pushed the label to the right of its column and added 12px around both. The
1850
- class is doubled so these resets outrank that rule, and a dossiq-style
1851
- `:deep()` override in an app is not needed. */
1848
+ /* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding, a
1849
+ 130px-wide `dt`), which pushed the label to the right of its column, added
1850
+ 12px around both and widened the label column to 130px. The class is
1851
+ doubled so these resets outrank that rule, and a dossiq-style `:deep()`
1852
+ override in an app is not needed. */
1852
1853
  .cn-facts-widget__label.cn-facts-widget__label {
1854
+ width: auto;
1853
1855
  margin: 0;
1854
1856
  padding: 0;
1855
1857
  text-align: start;
@@ -3278,6 +3278,17 @@ export default {
3278
3278
  font-size: 14px;
3279
3279
  }
3280
3280
 
3281
+ /* Core's apps.css draws every dt/dd inline-block with a fixed, end-aligned dt.
3282
+ @spec openspec/changes/files-side-facts-core-dl/specs/files-side-facts-core-dl/spec.md#requirement-a-file-side-cards-facts-are-not-drawn-by-cores-dl-rules */
3283
+ .cn-files-browser__side-facts dt,
3284
+ .cn-files-browser__side-facts dd {
3285
+ display: block;
3286
+ width: auto;
3287
+ padding: 0;
3288
+ text-align: start;
3289
+ white-space: normal;
3290
+ }
3291
+
3281
3292
  .cn-files-browser__side-facts dt {
3282
3293
  color: var(--color-text-maxcontrast);
3283
3294
  }
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cn-index-page"
3
- :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--quick-filters-toolbar': effectiveQuickFilterPlacement === 'toolbar' }]"
3
+ :class="[lookClass, { 'cn-index-page--with-side-panel': hasSidePanel, 'cn-index-page--sidebar-open': sidebarPanelOpen, 'cn-index-page--quick-filters-toolbar': effectiveQuickFilterPlacement === 'toolbar' }]"
4
4
  data-testid="cn-index-page"
5
5
  @keydown="onListKeydown">
6
6
  <!-- Header — overridable via #header slot. CnPageHeader ALWAYS renders:
@@ -172,7 +172,7 @@
172
172
  :maxVisible="quickFilterMaxVisible"
173
173
  :fit="false"
174
174
  :multiple="quickFilterMultiple"
175
- :activeIndex="activeQuickFilterIndex"
175
+ :activeIndex="shownQuickFilterIndex"
176
176
  :selectedIndices="selectedQuickFilterIndices"
177
177
  :counts="effectiveTabCounts"
178
178
  @update:activeIndex="onQuickFilterChange"
@@ -2240,6 +2240,21 @@ export default {
2240
2240
  default: '',
2241
2241
  },
2242
2242
 
2243
+ /**
2244
+ * Board look: the count line while a saved view is applied (manifest
2245
+ * `config.savedViewCountText`), a template as `countText` that may name
2246
+ * the view's conditions as `{view}`: its description, else its filters
2247
+ * in words (DqFilterInMenu "3 van 48 lopende zaken · termijn binnen 7
2248
+ * dagen, van mij · gesorteerd op termijn"). A view's own `countText`
2249
+ * override and a scope's come first.
2250
+ *
2251
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
2252
+ */
2253
+ savedViewCountText: {
2254
+ type: String,
2255
+ default: '',
2256
+ },
2257
+
2243
2258
  /**
2244
2259
  * Board look: a count line per view (manifest `config.countTextByView`),
2245
2260
  * `{ table?, cards?, list?, board?, map? }`, each a template as
@@ -4580,6 +4595,8 @@ export default {
4580
4595
  savedViewCounts: null,
4581
4596
  // The id (or slug) of the saved view that is applied; its chip shows selected under the board look.
4582
4597
  appliedSavedViewId: '',
4598
+ /** Whether the reader picked a lens chip after the applied view (then their chip shows). */
4599
+ lensChosenByHand: false,
4583
4600
  // The sort option the applied view's `overrides.sortOption` selected, so leaving the view can undo it.
4584
4601
  viewSortOption: '',
4585
4602
  // The applied view's filters as one comparable key, so the address keeps naming the view until they change.
@@ -6419,13 +6436,53 @@ export default {
6419
6436
  * @spec openspec/changes/working-list-row-actions/specs/index-page/spec.md
6420
6437
  */
6421
6438
  /**
6422
- * The chip the strip lights: the active tab, else the default tab of
6423
- * tabs that arrived after setup (`quickFiltersFrom`), which is the
6424
- * one the fetch applies.
6439
+ * The lens chip an applied saved view narrows within: the non-default
6440
+ * tab whose every condition the view's own conditions (its filters
6441
+ * over its locked filters, tokens as written) hold, the most specific
6442
+ * first (DqFilterInMenu: "Termijn deze week" lit for the view of that
6443
+ * name). Null without an applied view or a fitting tab.
6444
+ *
6445
+ * @return {number|null} The tab index.
6446
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
6447
+ */
6448
+ appliedViewLensIndex() {
6449
+ const view = this.appliedSavedView
6450
+ const tabs = Array.isArray(this.tabStripEntries) ? this.tabStripEntries : []
6451
+ if (!view || tabs.length === 0) {
6452
+ return null
6453
+ }
6454
+ const query = view.query && typeof view.query === 'object' ? view.query : {}
6455
+ const locked = query.overrides && typeof query.overrides.lockedFilters === 'object' ? query.overrides.lockedFilters : {}
6456
+ const conditions = { ...(locked || {}), ...(query.filters || view.filters || {}) }
6457
+ const same = (a, b) => JSON.stringify(Array.isArray(a) ? a.map(String) : String(a)) === JSON.stringify(Array.isArray(b) ? b.map(String) : String(b))
6458
+ let best = null
6459
+ let bestSize = 0
6460
+ tabs.forEach((tab, index) => {
6461
+ const filter = tab && tab.filter && typeof tab.filter === 'object' ? tab.filter : {}
6462
+ const keys = Object.keys(filter)
6463
+ if (tab.default === true || keys.length === 0 || keys.length <= bestSize) {
6464
+ return
6465
+ }
6466
+ if (keys.every((key) => Object.hasOwn(conditions, key) && same(conditions[key], filter[key]))) {
6467
+ best = index
6468
+ bestSize = keys.length
6469
+ }
6470
+ })
6471
+ return best
6472
+ },
6473
+
6474
+ /**
6475
+ * The chip the strip lights: the lens an applied saved view narrows
6476
+ * within (until the reader picks a chip), else the active tab, else
6477
+ * the default tab of tabs that arrived after setup
6478
+ * (`quickFiltersFrom`), which is the one the fetch applies.
6425
6479
  *
6426
6480
  * @return {number|null} The tab index, or null without tabs.
6427
6481
  */
6428
6482
  shownQuickFilterIndex() {
6483
+ if (!this.quickFilterMultiple && !this.lensChosenByHand && this.appliedViewLensIndex !== null) {
6484
+ return this.appliedViewLensIndex
6485
+ }
6429
6486
  if (this.activeQuickFilterIndex !== null && this.activeQuickFilterIndex !== undefined) {
6430
6487
  return this.activeQuickFilterIndex
6431
6488
  }
@@ -8815,6 +8872,11 @@ export default {
8815
8872
  * @return {string}
8816
8873
  */
8817
8874
  presentedCountText() {
8875
+ const viewLine = this.savedViewCountText
8876
+ const view = this.appliedSavedView
8877
+ if (view && typeof viewLine === 'string' && viewLine !== '' && this.activeScopePresentation.countText === undefined && !savedViewOverrides(view)?.countText) {
8878
+ return viewLine
8879
+ }
8818
8880
  if (this.activeScopePresentation.countText === undefined) {
8819
8881
  const byView = this.countTextByView
8820
8882
  const own = byView && typeof byView === 'object' ? byView[this.currentViewMode] : undefined
@@ -8825,6 +8887,23 @@ export default {
8825
8887
  return this.presented('countText')
8826
8888
  },
8827
8889
 
8890
+ /**
8891
+ * The applied saved view's conditions for the count line's `{view}`:
8892
+ * its description, else its filters in words, starting lower case.
8893
+ *
8894
+ * @return {string}
8895
+ * @spec openspec/changes/applied-view-lights-its-lens/specs/applied-view-lights-its-lens/spec.md
8896
+ */
8897
+ appliedViewConditionsText() {
8898
+ const view = this.appliedSavedView
8899
+ if (!view) {
8900
+ return ''
8901
+ }
8902
+ const own = typeof view.description === 'string' ? view.description.trim() : ''
8903
+ const text = own !== '' ? own : String(this.viewFilterSummary(view) || '')
8904
+ return text.charAt(0).toLowerCase() + text.slice(1)
8905
+ },
8906
+
8828
8907
  headerDescriptionBase() {
8829
8908
  const total = this.effectivePagination?.total
8830
8909
  // Board look: the count line is a template over the rows on this page
@@ -8848,6 +8927,7 @@ export default {
8848
8927
  .replace('{shown}', String(shown))
8849
8928
  .replace('{total}', String(total))
8850
8929
  .replace(/\{term\}/g, this.sourceTerm)
8930
+ .replace(/\{view\}/g, this.appliedViewConditionsText)
8851
8931
  }
8852
8932
  return this.countLine(this.countSubtitle, total, shown)
8853
8933
  }
@@ -8913,6 +8993,17 @@ export default {
8913
8993
  return !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false
8914
8994
  },
8915
8995
 
8996
+ /**
8997
+ * Whether the page's side panel is open now; the board look then lays
8998
+ * the header out narrower (DqZakenSorteren).
8999
+ *
9000
+ * @spec openspec/changes/board-header-buttons-under-title-beside-side-panel/specs/board-header-buttons-under-title-beside-side-panel/spec.md#requirement-the-header-buttons-sit-under-the-title-while-the-side-panel-is-open
9001
+ * @return {boolean}
9002
+ */
9003
+ sidebarPanelOpen() {
9004
+ return Boolean(this.sidebarOpen && this.hasSidebar)
9005
+ },
9006
+
8916
9007
  /**
8917
9008
  * Whether the board toolbar draws the Filter button as a menu: the
8918
9009
  * board look, `filterMenu` on, something to filter by, and no filter
@@ -8955,7 +9046,9 @@ export default {
8955
9046
 
8956
9047
  /** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */
8957
9048
  sidebarSearchProps() {
8958
- return (this.sidebar && this.sidebar.search) || {}
9049
+ // The page's own search hint, unless `sidebar.search` names one.
9050
+ const own = this.shownSearchPlaceholder ? { searchPlaceholder: this.shownSearchPlaceholder } : {}
9051
+ return { ...own, ...((this.sidebar && this.sidebar.search) || {}) }
8959
9052
  },
8960
9053
 
8961
9054
  /**
@@ -10778,6 +10871,7 @@ export default {
10778
10871
  // `activeQuickFilterIndex` is a setup-returned ref; the
10779
10872
  // Vue 2 ref-unwrap proxy makes plain assignment work.
10780
10873
  this.activeQuickFilterIndex = index
10874
+ this.lensChosenByHand = true
10781
10875
  this.$emit('quick-filter-change', index)
10782
10876
  },
10783
10877
 
@@ -13213,6 +13307,7 @@ export default {
13213
13307
  */
13214
13308
  onApplySavedView(view, { keepSavedViewKey = false } = {}) {
13215
13309
  this.appliedSavedViewId = view && (view.id || view.slug) ? String(view.id || view.slug) : ''
13310
+ this.lensChosenByHand = false
13216
13311
  const state = extractViewState(view)
13217
13312
  // A view's filters may carry tokens (`@me`, `@myTeam`): resolved here,
13218
13313
  // and an optional one that answers nothing (no team) is dropped. A
@@ -32,11 +32,17 @@
32
32
  </div>
33
33
 
34
34
  <div class="cn-index-sidebar__section">
35
+ <!-- The board look names the field by its hint, as the list's
36
+ own search field, with the magnifier (DqZakenSorteren). -->
35
37
  <NcTextField
36
38
  :modelValue="searchValue || ''"
37
39
  :placeholder="searchPlaceholder"
38
- :label="searchLabel"
39
- @update:modelValue="$emit('search', $event)" />
40
+ :label="isBoardLook ? searchPlaceholder : searchLabel"
41
+ @update:modelValue="$emit('search', $event)">
42
+ <template v-if="isBoardLook" #icon>
43
+ <Magnify :size="18" />
44
+ </template>
45
+ </NcTextField>
40
46
  </div>
41
47
 
42
48
  <!-- Sort (list, card and map views; the table sorts by its headers):
@@ -355,6 +361,7 @@ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
355
361
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
356
362
  import FilterRemoveOutline from 'vue-material-design-icons/FilterRemoveOutline.vue'
357
363
  import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
364
+ import Magnify from 'vue-material-design-icons/Magnify.vue'
358
365
  import Pin from 'vue-material-design-icons/Pin.vue'
359
366
  import PinOutline from 'vue-material-design-icons/PinOutline.vue'
360
367
  import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
@@ -410,6 +417,7 @@ export default {
410
417
  Pin,
411
418
  PinOutline,
412
419
  InformationOutline,
420
+ Magnify,
413
421
  },
414
422
 
415
423
  inject: {
@@ -860,7 +860,7 @@ export default {
860
860
  // `secondary` (a second, muted line under the cell) rides
861
861
  // along too: the table draws it, and dropping it here took the
862
862
  // list rows' second line away (zuiddrecht-pixel-gaps-3).
863
- for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow']) {
863
+ for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass']) {
864
864
  if (c[k] !== undefined) {
865
865
  out[k] = c[k]
866
866
  }
@@ -6,9 +6,11 @@
6
6
  import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
7
7
  import { generateOcsUrl, generateUrl } from '@nextcloud/router'
8
8
  import { reactive } from 'vue'
9
+ import AccountOutline from 'vue-material-design-icons/AccountOutline.vue'
9
10
  import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
10
11
  import Cancel from 'vue-material-design-icons/Cancel.vue'
11
12
  import Check from 'vue-material-design-icons/Check.vue'
13
+ import EmailOutline from 'vue-material-design-icons/EmailOutline.vue'
12
14
  import HandBackRight from 'vue-material-design-icons/HandBackRight.vue'
13
15
  import LightningBolt from 'vue-material-design-icons/LightningBolt.vue'
14
16
  import Pencil from 'vue-material-design-icons/Pencil.vue'
@@ -19,6 +21,7 @@ import { fillTemplate } from '../utils/fillTemplate.js'
19
21
  import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
20
22
  import { fetchGroupMembers, fetchPresence, fetchStatuses, fetchUsers } from '../utils/nextcloudDirectory.js'
21
23
  import { readPath } from '../utils/readPath.js'
24
+ import { resolveNextcloudUser } from '../utils/userAutocomplete.js'
22
25
  import { useFlowStore } from './useFlowStore.js'
23
26
  import { organisationRecord, useOrganisationStore } from './useOrganisationStore.js'
24
27
  import { useTaskInboxStore } from './useTaskInboxStore.js'
@@ -1088,7 +1091,7 @@ function fillRowTemplate(template, row) {
1088
1091
  * and its count (DqZoekresultaten).
1089
1092
  *
1090
1093
  * `sourceConfig.kinds` lists what is searched:
1091
- * `{ id, label, register, schema, titleField?, subtitleField?, subtitleTemplate?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
1094
+ * `{ id, label, register, schema, titleField?, subtitleField?, subtitleTemplate?, statusField?, statusResolve?, linkedField?, linkedTemplate?, linkedUser?, href?, limit? }`
1092
1095
  * for objects (OpenRegister's `_search`, so the reader sees what they may
1093
1096
  * read), or `{ id, label, files: true, limit? }` for the reader's files
1094
1097
  * (Nextcloud's unified search). The term is `sourceConfig.term`, else the
@@ -1104,6 +1107,8 @@ function fillRowTemplate(template, row) {
1104
1107
  * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
1105
1108
  * `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
1106
1109
  * result, else `subtitleField`.
1110
+ * `linkedUser: true` reads that value as a user id and shows the user's
1111
+ * display name. `sourceConfig.columnWidths` sets a width per column key.
1107
1112
  * The page's title and count line can name the term as `{term}`, and the
1108
1113
  * count is the active kind's total. Kind, link and status are filterable.
1109
1114
  *
@@ -1113,6 +1118,23 @@ function fillRowTemplate(template, row) {
1113
1118
  * @return {object} The source adapter.
1114
1119
  * @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
1115
1120
  */
1121
+ /**
1122
+ * Columns with the widths a page names per column key
1123
+ * (`sourceConfig.columnWidths`, `{ title: '250px' }`); a column it does not
1124
+ * name keeps none.
1125
+ *
1126
+ * @param {Array<object>} columns The columns.
1127
+ * @param {object|undefined} widths The widths by key.
1128
+ * @return {Array<object>} The columns.
1129
+ * @spec openspec/changes/search-linked-user-and-widths/specs/search-linked-user-and-widths/spec.md
1130
+ */
1131
+ function withColumnWidths(columns, widths) {
1132
+ if (!widths || typeof widths !== 'object') {
1133
+ return columns
1134
+ }
1135
+ return columns.map((col) => (typeof widths[col.key] === 'string' && widths[col.key] !== '' ? { ...col, width: widths[col.key] } : col))
1136
+ }
1137
+
1116
1138
  function searchSource(config = {}, context = {}) {
1117
1139
  const translate = typeof context.translate === 'function' ? context.translate : (text) => text
1118
1140
  const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
@@ -1208,12 +1230,20 @@ function searchSource(config = {}, context = {}) {
1208
1230
  return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
1209
1231
  }
1210
1232
 
1233
+ // `linkedUser`: the Linked cell holds a user id; name the user instead.
1234
+ const linkedUserNames = async (kind, objects) => {
1235
+ const ids = [...new Set(objects.map((object) => linkedOf(kind, object)).filter((id) => id !== ''))]
1236
+ const options = await Promise.all(ids.map((id) => resolveNextcloudUser(id).catch(() => null)))
1237
+ return new Map(ids.map((id, index) => [id, (options[index] && options[index].label) || id]))
1238
+ }
1239
+
1211
1240
  const searchObjects = async (axios, kind, term) => {
1212
1241
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
1213
1242
  const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
1214
1243
  const body = (res && res.data) || {}
1215
1244
  const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
1216
1245
  const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
1246
+ const userNames = kind.linkedUser === true ? await linkedUserNames(kind, results) : null
1217
1247
  return {
1218
1248
  total: typeof body.total === 'number' ? body.total : results.length,
1219
1249
  rows: results.map((object) => {
@@ -1230,7 +1260,7 @@ function searchSource(config = {}, context = {}) {
1230
1260
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
1231
1261
  subtitle: subtitleOf(kind, object),
1232
1262
  status,
1233
- linked: linkedOf(kind, object),
1263
+ linked: userNames ? (userNames.get(linkedOf(kind, object)) || linkedOf(kind, object)) : linkedOf(kind, object),
1234
1264
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
1235
1265
  object,
1236
1266
  }
@@ -1338,7 +1368,7 @@ function searchSource(config = {}, context = {}) {
1338
1368
  */
1339
1369
  loading: () => state.loading,
1340
1370
 
1341
- columns: [
1371
+ columns: withColumnWidths([
1342
1372
  { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true },
1343
1373
  // `kindAsText`: what one row is, as plain text ("Zaak", "Lantaarnpaal",
1344
1374
  // DqZoekresultaten), instead of the plural kind as a pill.
@@ -1353,7 +1383,7 @@ function searchSource(config = {}, context = {}) {
1353
1383
  widgetProps: { colorMap: (config && config.statusColorMap) || {} },
1354
1384
  sortable: true,
1355
1385
  },
1356
- ],
1386
+ ], config && config.columnWidths),
1357
1387
 
1358
1388
  /**
1359
1389
  * Every column header carries a filter button (DqZoekresultaten); the
@@ -1603,6 +1633,15 @@ function usersSource() {
1603
1633
  /** A filter button on every head (DqAfdelingen); the members read are narrowed here. */
1604
1634
  headerFilters: true,
1605
1635
 
1636
+ /**
1637
+ * A member's row menu (DqAfdelingen): their Nextcloud profile, and an
1638
+ * e-mail when they have an address. Both are links.
1639
+ */
1640
+ rowActions: [
1641
+ { id: 'open-profile', label: t('nextcloud-vue', 'Open profile'), icon: AccountOutline, href: (row) => generateUrl(`/u/${encodeURIComponent(row.id)}`) },
1642
+ { id: 'send-email', label: t('nextcloud-vue', 'Send email'), icon: EmailOutline, href: (row) => `mailto:${row.email}`, visible: (row) => Boolean(row && row.email) },
1643
+ ],
1644
+
1606
1645
  showAdd: false,
1607
1646
  }
1608
1647
  }
@@ -204,6 +204,24 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
204
204
  gap: 10px;
205
205
  }
206
206
 
207
+ /* A choice card with a checkbox (CnAssignDialog's toggles) is as tight as
208
+ the board draws it (DqOverdragen, 76px for a label and a two-line note):
209
+ 10px 14px padding, the checkbox row at the 24px minimum target instead of
210
+ NcCheckboxRadioSwitch's 34px, the note at 1.2 line height. */
211
+ .cn-dialog-board .cn-assign-dialog__toggle {
212
+ padding: 10px 14px;
213
+ }
214
+
215
+ .cn-dialog-board .cn-assign-dialog__toggle .checkbox-radio-switch-checkbox .checkbox-content,
216
+ .cn-dialog-board .cn-assign-dialog__toggle .checkbox-radio-switch__input {
217
+ min-height: 24px;
218
+ padding-block: 0;
219
+ }
220
+
221
+ .cn-dialog-board .cn-assign-dialog__toggle-note {
222
+ line-height: 1.2;
223
+ }
224
+
207
225
  /* Body: padding 20px 24px, 18px between blocks. */
208
226
  .cn-dialog-board .dialog__content {
209
227
  display: flex;
@@ -579,9 +579,16 @@
579
579
  .cn-look-board .cn-conversation-thread .cn-conversation-thread__meta:not(.cn-look-nextcloud *),
580
580
  .cn-look-board .cn-conversation-thread .cn-conversation-thread__meta time:not(.cn-look-nextcloud *) {
581
581
  color: var(--cn-board-text-soft);
582
+ font-size: 13px;
582
583
  line-height: normal;
583
584
  }
584
585
 
586
+ /* 6px between the meta line and the text.
587
+ @spec openspec/changes/board-conversation-bubble-meta-gap/specs/board-conversation-bubble-meta-gap/spec.md#requirement-a-board-message-sets-its-meta-line-at-13px-6px-above-the-text */
588
+ .cn-look-board .cn-conversation-thread .cn-conversation-thread__bubble:not(.cn-look-nextcloud *) {
589
+ gap: 6px;
590
+ }
591
+
585
592
  .cn-look-board .cn-conversation-thread .cn-conversation-thread__message--us .cn-conversation-thread__bubble:not(.cn-look-nextcloud *) {
586
593
  color: var(--color-main-text);
587
594
  }
@@ -1270,6 +1277,11 @@
1270
1277
  line-height: normal;
1271
1278
  }
1272
1279
 
1280
+ /* Head and body text at line height 1.2, as the board draws the table. */
1281
+ .cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table :is(th, td) {
1282
+ line-height: 1.2;
1283
+ }
1284
+
1273
1285
  /* A long task name wraps under itself, as the board draws it, rather than
1274
1286
  widening the table past the card. */
1275
1287
  .cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table :is(.cn-tasks-widget__name, .cn-tasks-widget__meta) {
@@ -1569,3 +1581,15 @@
1569
1581
  font-size: 14px;
1570
1582
  line-height: 1.2;
1571
1583
  }
1584
+
1585
+ /* The contacts tab inside a detail card (DqZaakBetrokkenen "Partijen") takes
1586
+ the card's padding instead of its own, and its buttons start at the card's
1587
+ edge under the subtitle.
1588
+ @spec openspec/changes/board-contacts-in-card/specs/contacts/spec.md */
1589
+ .cn-look-board .cn-detail-page__leaf-card .cn-contacts-tab {
1590
+ padding: 0;
1591
+ }
1592
+
1593
+ .cn-look-board .cn-detail-page__leaf-card .cn-contacts-tab .cn-contacts-tab__header {
1594
+ justify-content: flex-start;
1595
+ }
@@ -71,6 +71,27 @@
71
71
  margin-inline-start: 0;
72
72
  }
73
73
 
74
+ /* With the side panel open the buttons take the row under the title, the
75
+ breadcrumb and the count line follow (DqZakenSorteren).
76
+ @spec openspec/changes/board-header-buttons-under-title-beside-side-panel/specs/board-header-buttons-under-title-beside-side-panel/spec.md#requirement-the-header-buttons-sit-under-the-title-while-the-side-panel-is-open */
77
+ .cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-index-page__header-buttons,
78
+ .cn-look-board .cn-index-page--sidebar-open .cn-page-header .cn-index-page__header-buttons {
79
+ grid-column: 1 / -1;
80
+ grid-row: 2;
81
+ justify-self: start;
82
+ margin-block: 6px 3px;
83
+ }
84
+
85
+ .cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-header-trail,
86
+ .cn-look-board .cn-index-page--sidebar-open .cn-page-header .cn-header-trail {
87
+ grid-row: 3;
88
+ }
89
+
90
+ .cn-look-board.cn-index-page--sidebar-open .cn-page-header .cn-page-header__text > .cn-page-header__description,
91
+ .cn-look-board .cn-index-page--sidebar-open .cn-page-header .cn-page-header__text > .cn-page-header__description {
92
+ grid-row: 4;
93
+ }
94
+
74
95
  /* The header buttons keep the board's button under a theme that styles every
75
96
  secondary or primary button with `!important` (the nldesign theme: a
76
97
  primary border, 8px 16px padding): Download and the Actions menu are 40px,
@@ -321,6 +321,20 @@
321
321
  border: 0;
322
322
  }
323
323
 
324
+ /* A dashboard banner's kicker is a 13px bold line in sentence case, no
325
+ tracking (DqMijnWerk "Vandaag eerst", BqMijnGoedkeuringen "First, today");
326
+ the plain look keeps its spaced capitals. The kicker is a paragraph, so its
327
+ size and weight also go to the theme's paragraph tokens.
328
+ @spec openspec/changes/board-banner-kicker-case/specs/board-banner-kicker-case/spec.md#requirement-a-board-dashboard-banners-kicker-is-in-sentence-case */
329
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-banner-widget__kicker {
330
+ --nldesign-component-paragraph-font-size: 13px;
331
+ --nldesign-component-paragraph-font-weight: 700;
332
+ font-size: 13px;
333
+ font-weight: 700;
334
+ letter-spacing: 0;
335
+ text-transform: none;
336
+ }
337
+
324
338
  /* ---- A workspace filter form (CnWorkspaceFilterWidget) ----------------- */
325
339
  /* The boards draw the form's field and its load button 40px tall, the button
326
340
  16px in at both sides (DqTermijnen "Jaar" with "Rapport laden"); the form
@@ -647,6 +661,13 @@
647
661
  --button-size: 34px;
648
662
  }
649
663
 
664
+ /* Two buttons in a card's title row stand 8px apart (DqZaakContact "Live
665
+ gesprek", DqZaakEmail "Mail koppelen" / "Mail opstellen").
666
+ @spec openspec/changes/board-card-header-actions-gap/specs/board-card-header-actions-gap/spec.md#requirement-a-board-cards-title-row-buttons-stand-8px-apart */
667
+ .cn-look-board .cn-widget-wrapper__actions:not(.cn-look-nextcloud *) {
668
+ gap: 8px;
669
+ }
670
+
650
671
  /* A detail page marked `cardButtons: "regular"` draws them at the 40px
651
672
  control height: the case page's tab cards (DqZaakContact "Live gesprek",
652
673
  DqZaakHistorie, DqZaakBetrokkenen), where setting and case-type pages keep 34.