@conduction/nextcloud-vue 2.111.0 → 2.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
  8. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  11. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  12. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  13. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  17. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  18. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  21. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  26. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  27. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +32 -8
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -3
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  34. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  35. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  39. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +166 -2
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +19 -4
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  47. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  48. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  49. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  50. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  51. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  52. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  54. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  56. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  57. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  58. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  59. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  60. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  61. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  62. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  63. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  64. package/dist/esm/composables/useRefLabels.js +26 -6
  65. package/dist/esm/composables/useRefLabels.js.map +1 -1
  66. package/dist/esm/integrations/registry.js +4 -0
  67. package/dist/esm/integrations/registry.js.map +1 -1
  68. package/dist/esm/l10n/en.json.js +3 -1
  69. package/dist/esm/l10n/en.json.js.map +1 -1
  70. package/dist/esm/l10n/nl.json.js +3 -1
  71. package/dist/esm/l10n/nl.json.js.map +1 -1
  72. package/dist/esm/library-style.js +1 -1
  73. package/dist/esm/nextcloud-vue.css +533 -273
  74. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  75. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  76. package/dist/esm/store/plugins/logs.js +2 -1
  77. package/dist/esm/store/plugins/logs.js.map +1 -1
  78. package/dist/esm/utils/detailActionModel.js +28 -1
  79. package/dist/esm/utils/detailActionModel.js.map +1 -1
  80. package/dist/esm/utils/fillTemplate.js +13 -5
  81. package/dist/esm/utils/fillTemplate.js.map +1 -1
  82. package/dist/esm/utils/headerFieldChips.js +21 -2
  83. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  84. package/dist/esm/utils/mapList.js +26 -11
  85. package/dist/esm/utils/mapList.js.map +1 -1
  86. package/dist/esm/utils/savedViewMenu.js +12 -5
  87. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  88. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  89. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  90. package/dist/nextcloud-vue.cjs.js +2562 -1886
  91. package/dist/nextcloud-vue.cjs.js.map +2 -2
  92. package/dist/nextcloud-vue.css +533 -273
  93. package/l10n/en.json +3 -1
  94. package/l10n/nl.json +3 -1
  95. package/package.json +1 -1
  96. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  97. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  98. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  99. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  100. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  101. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  102. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  103. package/src/components/CnDetailPage/CnDetailPage.vue +42 -9
  104. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  105. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  106. package/src/components/CnIndexPage/CnIndexPage.vue +174 -4
  107. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  108. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  109. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  110. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  111. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  112. package/src/composables/useRefLabels.js +25 -5
  113. package/src/css/dialog.css +53 -3
  114. package/src/css/index-page.css +26 -1
  115. package/src/css/look-board-detail.css +36 -15
  116. package/src/css/look-board-index.css +69 -3
  117. package/src/css/look-board.css +30 -2
  118. package/src/integrations/registry.js +4 -0
  119. package/src/schemas/app-manifest-v2.schema.json +11 -5
  120. package/src/store/plugins/liveUpdates.js +3 -1
  121. package/src/store/plugins/logs.js +2 -1
  122. package/src/utils/detailActionModel.js +27 -0
  123. package/src/utils/fillTemplate.js +13 -5
  124. package/src/utils/headerFieldChips.js +21 -2
  125. package/src/utils/mapList.js +26 -11
  126. package/src/utils/savedViewMenu.js +12 -5
  127. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -2,7 +2,7 @@
2
2
  <CnDialog
3
3
  :look="look"
4
4
  :width="width"
5
- defaultWidth="form"
5
+ :defaultWidth="columns === 2 ? 'wizard' : 'form'"
6
6
  :eyebrow="eyebrow"
7
7
  :subtitle="subtitle"
8
8
  :name="resolvedTitle"
@@ -947,7 +947,11 @@
947
947
  <CnStatusBadge v-for="(pill, index) in item.pills"
948
948
  :key="index"
949
949
  :label="pill.label"
950
- :variant="pill.variant" />
950
+ :variant="pill.variant">
951
+ <template v-if="pill.icon" #icon>
952
+ <CnIcon :name="pill.icon" :size="14" />
953
+ </template>
954
+ </CnStatusBadge>
951
955
  <span v-if="item.due" class="cn-index-page__map-due">{{ item.due }}</span>
952
956
  </span>
953
957
  </div>
@@ -995,6 +999,7 @@
995
999
  :swimlaneField="board.swimlaneField || ''"
996
1000
  :dueRule="board.dueRule || null"
997
1001
  :cardRoles="board.card || null"
1002
+ :pillLooks="refLooks"
998
1003
  :colorField="board.colorField || ''"
