@conduction/nextcloud-vue 2.110.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 (142) 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/CnAppRoot/CnAppRoot.vue2.js +2 -1
  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 +3 -2
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  15. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
  16. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  17. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
  18. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
  23. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  27. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
  28. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  29. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  30. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -8
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +98 -63
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +203 -14
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +19 -4
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -1
  51. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
  53. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
  54. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
  60. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  61. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
  62. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
  63. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
  64. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  65. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
  66. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  67. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
  68. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
  69. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
  70. package/dist/esm/composables/useRefLabels.js +26 -6
  71. package/dist/esm/composables/useRefLabels.js.map +1 -1
  72. package/dist/esm/integrations/registry.js +4 -0
  73. package/dist/esm/integrations/registry.js.map +1 -1
  74. package/dist/esm/l10n/en.json.js +3 -1
  75. package/dist/esm/l10n/en.json.js.map +1 -1
  76. package/dist/esm/l10n/nl.json.js +3 -1
  77. package/dist/esm/l10n/nl.json.js.map +1 -1
  78. package/dist/esm/library-style.js +1 -1
  79. package/dist/esm/nextcloud-vue.css +630 -303
  80. package/dist/esm/store/plugins/liveUpdates.js +3 -1
  81. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  82. package/dist/esm/store/plugins/logs.js +2 -1
  83. package/dist/esm/store/plugins/logs.js.map +1 -1
  84. package/dist/esm/utils/detailActionModel.js +28 -1
  85. package/dist/esm/utils/detailActionModel.js.map +1 -1
  86. package/dist/esm/utils/fillTemplate.js +13 -5
  87. package/dist/esm/utils/fillTemplate.js.map +1 -1
  88. package/dist/esm/utils/headerFieldChips.js +21 -2
  89. package/dist/esm/utils/headerFieldChips.js.map +1 -1
  90. package/dist/esm/utils/mapList.js +26 -11
  91. package/dist/esm/utils/mapList.js.map +1 -1
  92. package/dist/esm/utils/savedViewHelpers.js +40 -3
  93. package/dist/esm/utils/savedViewHelpers.js.map +1 -1
  94. package/dist/esm/utils/savedViewMenu.js +12 -5
  95. package/dist/esm/utils/savedViewMenu.js.map +1 -1
  96. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  97. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  98. package/dist/nextcloud-vue.cjs.js +2760 -1995
  99. package/dist/nextcloud-vue.cjs.js.map +2 -2
  100. package/dist/nextcloud-vue.css +630 -303
  101. package/l10n/en.json +3 -1
  102. package/l10n/nl.json +3 -1
  103. package/package.json +1 -1
  104. package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
  105. package/src/components/CnAppRoot/CnAppRoot.vue +2 -1
  106. package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
  107. package/src/components/CnBoardView/CnBoardView.vue +58 -5
  108. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  109. package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
  110. package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
  111. package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
  112. package/src/components/CnDetailPage/CnDetailPage.vue +77 -20
  113. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
  114. package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
  115. package/src/components/CnIndexPage/CnIndexPage.md +1 -1
  116. package/src/components/CnIndexPage/CnIndexPage.vue +211 -16
  117. package/src/components/CnIndexSidebar/CnIndexSidebar.md +1 -1
  118. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +2 -2
  119. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
  120. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  121. package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
  122. package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
  123. package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
  124. package/src/composables/useRefLabels.js +25 -5
  125. package/src/css/board.css +5 -2
  126. package/src/css/dialog.css +53 -3
  127. package/src/css/index-page.css +26 -1
  128. package/src/css/look-board-detail.css +55 -22
  129. package/src/css/look-board-index.css +82 -12
  130. package/src/css/look-board-reports.css +1 -1
  131. package/src/css/look-board.css +84 -11
  132. package/src/integrations/registry.js +4 -0
  133. package/src/schemas/app-manifest-v2.schema.json +11 -5
  134. package/src/store/plugins/liveUpdates.js +3 -1
  135. package/src/store/plugins/logs.js +2 -1
  136. package/src/utils/detailActionModel.js +27 -0
  137. package/src/utils/fillTemplate.js +13 -5
  138. package/src/utils/headerFieldChips.js +21 -2
  139. package/src/utils/mapList.js +26 -11
  140. package/src/utils/savedViewHelpers.js +39 -2
  141. package/src/utils/savedViewMenu.js +12 -5
  142. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -147,6 +147,39 @@
