@conduction/nextcloud-vue 2.137.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 (105) hide show
  1. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  2. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +6 -4
  3. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  4. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDetailCard/CnDetailCard.vue.js +1 -1
  9. package/dist/esm/components/CnDetailCard/CnDetailCard.vue2.js +6 -0
  10. package/dist/esm/components/CnDetailCard/CnDetailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDetailCard/CnDetailCard.vue3.js +24 -7
  12. package/dist/esm/components/CnDetailCard/CnDetailCard.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +2 -0
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  17. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  21. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +43 -1
  22. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  23. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +133 -6
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  26. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -17
  27. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +2 -0
  29. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  30. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +13 -2
  31. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  32. package/dist/esm/components/CnMailTab/dashboardRegistration.js +4 -0
  33. package/dist/esm/components/CnMailTab/dashboardRegistration.js.map +1 -1
  34. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  35. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  36. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  37. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  38. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +2 -0
  43. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  45. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  46. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  47. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +6 -0
  48. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  49. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +3 -3
  50. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +16 -0
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +11 -5
  55. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  56. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue.js +1 -1
  57. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue2.js.map +1 -1
  58. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js +1 -0
  59. package/dist/esm/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue3.js.map +1 -1
  60. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  61. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  62. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  63. package/dist/esm/composables/indexSources.js +78 -12
  64. package/dist/esm/composables/indexSources.js.map +1 -1
  65. package/dist/esm/l10n/en.json.js +12 -1
  66. package/dist/esm/l10n/en.json.js.map +1 -1
  67. package/dist/esm/l10n/nl.json.js +12 -1
  68. package/dist/esm/l10n/nl.json.js.map +1 -1
  69. package/dist/esm/library-style.js +1 -1
  70. package/dist/esm/nextcloud-vue.css +685 -512
  71. package/dist/esm/utils/resolveFilterTokens.js +9 -0
  72. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  73. package/dist/esm/utils/sentinelTokens.js +1 -1
  74. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  75. package/dist/nextcloud-vue.cjs.js +580 -236
  76. package/dist/nextcloud-vue.cjs.js.map +2 -2
  77. package/dist/nextcloud-vue.css +685 -512
  78. package/l10n/en.json +12 -1
  79. package/l10n/nl.json +12 -1
  80. package/package.json +1 -1
  81. package/src/components/CnCellRenderer/CnCellRenderer.vue +6 -4
  82. package/src/components/CnChartWidget/CnChartWidget.vue +4 -0
  83. package/src/components/CnDetailCard/CnDetailCard.md +1 -0
  84. package/src/components/CnDetailCard/CnDetailCard.vue +25 -1
  85. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -0
  86. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  87. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +54 -1
  88. package/src/components/CnIndexPage/CnIndexPage.vue +135 -7
  89. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +10 -2
  90. package/src/components/CnMailTab/dashboardRegistration.js +4 -0
  91. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -0
  92. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  93. package/src/components/CnObjectSidebar/CnFilesTab.vue +3 -0
  94. package/src/components/CnOrderedListField/CnOrderedListField.vue +11 -1
  95. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +22 -3
  96. package/src/components/CnSavedViewMenuSettings/CnSavedViewMenuSettings.vue +1 -0
  97. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +3 -0
  98. package/src/composables/indexSources.js +78 -12
  99. package/src/css/dialog.css +26 -0
  100. package/src/css/index-page.css +5 -0
  101. package/src/css/look-board-detail.css +36 -0
  102. package/src/css/look-board-index.css +21 -0
  103. package/src/css/look-board.css +47 -0
  104. package/src/utils/resolveFilterTokens.js +9 -0
  105. package/src/utils/sentinelTokens.js +1 -1
@@ -5,7 +5,11 @@
5
5
 
6
6
  <template>
7
7
  <div class="cn-ordered-list-field" data-testid="cn-ordered-list-field">
8
- <p v-if="label" :id="labelId" class="cn-ordered-list-field__label">
8
+ <p
9
+ v-if="label"
10
+ :id="labelId"
11
+ class="cn-ordered-list-field__label"
12
+ :class="{ 'hidden-visually': hideLabel }">
9
13
  {{ label }}
10
14
  </p>
11
15
  <ol
@@ -192,6 +196,12 @@ export default {
192
196
  default: '',
193
197
  },
194
198
 
199
+ /** Keep the label for assistive technology only, when a heading above already says it. */
200
+ hideLabel: {
201
+ type: Boolean,
202
+ default: false,
203
+ },
204
+
195
205
  /** The picker's label. */