999
1004
  :sumField="board.sumField || ''"
1000
1005
  :sumFormat="boardSumFormat"
@@ -1047,7 +1052,7 @@
1047
1052
  v-else-if="currentViewMode === 'list'"
1048
1053
  :objects="displayObjects"
1049
1054
  :schema="effectiveSchema"
1050
- :config="listConfig"
1055
+ :config="renderedListConfig"
1051
1056
  :selectable="selectable"
1052
1057
  :selectedIds="internalSelectedIds"
1053
1058
  :rowKey="rowKey"
@@ -1107,6 +1112,7 @@
1107
1112
  :clickToView="rowClickOpens"
1108
1113
  :cardFields="resolvedCardFields"
1109
1114
  :cardLayout="cardLayout"
1115
+ :pillLooks="refLooks"
1110
1116
  :cardFieldColumns="cardFieldColumns"
1111
1117
  :accentOf="cardAccent"
1112
1118
  :selectedIds="internalSelectedIds"
@@ -1763,6 +1769,8 @@ export default {
1763
1769
 
1764
1770
  inject: {
1765
1771
  cnCustomComponents: { default: () => ({}) },
1772
+ /** The app's named formatters, provided by CnAppRoot, for the map list's pills and template tokens. */
1773
+ cnFormatters: { default: () => ({}) },
1766
1774
  /** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
1767
1775
  cnPermissions: { default: () => [] },
1768
1776
  /** The app config an ancestor provides, for `@config.<key>` in quick-filter labels. */
@@ -4121,6 +4129,8 @@ export default {
4121
4129
  boardPrevPageSize: this.viewMode === 'board' ? ((this.pagination && this.pagination.limit) || 20) : null,
4122
4130
  /** Resolved labels of reference columns: `{ [columnKey]: { [id]: label|null } }`. */
4123
4131
  refLabels: {},
4132
+ /** Colour and icon names of the records badge columns point at: `{ [columnKey]: { [id]: { color, icon } } }`. */
4133
+ refLooks: {},
4124
4134
  /** Count per tab-strip index, for the entries that asked for one; null = none. */
4125
4135
  tabCounts: null,
4126
4136
  // The counts of the `filterStrip` pills, by pill key ('all', 'v0', ...).
@@ -6035,6 +6045,12 @@ export default {
6035
6045
  */
6036
6046
  renderedColumns() {
6037
6047
  let cols = this.tableColumns
6048
+ if (this.refLookSpecs.length > 0) {
6049
+ const lookKeys = new Set(this.refLookSpecs.map((spec) => spec.key))
6050
+ cols = cols.map((col) => (col && typeof col === 'object' && lookKeys.has(col.key)
6051
+ ? { ...col, widgetProps: { ...(col.widgetProps || {}), looks: this.refLooks[col.key] || {} } }
6052
+ : col))
6053
+ }
6038
6054
  if (this.refLabelSpecs.length > 0) {
6039
6055
  const byKey = new Map(this.refLabelSpecs.map((s) => [s.key, s]))
6040
6056
  cols = cols.map((col) => {
@@ -6105,6 +6121,108 @@ export default {
6105
6121
  *
6106
6122
  * @return {Object<string, string[]>} Distinct ids per column key.
6107
6123
  */
6124
+ /**
6125
+ * The pills that take their tone and icon from the record they point at
6126
+ * (`resolve: { colorField?, iconField?, register?, schema? }`): badge
6127
+ * columns and the list view's leading and trailing cells
6128
+ * (`widgetProps.resolve`), the board card's pill (`board.card.resolve`)
6129
+ * and the record card's pills (`cardLayout` headPill, footerPill, chips).
6130
+ * Register and schema default to the field's reference in the schema.
6131
+ *
6132
+ * @return {Array<{key: string, register: string, schema: string, colorField: string, iconField: string}>} The specs.
6133
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-badge-cell-takes-its-tone-and-icon-from-the-referenced-record
6134
+ */
6135
+ refLookSpecs() {
6136
+ const list = this.listConfig || {}
6137
+ const listCells = [list.leading, ...(Array.isArray(list.trailing) ? list.trailing : [list.trailing])]
6138
+ const badgeSources = [...this.tableColumns, ...listCells]
6139
+ .filter((c) => c && typeof c === 'object' && c.widget === 'badge' && c.widgetProps)
6140
+ .map((c) => ({ key: c.key, resolve: c.widgetProps.resolve }))
6141
+ // The board card's pill (`board.card.pill` with `board.card.resolve`)
6142
+ // and the record card's pills (`cardLayout` headPill, footerPill,
6143
+ // chips, each with its own `resolve`).
6144
+ const card = this.board && this.board.card && typeof this.board.card === 'object' ? this.board.card : {}
6145
+ const layout = this.cardLayout && typeof this.cardLayout === 'object' ? this.cardLayout : {}
6146
+ const layoutPills = [layout.headPill, layout.footerPill, ...(Array.isArray(layout.chips) ? layout.chips : [])]
6147
+ .filter((def) => def && typeof def === 'object')
6148
+ .map((def) => ({ key: def.field, resolve: def.resolve }))
6149
+ // The map list's pills (`mapConfig.list.pills`, each with its own `resolve`).
6150
+ const mapListPills = (this.mapListEnabled && Array.isArray(this.mapConfig.list.pills) ? this.mapConfig.list.pills : [])
6151
+ .filter((def) => def && typeof def === 'object')
6152
+ .map((def) => ({ key: def.path, resolve: def.resolve }))
6153
+ const seen = new Set()
6154
+ const cols = [...badgeSources, { key: card.pill, resolve: card.resolve }, ...layoutPills, ...mapListPills].filter((c) => {
6155
+ const wanted = typeof c.key === 'string' && c.key !== '' && c.resolve && typeof c.resolve === 'object'
6156
+ && (c.resolve.colorField || c.resolve.iconField) && !seen.has(c.key)
6157
+ if (wanted) {
6158
+ seen.add(c.key)
6159
+ }
6160
+ return wanted
6161
+ })
6162
+ if (cols.length === 0) {
6163
+ return []
6164
+ }
6165
+ const fields = this.effectiveSchema ? fieldsFromSchema(this.effectiveSchema, { includeReadOnly: true }) : []
6166
+ const specs = []
6167
+ for (const col of cols) {
6168
+ const resolve = col.resolve
6169
+ const field = fields.find((f) => f.key === col.key)
6170
+ const ref = (field && field.reference) || {}
6171
+ const register = resolve.register || ref.register || (typeof this.register === 'string' ? this.register : '')
6172
+ const schema = resolve.schema || (ref.schema !== undefined && ref.schema !== null ? String(ref.schema) : '')
6173
+ if (!register || !schema) {
6174
+ continue
6175
+ }
6176
+ specs.push({ key: col.key, register, schema, colorField: resolve.colorField || '', iconField: resolve.iconField || '' })
6177
+ }
6178
+ return specs
6179
+ },
6180
+
6181
+ /**
6182
+ * `listConfig` with the referenced records' looks on its badge cells,
6183
+ * as `renderedColumns` does for the table.
6184
+ *
6185
+ * @return {object} The list view's config.
6186
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-badge-cell-takes-its-tone-and-icon-from-the-referenced-record
6187
+ */
6188
+ renderedListConfig() {
6189
+ const list = this.listConfig
6190
+ if (!list || this.refLookSpecs.length === 0) {
6191
+ return list
6192
+ }
6193
+ const lookKeys = new Set(this.refLookSpecs.map((spec) => spec.key))
6194
+ const withLooks = (cell) => (cell && typeof cell === 'object' && lookKeys.has(cell.key)
6195
+ ? { ...cell, widgetProps: { ...(cell.widgetProps || {}), looks: this.refLooks[cell.key] || {} } }
6196
+ : cell)
6197
+ return {
6198
+ ...list,
6199
+ ...(list.leading ? { leading: withLooks(list.leading) } : {}),
6200
+ ...(list.trailing ? { trailing: Array.isArray(list.trailing) ? list.trailing.map(withLooks) : withLooks(list.trailing) } : {}),
6201
+ }
6202
+ },
6203
+
6204
+ /**
6205
+ * The ids each `refLookSpecs` column shows on screen.
6206
+ *
6207
+ * @return {{[key: string]: Array<string>}} Sorted ids by column key.
6208
+ */
6209
+ refLookIds() {
6210
+ const out = {}
6211
+ for (const spec of this.refLookSpecs) {
6212
+ const ids = new Set()
6213
+ for (const row of this.displayObjects) {
6214
+ const v = row ? row[spec.key] : undefined
6215
+ ;(Array.isArray(v) ? v : [v]).forEach((id) => {
6216
+ if (id !== null && id !== undefined && id !== '' && typeof id !== 'object') {
6217
+ ids.add(String(id))
6218
+ }
6219
+ })
6220
+ }
6221
+ out[spec.key] = [...ids].sort()
6222
+ }
6223
+ return out
6224
+ },
6225
+
6108
6226
  refLabelIds() {
6109
6227
  const out = {}
6110
6228
  const facets = this.storeFacets || this.resolvedSidebar.facets || {}
@@ -6372,13 +6490,15 @@ export default {
6372
6490
  const translated = {
6373
6491
  ...list,
6374
6492
  title: this.cnTranslate(list.title),
6375
- sub: list.sub ? this.cnTranslate(list.sub) : '',
6493
+ sub: Array.isArray(list.sub) ? list.sub.map((s) => this.cnTranslate(s)) : (list.sub ? this.cnTranslate(list.sub) : ''),
6376
6494
  due: list.due ? this.cnTranslate(list.due) : '',
6377
6495
  }
6378
6496
  return mapListItems(this.displayObjects, translated, {
6379
6497
  rowKey: this.rowKey,
6380
6498
  located: (row) => Boolean(this.resolveRowGeometry(row)),
6381
6499
  translate: (text) => this.cnTranslate(text),
6500
+ formatters: this.cnFormatters,
6501
+ looks: this.refLooks,
6382
6502
  })
6383
6503
  },
6384
6504
 
@@ -8002,6 +8122,15 @@ export default {
8002
8122
  },
8003
8123
  },
8004
8124
 
8125
+ // Read the colour and icon of the records badge columns point at.
8126
+ refLookIds: {
8127
+ immediate: true,
8128
+ deep: true,
8129
+ handler() {
8130
+ this.loadRefLooks()
8131
+ },
8132
+ },
8133
+
8005
8134
  // The Active line names a referenced value, not its id.
8006
8135
  activeFilterRefIds: {
8007
8136
  deep: true,
@@ -8453,6 +8582,45 @@ export default {
8453
8582
  }))
8454
8583
  },
8455
8584
 
8585
+ /**
8586
+ * Read the colour and icon names `refLookSpecs` columns need, for the
8587
+ * rows on screen, with the same resolver as the labels. A failure
8588
+ * leaves the badge as declared.
8589
+ *
8590
+ * @return {Promise<void>}
8591
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-badge-cell-takes-its-tone-and-icon-from-the-referenced-record
8592
+ */
8593
+ async loadRefLooks() {
8594
+ if (this.refLookSpecs.length === 0) {
8595
+ return
8596
+ }
8597
+ if (!this._refLabelResolver) {
8598
+ this._refLabelResolver = createRefLabelResolver(() => {
8599
+ try {
8600
+ return (this.list && this.list.objectStore) || useObjectStore()
8601
+ } catch {
8602
+ return null
8603
+ }
8604
+ })
8605
+ }
8606
+ const read = (spec, ids, name) => (name
8607
+ ? this._refLabelResolver.resolve(spec.register, spec.schema, ids, name, { strict: true }).catch(() => ({}))
8608
+ : Promise.resolve({}))
8609
+ await Promise.all(this.refLookSpecs.map(async (spec) => {
8610
+ const known = this.refLooks[spec.key] || {}
8611
+ const ids = (this.refLookIds[spec.key] || []).filter((id) => !Object.hasOwn(known, id))
8612
+ if (ids.length === 0) {
8613
+ return
8614
+ }
8615
+ const [colors, icons] = await Promise.all([read(spec, ids, spec.colorField), read(spec, ids, spec.iconField)])
8616
+ const found = {}
8617
+ ids.forEach((id) => {
8618
+ found[id] = { color: (colors && colors[id]) || null, icon: (icons && icons[id]) || null }
8619
+ })
8620
+ this.refLooks = { ...this.refLooks, [spec.key]: { ...(this.refLooks[spec.key] || {}), ...found } }
8621
+ }))
8622
+ },
8623
+
8456
8624
  /**
8457
8625
  * Whether a saved view shows its record count (`viewCounts`).
8458
8626
  *
@@ -11614,7 +11782,7 @@ export default {
11614
11782
  return
11615
11783
  }
11616
11784
  const badge = this.savedViewMenuBadge(view)
11617
- menu.toggle({ id: String(view.id || view.slug), page: this.savedViewPage, name: view.name, ...(badge ? { badge } : {}) })
11785
+ menu.toggle({ id: String(view.id || view.slug), page: this.savedViewPage, name: view.name, ...(typeof view.icon === 'string' && view.icon !== '' ? { icon: view.icon } : {}), ...(badge ? { badge } : {}) })
11618
11786
  },
11619
11787
 
11620
11788
  /**
@@ -12149,7 +12317,9 @@ export default {
12149
12317
  if (resultData && resultData.success && this._refLabelResolver) {
12150
12318
  this._refLabelResolver.invalidate()
12151
12319
  this.refLabels = {}
12320
+ this.refLooks = {}
12152
12321
  this.loadRefLabels()
12322
+ this.loadRefLooks()
12153
12323
  }
12154
12324
  },
12155
12325
 
@@ -32,6 +32,7 @@
32
32
  import { translate as t } from '@nextcloud/l10n'
33
33
  import { generateUrl } from '@nextcloud/router'
34
34
  import { fetchEndpointSource } from '../../composables/useEndpointSource.js'
35
+ import { resolveDue } from '../../utils/dueText.js'
35
36
  import { fillTemplate } from '../../utils/fillTemplate.js'
36
37
  import { openRowTarget } from '../../utils/linkNavigation.js'
37
38
  import { readPath } from '../../utils/readPath.js'
@@ -70,6 +71,9 @@ function appUrl(url) {
70
71
  * - `itemsPath`: the rows' path in the response, when not `results`.
71
72
  * - `limit`: the most rows shown (default 7).
72
73
  * - `title`, `subtitle`: templates filled from a row (default `{title}`, none).
74
+ * - `due`: `{ field, ...dueText config }`: the trailing line says how a date
75
+ * stands ("3 days overdue", "Due today", the date or the days left), in
76
+ * place of `subtitle` (see `resolveDue`).
73
77
  * - `href`: a template for a row's page (`/apps/dossiq/cases/{id}`).
74
78
  * - `viewAll`: `{ href, label? }`.
75
79
  * - `emptyText`: text for an empty list.
@@ -125,7 +129,7 @@ export default {
125
129
  return {
126
130
  id: String(id),
127
131
  mainText: this.fill(this.config.title || '{title}', item) || String(id),
128
- subText: this.fill(this.config.subtitle, item),
132
+ subText: this.subtitleOf(item),
129
133
  href: this.config.href ? appUrl(fillTemplate(this.config.href, { ...item, id })) : '',
130
134
  }
131
135
  })
@@ -155,6 +159,22 @@ export default {
155
159
  return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
156
160
  },
157
161
 
162
+ /**
163
+ * A row's trailing line: how its due date stands, or the subtitle template.
164
+ *
165
+ * @spec openspec/changes/nc-dashboard-due/specs/nc-dashboard-widgets/spec.md#requirement-a-dashboard-row-says-how-its-date-stands
166
+ * @param {object} item The row.
167
+ * @return {string}
168
+ */
169
+ subtitleOf(item) {
170
+ const due = this.config.due
171
+ if (due && typeof due === 'object' && due.field) {
172
+ const resolved = resolveDue(readPath(item, due.field), due, (text) => this.cnTranslate(text))
173
+ return resolved ? resolved.text : ''
174
+ }
175
+ return this.fill(this.config.subtitle, item)
176
+ },
177
+
158
178
  /**
159
179
  * A template filled from a row, translated first.
160
180
  *
@@ -138,6 +138,7 @@ export default {
138
138
  |---|---|---|---|
139
139
  | `maxMetadata` | `Number` | `4` | Maximum number of schema properties to show in the metadata section. Properties are sorted by their `order` value before slicing |
140
140
  | `accent` | `Object` | `null` | Status accent `{ variant, icon?, label? }`: a colored start border and, with `icon`, a colored icon before the title. `variant` is `success`, `warning`, `error`, `info` or `primary`; `label` is the icon's tooltip and accessible name |
141
+ | `pillLooks` | `Object` | `{}` | The referenced records' colour and icon names, by field and id (`{ [field]: { [id]: { color, icon } } }`, read by CnIndexPage). A record layout pill with `resolve: { colorField?, iconField? }` takes its record's tone and icon from here. |
141
142
 
142
143
  ## Slots
143
144
 
@@ -24,7 +24,11 @@
24
24
  class="cn-object-card__record-pill"
25
25
  :label="recordHeadPill.label"
26
26
  :variant="recordHeadPill.variant"
27
- data-testid="cn-object-card-head-pill" />
27
+ data-testid="cn-object-card-head-pill">
28
+ <template v-if="recordHeadPill.icon" #icon>
29
+ <CnIcon :name="recordHeadPill.icon" :size="14" />
30
+ </template>
31
+ </CnStatusBadge>
28
32
  <span v-if="recordNumber" class="cn-object-card__record-number" data-testid="cn-object-card-number">{{ recordNumber }}</span>
29
33
  <div
30
34
  v-if="$slots.actions"
@@ -63,7 +67,11 @@
63
67
  :key="chip.field"
64
68
  class="cn-object-card__record-pill"
65
69
  :label="chip.label"
66
- :variant="chip.variant" />
70
+ :variant="chip.variant">
71
+ <template v-if="chip.icon" #icon>
72
+ <CnIcon :name="chip.icon" :size="14" />
73
+ </template>
74
+ </CnStatusBadge>
67
75
  </div>
68
76
  <div v-if="$slots.badges" class="cn-object-card__badges">
69
77
  <slot name="badges" :object="object" />
@@ -74,7 +82,11 @@
74
82
  class="cn-object-card__record-pill"
75
83
  :label="recordFootPill.label"
76
84
  :variant="recordFootPill.variant"
77
- data-testid="cn-object-card-foot-pill" />
85
+ data-testid="cn-object-card-foot-pill">
86
+ <template v-if="recordFootPill.icon" #icon>
87
+ <CnIcon :name="recordFootPill.icon" :size="14" />
88
+ </template>
89
+ </CnStatusBadge>
78
90
  <span
79
91
  v-if="recordDate"
80
92
  class="cn-object-card__record-date"
@@ -207,6 +219,7 @@ import { normalizeLook } from '../../composables/useLook.js'
207
219
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
208
220
  import { formatBoardDate } from '../../utils/boardDate.js'
209
221
  import { resolveDateVariant } from '../../utils/dateVariant.js'
222
+ import { pillLook } from '../../utils/detailActionModel.js'
210
223
  import { resolveDue } from '../../utils/dueText.js'
211
224
  import { readPath } from '../../utils/readPath.js'
212
225
  import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
@@ -375,6 +388,20 @@ export default {
375
388
  default: null,
376
389
  },
377
390
 
391
+ /**
392
+ * The referenced records' colour and icon names, by field and id
393
+ * (`{ [field]: { [id]: { color, icon } } }`, CnIndexPage reads them).
394
+ * A record layout pill with `resolve: { colorField?, iconField? }` takes
395
+ * its record's tone and icon from here.
396
+ *
397
+ * @type {object}
398
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-card-pill-takes-its-tone-and-icon-from-the-referenced-record
399
+ */
400
+ pillLooks: {
401
+ type: Object,
402
+ default: () => ({}),
403
+ },
404
+
378
405
  /**
379
406
  * The property keys the facts list shows, in order (manifest key
380
407
  * `config.cardFields`). Without it the card shows the first
@@ -588,7 +615,7 @@ export default {
588
615
  const labels = def.labels && typeof def.labels === 'object' ? def.labels : {}
589
616
  const shown = typeof labels[String(raw)] === 'string' ? this.cnTranslate(labels[String(raw)]) : label
590
617
  const variant = mapped || def.variant
591
- return { field: def.field, label: shown, variant: isBadgeVariant(variant) ? variant : 'default' }
618
+ return this.withLook({ field: def.field, label: shown, variant: isBadgeVariant(variant) ? variant : 'default' }, def)
592
619
  })
593
620
  .filter(Boolean)
594
621
  },
@@ -838,7 +865,26 @@ export default {
838
865
  const colorMap = def.colorMap && typeof def.colorMap === 'object' ? def.colorMap : {}
839
866
  const key = Object.keys(colorMap).find((k) => k.toLowerCase() === value.toLowerCase())
840
867
  const variant = key !== undefined ? colorMap[key] : def.variant
841
- return { label, variant: isBadgeVariant(variant) ? variant : 'default' }
868
+ return this.withLook({ label, variant: isBadgeVariant(variant) ? variant : 'default' }, def)
869
+ },
870
+
871
+ /**
872
+ * Lay the referenced record's tone and icon over a layout pill that
873
+ * declares `resolve` (see `pillLooks`).
874
+ *
875
+ * @param {{label: string, variant: string}} pill The pill as declared.
876
+ * @param {object} def The pill's definition.
877
+ * @return {{label: string, variant: string, icon?: string}} The pill.
878
+ * @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-card-pill-takes-its-tone-and-icon-from-the-referenced-record
879
+ */
880
+ withLook(pill, def) {
881
+ if (!def || !def.resolve || !this.pillLooks || typeof this.pillLooks !== 'object') {
882
+ return pill
883
+ }
884
+ const looks = this.pillLooks[def.field]
885
+ const raw = readPath(this.object, def.field)
886
+ const look = pillLook(looks && raw !== null && raw !== undefined ? looks[String(raw)] : null)
887
+ return { ...pill, ...(look.variant ? { variant: look.variant } : {}), ...(look.icon ? { icon: look.icon } : {}) }
842
888
  },
843
889
 
844
890
  /**
@@ -3,24 +3,39 @@
3
3
  - SPDX-License-Identifier: EUPL-1.2
4
4
  -->
5
5
  <template>
6
- <CnSearchSelect
7
- class="cn-party-picker"
8
- :modelValue="selectedOption"
9
- :options="options"
10
- :inputLabel="inputLabel"
11
- :labelOutside="labelOutside"
12
- :placeholder="placeholder ? cnTranslate(placeholder) : ''"
13
- :loading="searching"
14
- :filterable="false"
15
- :clearable="clearable"
16
- :disabled="disabled"
17
- data-testid="cn-party-picker"
18
- @search="onSearch"
19
- @update:modelValue="onPick" />
6
+ <div class="cn-party-picker">
7
+ <CnSearchSelect
8
+ :modelValue="selectedOption"
9
+ :options="options"
10
+ :inputLabel="inputLabel"
11
+ :labelOutside="labelOutside"
12
+ :placeholder="placeholder ? cnTranslate(placeholder) : ''"
13
+ :loading="searching"
14
+ :filterable="false"
15
+ :clearable="clearable"
16
+ :disabled="disabled"
17
+ data-testid="cn-party-picker"
18
+ @search="onSearch"
19
+ @update:modelValue="onPick" />
20
+ <p v-if="refusal"
21
+ class="cn-party-picker__refusal"
22
+ role="alert"
23
+ data-testid="cn-party-picker-refusal">
24
+ {{ t('nextcloud-vue', 'This search could not be read.') }} {{ refusal }}
25
+ </p>
26
+ <p v-for="name in unavailable"
27
+ :key="name"
28
+ class="cn-party-picker__unavailable"
29
+ role="status"
30
+ data-testid="cn-party-picker-unavailable">
31
+ {{ t('nextcloud-vue', '{source} could not be searched, so this list may be incomplete.', { source: name }) }}
32
+ </p>
33
+ </div>
20
34
  </template>
21
35
 
22
36
  <script>
23
37
  import axios from '@nextcloud/axios'
38
+ import { translate as t } from '@nextcloud/l10n'
24
39
  import { generateUrl } from '@nextcloud/router'
25
40
  import { fillTemplate } from '../../utils/fillTemplate.js'
26
41
  import { CnSearchSelect } from '../CnSearchSelect/index.js'
@@ -142,6 +157,8 @@ export default {
142
157
  known: null,
143
158
  timer: null,
144
159
  searchSeq: 0,
160
+ refusal: '',
161
+ unavailable: [],
145
162
  }
146
163
  },
147
164
 
@@ -185,6 +202,8 @@ export default {
185
202
  },
186
203
 
187
204
  methods: {
205
+ t,
206
+
188
207
  /**
189
208
  * A source's row as a picker option.
190
209
  *
@@ -244,7 +263,9 @@ export default {
244
263
  },
245
264
 
246
265
  /**
247
- * Search every source at once; a source that fails adds nothing.
266
+ * Search every source at once. A term a source refused (400) is said
267
+ * under the field in its words; a source that could not be searched at
268
+ * all is named, so an empty list never hides a missing source.
248
269
  *
249
270
  * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party
250
271
  * @param {string} term The search term.
@@ -253,6 +274,8 @@ export default {
253
274
  async search(term) {
254
275
  const seq = ++this.searchSeq
255
276
  this.searching = true
277
+ let refusal = ''
278
+ const unavailable = []
256
279
  const sets = await Promise.all((this.sources || []).map(async (source) => {
257
280
  try {
258
281
  if (source.type === 'contacts') {
@@ -264,7 +287,16 @@ export default {
264
287
  }
265
288
  const { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })
266
289
  return (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))
267
- } catch {
290
+ } catch (failure) {
291
+ // OpenRegister refuses a term it cannot parse with a 400 rather
292
+ // than answering nothing, so that refusal is said, not hidden.
293
+ const response = failure && failure.response
294
+ if (response && response.status === 400) {
295
+ const body = response.data
296
+ refusal = String((body && (body.message || body.error)) || (typeof body === 'string' ? body : '') || refusal)
297
+ } else {
298
+ unavailable.push(source.label ? this.cnTranslate(source.label) : String(source.id || ''))
299
+ }
268
300
  return []
269
301
  }
270
302
  }))
@@ -272,6 +304,8 @@ export default {
272
304
  return
273
305
  }
274
306
  this.options = sets.flat().filter(Boolean)
307
+ this.refusal = refusal
308
+ this.unavailable = unavailable.filter(Boolean)
275
309
  this.searching = false
276
310
  },
277
311
 
@@ -315,3 +349,15 @@ export default {
315
349
  },
316
350
  }
317
351
  </script>
352
+
353
+ <style scoped>
354
+ .cn-party-picker__refusal {
355
+ margin: 4px 0 0;
356
+ color: var(--color-error-text, var(--color-error));
357
+ }
358
+
359
+ .cn-party-picker__unavailable {
360
+ margin: 4px 0 0;
361
+ color: var(--color-text-maxcontrast);
362
+ }
363
+ </style>
@@ -214,6 +214,13 @@ export default {
214
214
  position: relative;
215
215
  }
216
216
 
217
+ /* The field takes the row: NcSelect is an inline block of at least 260px,
218
+ which left the magnifier, placed at the row's end, outside the field
219
+ (DqOverdragen "Nieuw team"). */
220
+ .cn-search-select :deep(.v-select) {
221
+ width: 100%;
222
+ }
223
+
217
224
  /* The magnifier sits where the chevron was: in the field, at its end. */
218
225
  .cn-search-select__magnifier {
219
226
  position: absolute;
@@ -222,6 +229,7 @@ export default {
222
229
  height: var(--default-clickable-area, 34px);
223
230
  display: flex;
224
231
  align-items: center;
232
+ z-index: 1;
225
233
  color: var(--color-text-maxcontrast);
226
234
  pointer-events: none;
227
235
  }
@@ -65,6 +65,22 @@ export function pickRefLabel(obj, labelField) {
65
65
  return null
66
66
  }
67
67
 
68
+ /**
69
+ * Read one field off a referenced object, with no fallback: a colour or an
70
+ * icon name must not fall back to the title.
71
+ *
72
+ * @param {object|null} obj The referenced object.
73
+ * @param {string} field The (possibly nested) property.
74
+ * @return {string|null} The value as text, or null when it is empty.
75
+ */
76
+ export function pickRefField(obj, field) {
77
+ if (!obj || typeof obj !== 'object' || !field) {
78
+ return null
79
+ }
80
+ const text = asText(readPath(obj, field))
81
+ return text !== '' ? text : null
82
+ }
83
+
68
84
  /**
69
85
  * Create a resolver bound to an object store. Labels are cached per register,
70
86
  * schema and label field for the lifetime of the resolver.
@@ -91,9 +107,13 @@ export function createRefLabelResolver(getStore) {
91
107
  * @param {string} schema Schema slug of the referenced objects.
92
108
  * @param {string[]} ids Ids to resolve (duplicates are dropped).
93
109
  * @param {string} labelField Property holding the label (dotted paths allowed).
110
+ * @param {{strict?: boolean}} [options] `strict`: read exactly that field, with no
111
+ * fallback to the title or name (a colour or an icon name).
94
112
  * @return {Promise<Object<string, string|null>>} Label by id.
95
113
  */
96
- async resolve(register, schema, ids, labelField) {
114
+ async resolve(register, schema, ids, labelField, options = {}) {
115
+ const strict = options.strict === true
116
+ const pick = strict ? pickRefField : pickRefLabel
97
117
  const distinct = [...new Set((ids || []).filter((id) => id !== null && id !== undefined && id !== '').map(String))]
98
118
  const out = {}
99
119
  if (distinct.length === 0) {
@@ -115,14 +135,14 @@ export function createRefLabelResolver(getStore) {
115
135
  })
116
136
  return out
117
137
  }
118
- const hit = bucket(`${type}::${labelField || ''}`)
138
+ const hit = bucket(`${type}::${labelField || ''}::${strict ? 'field' : 'label'}`)
119
139
  const missing = []
120
140
  for (const id of distinct) {
121
141
  const stored = store.objects && store.objects[type] && store.objects[type][id]
122
142
  if (hit.has(id)) {
123
143
  continue
124
144
  }
125
- const label = stored ? pickRefLabel(stored, labelField) : null
145
+ const label = stored ? pick(stored, labelField) : null
126
146
  if (label !== null) {
127
147
  hit.set(id, label)
128
148
  } else {
@@ -136,7 +156,7 @@ export function createRefLabelResolver(getStore) {
136
156
  rows = await store.fetchCollectionForOptions(type, {
137
157
  _ids: missing.join(','),
138
158
  _limit: missing.length,
139
- _fields: [...new Set([top, 'title', 'name'])].join(','),
159
+ _fields: (strict ? [top] : [...new Set([top, 'title', 'name'])]).join(','),
140
160
  })
141
161
  } catch {
142
162
  rows = []
@@ -148,7 +168,7 @@ export function createRefLabelResolver(getStore) {
148
168
  byId.set(String(id), row)
149
169
  }
150
170
  })
151
- missing.forEach((id) => hit.set(id, pickRefLabel(byId.get(id), labelField)))
171
+ missing.forEach((id) => hit.set(id, pick(byId.get(id), labelField)))
152
172
  }
153
173
  distinct.forEach((id) => {
154
174
  out[id] = hit.has(id) ? hit.get(id) : null