@conduction/nextcloud-vue 2.124.0 → 2.125.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 (66) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +120 -11
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +78 -32
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +8 -7
  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 +2 -1
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -2
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDialog/CnDialog.vue2.js +5 -0
  15. package/dist/esm/components/CnDialog/CnDialog.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDialog/CnDialog.vue3.js +1 -1
  17. package/dist/esm/components/CnDialog/CnDialog.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +13 -0
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  21. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  23. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +7 -2
  24. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  27. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +159 -10
  28. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +151 -90
  30. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +12 -0
  32. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
  33. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
  34. package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
  35. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  36. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +39 -68
  37. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  38. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +70 -116
  39. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  40. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  41. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +19 -3
  42. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  43. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  44. package/dist/esm/l10n/en.json.js +8 -8
  45. package/dist/esm/l10n/nl.json.js +8 -8
  46. package/dist/esm/library-style.js +1 -1
  47. package/dist/esm/nextcloud-vue.css +316 -185
  48. package/dist/nextcloud-vue.cjs.js +1149 -808
  49. package/dist/nextcloud-vue.cjs.js.map +2 -2
  50. package/dist/nextcloud-vue.css +316 -185
  51. package/l10n/en.json +8 -8
  52. package/l10n/nl.json +8 -8
  53. package/package.json +1 -1
  54. package/src/components/CnActionDialog/CnActionDialog.vue +173 -14
  55. package/src/components/CnAppRoot/CnAppRoot.vue +8 -7
  56. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -3
  57. package/src/components/CnDialog/CnDialog.vue +6 -1
  58. package/src/components/CnIndexPage/CnIndexPage.vue +14 -0
  59. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -2
  60. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +237 -23
  61. package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +13 -1
  62. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +147 -145
  63. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +19 -3
  64. package/src/css/dialog.css +23 -0
  65. package/src/css/look-board-index.css +5 -1
  66. package/src/css/look-board.css +19 -0
@@ -6,7 +6,7 @@
6
6
  <template>
7
7
  <div
8
8
  class="cn-record-list"
9
- :class="`cn-record-list--${presentation}`"
9
+ :class="[`cn-record-list--${presentation}`, { 'cn-record-list--compact': content.density === 'compact' }]"
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 }}
@@ -40,7 +40,8 @@
40
40
  </p>
41
41
  </div>
42
42
  <!-- table: one row per record under a header, one cell per `columns`
43
- entry; a cell past its `alertAbove` reads bold in the danger colour. -->
43
+ entry; a cell past its `alertAbove` reads bold in the danger colour,
44
+ a `tone` cell is a toned label, a `sortable` header sorts the rows. -->
44
45
  <table v-else-if="presentation === 'table'" class="cn-record-list__table" data-testid="cn-record-list-table">
45
46
  <thead>
46
47
  <tr>
@@ -48,18 +49,41 @@
48
49
  v-for="col in tableColumns"
49
50
  :key="col.key"
50
51
  scope="col"
52
+ :aria-sort="col.sortable ? sortStateOf(col.key) : undefined"
51
53
  :class="{ 'cn-record-list__cell--end': col.align === 'end' }">
52
- {{ col.label }}
54
+ <button
55
+ v-if="col.sortable"
56
+ type="button"
57
+ class="cn-record-list__sort"
58
+ :data-testid="`cn-record-list-sort-${col.key}`"
59
+ @click="toggleTableSort(col.key)">
60
+ {{ col.label }}
61
+ <span class="cn-record-list__sort-mark" aria-hidden="true">{{ sortMarkOf(col.key) }}</span>
62
+ </button>
63
+ <template v-else>
64
+ {{ col.label }}
65
+ </template>
53
66
  </th>
54
67
  </tr>
55
68
  </thead>
56
69
  <tbody>
57
70
  <tr v-for="item in tableRows" :key="item.key" data-testid="cn-record-list-item">
58
71
  <td
59
- v-for="cell in item.cells"
72
+ v-for="(cell, cellIndex) in item.cells"
60
73
  :key="cell.key"
74
+ :style="cellIndex === 0 && item.depth > 0 ? `padding-inline-start: calc(${item.depth} * 16px)` : undefined"
61
75
  :class="{ 'cn-record-list__cell--end': cell.align === 'end', 'cn-record-list__cell--alert': cell.alert }">