147
147
  hidden
148
148
  data-testid="cn-detail-widget-props-pending" />
149
149
 
150
+ <!-- A mount-mode leaf whose descriptor asks for the library card
151
+ (`chrome: "card"`): the host draws the card, its title with the
152
+ count the leaf reports, the leaf's actions in the title row and the
153
+ library empty state. The leaf reports them through the `onHeader`
154
+ mount prop and renders only its rows. -->
155
+ <CnWidgetWrapper
156
+ v-else-if="wrapsMountLeaf"
157
+ :title="leafCardTitle"
158
+ titleIconPosition="left"
159
+ :showActions="false"
160
+ :showRefresh="false"
161
+ :showRequestFeature="false"
162
+ :headerLink="cardHeaderLink"
163
+ class="cn-detail-page__leaf-card"
164
+ data-testid="cn-detail-widget-leaf-card">
165
+ <template v-if="leafCardIcon" #title-icon>
166
+ <CnIcon :name="leafCardIcon" :size="20" />
167
+ </template>
168
+ <template v-if="rendererHeader.count !== null" #title-meta>
169
+ <span class="cn-detail-page__catalog-count" data-testid="cn-detail-widget-header-count">{{ rendererHeader.count }}</span>
170
+ </template>
171
+ <template v-if="hasRendererHeaderItems" #actions>
172
+ <CnDetailWidgetHeaderItems :header="rendererHeader" />
173
+ </template>
174
+ <CnWidgetEmptyState
175
+ v-if="rendererHeader.empty"
176
+ :name="rendererHeader.empty.name"
177
+ :description="rendererHeader.empty.description" />
178
+ <CnLeafMountHost
179
+ :provider="integrationProvider"
180
+ :mountProps="integrationMountProps" />
181
+ </CnWidgetWrapper>
182
+
150
183
  <CnLeafMountHost
151
184
  v-else-if="isMountIntegration"
152
185
  :provider="integrationProvider"
@@ -293,6 +326,7 @@ import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
293
326
  import CnObjectFilesWidget from '../CnObjectFilesWidget/CnObjectFilesWidget.vue'
294
327
  import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
295
328
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
329
+ import CnWidgetEmptyState from '../CnWidgetEmptyState/CnWidgetEmptyState.vue'
296
330
  import CnDetailWidgetHeaderItems from './CnDetailWidgetHeaderItems.vue'
297
331
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
298
332
  import { normalizeLook } from '../../composables/useLook.js'
@@ -373,6 +407,7 @@ export default {
373
407
  CnObjectDataWidget,
374
408
  CnObjectGeoWidget,
375
409
  CnRelatedObjectsWidget,
410
+ CnWidgetEmptyState,
376
411
  CnWidgetWrapper,
377
412
  NcActionButton,
378
413
  CnEmptyContent,
@@ -628,7 +663,7 @@ export default {
628
663
  * `header` event (screens-relation-pages): a link and a note,
629
664
  * with the list's total already filled in.
630
665
  */
631
- rendererHeader: { link: null, note: '', actions: [], pill: null },
666
+ rendererHeader: { link: null, note: '', actions: [], pill: null, count: null, empty: null },
632
667
  }
633
668
  },
634
669
 
@@ -844,6 +879,34 @@ export default {
844
879
  && typeof provider.unmount === 'function'
845
880
  },
846
881
 