196
206
  addLabel: {
197
207
  type: String,
@@ -20,9 +20,11 @@
20
20
  <div v-else-if="loading && rows.length === 0" class="cn-record-list__loading">
21
21
  <NcLoadingIcon :size="20" :name="loadingLabel" />
22
22
  </div>
23
- <p v-else-if="rows.length === 0" class="cn-record-list__empty" data-testid="cn-record-list-empty">
24
- {{ emptyLabel }}
25
- </p>
23
+ <template v-else-if="rows.length === 0">
24
+ <p v-if="emptyLabel !== ''" class="cn-record-list__empty" data-testid="cn-record-list-empty">
25
+ {{ emptyLabel }}
26
+ </p>
27
+ </template>
26
28
  <!-- notice: the rows as sentences in one toned box ("2 waarschuwingen: ..."). -->
27
29
  <div
28
30
  v-else-if="presentation === 'notice'"
@@ -251,6 +253,7 @@
251
253
  <component
252
254
  :is="item.action.tag"
253
255
  class="cn-record-list__action"
256
+ :class="{ 'cn-record-list__action--outlined': rowActionOutlined }"
254
257
  v-bind="item.action.attrs"
255
258
  data-testid="cn-record-list-action"
256
259
  v-on="item.action.tag === 'button' ? { click: () => runRowAction(item.row) } : {}">
@@ -1469,6 +1472,18 @@ export default {
1469
1472
  * @return {string}
1470
1473
  * @spec openspec/changes/record-list-table-row-actions/specs/record-list-table/spec.md#requirement-a-table-row-can-carry-a-second-line-and-an-actions-menu
1471
1474
  */
1475
+ /**
1476
+ * Whether the row action is drawn as an outlined button sized to its
1477
+ * label (`rowAction.variant: "outlined"`, DqMijnTeam "Oppakken") instead
1478
+ * of the 150px pill (DcMijnActies "Stem uitbrengen").
1479
+ *
1480
+ * @return {boolean} True for the outlined variant.
1481
+ * @spec openspec/changes/board-row-action-outlined/specs/board-row-action-outlined/spec.md#requirement-a-dashboard-rows-action-is-an-outlined-button
1482
+ */
1483
+ rowActionOutlined() {
1484
+ return this.content?.rowAction?.variant === 'outlined'
1485
+ },
1486
+
1472
1487
  rowActionsHeader() {
1473
1488
  const own = this.content?.rowActionsHeader
1474
1489
  return typeof own === 'string' && own !== '' ? this.translate(own) : t('nextcloud-vue', 'Actions')
@@ -1511,6 +1526,10 @@ export default {
1511
1526
  if (this.conditionalEmptyText !== '') {
1512
1527
  return this.conditionalEmptyText
1513
1528
  }
1529
+ // `emptyText: ""` says nothing on an empty list.
1530
+ if (this.content?.emptyText === '') {
1531
+ return ''
1532
+ }
1514
1533
  return this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')
1515
1534
  },
1516
1535
 
@@ -12,6 +12,7 @@
12
12
  :modelValue="values"
13
13
  :options="options"
14
14
  :label="t('nextcloud-vue', 'Views in my menu')"
15
+ :hideLabel="true"
15
16
  :addLabel="t('nextcloud-vue', 'Add view')"
16
17
  :addPlaceholder="t('nextcloud-vue', 'Pick a saved view')"
17
18
  :addHint="t('nextcloud-vue', 'You see your own views, the views shared with you and the default views. A new view goes to the bottom.')"
@@ -400,7 +400,10 @@ export default {
400
400
  background: var(--color-main-background);
401
401
  }
402
402
 
403
+ /* A section's heading and description start at the text's edge, as the
404
+ board draws them (NcAppSettingsSection indents them 12px). */
403
405
  .cn-user-settings-dialog__sections :deep(.app-settings-section) {
406
+ --app-settings-section-text-offset: 0;
404
407
  margin: 0;
405
408
  padding: 0;
406
409
  border: 0;
@@ -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?, 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
@@ -1102,6 +1105,10 @@ function fillRowTemplate(template, row) {
1102
1105
  * coloured by `sourceConfig.statusColorMap`. `linkedTemplate` (a field
1103
1106
  * template, "{case.title}") or `linkedField` (a path) fills the "Linked"
1104
1107
  * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
1108
+ * `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
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.
1105
1112
  * The page's title and count line can name the term as `{term}`, and the
1106
1113
  * count is the active kind's total. Kind, link and status are filterable.
1107
1114
  *
@@ -1111,6 +1118,23 @@ function fillRowTemplate(template, row) {
1111
1118
  * @return {object} The source adapter.
1112
1119
  * @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
1113
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
+
1114
1138
  function searchSource(config = {}, context = {}) {
1115
1139
  const translate = typeof context.translate === 'function' ? context.translate : (text) => text
1116
1140
  const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
@@ -1199,12 +1223,27 @@ function searchSource(config = {}, context = {}) {
1199
1223
  return ''
1200
1224
  }
1201
1225
 
1226
+ const subtitleOf = (kind, object) => {
1227
+ if (kind.subtitleTemplate) {
1228
+ return fillTemplate(translate(kind.subtitleTemplate), object)
1229
+ }
1230
+ return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
1231
+ }
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
+
1202
1240
  const searchObjects = async (axios, kind, term) => {
1203
1241
  const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
1204
1242
  const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
1205
1243
  const body = (res && res.data) || {}
1206
1244
  const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
1207
1245
  const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
1246
+ const userNames = kind.linkedUser === true ? await linkedUserNames(kind, results) : null
1208
1247
  return {
1209
1248
  total: typeof body.total === 'number' ? body.total : results.length,
1210
1249
  rows: results.map((object) => {
@@ -1219,9 +1258,9 @@ function searchSource(config = {}, context = {}) {
1219
1258
  kindLabel: kind.label,
1220
1259
  kindText: kindTextOf(kind, object),
1221
1260
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
1222
- subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
1261
+ subtitle: subtitleOf(kind, object),
1223
1262
  status,
1224
- linked: linkedOf(kind, object),
1263
+ linked: userNames ? (userNames.get(linkedOf(kind, object)) || linkedOf(kind, object)) : linkedOf(kind, object),
1225
1264
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
1226
1265
  object,
1227
1266
  }
@@ -1329,7 +1368,7 @@ function searchSource(config = {}, context = {}) {
1329
1368
  */
1330
1369
  loading: () => state.loading,
1331
1370
 
1332
- columns: [
1371
+ columns: withColumnWidths([
1333
1372
  { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true },
1334
1373
  // `kindAsText`: what one row is, as plain text ("Zaak", "Lantaarnpaal",
1335
1374
  // DqZoekresultaten), instead of the plural kind as a pill.
@@ -1344,7 +1383,7 @@ function searchSource(config = {}, context = {}) {
1344
1383
  widgetProps: { colorMap: (config && config.statusColorMap) || {} },
1345
1384
  sortable: true,
1346
1385
  },
1347
- ],
1386
+ ], config && config.columnWidths),
1348
1387
 
1349
1388
  /**
1350
1389
  * Every column header carries a filter button (DqZoekresultaten); the
@@ -1406,14 +1445,24 @@ function searchSource(config = {}, context = {}) {
1406
1445
  * the pager asks for.
1407
1446
  *
1408
1447
  * @param {Array<object>} members The members, by name.
1409
- * @param {object} config `search`, `sortKey`, `sortOrder`, `limit`, `offset`.
1448
+ * @param {object} config `search`, `nameLike`, `rolesLike`, `emailLike`, `today`, `sortKey`, `sortOrder`, `limit`, `offset`.
1410
1449
  * @return {{rows: Array<object>, total: number}} The page and how many match.
1411
1450
  */
1412
1451
  function membersPage(members, config = {}) {
1452
+ const has = (member, fields, text) => fields.some((field) => String(member[field] || '').toLowerCase().includes(text))
1413
1453
  const term = String(config.search || '').trim().toLowerCase()
1414
- let rows = term === ''
1415
- ? members.slice()
1416
- : members.filter((member) => ['displayname', 'function', 'roles', 'email'].some((field) => String(member[field] || '').toLowerCase().includes(term)))
1454
+ let rows = term === '' ? members.slice() : members.filter((member) => has(member, ['displayname', 'function', 'roles', 'email'], term))
1455
+ // The table heads' filters: text contains, and the Today values picked.
1456
+ for (const [param, fields] of [['nameLike', ['displayname', 'function']], ['rolesLike', ['roles']], ['emailLike', ['email']]]) {
1457
+ const text = String(config[param] || '').trim().toLowerCase()
1458
+ if (text !== '') {
1459
+ rows = rows.filter((member) => has(member, fields, text))
1460
+ }
1461
+ }
1462
+ const today = (Array.isArray(config.today) ? config.today : (config.today ? [config.today] : [])).map(String)
1463
+ if (today.length > 0) {
1464
+ rows = rows.filter((member) => today.includes(member.today))
1465
+ }
1417
1466
  if (config.sortKey) {
1418
1467
  const sign = config.sortOrder === 'desc' ? -1 : 1
1419
1468
  rows = rows.sort((a, b) => sign * String(a[config.sortKey] ?? '').localeCompare(String(b[config.sortKey] ?? '')))
@@ -1475,10 +1524,11 @@ function usersSource() {
1475
1524
  */
1476
1525
  load: async (config) => {
1477
1526
  const group = String((config && (config.group || config.defaultGroup)) || '')
1478
- const { search, sortKey, sortOrder, limit, offset, ...request } = config || {}
1527
+ const { search, sortKey, sortOrder, limit, offset, nameLike, rolesLike, emailLike, today, ...request } = config || {}
1479
1528
  const key = JSON.stringify({ ...request, group })
1480
- // A new page, term or order of the members already read: no request.
1481
- if (key === membersKey && (offset !== undefined || search !== undefined || sortKey !== undefined)) {
1529
+ const narrows = [offset, search, sortKey, nameLike, rolesLike, emailLike, today].some((value) => value !== undefined)
1530
+ // A new page, term, order or head filter of the members already read: no request.
1531
+ if (key === membersKey && narrows) {
1482
1532
  const page = membersPage(readMembers, config)
1483
1533
  state.rows = page.rows
1484
1534
  state.total = page.total
@@ -1574,8 +1624,24 @@ function usersSource() {
1574
1624
 
1575
1625
  searchFields: {
1576
1626
  group: { param: 'group', single: true },
1627
+ 'displayname[like]': { param: 'nameLike', single: true },
1628
+ 'roles[like]': { param: 'rolesLike', single: true },
1629
+ 'email[like]': { param: 'emailLike', single: true },
1630
+ today: { param: 'today' },
1577
1631
  },
1578
1632
 
1633
+ /** A filter button on every head (DqAfdelingen); the members read are narrowed here. */
1634
+ headerFilters: true,
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
+
1579
1645
  showAdd: false,
1580
1646
  }
1581
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;
@@ -212,6 +230,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
212
230
  padding: 22px 24px 20px;
213
231
  }
214
232
 
233
+ /* NcDialog's scoped `.dialog__content[data-v-…]` sets padding-inline-end
234
+ 12px with the same specificity as the rule above, so wherever its chunk
235
+ loads later (an admin page's setup wizard) the body ran 12px wider than
236
+ the board (DqInstellen 684 for 672). This one outranks it. */
237
+ .cn-dialog-board .dialog .dialog__content {
238
+ padding-inline-end: 24px;
239
+ }
240
+
215
241
  .cn-dialog-board .dialog__content > .cn-dialog-header {
216
242
  margin-bottom: -2px;
217
243
  }
@@ -221,6 +221,11 @@
221
221
  font-size: 14px;
222
222
  }
223
223
 
224
+ .cn-index-page__map-missing-note {
225
+ color: var(--color-main-text);
226
+ line-height: 1.45;
227
+ }
228
+
224
229
  /* The selected folder's heading above the list. */
225
230
  /* A sort option whose endpoint failed: the note above the fallen-back list. */
226
231
  .cn-index-page__sort-source-note {
@@ -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,23 @@
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
+
1285
+ /* A long task name wraps under itself, as the board draws it, rather than
1286
+ widening the table past the card. */
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) {
1288
+ white-space: normal;
1289
+ }
1290
+
1291
+ /* A row's button and its task menu stand on one line. */
1292
+ .cn-look-board .cn-detail-page .cn-widget-wrapper__content > .cn-tasks-widget .cn-tasks-widget__table .cn-tasks-widget__cell-actions > * {
1293
+ display: inline-flex;
1294
+ vertical-align: middle;
1295
+ }
1296
+
1273
1297
  /* ---- An object list table edge to edge (`content.tableBleed`) ------------ */
1274
1298
  /* The case-tab boards draw a list's table edge to edge in its card
1275
1299
  (DqZaakContact "Contactmomenten op deze zaak", DqZaakGerelateerd): it runs
@@ -1557,3 +1581,15 @@
1557
1581
  font-size: 14px;
1558
1582
  line-height: 1.2;
1559
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.
@@ -2335,6 +2356,32 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
2335
2356
  font-weight: 600;
2336
2357
  }
2337
2358
 
2359
+ /* A row whose one action is outlined (`rowAction.variant: "outlined"`,
2360
+ DqMijnTeam "Oppakken"): its title link reads as the row's title in the main
2361
+ ink (the theme's link rule reads --nldesign-color-on-surface), and the
2362
+ action is an outlined button sized to its label. Without the variant the
2363
+ action keeps its 150px pill (DcMijnActies).
2364
+ @spec openspec/changes/board-row-action-outlined/specs/board-row-action-outlined/spec.md#requirement-a-dashboard-rows-action-is-an-outlined-button */
2365
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__row--action:has(.cn-record-list__action--outlined) .cn-record-list__title--link {
2366
+ --nldesign-color-on-surface: var(--color-main-text);
2367
+ color: var(--color-main-text);
2368
+ }
2369
+
2370
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__action--outlined {
2371
+ --nldesign-color-on-surface: var(--color-main-text);
2372
+ flex: none;
2373
+ padding: 0 12px;
2374
+ border: 1px solid var(--cn-board-control-border);
2375
+ border-radius: 8px;
2376
+ background: var(--color-main-background);
2377
+ color: var(--color-main-text);
2378
+ }
2379
+
2380
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__action--outlined:hover,
2381
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__action--outlined:focus-visible {
2382
+ background: var(--color-background-hover);
2383
+ }
2384
+
2338
2385
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__meta {
2339
2386
  color: var(--color-text-maxcontrast);
2340
2387
  font-size: 13px;
@@ -43,6 +43,9 @@
43
43
  * - `@object.<field>` → a field off the current detail-page object (needs `ctx`)
44
44
  * - `@workspace.<key>` → a value off the page-level workspace context (needs `ctx.workspace`)
45
45
  * - `@config.<key>` → a value off the page-level app config (needs `ctx.config`)
46
+ * - `@context.<name>.<path>` → a value off a record the detail page reads beside
47
+ * the loaded one (needs `ctx.context`); left as written
48
+ * while that record is not read
46
49
  *
47
50
  * `@objectId` / `@object.<field>` are OBJECT-CONTEXT tokens: they resolve only
48
51
  * when a `ctx` `{ objectId, object }` is supplied (a detail page provides it via
@@ -75,6 +78,7 @@ import { getCurrentUser } from '@nextcloud/auth'
75
78
  // userGroupsPeek, not widgetVisibility: that one imports @nextcloud/axios, and
76
79
  // this resolver runs on public pages and in node (see userGroupsPeek.js).
77
80
  import { peekMyTeam } from './myTeam.js'
81
+ import { readPath } from './readPath.js'
78
82
  import { TODAY_DELTA_RE } from './sentinelTokens.js'
79
83
  import { peekCurrentUserGroups } from './userGroupsPeek.js'
80
84
 
@@ -195,6 +199,11 @@ export function resolveFilterValue(v, ctx) {
195
199
  }
196
200
  return v
197
201
  }
202
+ if (v.startsWith('@context.')) {
203
+ // @spec openspec/changes/files-second-line-fields/specs/files-second-line-fields/spec.md#requirement-widget-props-can-read-the-detail-pages-context-records
204
+ const found = ctx && ctx.context ? readPath(ctx.context, v.slice('@context.'.length)) : undefined
205
+ return found === undefined ? v : found
206
+ }
198
207
  if (v.startsWith('@workspace.')) {
199
208
  // A trailing `?` marks the token OPTIONAL: when unset the caller drops the
200
209
  // filter key (show all) instead of waiting (see hasUnresolvedTokens /
@@ -188,7 +188,7 @@ export const SENTINEL_VOCABULARY = Object.freeze({
188
188
  members: ['@subject.<field>'],
189
189
  },
190
190
  context: {
191
- resolver: 'CnDetailPage visibilityContext (detailWidgetVisibility), read by a grid widget\'s or a tab\'s visibleWhen',
191
+ resolver: 'CnDetailPage visibilityContext (detailWidgetVisibility), read by a grid widget\'s or a tab\'s visibleWhen, and by a widget\'s props (CnDetailWidgetHost)',
192
192
  description: 'A record the detail page reads beside the loaded one, named in `config.visibilityContext` (a case\'s case type). A `visibleWhen` field reads it as `@context.<name>.<field>`, dot paths allowed (`@context.caseType.category`).',
193
193
  members: ['@context.<name>.<field>'],
194
194
  },