@conduction/nextcloud-vue 2.111.0 → 2.113.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.
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +4 -6
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -13
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +222 -15
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +24 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +29 -12
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +3 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
- package/dist/esm/composables/useRefLabels.js +26 -6
- package/dist/esm/composables/useRefLabels.js.map +1 -1
- package/dist/esm/integrations/registry.js +4 -0
- package/dist/esm/integrations/registry.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +680 -449
- package/dist/esm/store/plugins/liveUpdates.js +3 -1
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/plugins/logs.js +2 -1
- package/dist/esm/store/plugins/logs.js.map +1 -1
- package/dist/esm/utils/actionLink.js +104 -1
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +28 -1
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +13 -5
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerFieldChips.js +21 -2
- package/dist/esm/utils/headerFieldChips.js.map +1 -1
- package/dist/esm/utils/mapList.js +26 -11
- package/dist/esm/utils/mapList.js.map +1 -1
- package/dist/esm/utils/savedViewMenu.js +12 -5
- package/dist/esm/utils/savedViewMenu.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +2800 -1933
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +680 -449
- package/l10n/en.json +3 -1
- package/l10n/nl.json +3 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +4 -6
- package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
- package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
- package/src/components/CnBoardView/CnBoardView.vue +58 -5
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
- package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +0 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -5
- package/src/components/CnDetailPage/CnDetailPage.vue +62 -14
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
- package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +233 -18
- package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
- package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
- package/src/components/CnReportsPage/CnReportsPage.vue +31 -12
- package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
- package/src/composables/useRefLabels.js +25 -5
- package/src/css/badge.css +15 -13
- package/src/css/board.css +0 -1
- package/src/css/dialog.css +53 -3
- package/src/css/index-page.css +26 -1
- package/src/css/look-board-detail.css +36 -15
- package/src/css/look-board-index.css +67 -15
- package/src/css/look-board.css +32 -18
- package/src/integrations/registry.js +4 -0
- package/src/schemas/app-manifest-v2.schema.json +61 -13
- package/src/store/plugins/liveUpdates.js +3 -1
- package/src/store/plugins/logs.js +2 -1
- package/src/utils/actionLink.js +103 -0
- package/src/utils/detailActionModel.js +27 -0
- package/src/utils/fillTemplate.js +13 -5
- package/src/utils/headerFieldChips.js +21 -2
- package/src/utils/mapList.js +26 -11
- package/src/utils/savedViewMenu.js +12 -5
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
<NcActionLink
|
|
64
64
|
v-else-if="entry.href"
|
|
65
65
|
:href="entry.href"
|
|
66
|
-
target="_blank"
|
|
66
|
+
:target="entry.linkTarget || '_blank'"
|
|
67
67
|
rel="noopener noreferrer"
|
|
68
68
|
:data-testid="`cn-index-header-menu-${entry.key}`">
|
|
69
69
|
<template v-if="entry.icon" #icon>
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
<NcButton
|
|
104
104
|
v-else
|
|
105
105
|
:variant="button.variant"
|
|
106
|
+
:href="button.link ? button.link.href : undefined"
|
|
107
|
+
:target="button.link ? button.link.target : undefined"
|
|
106
108
|
:data-testid="`cn-index-header-button-${button.key}`"
|
|
107
109
|
@click="onHeaderButton(button)">
|
|
108
110
|
<template v-if="button.icon" #icon>
|
|
@@ -947,7 +949,11 @@
|
|
|
947
949
|
<CnStatusBadge v-for="(pill, index) in item.pills"
|
|
948
950
|
:key="index"
|
|
949
951
|
:label="pill.label"
|
|
950
|
-
:variant="pill.variant"
|
|
952
|
+
:variant="pill.variant">
|
|
953
|
+
<template v-if="pill.icon" #icon>
|
|
954
|
+
<CnIcon :name="pill.icon" :size="14" />
|
|
955
|
+
</template>
|
|
956
|
+
</CnStatusBadge>
|
|
951
957
|
<span v-if="item.due" class="cn-index-page__map-due">{{ item.due }}</span>
|
|
952
958
|
</span>
|
|
953
959
|
</div>
|
|
@@ -995,6 +1001,7 @@
|
|
|
995
1001
|
:swimlaneField="board.swimlaneField || ''"
|
|
996
1002
|
:dueRule="board.dueRule || null"
|
|
997
1003
|
:cardRoles="board.card || null"
|
|
1004
|
+
:pillLooks="refLooks"
|
|
998
1005
|
:colorField="board.colorField || ''"
|
|
999
1006
|
:sumField="board.sumField || ''"
|
|
1000
1007
|
:sumFormat="boardSumFormat"
|
|
@@ -1047,7 +1054,7 @@
|
|
|
1047
1054
|
v-else-if="currentViewMode === 'list'"
|
|
1048
1055
|
:objects="displayObjects"
|
|
1049
1056
|
:schema="effectiveSchema"
|
|
1050
|
-
:config="
|
|
1057
|
+
:config="renderedListConfig"
|
|
1051
1058
|
:selectable="selectable"
|
|
1052
1059
|
:selectedIds="internalSelectedIds"
|
|
1053
1060
|
:rowKey="rowKey"
|
|
@@ -1107,6 +1114,7 @@
|
|
|
1107
1114
|
:clickToView="rowClickOpens"
|
|
1108
1115
|
:cardFields="resolvedCardFields"
|
|
1109
1116
|
:cardLayout="cardLayout"
|
|
1117
|
+
:pillLooks="refLooks"
|
|
1110
1118
|
:cardFieldColumns="cardFieldColumns"
|
|
1111
1119
|
:accentOf="cardAccent"
|
|
1112
1120
|
:selectedIds="internalSelectedIds"
|
|
@@ -1341,8 +1349,8 @@ import { createRefLabelResolver } from '../../composables/useRefLabels.js'
|
|
|
1341
1349
|
import { useSavedViewsApi } from '../../composables/useSavedViewsApi.js'
|
|
1342
1350
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
1343
1351
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
1344
|
-
import { followItemActionLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
|
|
1345
|
-
import { buildOnSuccessRoute, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
1352
|
+
import { followItemActionLink, headerActionAsLink, resolveItemActionLink, routeHref } from '../../utils/actionLink.js'
|
|
1353
|
+
import { buildOnSuccessRoute, isExternalActionTarget, resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
1346
1354
|
import {
|
|
1347
1355
|
buildReferenceStages,
|
|
1348
1356
|
buildValueStages,
|
|
@@ -1763,6 +1771,8 @@ export default {
|
|
|
1763
1771
|
|
|
1764
1772
|
inject: {
|
|
1765
1773
|
cnCustomComponents: { default: () => ({}) },
|
|
1774
|
+
/** The app's named formatters, provided by CnAppRoot, for the map list's pills and template tokens. */
|
|
1775
|
+
cnFormatters: { default: () => ({}) },
|
|
1766
1776
|
/** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
|
|
1767
1777
|
cnPermissions: { default: () => [] },
|
|
1768
1778
|
/** The app config an ancestor provides, for `@config.<key>` in quick-filter labels. */
|
|
@@ -3907,11 +3917,16 @@ export default {
|
|
|
3907
3917
|
* (unless the handler is the `'none'` keyword) emits
|
|
3908
3918
|
* `@header-action({ action: id, id })`.
|
|
3909
3919
|
*
|
|
3920
|
+
* An entry with `href` instead renders as a real link and is never
|
|
3921
|
+
* dispatched or emitted: an absolute `http(s)`, `mailto:`, `tel:` or
|
|
3922
|
+
* `//host/...` URL is used as written, an instance path (leading `/`) goes through
|
|
3923
|
+
* `generateUrl()`. `target: '_blank'` opens it in a new tab.
|
|
3924
|
+
*
|
|
3910
3925
|
* Reserved ids (those used by built-ins on the bar — `refresh`,
|
|
3911
3926
|
* `import`, `export`, `copy`, `delete`) are dropped from the
|
|
3912
3927
|
* merged list with a `console.warn`.
|
|
3913
3928
|
*
|
|
3914
|
-
* @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, disabled?: boolean }>}
|
|
3929
|
+
* @type {Array<{ id: string, label: string, icon?: string, handler?: string|(() => void), route?: string, href?: string, target?: ('_blank'|'_self'), disabled?: boolean }>}
|
|
3915
3930
|
*/
|
|
3916
3931
|
headerActions: {
|
|
3917
3932
|
type: Array,
|
|
@@ -4121,6 +4136,8 @@ export default {
|
|
|
4121
4136
|
boardPrevPageSize: this.viewMode === 'board' ? ((this.pagination && this.pagination.limit) || 20) : null,
|
|
4122
4137
|
/** Resolved labels of reference columns: `{ [columnKey]: { [id]: label|null } }`. */
|
|
4123
4138
|
refLabels: {},
|
|
4139
|
+
/** Colour and icon names of the records badge columns point at: `{ [columnKey]: { [id]: { color, icon } } }`. */
|
|
4140
|
+
refLooks: {},
|
|
4124
4141
|
/** Count per tab-strip index, for the entries that asked for one; null = none. */
|
|
4125
4142
|
tabCounts: null,
|
|
4126
4143
|
// The counts of the `filterStrip` pills, by pill key ('all', 'v0', ...).
|
|
@@ -4362,7 +4379,7 @@ export default {
|
|
|
4362
4379
|
/**
|
|
4363
4380
|
* The header actions that open a dialog: `type: "open-form"` (the
|
|
4364
4381
|
* schema-driven create form, as the navigation's primary action opens
|
|
4365
|
-
* it) or `type: "open-modal"`, without a `handler` of their own.
|
|
4382
|
+
* it) or `type: "open-modal"`, without a `handler` or `href` of their own.
|
|
4366
4383
|
* Reserved ids are left out, as in `mergedHeaderActions`.
|
|
4367
4384
|
*
|
|
4368
4385
|
* @spec openspec/changes/index-header-open-form/specs/index-header/spec.md#requirement-an-index-header-action-can-open-the-create-form
|
|
@@ -4374,9 +4391,18 @@ export default {
|
|
|
4374
4391
|
&& typeof entry.id === 'string'
|
|
4375
4392
|
&& !reserved.has(entry.id)
|
|
4376
4393
|
&& (entry.handler === undefined || entry.handler === null || entry.handler === '')
|
|
4394
|
+
&& !entry.href
|
|
4377
4395
|
&& (entry.type === 'open-form' || entry.type === 'open-modal'))
|
|
4378
4396
|
},
|
|
4379
4397
|
|
|
4398
|
+
/**
|
|
4399
|
+
* The header actions without reserved ids, each through
|
|
4400
|
+
* `resolveHeaderHandler` (a manifest `href` entry becomes a link with
|
|
4401
|
+
* `_dispatchSuppress`, so the page neither runs nor emits it).
|
|
4402
|
+
*
|
|
4403
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
4404
|
+
* @return {Array<object>}
|
|
4405
|
+
*/
|
|
4380
4406
|
mergedHeaderActions() {
|
|
4381
4407
|
const reserved = new Set(['refresh', 'import', 'export', 'copy', 'delete'])
|
|
4382
4408
|
const merged = []
|
|
@@ -6035,6 +6061,12 @@ export default {
|
|
|
6035
6061
|
*/
|
|
6036
6062
|
renderedColumns() {
|
|
6037
6063
|
let cols = this.tableColumns
|
|
6064
|
+
if (this.refLookSpecs.length > 0) {
|
|
6065
|
+
const lookKeys = new Set(this.refLookSpecs.map((spec) => spec.key))
|
|
6066
|
+
cols = cols.map((col) => (col && typeof col === 'object' && lookKeys.has(col.key)
|
|
6067
|
+
? { ...col, widgetProps: { ...(col.widgetProps || {}), looks: this.refLooks[col.key] || {} } }
|
|
6068
|
+
: col))
|
|
6069
|
+
}
|
|
6038
6070
|
if (this.refLabelSpecs.length > 0) {
|
|
6039
6071
|
const byKey = new Map(this.refLabelSpecs.map((s) => [s.key, s]))
|
|
6040
6072
|
cols = cols.map((col) => {
|
|
@@ -6105,6 +6137,108 @@ export default {
|
|
|
6105
6137
|
*
|
|
6106
6138
|
* @return {Object<string, string[]>} Distinct ids per column key.
|
|
6107
6139
|
*/
|
|
6140
|
+
/**
|
|
6141
|
+
* The pills that take their tone and icon from the record they point at
|
|
6142
|
+
* (`resolve: { colorField?, iconField?, register?, schema? }`): badge
|
|
6143
|
+
* columns and the list view's leading and trailing cells
|
|
6144
|
+
* (`widgetProps.resolve`), the board card's pill (`board.card.resolve`)
|
|
6145
|
+
* and the record card's pills (`cardLayout` headPill, footerPill, chips).
|
|
6146
|
+
* Register and schema default to the field's reference in the schema.
|
|
6147
|
+
*
|
|
6148
|
+
* @return {Array<{key: string, register: string, schema: string, colorField: string, iconField: string}>} The specs.
|
|
6149
|
+
* @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
|
|
6150
|
+
*/
|
|
6151
|
+
refLookSpecs() {
|
|
6152
|
+
const list = this.listConfig || {}
|
|
6153
|
+
const listCells = [list.leading, ...(Array.isArray(list.trailing) ? list.trailing : [list.trailing])]
|
|
6154
|
+
const badgeSources = [...this.tableColumns, ...listCells]
|
|
6155
|
+
.filter((c) => c && typeof c === 'object' && c.widget === 'badge' && c.widgetProps)
|
|
6156
|
+
.map((c) => ({ key: c.key, resolve: c.widgetProps.resolve }))
|
|
6157
|
+
// The board card's pill (`board.card.pill` with `board.card.resolve`)
|
|
6158
|
+
// and the record card's pills (`cardLayout` headPill, footerPill,
|
|
6159
|
+
// chips, each with its own `resolve`).
|
|
6160
|
+
const card = this.board && this.board.card && typeof this.board.card === 'object' ? this.board.card : {}
|
|
6161
|
+
const layout = this.cardLayout && typeof this.cardLayout === 'object' ? this.cardLayout : {}
|
|
6162
|
+
const layoutPills = [layout.headPill, layout.footerPill, ...(Array.isArray(layout.chips) ? layout.chips : [])]
|
|
6163
|
+
.filter((def) => def && typeof def === 'object')
|
|
6164
|
+
.map((def) => ({ key: def.field, resolve: def.resolve }))
|
|
6165
|
+
// The map list's pills (`mapConfig.list.pills`, each with its own `resolve`).
|
|
6166
|
+
const mapListPills = (this.mapListEnabled && Array.isArray(this.mapConfig.list.pills) ? this.mapConfig.list.pills : [])
|
|
6167
|
+
.filter((def) => def && typeof def === 'object')
|
|
6168
|
+
.map((def) => ({ key: def.path, resolve: def.resolve }))
|
|
6169
|
+
const seen = new Set()
|
|
6170
|
+
const cols = [...badgeSources, { key: card.pill, resolve: card.resolve }, ...layoutPills, ...mapListPills].filter((c) => {
|
|
6171
|
+
const wanted = typeof c.key === 'string' && c.key !== '' && c.resolve && typeof c.resolve === 'object'
|
|
6172
|
+
&& (c.resolve.colorField || c.resolve.iconField) && !seen.has(c.key)
|
|
6173
|
+
if (wanted) {
|
|
6174
|
+
seen.add(c.key)
|
|
6175
|
+
}
|
|
6176
|
+
return wanted
|
|
6177
|
+
})
|
|
6178
|
+
if (cols.length === 0) {
|
|
6179
|
+
return []
|
|
6180
|
+
}
|
|
6181
|
+
const fields = this.effectiveSchema ? fieldsFromSchema(this.effectiveSchema, { includeReadOnly: true }) : []
|
|
6182
|
+
const specs = []
|
|
6183
|
+
for (const col of cols) {
|
|
6184
|
+
const resolve = col.resolve
|
|
6185
|
+
const field = fields.find((f) => f.key === col.key)
|
|
6186
|
+
const ref = (field && field.reference) || {}
|
|
6187
|
+
const register = resolve.register || ref.register || (typeof this.register === 'string' ? this.register : '')
|
|
6188
|
+
const schema = resolve.schema || (ref.schema !== undefined && ref.schema !== null ? String(ref.schema) : '')
|
|
6189
|
+
if (!register || !schema) {
|
|
6190
|
+
continue
|
|
6191
|
+
}
|
|
6192
|
+
specs.push({ key: col.key, register, schema, colorField: resolve.colorField || '', iconField: resolve.iconField || '' })
|
|
6193
|
+
}
|
|
6194
|
+
return specs
|
|
6195
|
+
},
|
|
6196
|
+
|
|
6197
|
+
/**
|
|
6198
|
+
* `listConfig` with the referenced records' looks on its badge cells,
|
|
6199
|
+
* as `renderedColumns` does for the table.
|
|
6200
|
+
*
|
|
6201
|
+
* @return {object} The list view's config.
|
|
6202
|
+
* @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
|
|
6203
|
+
*/
|
|
6204
|
+
renderedListConfig() {
|
|
6205
|
+
const list = this.listConfig
|
|
6206
|
+
if (!list || this.refLookSpecs.length === 0) {
|
|
6207
|
+
return list
|
|
6208
|
+
}
|
|
6209
|
+
const lookKeys = new Set(this.refLookSpecs.map((spec) => spec.key))
|
|
6210
|
+
const withLooks = (cell) => (cell && typeof cell === 'object' && lookKeys.has(cell.key)
|
|
6211
|
+
? { ...cell, widgetProps: { ...(cell.widgetProps || {}), looks: this.refLooks[cell.key] || {} } }
|
|
6212
|
+
: cell)
|
|
6213
|
+
return {
|
|
6214
|
+
...list,
|
|
6215
|
+
...(list.leading ? { leading: withLooks(list.leading) } : {}),
|
|
6216
|
+
...(list.trailing ? { trailing: Array.isArray(list.trailing) ? list.trailing.map(withLooks) : withLooks(list.trailing) } : {}),
|
|
6217
|
+
}
|
|
6218
|
+
},
|
|
6219
|
+
|
|
6220
|
+
/**
|
|
6221
|
+
* The ids each `refLookSpecs` column shows on screen.
|
|
6222
|
+
*
|
|
6223
|
+
* @return {{[key: string]: Array<string>}} Sorted ids by column key.
|
|
6224
|
+
*/
|
|
6225
|
+
refLookIds() {
|
|
6226
|
+
const out = {}
|
|
6227
|
+
for (const spec of this.refLookSpecs) {
|
|
6228
|
+
const ids = new Set()
|
|
6229
|
+
for (const row of this.displayObjects) {
|
|
6230
|
+
const v = row ? row[spec.key] : undefined
|
|
6231
|
+
;(Array.isArray(v) ? v : [v]).forEach((id) => {
|
|
6232
|
+
if (id !== null && id !== undefined && id !== '' && typeof id !== 'object') {
|
|
6233
|
+
ids.add(String(id))
|
|
6234
|
+
}
|
|
6235
|
+
})
|
|
6236
|
+
}
|
|
6237
|
+
out[spec.key] = [...ids].sort()
|
|
6238
|
+
}
|
|
6239
|
+
return out
|
|
6240
|
+
},
|
|
6241
|
+
|
|
6108
6242
|
refLabelIds() {
|
|
6109
6243
|
const out = {}
|
|
6110
6244
|
const facets = this.storeFacets || this.resolvedSidebar.facets || {}
|
|
@@ -6372,13 +6506,15 @@ export default {
|
|
|
6372
6506
|
const translated = {
|
|
6373
6507
|
...list,
|
|
6374
6508
|
title: this.cnTranslate(list.title),
|
|
6375
|
-
sub: list.sub ? this.cnTranslate(list.sub) : '',
|
|
6509
|
+
sub: Array.isArray(list.sub) ? list.sub.map((s) => this.cnTranslate(s)) : (list.sub ? this.cnTranslate(list.sub) : ''),
|
|
6376
6510
|
due: list.due ? this.cnTranslate(list.due) : '',
|
|
6377
6511
|
}
|
|
6378
6512
|
return mapListItems(this.displayObjects, translated, {
|
|
6379
6513
|
rowKey: this.rowKey,
|
|
6380
6514
|
located: (row) => Boolean(this.resolveRowGeometry(row)),
|
|
6381
6515
|
translate: (text) => this.cnTranslate(text),
|
|
6516
|
+
formatters: this.cnFormatters,
|
|
6517
|
+
looks: this.refLooks,
|
|
6382
6518
|
})
|
|
6383
6519
|
},
|
|
6384
6520
|
|
|
@@ -7305,11 +7441,13 @@ export default {
|
|
|
7305
7441
|
*
|
|
7306
7442
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
7307
7443
|
* @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
|
|
7308
|
-
* @
|
|
7444
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
7445
|
+
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string, link: ({href: string, target: (string|undefined)}|null)}>}
|
|
7309
7446
|
*/
|
|
7310
7447
|
resolvedHeaderButtons() {
|
|
7311
7448
|
const headerButtons = this.presented('headerButtons')
|
|
7312
7449
|
const declared = Array.isArray(headerButtons) ? headerButtons : []
|
|
7450
|
+
const builtInActions = new Set(['add', 'export', 'import', 'refresh', 'save-view', 'actions-menu'])
|
|
7313
7451
|
return declared
|
|
7314
7452
|
.filter((button) => button && typeof button === 'object' && typeof button.action === 'string' && button.action !== '')
|
|
7315
7453
|
.map((button, index) => {
|
|
@@ -7336,6 +7474,13 @@ export default {
|
|
|
7336
7474
|
if (label === '' && button.action === 'save-view') {
|
|
7337
7475
|
label = t('nextcloud-vue', 'Save view')
|
|
7338
7476
|
}
|
|
7477
|
+
const run = typeof button.run === 'string' ? button.run : ''
|
|
7478
|
+
// A button that runs a manifest `href` header action is that link;
|
|
7479
|
+
// a `type: "navigate"` or function-handler entry keeps dispatching.
|
|
7480
|
+
const actionId = button.action === 'export' ? run : (builtInActions.has(button.action) ? '' : button.action)
|
|
7481
|
+
const linked = actionId
|
|
7482
|
+
? this.mergedHeaderActions.find((entry) => entry && entry.id === actionId && entry.href && entry._dispatchSuppress && (!entry.type || entry.type === 'handler'))
|
|
7483
|
+
: null
|
|
7339
7484
|
return {
|
|
7340
7485
|
key: String(button.id || button.action || index),
|
|
7341
7486
|
label,
|
|
@@ -7343,7 +7488,8 @@ export default {
|
|
|
7343
7488
|
variant: button.variant === 'primary' ? 'primary' : 'secondary',
|
|
7344
7489
|
icon,
|
|
7345
7490
|
format: button.format === 'excel' ? 'excel' : 'csv',
|
|
7346
|
-
run
|
|
7491
|
+
run,
|
|
7492
|
+
link: linked ? { href: linked.href, target: linked.linkTarget || undefined } : null,
|
|
7347
7493
|
items: Array.isArray(button.items)
|
|
7348
7494
|
? button.items.filter((item) => (typeof item === 'string' && item !== '')
|
|
7349
7495
|
|| (item && typeof item === 'object' && typeof item.heading === 'string' && item.heading !== ''))
|
|
@@ -8002,6 +8148,15 @@ export default {
|
|
|
8002
8148
|
},
|
|
8003
8149
|
},
|
|
8004
8150
|
|
|
8151
|
+
// Read the colour and icon of the records badge columns point at.
|
|
8152
|
+
refLookIds: {
|
|
8153
|
+
immediate: true,
|
|
8154
|
+
deep: true,
|
|
8155
|
+
handler() {
|
|
8156
|
+
this.loadRefLooks()
|
|
8157
|
+
},
|
|
8158
|
+
},
|
|
8159
|
+
|
|
8005
8160
|
// The Active line names a referenced value, not its id.
|
|
8006
8161
|
activeFilterRefIds: {
|
|
8007
8162
|
deep: true,
|
|
@@ -8453,6 +8608,45 @@ export default {
|
|
|
8453
8608
|
}))
|
|
8454
8609
|
},
|
|
8455
8610
|
|
|
8611
|
+
/**
|
|
8612
|
+
* Read the colour and icon names `refLookSpecs` columns need, for the
|
|
8613
|
+
* rows on screen, with the same resolver as the labels. A failure
|
|
8614
|
+
* leaves the badge as declared.
|
|
8615
|
+
*
|
|
8616
|
+
* @return {Promise<void>}
|
|
8617
|
+
* @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
|
|
8618
|
+
*/
|
|
8619
|
+
async loadRefLooks() {
|
|
8620
|
+
if (this.refLookSpecs.length === 0) {
|
|
8621
|
+
return
|
|
8622
|
+
}
|
|
8623
|
+
if (!this._refLabelResolver) {
|
|
8624
|
+
this._refLabelResolver = createRefLabelResolver(() => {
|
|
8625
|
+
try {
|
|
8626
|
+
return (this.list && this.list.objectStore) || useObjectStore()
|
|
8627
|
+
} catch {
|
|
8628
|
+
return null
|
|
8629
|
+
}
|
|
8630
|
+
})
|
|
8631
|
+
}
|
|
8632
|
+
const read = (spec, ids, name) => (name
|
|
8633
|
+
? this._refLabelResolver.resolve(spec.register, spec.schema, ids, name, { strict: true }).catch(() => ({}))
|
|
8634
|
+
: Promise.resolve({}))
|
|
8635
|
+
await Promise.all(this.refLookSpecs.map(async (spec) => {
|
|
8636
|
+
const known = this.refLooks[spec.key] || {}
|
|
8637
|
+
const ids = (this.refLookIds[spec.key] || []).filter((id) => !Object.hasOwn(known, id))
|
|
8638
|
+
if (ids.length === 0) {
|
|
8639
|
+
return
|
|
8640
|
+
}
|
|
8641
|
+
const [colors, icons] = await Promise.all([read(spec, ids, spec.colorField), read(spec, ids, spec.iconField)])
|
|
8642
|
+
const found = {}
|
|
8643
|
+
ids.forEach((id) => {
|
|
8644
|
+
found[id] = { color: (colors && colors[id]) || null, icon: (icons && icons[id]) || null }
|
|
8645
|
+
})
|
|
8646
|
+
this.refLooks = { ...this.refLooks, [spec.key]: { ...(this.refLooks[spec.key] || {}), ...found } }
|
|
8647
|
+
}))
|
|
8648
|
+
},
|
|
8649
|
+
|
|
8456
8650
|
/**
|
|
8457
8651
|
* Whether a saved view shows its record count (`viewCounts`).
|
|
8458
8652
|
*
|
|
@@ -8953,8 +9147,11 @@ export default {
|
|
|
8953
9147
|
* manifest-actions-dispatch — `navigate`, `emit`, `none`, or a
|
|
8954
9148
|
* registry name (resolved through `resolveRegisteredHandler`).
|
|
8955
9149
|
*
|
|
9150
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
8956
9151
|
* @param {object} entry Raw headerActions entry.
|
|
8957
|
-
* @return {object} Possibly-mutated copy:
|
|
9152
|
+
* @return {object} Possibly-mutated copy: an `href` becomes a
|
|
9153
|
+
* resolved link with `linkTarget`, no handler (a function one
|
|
9154
|
+
* included) and `_dispatchSuppress: true`; function-typed
|
|
8958
9155
|
* handlers untouched; `'emit'` and unknown registry names
|
|
8959
9156
|
* strip the `handler` (emit-only fall-through); `'navigate'`
|
|
8960
9157
|
* becomes a `$router.push` thunk (with the entry's literal
|
|
@@ -8966,14 +9163,23 @@ export default {
|
|
|
8966
9163
|
if (!entry) {
|
|
8967
9164
|
return entry
|
|
8968
9165
|
}
|
|
9166
|
+
// An `href` makes the entry a link, which only navigates: it
|
|
9167
|
+
// carries no handler, and the page emits nothing for it.
|
|
9168
|
+
if (typeof entry.href === 'string' && entry.href !== '') {
|
|
9169
|
+
const link = headerActionAsLink(entry)
|
|
9170
|
+
return link.href ? { ...link, linkTarget: link.target, _dispatchSuppress: true } : link
|
|
9171
|
+
}
|
|
8969
9172
|
const handler = entry.handler
|
|
8970
9173
|
if (typeof handler === 'function') {
|
|
8971
9174
|
return { ...entry }
|
|
8972
9175
|
}
|
|
8973
9176
|
// `type: "navigate"` with a `target` URL is a link: the bar and the
|
|
8974
|
-
// header's Actions menu render it as a real anchor
|
|
9177
|
+
// header's Actions menu render it as a real anchor, an external URL
|
|
9178
|
+
// in a new tab.
|
|
8975
9179
|
if (typeof handler !== 'string' && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '' && !entry.href) {
|
|
8976
|
-
return
|
|
9180
|
+
return isExternalActionTarget(entry.target)
|
|
9181
|
+
? { ...entry, href: entry.target, linkTarget: '_blank' }
|
|
9182
|
+
: { ...entry, href: entry.target }
|
|
8977
9183
|
}
|
|
8978
9184
|
if (typeof handler !== 'string') {
|
|
8979
9185
|
// No handler declared — emit-only fall-through.
|
|
@@ -9144,8 +9350,9 @@ export default {
|
|
|
9144
9350
|
*
|
|
9145
9351
|
* @spec openspec/changes/screens-index-header-actions-menu/specs/index-header/spec.md#requirement-the-header-actions-menu-lists-what-its-items-name
|
|
9146
9352
|
* @spec openspec/changes/screens-index-header-menu-headings/specs/index-header/spec.md#requirement-the-header-actions-menu-can-draw-group-headings-and-separators
|
|
9353
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
9147
9354
|
* @param {{items?: Array<(string|{heading: string})>|null, format?: string}} button The resolved header button.
|
|
9148
|
-
* @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, disabled?: boolean, title?: string, iconComponent?: object, run?:
|
|
9355
|
+
* @return {Array<{key: string, label: string, kind?: ('heading'|'separator'), icon?: string, href?: string, linkTarget?: string, disabled?: boolean, title?: string, iconComponent?: object, run?: () => void}>}
|
|
9149
9356
|
*/
|
|
9150
9357
|
headerMenuEntries(button) {
|
|
9151
9358
|
const fromAction = (entry) => ({
|
|
@@ -9153,6 +9360,7 @@ export default {
|
|
|
9153
9360
|
label: entry.label ? this.cnTranslate(entry.label) : '',
|
|
9154
9361
|
icon: entry.icon && typeof entry.icon === 'string' && !entry.icon.startsWith('icon-') ? entry.icon : '',
|
|
9155
9362
|
href: typeof entry.href === 'string' && entry.href !== '' ? entry.href : '',
|
|
9363
|
+
linkTarget: entry.linkTarget || '',
|
|
9156
9364
|
// A `handler: "navigate"` entry whose route resolves carries `to`
|
|
9157
9365
|
// (resolveHeaderHandler drops its handler): the menu draws it as
|
|
9158
9366
|
// an in-app link, as the actions bar does, so it navigates.
|
|
@@ -9186,7 +9394,7 @@ export default {
|
|
|
9186
9394
|
}),
|
|
9187
9395
|
|
|
9188
9396
|
documentation: () => (this.documentationUrl
|
|
9189
|
-
? { key: 'documentation', label: this.documentationLabel || t('nextcloud-vue', 'Documentation'), icon: 'BookOpenVariantOutline', href: this.documentationUrl }
|
|
9397
|
+
? { key: 'documentation', label: this.documentationLabel || t('nextcloud-vue', 'Documentation'), icon: 'BookOpenVariantOutline', href: this.documentationUrl, linkTarget: '_blank' }
|
|
9190
9398
|
: null),
|
|
9191
9399
|
}
|
|
9192
9400
|
const entries = []
|
|
@@ -9282,13 +9490,18 @@ export default {
|
|
|
9282
9490
|
|
|
9283
9491
|
/**
|
|
9284
9492
|
* A header button was clicked: run its built-in action, or dispatch
|
|
9285
|
-
* it as the `headerActions` entry with that id.
|
|
9493
|
+
* it as the `headerActions` entry with that id. A button that is a
|
|
9494
|
+
* link is left to the browser.
|
|
9286
9495
|
*
|
|
9287
9496
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
9288
|
-
* @
|
|
9497
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
9498
|
+
* @param {{action: string, format: string, link?: object}} button The resolved button.
|
|
9289
9499
|
* @return {void}
|
|
9290
9500
|
*/
|
|
9291
9501
|
onHeaderButton(button) {
|
|
9502
|
+
if (button.link) {
|
|
9503
|
+
return
|
|
9504
|
+
}
|
|
9292
9505
|
if (button.action === 'add') {
|
|
9293
9506
|
this.onAddClick()
|
|
9294
9507
|
} else if (button.action === 'export') {
|
|
@@ -11614,7 +11827,7 @@ export default {
|
|
|
11614
11827
|
return
|
|
11615
11828
|
}
|
|
11616
11829
|
const badge = this.savedViewMenuBadge(view)
|
|
11617
|
-
menu.toggle({ id: String(view.id || view.slug), page: this.savedViewPage, name: view.name, ...(badge ? { badge } : {}) })
|
|
11830
|
+
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
11831
|
},
|
|
11619
11832
|
|
|
11620
11833
|
/**
|
|
@@ -12149,7 +12362,9 @@ export default {
|
|
|
12149
12362
|
if (resultData && resultData.success && this._refLabelResolver) {
|
|
12150
12363
|
this._refLabelResolver.invalidate()
|
|
12151
12364
|
this.refLabels = {}
|
|
12365
|
+
this.refLooks = {}
|
|
12152
12366
|
this.loadRefLabels()
|
|
12367
|
+
this.loadRefLooks()
|
|
12153
12368
|
}
|
|
12154
12369
|
},
|
|
12155
12370
|
|
|
@@ -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.
|
|
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
|
/**
|