882
+ /**
883
+ * Whether a mount-mode leaf gets the library card: on a card surface,
884
+ * grid cell included, when its descriptor asks for it (`chrome:
885
+ * "card"`). Opt-in, because a leaf that draws its own card would
886
+ * otherwise sit in two.
887
+ *
888
+ * @return {boolean}
889
+ * @spec openspec/changes/integration-leaf-card/specs/integration-leaf-card/spec.md#requirement-a-mount-mode-leaf-can-take-the-library-card
890
+ */
891
+ wrapsMountLeaf() {
892
+ return !this.isBare && this.isMountIntegration && this.integrationProvider.chrome === 'card'
893
+ },
894
+
895
+ /**
896
+ * The leaf card's title: the manifest's, else the descriptor's label.
897
+ *
898
+ * @return {string} The title in the user's language.
899
+ */
900
+ leafCardTitle() {
901
+ const title = widgetTitleOf(this.widget)
902
+ return title ? this.translateLabel(title) : (this.integrationProvider?.label || '')
903
+ },
904
+
905
+ /** @return {string} The leaf card's icon: the manifest's, else the descriptor's. */
906
+ leafCardIcon() {
907
+ return this.widget?.icon || this.integrationProvider?.icon || ''
908
+ },
909
+
847
910
  /**
848
911
  * Whether this render is using a provider's widget as its bare surface.
849
912
  *
@@ -1017,6 +1080,8 @@ export default {
1017
1080
  // one that reads `objectId`.
1018
1081
  integrationContext: this.widgetIntegrationContext,
1019
1082
  ...this.resolvedWidgetProps,
1083
+ // A carded leaf reports its title row and empty state here.
1084
+ ...(this.wrapsMountLeaf ? { onHeader: this.onRendererHeader } : {}),
1020
1085
  }
1021
1086
  },
1022
1087
 
@@ -1221,11 +1286,12 @@ export default {
1221
1286
 
1222
1287
  methods: {
1223
1288
  /**
1224
- * Take the header a content-only renderer, or an app widget in the
1225
- * titled card, reports.
1289
+ * Take the header a content-only renderer, an app widget in the
1290
+ * titled card, or a carded mount-mode leaf (`onHeader`) reports.
1226
1291
  *
1227
- * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null, actions?: Array<{label: string, icon?: string, run: () => unknown}>}} header The header parts.
1292
+ * @param {{link?: object|null, note?: string, action?: {label: string, icon?: string, run: () => unknown}|null, actions?: Array<{label: string, icon?: string, run: () => unknown}>, count?: number, empty?: {name: string, description?: string}|null}} header The header parts.
1228
1293
  * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
1294
+ * @spec openspec/changes/integration-leaf-card/specs/integration-leaf-card/spec.md#requirement-a-carded-leaf-reports-its-count-actions-and-empty-state
1229
1295
  */