62
- {{ cell.text }}
76
+ <CnStatusBadge
77
+ v-if="cell.tone"
78
+ :label="cell.text"
79
+ :variant="cell.tone"
80
+ size="small" />
81
+ <router-link v-else-if="cellIndex === 0 && item.to" :to="item.to">
82
+ {{ cell.text }}
83
+ </router-link>
84
+ <template v-else>
85
+ {{ cell.text }}
86
+ </template>
63
87
  </td>
64
88
  </tr>
65
89
  </tbody>
@@ -200,17 +224,25 @@
200
224
  :size="18"
201
225
  aria-hidden="true" />
202
226
  <NcActions
203
- v-if="item.tag === 'div' && rowActions.length > 0"
227
+ v-if="item.tag === 'div' && item.rowActions.length > 0"
204
228
  :aria-label="rowActionsLabel"
205
229
  data-testid="cn-record-list-row-actions">
206
- <NcActionButton
207
- v-for="(action, actionIndex) in rowActions"
208
- :key="`row-action-${actionIndex}`"
209
- :closeAfterClick="true"
210
- :data-testid="`cn-record-list-row-action-${actionIndex}`"
211
- @click="runRowActionEntry(action, item.row)">
212
- {{ action.label }}
213
- </NcActionButton>
230
+ <template v-for="entry in item.rowActions" :key="`row-action-${entry.index}`">
231
+ <NcActionLink
232
+ v-if="entry.href"
233
+ :href="entry.href"
234
+ :target="entry.external ? '_blank' : undefined"
235
+ :data-testid="`cn-record-list-row-action-${entry.index}`">
236
+ {{ entry.action.label }}
237
+ </NcActionLink>
238
+ <NcActionButton
239
+ v-else
240
+ :closeAfterClick="true"
241
+ :data-testid="`cn-record-list-row-action-${entry.index}`"
242
+ @click="runRowActionEntry(entry.action, item.row)">
243
+ {{ entry.action.label }}
244
+ </NcActionButton>
245
+ </template>
214
246
  </NcActions>
215
247
  </component>
216
248
 
@@ -327,7 +359,7 @@
327
359
  <script>
328
360
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
329
361
  import { translate as t } from '@nextcloud/l10n'
330
- import { NcActionButton, NcActions, NcButton, NcLoadingIcon } from '@nextcloud/vue'
362
+ import { NcActionButton, NcActionLink, NcActions, NcButton, NcLoadingIcon } from '@nextcloud/vue'
331
363
  import { inject, ref } from 'vue'
332
364
  import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
333
365
  import CnIcon from '../CnIcon/CnIcon.vue'
@@ -346,7 +378,7 @@ import { lensUnavailableText, readLensReports } from '../../utils/lensAvailabili
346
378
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
347
379
  import { readPath } from '../../utils/readPath.js'
348
380
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveDeepTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
349
- import { evaluateVisibleWhen } from '../../utils/visibleWhen.js'
381
+ import { evaluateVisibleWhen, evaluateVisibleWhenLocal } from '../../utils/visibleWhen.js'
350
382
 
351
383
  const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
352
384
 
@@ -354,6 +386,22 @@ const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
354
386
  let footerSeq = 0
355
387
  const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice', 'table', 'notes']
356
388
 
389
+ /**
390
+ * Compare two table cell texts: as numbers when both read as one, else as text.
391
+ *
392
+ * @param {string} a One cell text.
393
+ * @param {string} b The other.
394
+ * @return {number} Negative, zero or positive.
395
+ */
396
+ function compareCells(a, b) {
397
+ const x = Number(a)
398
+ const y = Number(b)
399
+ if (a !== '' && b !== '' && Number.isFinite(x) && Number.isFinite(y)) {
400
+ return x - y
401
+ }
402
+ return String(a).localeCompare(String(b))
403
+ }
404
+
357
405
  /** The tones a `notice` list takes. */
358
406
  const NOTICE_VARIANTS = ['info', 'warning', 'error']
359
407
 
