@conduction/nextcloud-vue 2.115.0 → 2.116.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 +92 -0
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +2 -2
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js +145 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js +58 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +376 -51
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +364 -285
- 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 +11 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +6 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +48 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +11 -2
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +11 -3
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +8 -3
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +55 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +94 -4
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +248 -239
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +9 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +282 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +202 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +64 -0
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useTaskInboxStore.js +85 -0
- package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +9 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js.map +1 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +107 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +110 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +19 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +23 -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 +1054 -911
- package/dist/esm/utils/actionsDispatcher.js +10 -5
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +6 -2
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.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/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17078 -15224
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1054 -911
- package/l10n/en.json +19 -1
- package/l10n/nl.json +23 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +93 -1
- package/src/components/CnChecklistWidget/CnChecklistWidget.vue +206 -0
- package/src/components/CnChecklistWidget/index.js +27 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +377 -10
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +15 -2
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +59 -1
- package/src/components/CnMacrosWidget/CnMacrosWidget.vue +16 -6
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +55 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +101 -3
- package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +508 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +10 -5
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +64 -0
- package/src/composables/useTaskInboxStore.js +85 -0
- package/src/dialogs/CnAccessPurposeDialog.vue +183 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +49 -3
- package/src/utils/actionsDispatcher.js +10 -5
- package/src/utils/detailActionModel.js +6 -2
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +1 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-record-notes" data-testid="cn-record-list-notes">
|
|
8
|
+
<p class="cn-record-notes__ready" data-testid="cn-record-list-notes-ready">
|
|
9
|
+
<span class="cn-record-notes__ready-figure">{{ readyCount }}</span>
|
|
10
|
+
<span class="cn-record-notes__ready-label">{{ readyLabel }}</span>
|
|
11
|
+
</p>
|
|
12
|
+
<div
|
|
13
|
+
v-for="item in items"
|
|
14
|
+
:key="item.key"
|
|
15
|
+
class="cn-record-notes__row"
|
|
16
|
+
data-testid="cn-record-list-item">
|
|
17
|
+
<div class="cn-record-notes__head">
|
|
18
|
+
<CnStatusBadge
|
|
19
|
+
v-if="item.pill"
|
|
20
|
+
:label="item.pill.label"
|
|
21
|
+
:variant="item.pill.variant"
|
|
22
|
+
size="small" />
|
|
23
|
+
<component
|
|
24
|
+
:is="item.tag"
|
|
25
|
+
v-if="item.title"
|
|
26
|
+
class="cn-record-notes__title"
|
|
27
|
+
:class="{ 'cn-record-notes__title--link': item.tag !== 'div' }"
|
|
28
|
+
v-bind="item.attrs">
|
|
29
|
+
{{ item.title }}
|
|
30
|
+
</component>
|
|
31
|
+
<span v-if="item.meta" class="cn-record-notes__meta">{{ item.meta }}</span>
|
|
32
|
+
<span class="cn-record-notes__spacer" />
|
|
33
|
+
<span
|
|
34
|
+
v-if="item.due"
|
|
35
|
+
class="cn-record-notes__due"
|
|
36
|
+
:class="`cn-record-notes__due--${dueToneOf(item.due)}`"
|
|
37
|
+
data-testid="cn-record-list-due">{{ item.due.text }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="cn-record-notes__fields">
|
|
40
|
+
<NcTextField
|
|
41
|
+
class="cn-record-notes__field"
|
|
42
|
+
:modelValue="drafts[item.key] || ''"
|
|
43
|
+
:label="fieldLabel"
|
|
44
|
+
:labelOutside="false"
|
|
45
|
+
:placeholder="placeholder"
|
|
46
|
+
:disabled="busy"
|
|
47
|
+
:data-testid="`cn-record-list-note-${item.key}`"
|
|
48
|
+
@update:modelValue="setDraft(item.key, $event)" />
|
|
49
|
+
<div v-if="leaf && leafProvider" class="cn-record-notes__leaf" :data-testid="`cn-record-list-leaf-${item.key}`">
|
|
50
|
+
<CnLeafMountHost
|
|
51
|
+
v-if="leafIsMount"
|
|
52
|
+
:provider="leafProvider"
|
|
53
|
+
:mountProps="leafProps(item.row)" />
|
|
54
|
+
<component :is="leafComponent" v-else-if="leafComponent" v-bind="leafProps(item.row)" />
|
|
55
|
+
</div>
|
|
56
|
+
<NcButton
|
|
57
|
+
variant="secondary"
|
|
58
|
+
:disabled="busy || !hasDraft(item.key)"
|
|
59
|
+
:data-testid="`cn-record-list-record-${item.key}`"
|
|
60
|
+
@click="record(item)">
|
|
61
|
+
{{ recordLabel }}
|
|
62
|
+
</NcButton>
|
|
63
|
+
</div>
|
|
64
|
+
<p
|
|
65
|
+
v-if="leafUnavailable"
|
|
66
|
+
class="cn-record-notes__leaf-missing"
|
|
67
|
+
:data-testid="`cn-record-list-leaf-missing-${item.key}`">
|
|
68
|
+
{{ leafUnavailableText }}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
<p v-if="error"
|
|
72
|
+
class="cn-record-notes__error"
|
|
73
|
+
role="alert"
|
|
74
|
+
data-testid="cn-record-list-notes-error">
|
|
75
|
+
{{ error }}
|
|
76
|
+
</p>
|
|
77
|
+
<div v-if="recordAll" class="cn-record-notes__all" data-testid="cn-record-list-record-all">
|
|
78
|
+
<div class="cn-record-notes__all-text">
|
|
79
|
+
<h3 v-if="recordAll.title" class="cn-record-notes__all-title">
|
|
80
|
+
{{ recordAll.title }}
|
|
81
|
+
</h3>
|
|
82
|
+
<p v-if="recordAll.text" class="cn-record-notes__all-sentence">
|
|
83
|
+
{{ recordAll.text }}
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="cn-record-notes__all-actions">
|
|
87
|
+
<NcButton v-if="recordAll.cancel" variant="secondary" :to="recordAll.cancel.to">
|
|
88
|
+
{{ recordAll.cancel.label }}
|
|
89
|
+
</NcButton>
|
|
90
|
+
<NcButton
|
|
91
|
+
variant="primary"
|
|
92
|
+
:disabled="busy || readyCount === 0"
|
|
93
|
+
data-testid="cn-record-list-record-all-button"
|
|
94
|
+
@click="recordEvery">
|
|
95
|
+
<template v-if="recordAll.icon" #icon>
|
|
96
|
+
<CnIcon :name="recordAll.icon" :size="20" />
|
|
97
|
+
</template>
|
|
98
|
+
{{ recordAll.label }}
|
|
99
|
+
</NcButton>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<script>
|
|
106
|
+
import { emit } from '@nextcloud/event-bus'
|
|
107
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
108
|
+
import { NcButton, NcTextField } from '@nextcloud/vue'
|
|
109
|
+
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
110
|
+
import CnLeafMountHost from '../CnLeafMountHost/CnLeafMountHost.vue'
|
|
111
|
+
import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
|
|
112
|
+
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
113
|
+
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
114
|
+
import { prefixUrl } from '../../utils/headers.js'
|
|
115
|
+
import { readPath } from '../../utils/readPath.js'
|
|
116
|
+
|
|
117
|
+
/** The bus channel every endpoint-bound widget on the page refetches on. */
|
|
118
|
+
const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The rows of a record list drawn as notes to record (`presentation:
|
|
122
|
+
* "notes"`, DqDagAfsluiten "Nog open van vandaag"): per row its pill, title,
|
|
123
|
+
* meta and due text, a field for what happened, an optional integration leaf
|
|
124
|
+
* (humaniq's hours) and a button that records the note on the row's own
|
|
125
|
+
* notes endpoint. What is typed and not yet recorded stays in this browser
|
|
126
|
+
* (session storage) and is counted as the "ready to record" figure; one
|
|
127
|
+
* button records every filled row. After a note is recorded the page's
|
|
128
|
+
* endpoint widgets refetch, so the row moves to whatever lists the server
|
|
129
|
+
* keeps of recorded work.
|
|
130
|
+
*
|
|
131
|
+
* @spec openspec/changes/record-list-notes/specs/record-list-notes/spec.md#requirement-a-record-list-can-collect-and-record-a-note-per-row
|
|
132
|
+
*/
|
|
133
|
+
export default {
|
|
134
|
+
name: 'CnRecordNoteRows',
|
|
135
|
+
|
|
136
|
+
components: { CnIcon, CnLeafMountHost, CnStatusBadge, NcButton, NcTextField },
|
|
137
|
+
|
|
138
|
+
props: {
|
|
139
|
+
/** The list's rows as the record list builds them (`key`, `row`, `tag`, `attrs`, `title`, `meta`, `pill`, `due`). */
|
|
140
|
+
items: {
|
|
141
|
+
type: Array,
|
|
142
|
+
default: () => [],
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `noteAction`, already translated: `{ urlField?, url?, bodyField?,
|
|
147
|
+
* label, fieldLabel, placeholder?, readyLabel, draftsKey?, errorText? }`.
|
|
148
|
+
*/
|
|
149
|
+
action: {
|
|
150
|
+
type: Object,
|
|
151
|
+
required: true,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** `recordAll`, already translated: `{ label, title?, text?, icon?, cancel?: { label, to } }`, or null. */
|
|
155
|
+
recordAll: {
|
|
156
|
+
type: Object,
|
|
157
|
+
default: null,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/** `rowLeaf`: `{ id, app?, register?, objectIdField, schemaField?, schema?, unavailableText? }`, or null. */
|
|
161
|
+
leaf: {
|
|
162
|
+
type: Object,
|
|
163
|
+
default: null,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
setup() {
|
|
168
|
+
const { getById } = useIntegrationRegistry()
|
|
169
|
+
return { getProvider: getById }
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
data() {
|
|
173
|
+
return { drafts: this.readDrafts(), busy: false, error: '' }
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
computed: {
|
|
177
|
+
/** @return {string} The session storage key of this list's drafts. */
|
|
178
|
+
draftsKey() {
|
|
179
|
+
return `cn-record-list-notes:${this.action.draftsKey || 'default'}`
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/** @return {number} The rows with a note typed and not yet recorded. */
|
|
183
|
+
readyCount() {
|
|
184
|
+
return this.items.filter((item) => this.hasDraft(item.key)).length
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/** @return {string} The label of the ready figure. */
|
|
188
|
+
readyLabel() {
|
|
189
|
+
return this.action.readyLabel || t('nextcloud-vue', 'filled in, not recorded yet')
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/** @return {string} */
|
|
193
|
+
recordLabel() {
|
|
194
|
+
return this.action.label || t('nextcloud-vue', 'Record')
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** @return {string} */
|
|
198
|
+
fieldLabel() {
|
|
199
|
+
return this.action.fieldLabel || t('nextcloud-vue', 'Note')
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/** @return {string} */
|
|
203
|
+
placeholder() {
|
|
204
|
+
return this.action.placeholder || ''
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/** @return {object|null} The leaf's integration provider, when one is registered. */
|
|
208
|
+
leafProvider() {
|
|
209
|
+
return this.leaf && this.leaf.id ? (this.getProvider(this.leaf.id) || null) : null
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/** @return {boolean} Whether the leaf mounts itself (`renderMode: "mount"`). */
|
|
213
|
+
leafIsMount() {
|
|
214
|
+
const provider = this.leafProvider
|
|
215
|
+
return Boolean(provider) && provider.renderMode === 'mount' && typeof provider.mount === 'function'
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/** @return {object|null} The leaf's component, in component mode. */
|
|
219
|
+
leafComponent() {
|
|
220
|
+
const provider = this.leafProvider
|
|
221
|
+
return provider ? (provider.tab || provider.widget || null) : null
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Whether the leaf's app is installed but the leaf is not available, so
|
|
226
|
+
* the row says so instead of silently showing no field.
|
|
227
|
+
*
|
|
228
|
+
* @return {boolean}
|
|
229
|
+
*/
|
|
230
|
+
leafUnavailable() {
|
|
231
|
+
return Boolean(this.leaf && this.leaf.app && !this.leafProvider && isAppInstalled(this.leaf.app))
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/** @return {string} */
|
|
235
|
+
leafUnavailableText() {
|
|
236
|
+
return this.leaf?.unavailableText || t('nextcloud-vue', 'This field is not available here.')
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
watch: {
|
|
241
|
+
drafts: {
|
|
242
|
+
deep: true,
|
|
243
|
+
handler() {
|
|
244
|
+
this.saveDrafts()
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
methods: {
|
|
250
|
+
/**
|
|
251
|
+
* The drafts this browser session kept, by row key.
|
|
252
|
+
*
|
|
253
|
+
* @return {Object<string, string>}
|
|
254
|
+
*/
|
|
255
|
+
readDrafts() {
|
|
256
|
+
try {
|
|
257
|
+
const raw = window.sessionStorage.getItem(`cn-record-list-notes:${this.action?.draftsKey || 'default'}`)
|
|
258
|
+
const parsed = raw ? JSON.parse(raw) : {}
|
|
259
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
|
260
|
+
} catch {
|
|
261
|
+
return {}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/** @return {void} */
|
|
266
|
+
saveDrafts() {
|
|
267
|
+
try {
|
|
268
|
+
window.sessionStorage.setItem(this.draftsKey, JSON.stringify(this.drafts))
|
|
269
|
+
} catch {
|
|
270
|
+
// Without session storage the drafts live as long as the page.
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The colour of a due text: a term that ended or ends today in the
|
|
276
|
+
* error colour, one still to come in the warning colour.
|
|
277
|
+
*
|
|
278
|
+
* @param {{tone: string}} due The due text.
|
|
279
|
+
* @return {string} `error`, `warning` or `muted`.
|
|
280
|
+
*/
|
|
281
|
+
dueToneOf(due) {
|
|
282
|
+
if (due.tone === 'danger' || due.tone === 'today') {
|
|
283
|
+
return 'error'
|
|
284
|
+
}
|
|
285
|
+
return due.tone === 'upcoming' ? 'warning' : 'muted'
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {string} key The row key.
|
|
290
|
+
* @param {string} value The typed note.
|
|
291
|
+
* @return {void}
|
|
292
|
+
*/
|
|
293
|
+
setDraft(key, value) {
|
|
294
|
+
this.drafts = { ...this.drafts, [key]: String(value ?? '') }
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @param {string} key The row key.
|
|
299
|
+
* @return {boolean} Whether the row has a note typed.
|
|
300
|
+
*/
|
|
301
|
+
hasDraft(key) {
|
|
302
|
+
return String(this.drafts[key] ?? '').trim() !== ''
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The props the row's leaf gets: the row's object, register and schema.
|
|
307
|
+
*
|
|
308
|
+
* @param {object} row The row.
|
|
309
|
+
* @return {object}
|
|
310
|
+
*/
|
|
311
|
+
leafProps(row) {
|
|
312
|
+
const leaf = this.leaf || {}
|
|
313
|
+
return {
|
|
314
|
+
objectId: String(readPath(row, leaf.objectIdField || 'id') ?? ''),
|
|
315
|
+
register: leaf.register || '',
|
|
316
|
+
schema: leaf.schemaField ? String(readPath(row, leaf.schemaField) ?? '') : (leaf.schema || ''),
|
|
317
|
+
surface: 'row',
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The row's notes endpoint: the row's `urlField`, else `url` with
|
|
323
|
+
* `{field}` filled from the row.
|
|
324
|
+
*
|
|
325
|
+
* @param {object} row The row.
|
|
326
|
+
* @return {string} The URL, or ''.
|
|
327
|
+
*/
|
|
328
|
+
urlOf(row) {
|
|
329
|
+
if (this.action.urlField) {
|
|
330
|
+
return String(readPath(row, this.action.urlField) ?? '')
|
|
331
|
+
}
|
|
332
|
+
const template = String(this.action.url || '')
|
|
333
|
+
return template.replace(/\{([A-Za-z0-9_.]+)\}/g, (_, path) => encodeURIComponent(String(readPath(row, path) ?? '')))
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Record one row's note, then let the page refetch.
|
|
338
|
+
*
|
|
339
|
+
* @param {object} item The row item.
|
|
340
|
+
* @param {boolean} [refresh] Whether to refresh the page after it.
|
|
341
|
+
* @return {Promise<boolean>} Whether it was recorded.
|
|
342
|
+
*/
|
|
343
|
+
async record(item, refresh = true) {
|
|
344
|
+
const text = String(this.drafts[item.key] ?? '').trim()
|
|
345
|
+
const url = this.urlOf(item.row)
|
|
346
|
+
if (text === '' || url === '') {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
this.busy = true
|
|
350
|
+
this.error = ''
|
|
351
|
+
try {
|
|
352
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
353
|
+
await axios.post(prefixUrl(url), { [this.action.bodyField || 'message']: text })
|
|
354
|
+
const { [item.key]: done, ...rest } = this.drafts
|
|
355
|
+
this.drafts = rest
|
|
356
|
+
if (refresh) {
|
|
357
|
+
emit(PAGE_REFRESH_CHANNEL, {})
|
|
358
|
+
}
|
|
359
|
+
return true
|
|
360
|
+
} catch {
|
|
361
|
+
this.error = this.action.errorText || t('nextcloud-vue', 'The note could not be recorded. It is kept here; try again.')
|
|
362
|
+
return false
|
|
363
|
+
} finally {
|
|
364
|
+
this.busy = false
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Record every row with a note, one after another, then refresh once.
|
|
370
|
+
*
|
|
371
|
+
* @return {Promise<void>}
|
|
372
|
+
*/
|
|
373
|
+
async recordEvery() {
|
|
374
|
+
const filled = this.items.filter((item) => this.hasDraft(item.key))
|
|
375
|
+
let any = false
|
|
376
|
+
for (const item of filled) {
|
|
377
|
+
any = (await this.record(item, false)) || any
|
|
378
|
+
}
|
|
379
|
+
if (any) {
|
|
380
|
+
emit(PAGE_REFRESH_CHANNEL, {})
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
</script>
|
|
386
|
+
|
|
387
|
+
<style scoped>
|
|
388
|
+
.cn-record-notes {
|
|
389
|
+
display: flex;
|
|
390
|
+
flex-direction: column;
|
|
391
|
+
gap: 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.cn-record-notes__ready {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: baseline;
|
|
397
|
+
gap: 6px;
|
|
398
|
+
margin: 0 0 8px;
|
|
399
|
+
color: var(--color-text-maxcontrast);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.cn-record-notes__ready-figure {
|
|
403
|
+
font-weight: 700;
|
|
404
|
+
color: var(--color-main-text);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.cn-record-notes__row {
|
|
408
|
+
display: flex;
|
|
409
|
+
flex-direction: column;
|
|
410
|
+
gap: 10px;
|
|
411
|
+
padding: 14px 0;
|
|
412
|
+
border-top: 1px solid var(--color-border);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.cn-record-notes__head {
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-wrap: wrap;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 8px;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.cn-record-notes__title {
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
color: var(--color-main-text);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.cn-record-notes__title--link {
|
|
428
|
+
text-decoration: underline;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.cn-record-notes__meta {
|
|
432
|
+
color: var(--color-text-maxcontrast);
|
|
433
|
+
font-size: 13px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.cn-record-notes__spacer {
|
|
437
|
+
flex: 1 1 auto;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.cn-record-notes__due {
|
|
441
|
+
font-weight: 600;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.cn-record-notes__due--error {
|
|
445
|
+
color: var(--color-error-text);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.cn-record-notes__due--warning {
|
|
449
|
+
color: var(--color-warning-text);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.cn-record-notes__due--muted {
|
|
453
|
+
color: var(--color-text-maxcontrast);
|
|
454
|
+
font-weight: 400;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.cn-record-notes__fields {
|
|
458
|
+
display: flex;
|
|
459
|
+
flex-wrap: wrap;
|
|
460
|
+
align-items: flex-end;
|
|
461
|
+
gap: 10px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.cn-record-notes__field {
|
|
465
|
+
flex: 1 1 260px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.cn-record-notes__leaf {
|
|
469
|
+
flex: 0 0 auto;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.cn-record-notes__leaf-missing,
|
|
473
|
+
.cn-record-notes__error {
|
|
474
|
+
margin: 0;
|
|
475
|
+
color: var(--color-text-maxcontrast);
|
|
476
|
+
font-size: 13px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.cn-record-notes__error {
|
|
480
|
+
color: var(--color-error-text);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.cn-record-notes__all {
|
|
484
|
+
display: flex;
|
|
485
|
+
flex-wrap: wrap;
|
|
486
|
+
align-items: center;
|
|
487
|
+
justify-content: space-between;
|
|
488
|
+
gap: 12px;
|
|
489
|
+
margin-top: 8px;
|
|
490
|
+
padding-top: 14px;
|
|
491
|
+
border-top: 1px solid var(--color-border);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.cn-record-notes__all-title {
|
|
495
|
+
margin: 0;
|
|
496
|
+
font-size: 16px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.cn-record-notes__all-sentence {
|
|
500
|
+
margin: 4px 0 0;
|
|
501
|
+
color: var(--color-text-maxcontrast);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.cn-record-notes__all-actions {
|
|
505
|
+
display: flex;
|
|
506
|
+
gap: 8px;
|
|
507
|
+
}
|
|
508
|
+
</style>
|
|
@@ -400,13 +400,16 @@ export default {
|
|
|
400
400
|
* `layout: [{ widgetId, gridWidth }]` sets a width (of 12) per widget, as
|
|
401
401
|
* the page grid does. Widgets wrap onto the next row.
|
|
402
402
|
*
|
|
403
|
+
* A tab with `default: true` is the one the strip opens on, unless a
|
|
404
|
+
* route hash names another tab or the marked tab sits under "More".
|
|
405
|
+
*
|
|
403
406
|
* `maxVisibleTabs` caps the strip: later tabs go under "More" (in the
|
|
404
407
|
* board look: into the Actions menu, with History kept in the strip).
|
|
405
408
|
* `hideEmpty: true` moves a tab whose count is 0 there too.
|
|
406
409
|
* `moreLabel` names that menu. `variant: "segmented"` draws the strip
|
|
407
410
|
* as a pill switch (CnTabs' segmented variant); `line` is the default.
|
|
408
411
|
*
|
|
409
|
-
* @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}), overflow?: boolean, columns?: number, layout?: Array<{widgetId: string, gridWidth: number}>}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
|
|
412
|
+
* @type {{ tabs?: Array<{widgetId?: string, widgetIds?: string[], label?: string, icon?: string, count?: number, countField?: string, countFrom?: ('widget'|{register: string, schema: string, filter?: object}), overflow?: boolean, default?: boolean, columns?: number, layout?: Array<{widgetId: string, gridWidth: number}>}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}
|
|
410
413
|
*/
|
|
411
414
|
content: {
|
|
412
415
|
type: Object,
|
|
@@ -696,6 +699,7 @@ export default {
|
|
|
696
699
|
count,
|
|
697
700
|
overflow,
|
|
698
701
|
pending: visibility === 'pending',
|
|
702
|
+
opensFirst: Boolean(tab && typeof tab === 'object' && tab.default === true),
|
|
699
703
|
})
|
|
700
704
|
})
|
|
701
705
|
return resolved
|
|
@@ -881,9 +885,9 @@ export default {
|
|
|
881
885
|
},
|
|
882
886
|
|
|
883
887
|
/**
|
|
884
|
-
* Open on the
|
|
885
|
-
* when tab 0 sits under "More" the strip
|
|
886
|
-
* chose to tuck away.
|
|
888
|
+
* Open on the tab marked `default: true`, else on the first tab that is in
|
|
889
|
+
* the strip. Tab 0 is not enough: when tab 0 sits under "More" the strip
|
|
890
|
+
* would open on a tab the author chose to tuck away.
|
|
887
891
|
*
|
|
888
892
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow
|
|
889
893
|
*/
|
|
@@ -899,8 +903,9 @@ export default {
|
|
|
899
903
|
if (named >= 0) {
|
|
900
904
|
this.hashTargetId = wanted
|
|
901
905
|
}
|
|
906
|
+
const marked = this.resolvedTabs.findIndex((tab) => tab.opensFirst && !tab.overflow && !tab.pending)
|
|
902
907
|
const first = this.resolvedTabs.findIndex((tab) => !tab.overflow && !tab.pending)
|
|
903
|
-
const fallback = first >= 0 ? first : this.resolvedTabs.findIndex((tab) => !tab.overflow)
|
|
908
|
+
const fallback = marked >= 0 ? marked : first >= 0 ? first : this.resolvedTabs.findIndex((tab) => !tab.overflow)
|
|
904
909
|
if (fallback > 0) {
|
|
905
910
|
this.activeIndex = fallback
|
|
906
911
|
}
|
|
@@ -84,6 +84,7 @@ import '../CnLinkedWidget/index.js'
|
|
|
84
84
|
import '../CnDeviceFramesWidget/index.js'
|
|
85
85
|
import '../CnGroupedCardsWidget/index.js'
|
|
86
86
|
import '../CnChecklistRunWidget/index.js'
|
|
87
|
+
import '../CnChecklistWidget/index.js'
|
|
87
88
|
import '../CnMailTab/index.js'
|
|
88
89
|
import '../CnWorkingCalendarWidget/index.js'
|
|
89
90
|
import '../CnPartyActionsWidget/index.js'
|
package/src/components/index.js
CHANGED
|
@@ -243,6 +243,7 @@ export { CnLinkedWidget } from './CnLinkedWidget/index.js'
|
|
|
243
243
|
export { CnDeviceFramesWidget } from './CnDeviceFramesWidget/index.js'
|
|
244
244
|
export { CnGroupedCardsWidget } from './CnGroupedCardsWidget/index.js'
|
|
245
245
|
export { CnChecklistRunWidget } from './CnChecklistRunWidget/index.js'
|
|
246
|
+
export { CnChecklistWidget } from './CnChecklistWidget/index.js'
|
|
246
247
|
export { CnMailTab } from './CnMailTab/index.js'
|
|
247
248
|
export { CnWorkingCalendarWidget } from './CnWorkingCalendarWidget/index.js'
|
|
248
249
|
export { CnPartyActionsWidget } from './CnPartyActionsWidget/index.js'
|
|
@@ -7,9 +7,12 @@ import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
|
7
7
|
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
|
8
8
|
import { reactive } from 'vue'
|
|
9
9
|
import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
|
|
10
|
+
import Cancel from 'vue-material-design-icons/Cancel.vue'
|
|
11
|
+
import Check from 'vue-material-design-icons/Check.vue'
|
|
10
12
|
import HandBackRight from 'vue-material-design-icons/HandBackRight.vue'
|
|
11
13
|
import LightningBolt from 'vue-material-design-icons/LightningBolt.vue'
|
|
12
14
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
15
|
+
import Undo from 'vue-material-design-icons/Undo.vue'
|
|
13
16
|
import { formatRelativeDate } from '../utils/boardDate.js'
|
|
14
17
|
import { fetchReferenceObjects } from '../utils/boardReferenceColumns.js'
|
|
15
18
|
import { fillTemplate } from '../utils/fillTemplate.js'
|
|
@@ -456,6 +459,13 @@ export function taskDueLabel(task) {
|
|
|
456
459
|
: t('nextcloud-vue', 'Due in {days} days', { days: until })
|
|
457
460
|
}
|
|
458
461
|
|
|
462
|
+
/**
|
|
463
|
+
* The CMMN states in which a task is finished: no verb applies to it any more.
|
|
464
|
+
*
|
|
465
|
+
* @type {string[]}
|
|
466
|
+
*/
|
|
467
|
+
const TASK_TERMINAL_STATES = ['completed', 'terminated', 'disabled']
|
|
468
|
+
|
|
459
469
|
/**
|
|
460
470
|
* The tasks source: the caller's inbox on OpenRegister's one task store.
|
|
461
471
|
*
|
|
@@ -557,6 +567,60 @@ function tasksSource() {
|
|
|
557
567
|
*/
|
|
558
568
|
recordError: (id) => store.recordErrors?.[String(id)] ?? null,
|
|
559
569
|
|
|
570
|
+
/**
|
|
571
|
+
* The lifecycle verbs a detail page offers on this task, read off the
|
|
572
|
+
* row and never guessed at: claim while nobody holds it, hand back
|
|
573
|
+
* while somebody does, complete and cancel while it is open. A finished
|
|
574
|
+
* task offers none. Whether the viewer MAY run one is the engine's
|
|
575
|
+
* call; it refuses with a message the page shows as-is.
|
|
576
|
+
*
|
|
577
|
+
* @param {object|null} task The task on the page.
|
|
578
|
+
* @return {Array<{id: string, label: string, icon: object, menu: boolean, successMessage?: string}>} The verbs, buttons first.
|
|
579
|
+
* @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs
|
|
580
|
+
*/
|
|
581
|
+
verbs: (task) => {
|
|
582
|
+
if (!task || TASK_TERMINAL_STATES.includes(String(task.state || ''))) {
|
|
583
|
+
return []
|
|
584
|
+
}
|
|
585
|
+
const held = String(task.assignee || '').trim() !== ''
|
|
586
|
+
return [
|
|
587
|
+
{ id: 'complete', label: t('nextcloud-vue', 'Finish'), icon: Check, menu: false, successMessage: t('nextcloud-vue', 'Task completed') },
|
|
588
|
+
held
|
|
589
|
+
? { id: 'unclaim', label: t('nextcloud-vue', 'Hand back'), icon: Undo, menu: false }
|
|
590
|
+
: { id: 'claim', label: t('nextcloud-vue', 'Pick up'), icon: HandBackRight, menu: false },
|
|
591
|
+
{ id: 'cancel', label: t('nextcloud-vue', 'Cancel task'), icon: Cancel, menu: true },
|
|
592
|
+
]
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Run a verb on a task and keep the page's record in step.
|
|
597
|
+
*
|
|
598
|
+
* @param {string} id The task uuid.
|
|
599
|
+
* @param {string} verb The verb id.
|
|
600
|
+
* @param {{url?: string}} [options] An app route to post the verb to instead of the engine's.
|
|
601
|
+
* @return {Promise<{ok: boolean, record: (object|null), message: string}>} The outcome.
|
|
602
|
+
*/
|
|
603
|
+
runVerb: (id, verb, options) => store.runRecordVerb(id, verb, options || {}),
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Whether this task's checklist can still be ticked: not once it is finished.
|
|
607
|
+
*
|
|
608
|
+
* @param {object|null} task The task on the page.
|
|
609
|
+
* @return {boolean}
|
|
610
|
+
*/
|
|
611
|
+
canCheckItems: (task) => Boolean(task) && !TASK_TERMINAL_STATES.includes(String(task.state || '')),
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Tick or untick one checklist item through the engine.
|
|
615
|
+
*
|
|
616
|
+
* @param {string} id The task uuid.
|
|
617
|
+
* @param {string} itemId The item id.
|
|
618
|
+
* @param {boolean} checked The new state.
|
|
619
|
+
* @return {Promise<{ok: boolean, record: (object|null), message: string}>} The outcome.
|
|
620
|
+
* @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source
|
|
621
|
+
*/
|
|
622
|
+
checkItem: (id, itemId, checked) => store.checkRecordItem(id, itemId, checked),
|
|
623
|
+
|
|
560
624
|
/**
|
|
561
625
|
* @return {boolean} Whether a load is in flight.
|
|
562
626
|
*/
|