1230
1296
  onRendererHeader(header) {
1231
1297
  const link = header && header.link && typeof header.link === 'object' ? header.link : null
@@ -1234,7 +1300,11 @@ export default {
1234
1300
  // `action` (one) and `actions` (several) both count, `action` first.
1235
1301
  const actions = [header && header.action, ...((header && Array.isArray(header.actions)) ? header.actions : [])].filter(usable)
1236
1302
  const pill = header && header.pill && typeof header.pill.label === 'string' && header.pill.label !== '' ? header.pill : null
1237
- this.rendererHeader = { link, note, actions, pill }
1303
+ const count = header && Number.isFinite(header.count) ? header.count : null
1304
+ const empty = header && header.empty && typeof header.empty.name === 'string' && header.empty.name !== ''
1305
+ ? { name: header.empty.name, description: typeof header.empty.description === 'string' ? header.empty.description : '' }
1306
+ : null
1307
+ this.rendererHeader = { link, note, actions, pill, count, empty }
1238
1308
  },
1239
1309
 
1240
1310
  /**
@@ -1372,6 +1442,31 @@ export default {
1372
1442
  white-space: nowrap;
1373
1443
  }
1374
1444
 
1445
+ /* A leaf card in a narrow cell keeps its title and count: the title row
1446
+ wraps and the actions move under it instead of squeezing the title out. */
1447
+ .cn-detail-page__leaf-card :deep(.cn-widget-wrapper__header) {
1448
+ flex-wrap: wrap;
1449
+ }
1450
+
1451
+ .cn-detail-page__leaf-card :deep(.cn-widget-wrapper__header-left) {
1452
+ flex: 1 0 auto;
1453
+ }
1454
+
1455
+ /* The count beside a card title: the grey 20px badge of the tab strip. */
1456
+ .cn-detail-page__catalog-count {
1457
+ background-color: var(--color-border);
1458
+ border-radius: 10px;
1459
+ box-sizing: border-box;
1460
+ color: var(--color-main-text);
1461
+ display: inline-block;
1462
+ font-size: 12px;
1463
+ font-weight: 600;
1464
+ line-height: 20px;
1465
+ min-width: 20px;
1466
+ padding: 0 6px;
1467
+ text-align: center;
1468
+ }
1469
+
1375
1470
  .cn-detail-page__catalog-note {
1376
1471
  color: var(--color-text-maxcontrast);
1377
1472
  font-size: 14px;
@@ -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"
@@ -368,7 +368,7 @@ export default {
368
368
  | `showMassImport` | Boolean | `true` | Whether to show the mass Import action |
369
369
  | `showMassCopy` | Boolean | `true` | Whether to show the mass Copy action |
370
370
  | `allowExport` | Boolean | `false` | Opt-in flag for the native Export menu (CSV/Excel) next to the Add button. Renders only when `true` AND the resolved schema is flagged `exportable: true`; navigates to OpenRegister's export leaf (`GET /apps/openregister/api/objects/{register}/{schema}/export`), passing `$route.query` through as filters. Distinct from `showMassExport`, which exports the fetched/selected rows via a blob download. |
371
- | `allowSavedViews` | Boolean | `false` | Opt-in flag for the saved-views control (saved-views-ui): a Views dropdown listing the user's OpenRegister saved-search views (`GET /apps/openregister/api/views`). Applying one writes its stored filters/search/sort into the route query (`_search` and `_order` reserved keys + plain filter keys); "Save current view…" persists the current route-query state via POST and toasts either way, naming the view (a failure also keeps the dialog open with the reason); own views can be deleted after confirmation. Emits `apply-view`. |
371
+ | `allowSavedViews` | Boolean | unset | Saved views (saved-views-ui). Unset, the sidebar's Views tab (save, apply, edit, share, add to menu, copy a default, delete) is on for every page with a sidebar and a source a view can be scoped to: register and schema, `entitySource` or `savedViewsScope`. `false` turns it off. `true` also forces the sidebar and adds the toolbar control: a Views dropdown listing the user's OpenRegister saved-search views (`GET /apps/openregister/api/views`). Applying one writes its stored filters/search/sort into the route query (`_search` and `_order` reserved keys + plain filter keys); "Save current view…" persists the current route-query state via POST and toasts either way, naming the view (a failure also keeps the dialog open with the reason); own views can be deleted after confirmation. Emits `apply-view`. |
372
372
  | `savedViewsScope` | String | `''` | Which pages share this page's saved views. By default a view is shared by every page over the same `register` and `schema` and shown on no other page, since a view is filters over one schema's fields. Set a name to share views across pages over different sources, or to keep two pages over one source apart. Written into the saved view's `query.scope` on save; views saved before scoping existed carry no scope and stay visible everywhere. |
373
373
  | `massActionNameField` | String | `'title'` | Property name used to display item names in dialogs |
374
374
  | `nameFormatter` | Function | `null` | Custom formatter for item names in dialogs; overrides `massActionNameField` |
@@ -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"
@@ -1379,7 +1385,7 @@ import { isRowActionVisible, rowActionPayload } from '../../utils/rowActionItem.
1379
1385
  import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
1380
1386
  import { compileRowClass } from '../../utils/rowClassRules.js'
1381
1387
  import { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'
1382
- import { buildRouteQueryFromViewState, buildViewCreatePayload, defaultFilterViews, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, savedViewScope, schemaSlug, viewMatchesScope } from '../../utils/savedViewHelpers.js'
1388
+ import { buildRouteQueryFromViewState, buildViewCreatePayload, defaultFilterViews, extractViewState, extractViewStateFromRouteQuery, normalizeSharedWith, pageOffersSavedViews, savedViewScope, schemaSlug, viewMatchesScope } from '../../utils/savedViewHelpers.js'
1383
1389
  import { savedViewBadge, savedViewOverrides } from '../../utils/savedViewOverrides.js'
1384
1390
  import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
1385
1391
  import { resolveScopeLayout, resolveScopePresentation } from '../../utils/scopeListLayout.js'
@@ -1500,11 +1506,22 @@ function declaredFilterFields(props) {
1500
1506
  * @return {object} The `{ fieldKey: values }` map, empty when the link carries none.
1501
1507
  */
1502
1508
  function namedFiltersFromRoute(instance, props) {
1509
+ const route = instance && instance.proxy && instance.proxy.$route
1510
+ return namedFiltersFromQuery((route && route.query) || {}, props)
1511
+ }
1512
+
1513
+ /**
1514
+ * The named-source filters a query carries, as `namedFiltersFromRoute` reads them.
1515
+ *
1516
+ * @param {object} query The route query.
1517
+ * @param {object} props The CnIndexPage props.
1518
+ *
1519
+ * @return {object} The `{ fieldKey: values }` map.
1520
+ */
1521
+ function namedFiltersFromQuery(query, props) {
1503
1522
  if (!props.entitySource) {
1504
1523
  return {}
1505
1524
  }
1506
- const route = instance && instance.proxy && instance.proxy.$route
1507
- const query = (route && route.query) || {}
1508
1525
  const properties = declaredFilterFields(props)
1509
1526
  const out = {}
1510
1527
 
@@ -1752,6 +1769,8 @@ export default {
1752
1769
 
1753
1770
  inject: {
1754
1771
  cnCustomComponents: { default: () => ({}) },
1772
+ /** The app's named formatters, provided by CnAppRoot, for the map list's pills and template tokens. */
1773
+ cnFormatters: { default: () => ({}) },
1755
1774
  /** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
1756
1775
  cnPermissions: { default: () => [] },
1757
1776
  /** The app config an ancestor provides, for `@config.<key>` in quick-filter labels. */
@@ -2947,9 +2966,11 @@ export default {
2947
2966
  },
2948
2967
 
2949
2968
  /**
2950
- * Opt-in flag for the saved-views control (saved-views-ui) rendered
2951
- * in the toolbar. Defaults to `false` — an app must explicitly
2952
- * enable it per page. When `true`, a Views dropdown lists the
2969
+ * Saved views (saved-views-ui). Unset, the sidebar's Views tab is on
2970
+ * for every page with a sidebar and a source a view can be scoped to
2971
+ * (register and schema, `entitySource` or `savedViewsScope`); `false`
2972
+ * turns it off. `true` also forces the sidebar and adds the toolbar
2973
+ * control, where a Views dropdown lists the
2953
2974
  * current user's OpenRegister saved-search views
2954
2975
  * (`GET /apps/openregister/api/views`); applying one writes its
2955
2976
  * stored filters/search/sort into the route query (reusing the
@@ -2961,7 +2982,7 @@ export default {
2961
2982
  */
2962
2983
  allowSavedViews: {
2963
2984
  type: Boolean,
2964
- default: false,
2985
+ default: null,
2965
2986
  },
2966
2987
 
2967
2988
  /**
@@ -4108,6 +4129,8 @@ export default {
4108
4129
  boardPrevPageSize: this.viewMode === 'board' ? ((this.pagination && this.pagination.limit) || 20) : null,
4109
4130
  /** Resolved labels of reference columns: `{ [columnKey]: { [id]: label|null } }`. */
4110
4131
  refLabels: {},
4132
+ /** Colour and icon names of the records badge columns point at: `{ [columnKey]: { [id]: { color, icon } } }`. */
4133
+ refLooks: {},
4111
4134
  /** Count per tab-strip index, for the entries that asked for one; null = none. */
4112
4135
  tabCounts: null,
4113
4136
  // The counts of the `filterStrip` pills, by pill key ('all', 'v0', ...).
@@ -5523,7 +5546,7 @@ export default {
5523
5546
  * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-6
5524
5547
  */
5525
5548
  savedViewsForSidebar() {
5526
- if (!this.allowSavedViews) {
5549
+ if (!this.savedViewsOffered) {
5527
5550
  return null
5528
5551
  }
5529
5552
  return {
@@ -6022,6 +6045,12 @@ export default {
6022
6045
  */
6023
6046
  renderedColumns() {
6024
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
+ }
6025
6054
  if (this.refLabelSpecs.length > 0) {
6026
6055
  const byKey = new Map(this.refLabelSpecs.map((s) => [s.key, s]))
6027
6056
  cols = cols.map((col) => {
@@ -6092,6 +6121,108 @@ export default {
6092
6121
  *
6093
6122
  * @return {Object<string, string[]>} Distinct ids per column key.
6094
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
+
6095
6226
  refLabelIds() {
6096
6227
  const out = {}
6097
6228
  const facets = this.storeFacets || this.resolvedSidebar.facets || {}
@@ -6359,13 +6490,15 @@ export default {
6359
6490
  const translated = {
6360
6491
  ...list,
6361
6492
  title: this.cnTranslate(list.title),
6362
- 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) : ''),
6363
6494
  due: list.due ? this.cnTranslate(list.due) : '',
6364
6495
  }
6365
6496
  return mapListItems(this.displayObjects, translated, {
6366
6497
  rowKey: this.rowKey,
6367
6498
  located: (row) => Boolean(this.resolveRowGeometry(row)),
6368
6499
  translate: (text) => this.cnTranslate(text),
6500
+ formatters: this.cnFormatters,
6501
+ looks: this.refLooks,
6369
6502
  })
6370
6503
  },
6371
6504
 
@@ -6664,7 +6797,16 @@ export default {
6664
6797
  * @return {{ scope: string, register: string, schema: object|string|null }} The page's source.
6665
6798
  */
6666
6799
  savedViewsPage() {
6667
- return { scope: this.savedViewsScope, register: this.register, schema: this.schema }
6800
+ return { scope: this.savedViewsScope, register: this.register, schema: this.schema, source: this.entitySource }
6801
+ },
6802
+
6803
+ /**
6804
+ * Whether this page offers saved views in its sidebar.
6805
+ *
6806
+ * @return {boolean}
6807
+ */
6808
+ savedViewsOffered() {
6809
+ return pageOffersSavedViews(this)
6668
6810
  },
6669
6811
 
6670
6812
  /**
@@ -7980,6 +8122,15 @@ export default {
7980
8122
  },
7981
8123
  },
7982
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
+
7983
8134
  // The Active line names a referenced value, not its id.
7984
8135
  activeFilterRefIds: {
7985
8136
  deep: true,
@@ -8237,7 +8388,7 @@ export default {
8237
8388
  created() {
8238
8389
  this.applyViewModeFromRoute()
8239
8390
  this.pushAiContext()
8240
- if (this.allowSavedViews) {
8391
+ if (this.savedViewsOffered) {
8241
8392
  this.fetchSavedViews()
8242
8393
  }
8243
8394
  this.selfActions = createSelfModeActions({
@@ -8431,6 +8582,45 @@ export default {
8431
8582
  }))
8432
8583
  },
8433
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
+
8434
8624
  /**
8435
8625
  * Whether a saved view shows its record count (`viewCounts`).
8436
8626
  *
@@ -11345,8 +11535,8 @@ export default {
11345
11535
 
11346
11536
  /**
11347
11537
  * Fetch the current user's saved views (own + public) from
11348
- * OpenRegister's views API. Called from created() when
11349
- * `allowSavedViews` is enabled.
11538
+ * OpenRegister's views API. Called from created() when the page
11539
+ * offers saved views.
11350
11540
  */
11351
11541
  async fetchSavedViews() {
11352
11542
  this.savedViewsLoading = true
@@ -11416,6 +11606,9 @@ export default {
11416
11606
  query.savedView = this.appliedSavedViewId
11417
11607
  }
11418
11608
  const nav = this.$router.replace({ query })
11609
+ if (this.isNamedSource) {
11610
+ this.namedActiveFilters = namedFiltersFromQuery(query, this)
11611
+ }
11419
11612
  // Swallow the duplicate-navigation rejection (Vue Router 3)
11420
11613
  // when the applied view matches the current query.
11421
11614
  if (nav && typeof nav.catch === 'function') {
@@ -11589,7 +11782,7 @@ export default {
11589
11782
  return
11590
11783
  }
11591
11784
  const badge = this.savedViewMenuBadge(view)
11592
- 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 } : {}) })
11593
11786
  },
11594
11787
 
11595
11788
  /**
@@ -12124,7 +12317,9 @@ export default {
12124
12317
  if (resultData && resultData.success && this._refLabelResolver) {
12125
12318
  this._refLabelResolver.invalidate()
12126
12319
  this.refLabels = {}
12320
+ this.refLooks = {}
12127
12321
  this.loadRefLabels()
12322
+ this.loadRefLooks()
12128
12323
  }
12129
12324
  },
12130
12325
 
@@ -216,7 +216,7 @@ With admin-only filter control:
216
216
  | `propertiesGroupLabel` | String | `''` | Override label for the schema properties group. Falls back to `schema.title` |
217
217
  | `defaultTab` | String | `'search-tab'` | Tab id that is active when the sidebar opens. Built-in ids: `'search-tab'`, `'columns-tab'` |
218
218
  | `activeTab` | String | `''` | Switch to a tab while mounted, e.g. `'filters-tab'` |
219
- | `savedViews` | Object | `null` | The page's saved views, shown in a third tab, Filters (CnSavedViewsPanel); every intent comes back as one `saved-view` event `{ action, view }` |
219
+ | `savedViews` | Object | `null` | The page's saved views, shown in a third tab, Views (`filters-tab`, CnSavedViewsPanel); every intent comes back as one `saved-view` event `{ action, view }` |
220
220
  | `userIsAdmin` | Boolean | `true` | When `false`, schema properties with `adminOnly: true` are hidden from filters |
221
221
  | `filterFields` | Object | `null` | Filter declarations for a page with no schema of its own. Wins over `schema`, and feeds the Search tab only |
222
222
 
@@ -287,11 +287,11 @@
287
287
  </div>
288
288
  </NcAppSidebarTab>
289
289
 
290
- <!-- Filters Tab: the page's saved views (save, list, manage, add to menu). -->
290
+ <!-- Views tab (id filters-tab): the page's saved views (save, list, manage, add to menu). -->
291
291
  <NcAppSidebarTab
292
292
  v-if="savedViews"
293
293
  id="filters-tab"
294
- :name="t('nextcloud-vue', 'Filters')"
294
+ :name="t('nextcloud-vue', 'Views')"
295
295
  :order="3">
296
296
  <template #icon>
297
297
  <BookmarkOutline :size="20" />
@@ -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