@@ -405,7 +453,7 @@ function unwrap(value) {
405
453
  export default {
406
454
  name: 'CnRecordListWidget',
407
455
 
408
- components: { ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, NcActionButton, NcActions, NcButton, NcLoadingIcon },
456
+ components: { ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, NcActionButton, NcActionLink, NcActions, NcButton, NcLoadingIcon },
409
457
 
410
458
  inject: {
411
459
  /** Detail-page object context (`{ objectId, object, register, schema }`). */
@@ -451,6 +499,8 @@ export default {
451
499
  * per value, headed by the field's `valueLabels` entry; `groupOrder`
452
500
  * (values) orders the groups and `groupEmptyLabel` heads the rows
453
501
  * without a value.
502
+ * `density: "compact"` draws the rows 10px above and below, at least
503
+ * 56px tall, with the title at 500 (DqProcesanalyse).
454
504
  *
455
505
  * @type {object}
456
506
  */
@@ -501,6 +551,8 @@ export default {
501
551
 
502
552
  data() {
503
553
  return {
554
+ /** The reader's sort of a `table`: the column key and `ascending` or `descending`. */
555
+ tableSort: { key: '', dir: '' },
504
556
  rows: [],
505
557
  total: 0,
506
558
  loading: false,
@@ -702,6 +754,7 @@ export default {
702
754
  dot: this.dotOf(row),
703
755
  flag: this.flagOf(row),
704
756
  links: this.linksOf(row),
757
+ rowActions: this.rowActionsOf(row),
705
758
  }
706
759
  })
707
760
  },
@@ -780,8 +833,11 @@ export default {
780
833
  * @spec openspec/changes/record-list-table/specs/record-list-table/spec.md#requirement-a-record-list-can-draw-its-rows-as-a-small-table
781
834
  */
782
835
  tableRows() {
783
- return this.items.map((item) => ({
836
+ const indentPath = this.content?.indentPath
837
+ const rows = this.items.map((item) => ({
784
838
  key: item.key,
839
+ to: item.attrs.to || null,
840
+ depth: indentPath ? Math.max(0, Math.floor(Number(readPath(item.row, indentPath)) || 0)) : 0,
785
841
  cells: this.tableColumns.map((col) => {
786
842
  const text = this.fill(col.value, item.row)
787
843
  const limit = Number(col.alertAbove)
@@ -791,9 +847,17 @@ export default {
791
847
  text,
792
848
  align: col.align,
793
849
  alert: col.alertAbove !== undefined && Number.isFinite(limit) && text !== '' && Number.isFinite(value) && value > limit,
850
+ tone: this.cellToneOf(col, item.row, text),
794
851
  }
795
852
  }),
796
853
  }))
854
+ const { key, dir } = this.tableSort
855
+ const index = this.tableColumns.findIndex((col) => col.key === key)
856
+ if (index < 0 || !dir) {
857
+ return rows
858
+ }
859
+ const sign = dir === 'descending' ? -1 : 1
860
+ return [...rows].sort((a, b) => sign * compareCells(a.cells[index].text, b.cells[index].text))
797
861
  },
798
862
 
799
863
  /** @return {string} The note under the list (`footnote`), translated. */
@@ -1455,6 +1519,123 @@ export default {
1455
1519
  }
1456
1520
  },
1457
1521
 
1522
+ /**
1523
+ * The badge variant of a `table` cell with a `tone`
1524
+ * (`{ field?, map, default?, override?: { when, tone } }`): the value at
1525
+ * `field` (else the cell text) looked up in `map`, else `default`. While
1526
+ * `override.when` (a condition over the row) holds, the cell takes
1527
+ * `override.tone` instead, so a verdict is never green unless a second
1528
+ * field allows it.
1529
+ *
1530
+ * @param {object} col The column.
1531
+ * @param {object} row The row.
1532
+ * @param {string} text The cell text.
1533
+ * @return {string} A badge variant, or '' for a plain cell.
1534
+ * @spec openspec/changes/record-list-table-shape/specs/record-list-table-shape/spec.md#requirement-a-record-list-table-takes-its-shape-from-the-rows
1535
+ */
1536
+ cellToneOf(col, row, text) {
1537
+ const tone = col.tone
1538
+ if (!tone || typeof tone !== 'object') {
1539
+ return ''
1540
+ }
1541
+ let variant
1542
+ if (tone.override && typeof tone.override === 'object' && evaluateVisibleWhenLocal(tone.override.when, row || {})) {
1543
+ variant = tone.override.tone
1544
+ } else {
1545
+ const value = tone.field ? readPath(row, tone.field) : text
1546
+ const map = tone.map && typeof tone.map === 'object' ? tone.map : {}
1547
+ variant = Object.hasOwn(map, String(value)) ? map[String(value)] : tone.default
1548
+ }
1549
+ return isBadgeVariant(variant) ? variant : 'default'
1550
+ },
1551
+
1552
+ /**
1553
+ * The `aria-sort` of a sortable `table` column.
1554
+ *
1555
+ * @param {string} key The column key.
1556
+ * @return {string} `ascending`, `descending` or `none`.
1557
+ */
1558
+ sortStateOf(key) {
1559
+ return this.tableSort.key === key && this.tableSort.dir ? this.tableSort.dir : 'none'
1560
+ },
1561
+
1562
+ /**
1563
+ * The arrow after a sortable column's label.
1564
+ *
1565
+ * @param {string} key The column key.
1566
+ * @return {string} An up or down arrow, or '' while the column is not the sort.
1567
+ */
1568
+ sortMarkOf(key) {
1569
+ const state = this.sortStateOf(key)
1570
+ if (state === 'ascending') {
1571
+ return '▲'
1572
+ }
1573
+ return state === 'descending' ? '▼' : ''
1574
+ },
1575
+
1576
+ /**
1577
+ * Sort the `table` by a column: ascending first, then descending.
1578
+ *
1579
+ * @param {string} key The column key.
1580
+ * @spec openspec/changes/record-list-table-shape/specs/record-list-table-shape/spec.md#requirement-a-record-list-table-takes-its-shape-from-the-rows
1581
+ */
1582
+ toggleTableSort(key) {
1583
+ const dir = this.tableSort.key === key && this.tableSort.dir === 'ascending' ? 'descending' : 'ascending'
1584
+ this.tableSort = { key, dir }
1585
+ },
1586
+
1587
+ /**
1588
+ * The rows of a read with `flattenPath` set: each entry of the list at
1589
+ * that path on a record becomes a row, with the record at `@parent`.
1590
+ * A record without the list adds no row. Without `flattenPath` the
1591
+ * records are the rows.
1592
+ *
1593
+ * @param {Array<object>} records The records read.
1594
+ * @return {Array<object>} The rows.
1595
+ * @spec openspec/changes/record-list-table-shape/specs/record-list-table-shape/spec.md#requirement-a-record-list-table-takes-its-shape-from-the-rows
1596
+ */
1597
+ flattenRows(records) {
1598
+ const path = this.content?.flattenPath
1599
+ if (typeof path !== 'string' || path === '') {
1600
+ return records
1601
+ }
1602
+ return records.flatMap((record) => {
1603
+ const list = readPath(record, path)
1604
+ return Array.isArray(list)
1605
+ ? list.filter((entry) => entry && typeof entry === 'object').map((entry) => ({ ...entry, '@parent': record }))
1606
+ : []
1607
+ })
1608
+ },
1609
+
1610
+ /**
1611
+ * The row actions shown on one row: those whose `visibleWhen` (a
1612
+ * condition over the row) holds. An `href` action is a template over
1613
+ * the row and shows only when it fills to a web address or an
1614
+ * instance path; a web address opens in a new tab.
1615
+ *
1616
+ * @param {object} row The row.
1617
+ * @return {Array<{action: object, index: number, href: string, external: boolean}>} The visible actions, with their place in `rowActions`.
1618
+ * @spec openspec/changes/record-list-row-rules/specs/record-list-row-rules/spec.md#requirement-a-row-action-can-depend-on-the-row
1619
+ */
1620
+ rowActionsOf(row) {
1621
+ const shown = []
1622
+ this.rowActions.forEach((action, index) => {
1623
+ if (action.visibleWhen && !evaluateVisibleWhenLocal(action.visibleWhen, row || {})) {
1624
+ return
1625
+ }
1626
+ if (typeof action.href !== 'string') {
1627
+ shown.push({ action, index, href: '', external: false })
1628
+ return
1629
+ }
1630
+ const href = this.fill(action.href, row).trim()
1631
+ const external = /^https?:\/\//i.test(href)
1632
+ if (external || (href.startsWith('/') && !href.startsWith('//'))) {
1633
+ shown.push({ action, index, href, external })
1634
+ }
1635
+ })
1636
+ return shown
1637
+ },
1638
+
1458
1639
  /**
1459
1640
  * A row's `links` line: the ids at `links.field`, each labelled from
1460
1641
  * `links.labelSource` (`{ register, schema, template }`) once read, else
@@ -1782,7 +1963,7 @@ export default {
1782
1963
  * @spec openspec/changes/endpoint-source-card-grid-record-list/specs/dashboard-page/spec.md#requirement-a-record-list-and-a-card-grid-can-read-an-endpoint
1783
1964
  */
1784
1965
  takeEndpointRows() {
1785
- this.rows = Array.isArray(this.epData) ? this.epData : []
1966
+ this.rows = this.flattenRows(Array.isArray(this.epData) ? this.epData : [])
1786
1967
  this.total = this.rows.length
1787
1968
  this.loading = this.epLoading === true
1788
1969
  this.error = Boolean(this.epError)
@@ -1859,14 +2040,14 @@ export default {
1859
2040
  if (run !== this.fetchRun) {
1860
2041
  return
1861
2042
  }
1862
- this.rows = ranked.rows
1863
- this.total = ranked.total
2043
+ this.rows = this.flattenRows(ranked.rows)
2044
+ this.total = this.content?.flattenPath ? this.rows.length : ranked.total
1864
2045
  return
1865
2046
  }
1866
2047
  const response = await axios.get(url, { params })
1867
2048
  const data = response && response.data ? response.data : {}
1868
- this.rows = Array.isArray(data.results) ? data.results : []
1869
- this.total = typeof data.total === 'number' ? data.total : this.rows.length
2049
+ this.rows = this.flattenRows(Array.isArray(data.results) ? data.results : [])
2050
+ this.total = typeof data.total === 'number' && !this.content?.flattenPath ? data.total : this.rows.length
1870
2051
  this.lenses = readLensReports(data)
1871
2052
  } catch (e) {
1872
2053
  // eslint-disable-next-line no-console
@@ -2031,6 +2212,16 @@ export default {
2031
2212
  font-weight: 700;
2032
2213
  }
2033
2214
 
2215
+ .cn-record-list--compact .cn-record-list__row {
2216
+ box-sizing: border-box;
2217
+ min-height: 56px;
2218
+ padding: 10px 0;
2219
+ }
2220
+
2221
+ .cn-record-list--compact .cn-record-list__title {
2222
+ font-weight: 500;
2223
+ }
2224
+
2034
2225
  .cn-record-list__meta {
2035
2226
  color: var(--color-text-maxcontrast);
2036
2227
  font-size: 13px;
@@ -2077,6 +2268,29 @@ export default {
2077
2268
  border-bottom: 1px solid var(--color-border);
2078
2269
  }
2079
2270
 
2271
+ .cn-record-list__sort {
2272
+ display: inline-flex;
2273
+ gap: 4px;
2274
+ align-items: center;
2275
+ min-height: 0;
2276
+ margin: 0;
2277
+ padding: 0;
2278
+ border: none;
2279
+ background: none;
2280
+ color: inherit;
2281
+ font: inherit;
2282
+ cursor: pointer;
2283
+ }
2284
+
2285
+ .cn-record-list__sort:focus-visible {
2286
+ outline: 2px solid var(--color-primary-element);
2287
+ outline-offset: 2px;
2288
+ }
2289
+
2290
+ .cn-record-list__sort-mark {
2291
+ font-size: 10px;
2292
+ }
2293
+
2080
2294
  .cn-record-list__table .cn-record-list__cell--end {
2081
2295
  padding-inline-start: 12px;
2082
2296
  text-align: end;
@@ -10,7 +10,7 @@
10
10
  sidebar's Filters tab (CnSavedViewsPanel).
11
11
  -->
12
12
  <div
13
- v-if="isBoardLook && chipRows.length > 0"
13
+ v-if="isBoardLook && chips && chipRows.length > 0"
14
14
  class="cn-saved-views__chips"
15
15
  role="group"
16
16
  :aria-label="t('nextcloud-vue', 'Saved views')"
@@ -81,6 +81,18 @@ export default {
81
81
  // The chips and the button are two roots under the board look.
82
82
  inheritAttrs: false,
83
83
 
84
+ props: {
85
+ /**
86
+ * Whether the board look draws the saved views as chips. `false`
87
+ * leaves only Save view, for a page whose views live in the side
88
+ * panel (DqZaken).
89
+ */
90
+ chips: {
91
+ type: Boolean,
92
+ default: true,
93
+ },
94
+ },
95
+
84
96
  emits: ['apply', 'save-request'],
85
97
 
86
98
  computed: {