@conduction/nextcloud-vue 2.56.0-beta.31 → 2.56.0-beta.32
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/CnBannerWidget/CnBannerWidget.vue2.js +55 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +51 -15
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +20 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +7 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +77 -3
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +0 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
- package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +4 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +1 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +1 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +843 -4
- package/dist/esm/utils/contentHeaderLink.js +51 -0
- package/dist/esm/utils/contentHeaderLink.js.map +1 -0
- package/dist/esm/utils/fetchFilterCounts.js +40 -5
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/initials.js +32 -0
- package/dist/esm/utils/initials.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +7 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectWrite.js +59 -0
- package/dist/esm/utils/objectWrite.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +10510 -6830
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +843 -4
- package/l10n/en.json +1 -0
- package/l10n/nl.json +1 -0
- package/package.json +1 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +93 -13
- package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +43 -3
- package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
- package/src/components/CnChipsFieldWidget/index.js +25 -0
- package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
- package/src/components/CnChoicePickerWidget/index.js +26 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +38 -2
- package/src/components/CnDataTable/CnDataTable.vue +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +45 -2
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -3
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +265 -4
- package/src/components/CnIndexPage/CnIndexPage.vue +0 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
- package/src/components/CnRadioCardsWidget/index.js +27 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +43 -1
- package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
- package/src/components/CnSavedViewsWidget/index.js +29 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
- package/src/components/CnSettingsFormWidget/index.js +26 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
- package/src/components/CnSwitchRowsWidget/index.js +25 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
- package/src/components/CnToggleTableWidget/index.js +26 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +28 -1
- package/src/components/index.js +7 -0
- package/src/css/look-board-detail.css +36 -2
- package/src/css/look-board-index.css +23 -0
- package/src/css/look-board.css +72 -0
- package/src/index.js +7 -0
- package/src/utils/contentHeaderLink.js +48 -0
- package/src/utils/fetchFilterCounts.js +39 -4
- package/src/utils/initials.js +29 -0
- package/src/utils/libraryWidgetKeys.js +7 -0
- package/src/utils/objectWrite.js +55 -0
- package/src/utils/widgetDispatch.js +1 -1
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-choice-picker" data-testid="cn-choice-picker">
|
|
8
|
+
<p v-if="loading" class="cn-choice-picker__note" data-testid="cn-choice-picker-loading">
|
|
9
|
+
{{ tr('Loading') }}
|
|
10
|
+
</p>
|
|
11
|
+
<p v-else-if="options.length === 0" class="cn-choice-picker__note" data-testid="cn-choice-picker-empty">
|
|
12
|
+
{{ emptyLabel }}
|
|
13
|
+
</p>
|
|
14
|
+
<fieldset v-else class="cn-choice-picker__cards">
|
|
15
|
+
<legend class="cn-choice-picker__legend">
|
|
16
|
+
{{ legend }}
|
|
17
|
+
</legend>
|
|
18
|
+
<label
|
|
19
|
+
v-for="option in options"
|
|
20
|
+
:key="option.key"
|
|
21
|
+
class="cn-choice-picker__card"
|
|
22
|
+
:class="{
|
|
23
|
+
'cn-choice-picker__card--selected': chosen === option.key,
|
|
24
|
+
'cn-choice-picker__card--refused': option.refusal !== '',
|
|
25
|
+
}"
|
|
26
|
+
:data-testid="`cn-choice-picker-option-${option.key}`">
|
|
27
|
+
<input
|
|
28
|
+
class="cn-choice-picker__input"
|
|
29
|
+
type="radio"
|
|
30
|
+
:name="groupName"
|
|
31
|
+
:value="option.key"
|
|
32
|
+
:checked="chosen === option.key"
|
|
33
|
+
:disabled="option.refusal !== '' || saving"
|
|
34
|
+
@change="choose(option)">
|
|
35
|
+
<span class="cn-choice-picker__swatches" aria-hidden="true">
|
|
36
|
+
<span
|
|
37
|
+
v-for="(colour, index) in option.swatches"
|
|
38
|
+
:key="index"
|
|
39
|
+
class="cn-choice-picker__swatch"
|
|
40
|
+
:style="{ background: colour }" />
|
|
41
|
+
</span>
|
|
42
|
+
<span class="cn-choice-picker__text">
|
|
43
|
+
<strong class="cn-choice-picker__title">{{ option.title }}</strong>
|
|
44
|
+
<span v-if="option.refusal" class="cn-choice-picker__refusal" :data-testid="`cn-choice-picker-refusal-${option.key}`">
|
|
45
|
+
{{ option.refusal }}
|
|
46
|
+
</span>
|
|
47
|
+
</span>
|
|
48
|
+
<CnStatusBadge
|
|
49
|
+
v-if="option.pill"
|
|
50
|
+
:label="option.pill.label"
|
|
51
|
+
:variant="option.pill.variant"
|
|
52
|
+
size="small" />
|
|
53
|
+
</label>
|
|
54
|
+
</fieldset>
|
|
55
|
+
|
|
56
|
+
<NcNoteCard v-if="selected && selected.warning.length > 0" type="warning" data-testid="cn-choice-picker-warning">
|
|
57
|
+
<p v-if="warningIntro" class="cn-choice-picker__warning-intro">
|
|
58
|
+
{{ warningIntro }}
|
|
59
|
+
</p>
|
|
60
|
+
<ul class="cn-choice-picker__warning-list">
|
|
61
|
+
<li v-for="(line, index) in selected.warning" :key="index">
|
|
62
|
+
{{ line }}
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</NcNoteCard>
|
|
66
|
+
|
|
67
|
+
<NcNoteCard v-if="notice" :type="noticeType" data-testid="cn-choice-picker-notice">
|
|
68
|
+
{{ notice }}
|
|
69
|
+
</NcNoteCard>
|
|
70
|
+
|
|
71
|
+
<div v-if="options.length > 0 && !readOnly" class="cn-choice-picker__actions">
|
|
72
|
+
<NcButton
|
|
73
|
+
variant="primary"
|
|
74
|
+
:disabled="saving || !canSave"
|
|
75
|
+
data-testid="cn-choice-picker-save"
|
|
76
|
+
@click="save(false)">
|
|
77
|
+
{{ saveLabel }}
|
|
78
|
+
</NcButton>
|
|
79
|
+
<NcButton
|
|
80
|
+
v-if="selected && selected.warning.length > 0"
|
|
81
|
+
variant="secondary"
|
|
82
|
+
:disabled="saving"
|
|
83
|
+
data-testid="cn-choice-picker-confirm"
|
|
84
|
+
@click="save(true)">
|
|
85
|
+
{{ confirmLabel }}
|
|
86
|
+
</NcButton>
|
|
87
|
+
</div>
|
|
88
|
+
<p v-if="hint" class="cn-choice-picker__hint">
|
|
89
|
+
{{ hint }}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<script>
|
|
95
|
+
import { NcButton, NcNoteCard } from '@nextcloud/vue'
|
|
96
|
+
import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
|
|
97
|
+
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
98
|
+
import { prefixUrl } from '../../utils/headers.js'
|
|
99
|
+
import { objectUrlOf } from '../../utils/objectWrite.js'
|
|
100
|
+
import { readPath } from '../../utils/readPath.js'
|
|
101
|
+
import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
|
|
102
|
+
|
|
103
|
+
let uid = 0
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* CnChoicePickerWidget — the `choice-picker` widget: choice cards read from a
|
|
107
|
+
* source (or a static list), each with a radio, colour swatches read from the
|
|
108
|
+
* item, a title and a state pill (a verdict such as "Leesbaar" / "Slecht
|
|
109
|
+
* leesbaar"), bound to one field of the CURRENT object.
|
|
110
|
+
*
|
|
111
|
+
* Save is a button. A card that carries a warning (a list of lines read from
|
|
112
|
+
* the item) shows them in a warning band under the cards and adds a second
|
|
113
|
+
* button, the confirm action, so a risky choice is saved only on purpose: the
|
|
114
|
+
* request tells the server which one was pressed (`@confirmed`). A card the
|
|
115
|
+
* source marks as refused is listed with its reason and cannot be chosen.
|
|
116
|
+
*
|
|
117
|
+
* @spec openspec/changes/abstract-choice-picker/specs/choice-picker/spec.md
|
|
118
|
+
*/
|
|
119
|
+
export default {
|
|
120
|
+
name: 'CnChoicePickerWidget',
|
|
121
|
+
|
|
122
|
+
components: { CnStatusBadge, NcButton, NcNoteCard },
|
|
123
|
+
|
|
124
|
+
inject: {
|
|
125
|
+
cnTranslate: { default: () => (key) => key },
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
props: {
|
|
129
|
+
/**
|
|
130
|
+
* The widget configuration.
|
|
131
|
+
*
|
|
132
|
+
* - `field` (required) — the top-level field of the object holding the chosen key.
|
|
133
|
+
* - `source` — `{ url, path?, key, label, swatches?, state?, refusal?, warning? }` (tokens `@objectId`, `@object.<field>` in the url), or `items` — the same item shape as a static list.
|
|
134
|
+
* - `swatches` — path to an array of colours (CSS colour strings).
|
|
135
|
+
* - `state` — `{ path, map?, variantMap?, empty?, emptyVariant? }`: the pill; `map` turns the value into text, `variantMap` into a badge variant, `empty` is the text when the value is missing.
|
|
136
|
+
* - `refusal` — path to a string: the card is listed with it and cannot be chosen.
|
|
137
|
+
* - `warning` — `{ list, item? }`: `list` is a path to an array; `item` a template with `{path}` placeholders for object items (strings are used as they are).
|
|
138
|
+
* - `legend`, `warningIntro`, `saveLabel`, `confirmLabel`, `emptyText`, `hint`, `readOnly`.
|
|
139
|
+
* - `save` — `{ method?, url?, payload? }`: a custom request (tokens `@value`, `@confirmed`, `@objectId`, `@object.<field>`); without it the field is PATCHed on the object.
|
|
140
|
+
*
|
|
141
|
+
* @type {object}
|
|
142
|
+
*/
|
|
143
|
+
content: {
|
|
144
|
+
type: Object,
|
|
145
|
+
default: () => ({}),
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** The current detail-page object. */
|
|
149
|
+
objectData: {
|
|
150
|
+
type: Object,
|
|
151
|
+
default: null,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** The current object id. */
|
|
155
|
+
objectId: {
|
|
156
|
+
type: [String, Number],
|
|
157
|
+
default: '',
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/** OpenRegister register slug of the surface. */
|
|
161
|
+
register: {
|
|
162
|
+
type: [String, Object],
|
|
163
|
+
default: '',
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/** OpenRegister schema slug of the surface. */
|
|
167
|
+
schema: {
|
|
168
|
+
type: [String, Object],
|
|
169
|
+
default: '',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
emits: ['saved'],
|
|
174
|
+
|
|
175
|
+
data() {
|
|
176
|
+
uid += 1
|
|
177
|
+
return {
|
|
178
|
+
groupName: `cn-choice-picker-${uid}`,
|
|
179
|
+
sourced: [],
|
|
180
|
+
loading: false,
|
|
181
|
+
saved: '',
|
|
182
|
+
chosen: '',
|
|
183
|
+
saving: false,
|
|
184
|
+
notice: '',
|
|
185
|
+
noticeType: 'success',
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
computed: {
|
|
190
|
+
spec() {
|
|
191
|
+
return this.content.source || {}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/** The cards, from the static items then the source. */
|
|
195
|
+
options() {
|
|
196
|
+
const items = [...(Array.isArray(this.content.items) ? this.content.items : []), ...this.sourced]
|
|
197
|
+
return items
|
|
198
|
+
.filter((item) => item && readPath(item, this.spec.key || 'key') !== undefined)
|
|
199
|
+
.map((item) => {
|
|
200
|
+
const key = String(readPath(item, this.spec.key || 'key'))
|
|
201
|
+
const refusal = this.spec.refusal ? readPath(item, this.spec.refusal) : ''
|
|
202
|
+
return {
|
|
203
|
+
key,
|
|
204
|
+
title: String(readPath(item, this.spec.label || 'label') ?? key),
|
|
205
|
+
swatches: this.listOf(item, this.spec.swatches).filter((colour) => typeof colour === 'string'),
|
|
206
|
+
pill: this.pillOf(item),
|
|
207
|
+
refusal: typeof refusal === 'string' ? refusal : '',
|
|
208
|
+
warning: this.warningOf(item),
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
selected() {
|
|
214
|
+
return this.options.find((option) => option.key === this.chosen) || null
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
canSave() {
|
|
218
|
+
return this.chosen !== '' && this.chosen !== this.saved && (this.selected ? this.selected.refusal === '' : false)
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
legend() {
|
|
222
|
+
return this.tr(this.content.legend || 'Choose one')
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
warningIntro() {
|
|
226
|
+
return this.tr(this.content.warningIntro || '')
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
saveLabel() {
|
|
230
|
+
return this.tr(this.content.saveLabel || 'Save')
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
confirmLabel() {
|
|
234
|
+
return this.tr(this.content.confirmLabel || 'Use it anyway')
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
emptyLabel() {
|
|
238
|
+
return this.tr(this.content.emptyText || 'Nothing to choose from')
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
hint() {
|
|
242
|
+
return this.tr(this.content.hint || '')
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
readOnly() {
|
|
246
|
+
return this.content.readOnly === true
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
objectUrl() {
|
|
250
|
+
return objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
watch: {
|
|
255
|
+
objectData() {
|
|
256
|
+
this.adopt()
|
|
257
|
+
this.loadSource()
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
created() {
|
|
262
|
+
this.adopt()
|
|
263
|
+
this.loadSource()
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
methods: {
|
|
267
|
+
tr(text) {
|
|
268
|
+
if (typeof text !== 'string' || text === '') {
|
|
269
|
+
return ''
|
|
270
|
+
}
|
|
271
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/** Read the current choice off the object. */
|
|
275
|
+
adopt() {
|
|
276
|
+
const stored = readPath(this.objectData || {}, this.content.field)
|
|
277
|
+
this.saved = stored === undefined || stored === null ? '' : String(stored)
|
|
278
|
+
this.chosen = this.saved
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* A list off an item.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} item The item.
|
|
285
|
+
* @param {string} path The path, or undefined.
|
|
286
|
+
* @return {Array}
|
|
287
|
+
*/
|
|
288
|
+
listOf(item, path) {
|
|
289
|
+
const list = path ? readPath(item, path) : []
|
|
290
|
+
return Array.isArray(list) ? list : []
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The state pill of an item.
|
|
295
|
+
*
|
|
296
|
+
* @param {object} item The item.
|
|
297
|
+
* @return {{label: string, variant: string}|null}
|
|
298
|
+
*/
|
|
299
|
+
pillOf(item) {
|
|
300
|
+
const state = this.spec.state
|
|
301
|
+
if (!state || !state.path) {
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
const raw = readPath(item, state.path)
|
|
305
|
+
const empty = raw === undefined || raw === null || raw === ''
|
|
306
|
+
if (empty) {
|
|
307
|
+
return state.empty ? { label: this.tr(state.empty), variant: isBadgeVariant(state.emptyVariant) ? state.emptyVariant : 'default' } : null
|
|
308
|
+
}
|
|
309
|
+
const label = state.map && Object.hasOwn(state.map, String(raw)) ? this.tr(state.map[String(raw)]) : String(raw)
|
|
310
|
+
const variant = state.variantMap && state.variantMap[String(raw)]
|
|
311
|
+
return { label, variant: isBadgeVariant(variant) ? variant : 'default' }
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The warning lines of an item.
|
|
316
|
+
*
|
|
317
|
+
* @param {object} item The item.
|
|
318
|
+
* @return {string[]}
|
|
319
|
+
*/
|
|
320
|
+
warningOf(item) {
|
|
321
|
+
const spec = this.spec.warning
|
|
322
|
+
if (!spec || !spec.list) {
|
|
323
|
+
return []
|
|
324
|
+
}
|
|
325
|
+
return this.listOf(item, spec.list)
|
|
326
|
+
.map((entry) => {
|
|
327
|
+
if (typeof entry === 'string') {
|
|
328
|
+
return entry
|
|
329
|
+
}
|
|
330
|
+
if (!entry || typeof entry !== 'object' || !spec.item) {
|
|
331
|
+
return ''
|
|
332
|
+
}
|
|
333
|
+
return this.tr(spec.item).replace(/\{([\w.]+)\}/g, (m, path) => String(readPath(entry, path) ?? ''))
|
|
334
|
+
})
|
|
335
|
+
.filter((line) => line !== '')
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Resolve the tokens in a url or payload.
|
|
340
|
+
*
|
|
341
|
+
* @param {unknown} value The value.
|
|
342
|
+
* @param {object} vars `{ value, confirmed }`.
|
|
343
|
+
* @return {unknown}
|
|
344
|
+
*/
|
|
345
|
+
resolve(value, vars) {
|
|
346
|
+
if (Array.isArray(value)) {
|
|
347
|
+
return value.map((v) => this.resolve(v, vars))
|
|
348
|
+
}
|
|
349
|
+
if (value && typeof value === 'object') {
|
|
350
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.resolve(v, vars)]))
|
|
351
|
+
}
|
|
352
|
+
if (typeof value !== 'string') {
|
|
353
|
+
return value
|
|
354
|
+
}
|
|
355
|
+
const object = this.objectData || {}
|
|
356
|
+
if (value === '@value') {
|
|
357
|
+
return vars.value
|
|
358
|
+
}
|
|
359
|
+
if (value === '@confirmed') {
|
|
360
|
+
return vars.confirmed
|
|
361
|
+
}
|
|
362
|
+
const whole = value.match(/^@object\.([\w.@]+)$/)
|
|
363
|
+
if (whole) {
|
|
364
|
+
return readPath(object, whole[1])
|
|
365
|
+
}
|
|
366
|
+
return value
|
|
367
|
+
.replace(/@objectId/g, () => encodeURIComponent(String(this.objectId || object.id || '')))
|
|
368
|
+
.replace(/@object\.([\w.@]+)/g, (m, path) => encodeURIComponent(String(readPath(object, path) ?? '')))
|
|
369
|
+
.replace(/@value/g, () => encodeURIComponent(String(vars.value)))
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/** Read the cards of `source`. */
|
|
373
|
+
async loadSource() {
|
|
374
|
+
if (!this.spec.url) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
this.loading = true
|
|
378
|
+
try {
|
|
379
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
380
|
+
const { data } = await axios.get(prefixUrl(String(this.resolve(this.spec.url, { value: '', confirmed: false }))))
|
|
381
|
+
const list = this.spec.path ? readPath(data, this.spec.path) : data
|
|
382
|
+
this.sourced = Array.isArray(list) ? list : []
|
|
383
|
+
} catch (e) {
|
|
384
|
+
this.noticeType = 'error'
|
|
385
|
+
this.notice = serverErrorMessage(e)
|
|
386
|
+
} finally {
|
|
387
|
+
this.loading = false
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Choose a card (nothing is written until Save).
|
|
393
|
+
*
|
|
394
|
+
* @param {object} option The card.
|
|
395
|
+
*/
|
|
396
|
+
choose(option) {
|
|
397
|
+
this.chosen = option.key
|
|
398
|
+
this.notice = ''
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Save the chosen card: the configured request, else a PATCH of the field.
|
|
403
|
+
*
|
|
404
|
+
* @param {boolean} confirmed Whether the confirm action was pressed.
|
|
405
|
+
* @return {Promise<void>}
|
|
406
|
+
* @spec openspec/changes/abstract-choice-picker/specs/choice-picker/spec.md#requirement-a-risky-choice-is-saved-only-on-purpose
|
|
407
|
+
*/
|
|
408
|
+
async save(confirmed) {
|
|
409
|
+
if (!this.selected) {
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
const request = this.content.save
|
|
413
|
+
if (!request && !this.objectUrl) {
|
|
414
|
+
this.noticeType = 'error'
|
|
415
|
+
this.notice = this.tr('This choice cannot be saved here.')
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
this.saving = true
|
|
419
|
+
this.notice = ''
|
|
420
|
+
try {
|
|
421
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
422
|
+
const vars = { value: this.selected.key, confirmed }
|
|
423
|
+
if (request && request.url) {
|
|
424
|
+
const method = String(request.method || 'POST').toLowerCase()
|
|
425
|
+
await axios[method](prefixUrl(String(this.resolve(request.url, vars))), this.resolve(request.payload || {}, vars))
|
|
426
|
+
} else {
|
|
427
|
+
await axios.patch(prefixUrl(this.objectUrl), { [this.content.field]: this.selected.key })
|
|
428
|
+
}
|
|
429
|
+
this.saved = this.selected.key
|
|
430
|
+
this.noticeType = 'success'
|
|
431
|
+
this.notice = this.tr(this.content.savedText || 'Saved.')
|
|
432
|
+
/**
|
|
433
|
+
* @event saved The choice was written.
|
|
434
|
+
* @type {{field: string, value: string, confirmed: boolean}}
|
|
435
|
+
*/
|
|
436
|
+
this.$emit('saved', { field: this.content.field, value: this.saved, confirmed })
|
|
437
|
+
} catch (e) {
|
|
438
|
+
this.noticeType = 'error'
|
|
439
|
+
this.notice = serverErrorMessage(e)
|
|
440
|
+
} finally {
|
|
441
|
+
this.saving = false
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
</script>
|
|
447
|
+
|
|
448
|
+
<style scoped>
|
|
449
|
+
.cn-choice-picker {
|
|
450
|
+
display: flex;
|
|
451
|
+
flex-direction: column;
|
|
452
|
+
gap: 12px;
|
|
453
|
+
min-width: 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.cn-choice-picker__cards {
|
|
457
|
+
display: flex;
|
|
458
|
+
flex-wrap: wrap;
|
|
459
|
+
gap: 10px;
|
|
460
|
+
margin: 0;
|
|
461
|
+
padding: 0;
|
|
462
|
+
border: 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.cn-choice-picker__legend {
|
|
466
|
+
position: absolute;
|
|
467
|
+
width: 1px;
|
|
468
|
+
height: 1px;
|
|
469
|
+
overflow: hidden;
|
|
470
|
+
clip-path: inset(50%);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.cn-choice-picker__card {
|
|
474
|
+
position: relative;
|
|
475
|
+
display: flex;
|
|
476
|
+
flex: 1 1 220px;
|
|
477
|
+
align-items: center;
|
|
478
|
+
gap: 12px;
|
|
479
|
+
padding: 10px 12px;
|
|
480
|
+
border: 1px solid var(--cn-board-hairline, var(--color-border));
|
|
481
|
+
border-radius: 10px;
|
|
482
|
+
background: var(--color-main-background);
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.cn-choice-picker__card--selected {
|
|
487
|
+
padding: 9px 11px;
|
|
488
|
+
border: 2px solid var(--color-primary-element);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.cn-choice-picker__card--refused {
|
|
492
|
+
cursor: not-allowed;
|
|
493
|
+
opacity: 0.7;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.cn-choice-picker__card:focus-within {
|
|
497
|
+
outline: 2px solid var(--color-primary-element);
|
|
498
|
+
outline-offset: 2px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.cn-choice-picker__input {
|
|
502
|
+
flex: 0 0 auto;
|
|
503
|
+
width: 18px;
|
|
504
|
+
height: 18px;
|
|
505
|
+
margin: 0;
|
|
506
|
+
accent-color: var(--color-primary-element);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.cn-choice-picker__swatches {
|
|
510
|
+
display: flex;
|
|
511
|
+
gap: 4px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.cn-choice-picker__swatch {
|
|
515
|
+
width: 18px;
|
|
516
|
+
height: 18px;
|
|
517
|
+
box-sizing: border-box;
|
|
518
|
+
border: 1px solid var(--color-border);
|
|
519
|
+
border-radius: 4px;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.cn-choice-picker__text {
|
|
523
|
+
display: flex;
|
|
524
|
+
flex: 1 1 auto;
|
|
525
|
+
flex-direction: column;
|
|
526
|
+
gap: 2px;
|
|
527
|
+
min-width: 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.cn-choice-picker__title {
|
|
531
|
+
font-size: 15px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.cn-choice-picker__refusal,
|
|
535
|
+
.cn-choice-picker__hint,
|
|
536
|
+
.cn-choice-picker__note {
|
|
537
|
+
margin: 0;
|
|
538
|
+
font-size: 13px;
|
|
539
|
+
color: var(--color-text-maxcontrast);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.cn-choice-picker__warning-intro {
|
|
543
|
+
margin: 0 0 4px;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.cn-choice-picker__warning-list {
|
|
547
|
+
margin: 0;
|
|
548
|
+
padding-inline-start: 18px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.cn-choice-picker__actions {
|
|
552
|
+
display: flex;
|
|
553
|
+
flex-wrap: wrap;
|
|
554
|
+
gap: 8px;
|
|
555
|
+
}
|
|
556
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnChoicePickerWidget — self-registers the `choice-picker` widget type (choice
|
|
6
|
+
* cards with colour swatches and a state pill, and a confirm-with-warning save)
|
|
7
|
+
* into the shared dashboardWidgetRegistry at module load.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import CnChoicePickerWidget from './CnChoicePickerWidget.vue'
|
|
11
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
12
|
+
|
|
13
|
+
registerDashboardWidget('choice-picker', {
|
|
14
|
+
renderer: CnChoicePickerWidget,
|
|
15
|
+
form: null,
|
|
16
|
+
defaultContent: {
|
|
17
|
+
field: '',
|
|
18
|
+
items: [],
|
|
19
|
+
},
|
|
20
|
+
displayName: 'Choice picker',
|
|
21
|
+
icon: 'Palette',
|
|
22
|
+
surfaces: ['detail-page'],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export { CnChoicePickerWidget }
|
|
26
|
+
export default CnChoicePickerWidget
|
|
@@ -452,6 +452,7 @@
|
|
|
452
452
|
:layout="section.layout"
|
|
453
453
|
:editable="section.editable"
|
|
454
454
|
:columns="columns"
|
|
455
|
+
:columnOpts="gridColumnOpts"
|
|
455
456
|
:cellHeight="cellHeight"
|
|
456
457
|
:margin="gridMargin"
|
|
457
458
|
:float="gridFloat"
|
|
@@ -933,6 +934,7 @@ import { normalizeLook } from '../../composables/useLook.js'
|
|
|
933
934
|
import { pageViews } from '../../mixins/pageViews.js'
|
|
934
935
|
import { mergeUserLayout, resolveUserLayoutApi } from '../../store/plugins/dashboardLayouts.js'
|
|
935
936
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
937
|
+
import { contentHeaderLink } from '../../utils/contentHeaderLink.js'
|
|
936
938
|
import { compareVisibleWhen, evaluateWorkspaceCondition, isWorkspaceCondition, readVisibleWhenValue } from '../../utils/visibleWhen.js'
|
|
937
939
|
import { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'
|
|
938
940
|
import { CnActionButtons } from '../CnActionButtons/index.js'
|
|
@@ -2082,6 +2084,22 @@ export default {
|
|
|
2082
2084
|
return this.compactDisplayLayout(visible)
|
|
2083
2085
|
},
|
|
2084
2086
|
|
|
2087
|
+
/**
|
|
2088
|
+
* The responsive columns of the grid. Under the board look a phone gets
|
|
2089
|
+
* one column: below 640px the widgets stack in reading order (top to
|
|
2090
|
+
* bottom, then left to right) instead of squeezing six columns into
|
|
2091
|
+
* 150px. Without the board look the grid keeps its fixed columns.
|
|
2092
|
+
*
|
|
2093
|
+
* @return {object|null} GridStack `columnOpts`, or null.
|
|
2094
|
+
* @spec openspec/changes/screens-dashboard-phone/specs/dashboard-phone/spec.md#requirement-a-board-dashboard-stacks-on-a-phone
|
|
2095
|
+
*/
|
|
2096
|
+
gridColumnOpts() {
|
|
2097
|
+
if (!this.isBoardLook) {
|
|
2098
|
+
return null
|
|
2099
|
+
}
|
|
2100
|
+
return { columnMax: this.columns, breakpoints: [{ w: 640, c: 1 }], layout: 'list', breakpointForWindow: true }
|
|
2101
|
+
},
|
|
2102
|
+
|
|
2085
2103
|
/**
|
|
2086
2104
|
* The layout this page renders: the user's arrangement when they have
|
|
2087
2105
|
* one, the manifest otherwise.
|
|
@@ -3405,7 +3423,8 @@ export default {
|
|
|
3405
3423
|
const layout = content.layout || props.layout || ''
|
|
3406
3424
|
const title = layout === 'attention' ? (content.title || props.title || '') : ''
|
|
3407
3425
|
return {
|
|
3408
|
-
|
|
3426
|
+
// A banner with only a bold `lead` has words to show too.
|
|
3427
|
+
text: content.text || props.text || content.lead || props.lead || title || '',
|
|
3409
3428
|
visibleWhen: (def && def.visibleWhen) || content.visibleWhen || props.visibleWhen || null,
|
|
3410
3429
|
layout,
|
|
3411
3430
|
}
|
|
@@ -4130,7 +4149,12 @@ export default {
|
|
|
4130
4149
|
return item.headerLink
|
|
4131
4150
|
}
|
|
4132
4151
|
const def = this.getWidgetDef(item.widgetId)
|
|
4133
|
-
|
|
4152
|
+
if (def?.headerLink && typeof def.headerLink === 'object') {
|
|
4153
|
+
return def.headerLink
|
|
4154
|
+
}
|
|
4155
|
+
// Last, what the widget's own content asks for (`headerLink`, or the
|
|
4156
|
+
// `viewAllRoute` / `viewAllLabel` of the tasks widget).
|
|
4157
|
+
return contentHeaderLink(item?.content) || contentHeaderLink(def?.content)
|
|
4134
4158
|
},
|
|
4135
4159
|
|
|
4136
4160
|
/**
|
|
@@ -4879,6 +4903,18 @@ export default {
|
|
|
4879
4903
|
.cn-dashboard-page__content-item {
|
|
4880
4904
|
width: 100%;
|
|
4881
4905
|
}
|
|
4906
|
+
/* On a phone (screens-dashboard-phone) the touch targets of a board dashboard
|
|
4907
|
+
are 44px: the widget Actions menu and the KPI tiles. */
|
|
4908
|
+
@media (max-width: 640px) {
|
|
4909
|
+
.cn-dashboard-page--board :deep(.cn-widget-wrapper__actions .action-item__menutoggle),
|
|
4910
|
+
.cn-dashboard-page--board :deep(.cn-widget-wrapper__actions .button-vue) {
|
|
4911
|
+
min-height: 44px;
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
.cn-dashboard-page--board .cn-dashboard-page__kpi-cell {
|
|
4915
|
+
min-height: 44px;
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4882
4918
|
</style>
|
|
4883
4919
|
|
|
4884
4920
|
<!-- Unscoped, anchored on the stable `data-testid` NcActions renders on both
|
|
@@ -321,6 +321,7 @@
|
|
|
321
321
|
:formatterOptions="col.formatterOptions || null"
|
|
322
322
|
:widget="col.widget || null"
|
|
323
323
|
:widgetProps="col.widgetProps || undefined"
|
|
324
|
+
:secondary="col.widget === 'avatar' ? secondaryValue(row, col) : ''"
|
|
324
325
|
:format="columnFormat(col)"
|
|
325
326
|
:row="row"
|
|
326
327
|
:rowKey="rowKey" />
|
|
@@ -330,7 +331,7 @@
|
|
|
330
331
|
of the row. Lets one column read "title" over "number ·
|
|
331
332
|
requester" without a custom cell. -->
|
|
332
333
|
<span
|
|
333
|
-
v-if="secondaryValue(row, col)"
|
|
334
|
+
v-if="col.widget !== 'avatar' && secondaryValue(row, col)"
|
|
334
335
|
class="cn-table-cell__secondary"
|
|
335
336
|
data-testid="cn-cell-secondary">{{ secondaryValue(row, col) }}</span>
|
|
336
337
|
<!-- A row that entered a file-content search through an attached
|