@conduction/nextcloud-vue 2.143.0 → 2.145.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/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +7 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue.js +1 -1
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js +9 -0
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue2.js.map +1 -1
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js +24 -13
- package/dist/esm/components/CnEventsCard/CnEventsCard.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +2 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +9 -4
- 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/CnMailTab/CnMailTab.vue.js +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
- package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +3 -0
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +19 -14
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +30 -0
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +158 -102
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +12 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +110 -2
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +146 -80
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +35 -9
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue.js +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js +45 -0
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js +67 -34
- package/dist/esm/integrations/builtin/calendar/CnCalendarCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue.js +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js +3 -22
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/CnCalendarTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/calendar/eventStatus.js +67 -0
- package/dist/esm/integrations/builtin/calendar/eventStatus.js.map +1 -0
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +39 -0
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +81 -32
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue.js +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js +5 -6
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/deckCardParts.js +59 -0
- package/dist/esm/integrations/builtin/deck/deckCardParts.js.map +1 -0
- package/dist/esm/l10n/en.json.js +6 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +8 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1505 -637
- package/dist/esm/utils/builtInFormatters.js +18 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +5 -2
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/localHeaderRows.js +95 -0
- package/dist/esm/utils/localHeaderRows.js.map +1 -0
- package/dist/esm/utils/nextcloudDirectory.js +19 -6
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1848 -924
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1505 -637
- package/l10n/en.json +6 -0
- package/l10n/nl.json +8 -0
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +36 -4
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +2 -1
- package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
- package/src/components/CnMailTab/CnMailTab.vue +6 -4
- package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
- package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +4 -1
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +131 -20
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
- package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
- package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
- package/src/composables/indexSources.js +35 -9
- package/src/css/dialog.css +25 -10
- package/src/css/kpi-card.css +3 -0
- package/src/css/look-board-detail.css +513 -0
- package/src/css/look-board-index.css +52 -4
- package/src/css/look-board.css +163 -8
- package/src/css/table.css +8 -1
- package/src/integrations/builtin/calendar/CnCalendarCard.vue +76 -11
- package/src/integrations/builtin/calendar/CnCalendarTab.vue +3 -22
- package/src/integrations/builtin/calendar/__tests__/CnCalendarCardBoardRows.spec.js +52 -0
- package/src/integrations/builtin/calendar/eventStatus.js +63 -0
- package/src/integrations/builtin/deck/CnDeckCard.vue +66 -0
- package/src/integrations/builtin/deck/CnDeckTab.vue +5 -6
- package/src/integrations/builtin/deck/__tests__/CnDeckCardBoardRows.spec.js +49 -0
- package/src/integrations/builtin/deck/deckCardParts.js +56 -0
- package/src/utils/builtInFormatters.js +17 -0
- package/src/utils/fetchFilterCounts.js +5 -2
- package/src/utils/localHeaderRows.js +91 -0
- package/src/utils/nextcloudDirectory.js +19 -6
|
@@ -93,6 +93,33 @@
|
|
|
93
93
|
<div v-else-if="cards.length === 0" class="cn-deck-card__empty">
|
|
94
94
|
{{ emptyLabel }}
|
|
95
95
|
</div>
|
|
96
|
+
<!-- The board look draws each linked card as a ruled row: its title,
|
|
97
|
+
one meta line (board, list, assignees) and its first label at
|
|
98
|
+
the end, then a link to Deck. -->
|
|
99
|
+
<div v-else-if="isBoard" class="cn-deck-card__rows">
|
|
100
|
+
<ul class="cn-deck-card__row-list">
|
|
101
|
+
<li v-for="card in cards"
|
|
102
|
+
:key="cardKey(card)"
|
|
103
|
+
class="cn-deck-card__row"
|
|
104
|
+
data-testid="cn-deck-card-row">
|
|
105
|
+
<span class="cn-deck-card__row-main">
|
|
106
|
+
<a class="cn-deck-card__row-title"
|
|
107
|
+
:href="cardUrl(card)"
|
|
108
|
+
target="_blank"
|
|
109
|
+
rel="noopener">{{ cardTitle(card) }}</a>
|
|
110
|
+
<span class="cn-deck-card__row-meta" data-testid="cn-deck-card-meta">{{ boardMetaOf(card) }}</span>
|
|
111
|
+
</span>
|
|
112
|
+
<span v-if="firstLabel(card)"
|
|
113
|
+
class="cn-status-badge cn-status-badge--purple"
|
|
114
|
+
data-testid="cn-deck-card-label">{{ firstLabel(card) }}</span>
|
|
115
|
+
</li>
|
|
116
|
+
</ul>
|
|
117
|
+
<a class="cn-deck-card__open"
|
|
118
|
+
:href="deckAppUrl"
|
|
119
|
+
target="_blank"
|
|
120
|
+
rel="noopener"
|
|
121
|
+
data-testid="cn-deck-card-open">{{ openDeckLabel }}</a>
|
|
122
|
+
</div>
|
|
96
123
|
<div v-else class="cn-deck-card__kanban">
|
|
97
124
|
<section
|
|
98
125
|
v-for="stack in miniColumns"
|
|
@@ -146,7 +173,9 @@ import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
|
146
173
|
import CnDeckCardCreate from '../../../components/CnDeckCardCreate/CnDeckCardCreate.vue'
|
|
147
174
|
import CnDeckCardPicker from '../../../components/CnDeckCardPicker/CnDeckCardPicker.vue'
|
|
148
175
|
import CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'
|
|
176
|
+
import { normalizeLook } from '../../../composables/useLook.js'
|
|
149
177
|
import { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'
|
|
178
|
+
import { assigneeDisplayName, cardAssignees, cardBoardTitle, cardLabels, cardStackTitle } from './deckCardParts.js'
|
|
150
179
|
|
|
151
180
|
const VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']
|
|
152
181
|
|
|
@@ -161,6 +190,10 @@ export default {
|
|
|
161
190
|
|
|
162
191
|
components: { CnDeckCardCreate, CnDeckCardPicker, CnDetailCard, LinkVariant, NcButton, NcLoadingIcon, Plus, ViewColumnOutline },
|
|
163
192
|
|
|
193
|
+
inject: {
|
|
194
|
+
cnLook: { default: 'nextcloud' },
|
|
195
|
+
},
|
|
196
|
+
|
|
164
197
|
props: {
|
|
165
198
|
/** Stable integration id (forwarded from the registry — always `'deck'`). */
|
|
166
199
|
integrationId: { type: String, default: 'deck' },
|
|
@@ -203,6 +236,8 @@ export default {
|
|
|
203
236
|
linkLabel: { type: String, default: () => t('nextcloud-vue', 'Link an existing card') },
|
|
204
237
|
/** Label of the `create` button. */
|
|
205
238
|
createLabel: { type: String, default: () => t('nextcloud-vue', 'New card') },
|
|
239
|
+
/** Label of the link to Deck under the rows (board look). */
|
|
240
|
+
openDeckLabel: { type: String, default: () => t('nextcloud-vue', 'Open Deck') },
|
|
206
241
|
},
|
|
207
242
|
|
|
208
243
|
data() {
|
|
@@ -231,6 +266,11 @@ export default {
|
|
|
231
266
|
return this.headerActions.filter((action, index, all) => ['create', 'link'].includes(action) && all.indexOf(action) === index)
|
|
232
267
|
},
|
|
233
268
|
|
|
269
|
+
/** @return {boolean} The board look draws ruled rows instead of the mini-kanban. */
|
|
270
|
+
isBoard() {
|
|
271
|
+
return normalizeLook(this.cnLook) === 'board'
|
|
272
|
+
},
|
|
273
|
+
|
|
234
274
|
cardHeaderTitle() {
|
|
235
275
|
return this.title || this.integrationId
|
|
236
276
|
},
|
|
@@ -325,6 +365,32 @@ export default {
|
|
|
325
365
|
},
|
|
326
366
|
|
|
327
367
|
methods: {
|
|
368
|
+
/**
|
|
369
|
+
* A card's meta line in the board look: its board, its list and its
|
|
370
|
+
* assignees, separated by middots.
|
|
371
|
+
*
|
|
372
|
+
* @param {object} card The card row.
|
|
373
|
+
* @return {string}
|
|
374
|
+
*/
|
|
375
|
+
boardMetaOf(card) {
|
|
376
|
+
const board = cardBoardTitle(card)
|
|
377
|
+
const stack = cardStackTitle(card)
|
|
378
|
+
return [
|
|
379
|
+
board ? t('nextcloud-vue', 'Board {name}', { name: board }) : '',
|
|
380
|
+
stack ? t('nextcloud-vue', 'list {name}', { name: stack }) : '',
|
|
381
|
+
...cardAssignees(card).map(assigneeDisplayName),
|
|
382
|
+
].filter(Boolean).join(' · ')
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {object} card The card row.
|
|
387
|
+
* @return {string} Its first label's title, or ''.
|
|
388
|
+
*/
|
|
389
|
+
firstLabel(card) {
|
|
390
|
+
const label = cardLabels(card)[0]
|
|
391
|
+
return label ? String(label.title ?? label.name ?? '') : ''
|
|
392
|
+
},
|
|
393
|
+
|
|
328
394
|
/** @return {string} The object's Deck endpoint the link and create flows post to. */
|
|
329
395
|
deckEndpoint() {
|
|
330
396
|
return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/deck`
|
|
@@ -169,6 +169,7 @@ import CnDeckCardPicker from '../../../components/CnDeckCardPicker/CnDeckCardPic
|
|
|
169
169
|
import CnStatusBadge from '../../../components/CnStatusBadge/CnStatusBadge.vue'
|
|
170
170
|
import { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'
|
|
171
171
|
import { userLocale } from '../../../utils/userLocale.js'
|
|
172
|
+
import { cardAssignees, cardBoardTitle, cardLabels, cardStackTitle } from './deckCardParts.js'
|
|
172
173
|
|
|
173
174
|
/** Maximum assignee avatars shown before collapsing the rest into a +N overflow badge. */
|
|
174
175
|
const MAX_VISIBLE_ASSIGNEES = 3
|
|
@@ -365,8 +366,7 @@ export default {
|
|
|
365
366
|
},
|
|
366
367
|
|
|
367
368
|
cardLabels(card) {
|
|
368
|
-
|
|
369
|
-
return Array.isArray(labels) ? labels : []
|
|
369
|
+
return cardLabels(card)
|
|
370
370
|
},
|
|
371
371
|
|
|
372
372
|
chipText(label) {
|
|
@@ -451,8 +451,8 @@ export default {
|
|
|
451
451
|
* @return {string} board (or board · stack) label, or '' when unknown
|
|
452
452
|
*/
|
|
453
453
|
boardBadge(card) {
|
|
454
|
-
const board = card
|
|
455
|
-
const stack = card
|
|
454
|
+
const board = cardBoardTitle(card)
|
|
455
|
+
const stack = cardStackTitle(card)
|
|
456
456
|
if (board && stack) {
|
|
457
457
|
return `${board} · ${stack}`
|
|
458
458
|
}
|
|
@@ -468,8 +468,7 @@ export default {
|
|
|
468
468
|
* @return {Array} assignee entries
|
|
469
469
|
*/
|
|
470
470
|
cardAssignees(card) {
|
|
471
|
-
|
|
472
|
-
return Array.isArray(raw) ? raw : []
|
|
471
|
+
return cardAssignees(card)
|
|
473
472
|
},
|
|
474
473
|
|
|
475
474
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CnDeckCard in the board look on a detail page (DqZaakTaken "Kaarten"):
|
|
3
|
+
* each linked card is a ruled row with its title, one meta line (board,
|
|
4
|
+
* list, assignees) and its first label at the end, with "Open Deck" under
|
|
5
|
+
* the rows, instead of the mini-kanban.
|
|
6
|
+
*
|
|
7
|
+
* @spec openspec/changes/board-deck-calendar-rows/specs/integrations/spec.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { flushPromises, mount } = require('@vue/test-utils')
|
|
11
|
+
const CnDeckCard = require('../CnDeckCard.vue').default
|
|
12
|
+
|
|
13
|
+
const CARD = { cardId: 7, boardId: 2, cardTitle: 'Lakken offerte lantaarnpalen', boardTitle: 'Woo en informatie', stackTitle: 'Deze week', assignedUsers: [{ participant: { uid: 'lars', displayname: 'Lars de Koning' } }], labels: [{ id: 1, title: 'Bezig' }] }
|
|
14
|
+
|
|
15
|
+
function mountCard(look) {
|
|
16
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({ results: [CARD] }) })
|
|
17
|
+
return mount(CnDeckCard, {
|
|
18
|
+
props: { register: 'reg', schema: 'case', objectId: 'c-1', surface: 'detail-page' },
|
|
19
|
+
global: { provide: { cnLook: look } },
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('CnDeckCard board rows', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
delete global.fetch
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('draws the linked card as a row with board, list and assignee, its label at the end, and Open Deck', async () => {
|
|
29
|
+
const w = mountCard('board')
|
|
30
|
+
await flushPromises()
|
|
31
|
+
expect(w.find('.cn-deck-card__kanban').exists()).toBe(false)
|
|
32
|
+
const row = w.find('[data-testid="cn-deck-card-row"]')
|
|
33
|
+
expect(row.text()).toContain('Lakken offerte lantaarnpalen')
|
|
34
|
+
const meta = row.find('[data-testid="cn-deck-card-meta"]').text()
|
|
35
|
+
expect(meta).toContain('Woo en informatie')
|
|
36
|
+
expect(meta).toContain('Deze week')
|
|
37
|
+
expect(meta).toContain('Lars de Koning')
|
|
38
|
+
expect(row.find('[data-testid="cn-deck-card-label"]').text()).toBe('Bezig')
|
|
39
|
+
expect(w.find('[data-testid="cn-deck-card-open"]').attributes('href')).toBe('/index.php/apps/deck')
|
|
40
|
+
w.unmount()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('keeps the mini-kanban in the Nextcloud look', async () => {
|
|
44
|
+
const w = mountCard('nextcloud')
|
|
45
|
+
await flushPromises()
|
|
46
|
+
expect(w.find('.cn-deck-card__kanban').exists()).toBe(true)
|
|
47
|
+
w.unmount()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The parts of a linked Deck card a row shows, shared by the Deck tab and
|
|
6
|
+
* the Deck card: its board, its stack, its assignees and its labels.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} card The card row.
|
|
11
|
+
* @return {string} The board's title, or ''.
|
|
12
|
+
*/
|
|
13
|
+
export function cardBoardTitle(card) {
|
|
14
|
+
return card?.boardTitle ?? card?.board?.title ?? ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} card The card row.
|
|
19
|
+
* @return {string} The stack's title, or ''.
|
|
20
|
+
*/
|
|
21
|
+
export function cardStackTitle(card) {
|
|
22
|
+
return card?.stackTitle ?? card?.stack?.title ?? ''
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deck's `assignedUsers` (each with a nested `participant`), or a plain
|
|
27
|
+
* `assignees` / `users` list of strings or objects.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} card The card row.
|
|
30
|
+
* @return {Array} The assignee entries.
|
|
31
|
+
*/
|
|
32
|
+
export function cardAssignees(card) {
|
|
33
|
+
const raw = card?.assignedUsers ?? card?.assignees ?? card?.users ?? []
|
|
34
|
+
return Array.isArray(raw) ? raw : []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object|string} assignee An assignee entry.
|
|
39
|
+
* @return {string} Its display name, or ''.
|
|
40
|
+
*/
|
|
41
|
+
export function assigneeDisplayName(assignee) {
|
|
42
|
+
if (typeof assignee === 'string') {
|
|
43
|
+
return assignee
|
|
44
|
+
}
|
|
45
|
+
const p = assignee?.participant ?? assignee ?? {}
|
|
46
|
+
return p.displayname ?? p.displayName ?? p.uid ?? p.id ?? ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} card The card row.
|
|
51
|
+
* @return {Array<object>} Its labels.
|
|
52
|
+
*/
|
|
53
|
+
export function cardLabels(card) {
|
|
54
|
+
const labels = card?.labels ?? card?.tags ?? []
|
|
55
|
+
return Array.isArray(labels) ? labels : []
|
|
56
|
+
}
|
|
@@ -2,6 +2,7 @@ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
|
2
2
|
import { formatRelativeDate } from './boardDate.js'
|
|
3
3
|
import { formatMoney } from './formatMetric.js'
|
|
4
4
|
import { parseDuration } from './isoDuration.js'
|
|
5
|
+
import { userDisplayName } from './userDisplayName.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @file Built-in formatters merged into the `cnFormatters` registry
|
|
@@ -523,6 +524,21 @@ export function formatIsoDuration(value) {
|
|
|
523
524
|
return words[period.unit] ? words[period.unit]() : String(value)
|
|
524
525
|
}
|
|
525
526
|
|
|
527
|
+
/**
|
|
528
|
+
* A Nextcloud user id as its display name (`userName` registry key): the id
|
|
529
|
+
* while the name is looked up, and for a user nobody answers for.
|
|
530
|
+
*
|
|
531
|
+
* @spec openspec/changes/user-name-formatter/specs/user-name-formatter/spec.md#requirement-a-user-id-can-read-as-its-display-name
|
|
532
|
+
* @param {unknown} value The user id.
|
|
533
|
+
* @return {string} The display name, the id, or ''.
|
|
534
|
+
*/
|
|
535
|
+
export function formatUserName(value) {
|
|
536
|
+
if (value === null || value === undefined || value === '' || typeof value === 'object') {
|
|
537
|
+
return ''
|
|
538
|
+
}
|
|
539
|
+
return userDisplayName(value) || String(value)
|
|
540
|
+
}
|
|
541
|
+
|
|
526
542
|
export const BUILT_IN_FORMATTERS = {
|
|
527
543
|
date: formatDate,
|
|
528
544
|
datetime: formatDateTime,
|
|
@@ -538,4 +554,5 @@ export const BUILT_IN_FORMATTERS = {
|
|
|
538
554
|
masked: formatMasked,
|
|
539
555
|
isoDuration: formatIsoDuration,
|
|
540
556
|
valueMap: formatValueMap,
|
|
557
|
+
userName: formatUserName,
|
|
541
558
|
}
|
|
@@ -176,10 +176,13 @@ export function singleEqualityOf(filter) {
|
|
|
176
176
|
* @param {Array<{key: (string|number), filter: object}>} options.entries The filters to count, each under its own key.
|
|
177
177
|
* @param {object} [options.baseFilter] A filter every count is narrowed by (the page's fixed filter).
|
|
178
178
|
* @param {object} [options.ctx] Token-resolution context for the filters.
|
|
179
|
+
* @param {string} [options.via] `'list'` counts every entry through the list endpoint, the request a list sends, so a
|
|
180
|
+
* count beside a list cannot disagree with it; else the aggregation endpoints where they apply.
|
|
179
181
|
* @return {Promise<{[key: string]: number}>} The count per entry key. An entry whose count failed is left out.
|
|
180
182
|
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-counts-on-filters-and-views
|
|
183
|
+
* @spec openspec/changes/index-counts-read-the-list-endpoint/specs/index-page/spec.md#requirement-an-index-pages-counts-come-from-the-list-request
|
|
181
184
|
*/
|
|
182
|
-
export async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx } = {}) {
|
|
185
|
+
export async function fetchFilterCounts({ register, schema, entries, baseFilter, ctx, via = '' } = {}) {
|
|
183
186
|
const counts = {}
|
|
184
187
|
if (!register || !schema || !Array.isArray(entries) || entries.length === 0) {
|
|
185
188
|
return counts
|
|
@@ -191,7 +194,7 @@ export async function fetchFilterCounts({ register, schema, entries, baseFilter,
|
|
|
191
194
|
// and `/value` returns exactly its number.
|
|
192
195
|
// A list of values anywhere in a request rules the aggregation endpoints
|
|
193
196
|
// out for it. A base filter with one rules them out for every entry.
|
|
194
|
-
const baseHasList = needsListEndpoint(base)
|
|
197
|
+
const baseHasList = via === 'list' || needsListEndpoint(base)
|
|
195
198
|
const byField = {}
|
|
196
199
|
const single = []
|
|
197
200
|
const viaList = []
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Header sort and filter applied to rows the table already holds (an
|
|
6
|
+
* `endpointSource` table): the same `{ field, dir }` sort and
|
|
7
|
+
* `{ paramKey: values[] }` filter map a self-fetching table sends to the
|
|
8
|
+
* server, read here in the browser.
|
|
9
|
+
*
|
|
10
|
+
* @module utils/localHeaderRows
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CONTAINS_OPERATOR } from './columnFilters.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A row value as one comparable scalar: an array counts as its joined items.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} value The raw value.
|
|
19
|
+
* @return {string|number|null} The value to compare.
|
|
20
|
+
*/
|
|
21
|
+
function scalar(value) {
|
|
22
|
+
if (value === null || value === undefined) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value.map((item) => (item && typeof item === 'object' ? (item.title || item.name || '') : String(item))).join(', ')
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
29
|
+
return typeof value === 'boolean' ? String(value) : value
|
|
30
|
+
}
|
|
31
|
+
return typeof value === 'object' ? (value.title || value.name || '') : String(value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a row holds a value the filter entry asks for.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} row The row.
|
|
38
|
+
* @param {string} param The filter key (`field`, `field[like]`, `field[gte]`, `field[lte]`).
|
|
39
|
+
* @param {Array<string>} values The chosen values.
|
|
40
|
+
* @return {boolean} True when the row passes.
|
|
41
|
+
*/
|
|
42
|
+
function passes(row, param, values) {
|
|
43
|
+
const match = /^(.+)\[(\w+)\]$/.exec(param)
|
|
44
|
+
const field = match ? match[1] : param
|
|
45
|
+
const op = match ? match[2] : ''
|
|
46
|
+
const value = scalar(row[field])
|
|
47
|
+
if (op === CONTAINS_OPERATOR) {
|
|
48
|
+
const term = String(values[0]).toLowerCase()
|
|
49
|
+
return value !== null && String(value).toLowerCase().includes(term)
|
|
50
|
+
}
|
|
51
|
+
if (op === 'gte' || op === 'lte') {
|
|
52
|
+
if (value === null) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
const left = typeof value === 'number' ? value : String(value)
|
|
56
|
+
const right = typeof value === 'number' ? Number(values[0]) : String(values[0])
|
|
57
|
+
return op === 'gte' ? left >= right : left <= right
|
|
58
|
+
}
|
|
59
|
+
return value !== null && values.map(String).includes(String(value))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The rows that pass every active header filter, in the header sort's order.
|
|
64
|
+
*
|
|
65
|
+
* @param {Array<object>} rows The rows.
|
|
66
|
+
* @param {{field: string, dir: string}|null} sort The header sort.
|
|
67
|
+
* @param {object} filters The header filters, `{ paramKey: values[] }`.
|
|
68
|
+
* @return {Array<object>} The rows to show (the same array when nothing applies).
|
|
69
|
+
*/
|
|
70
|
+
export function localHeaderRows(rows, sort, filters) {
|
|
71
|
+
if (!Array.isArray(rows)) {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
const active = Object.entries(filters || {}).filter(([, values]) => Array.isArray(values) && values.length > 0)
|
|
75
|
+
let out = active.length > 0 ? rows.filter((row) => row && active.every(([param, values]) => passes(row, param, values))) : rows
|
|
76
|
+
if (sort && sort.field) {
|
|
77
|
+
const sign = sort.dir === 'desc' ? -1 : 1
|
|
78
|
+
out = [...out].sort((a, b) => {
|
|
79
|
+
const left = scalar(a && a[sort.field])
|
|
80
|
+
const right = scalar(b && b[sort.field])
|
|
81
|
+
if (left === null || right === null) {
|
|
82
|
+
return left === right ? 0 : (left === null ? 1 : -1)
|
|
83
|
+
}
|
|
84
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
85
|
+
return sign * (left - right)
|
|
86
|
+
}
|
|
87
|
+
return sign * String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' })
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
@@ -26,20 +26,33 @@ function ocsData(res) {
|
|
|
26
26
|
return (res && res.data && res.data.ocs && res.data.ocs.data) || null
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** The most groups read, so a server that ignores the offset cannot loop forever. */
|
|
30
|
+
const MAX_GROUPS = 5000
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* The groups, with their member counts.
|
|
31
34
|
*
|
|
32
35
|
* @param {object} axios The axios instance.
|
|
33
|
-
* @param {{search?: string, limit?: number}} [options] Narrowing.
|
|
36
|
+
* @param {{search?: string, limit?: number}} [options] Narrowing; `limit` is the page size.
|
|
34
37
|
* @return {Promise<Array<{id: string, name: string, count: number}>>}
|
|
35
38
|
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
36
39
|
*/
|
|
37
40
|
export async function fetchGroups(axios, options = {}) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const limit = options.limit || 200
|
|
42
|
+
// Every page, while a page comes back full: one page cut an instance with
|
|
43
|
+
// more groups than the limit off before its last ones (DqAfdelingen).
|
|
44
|
+
const groups = []
|
|
45
|
+
for (let offset = 0; offset < MAX_GROUPS; offset += limit) {
|
|
46
|
+
const res = await axios.get(generateOcsUrl('cloud/groups/details'), {
|
|
47
|
+
params: { search: options.search || '', limit, offset, format: 'json' },
|
|
48
|
+
headers: OCS_HEADERS,
|
|
49
|
+
})
|
|
50
|
+
const batch = (ocsData(res) && ocsData(res).groups) || []
|
|
51
|
+
groups.push(...batch)
|
|
52
|
+
if (batch.length < limit) {
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
return groups.map((group) => ({
|
|
44
57
|
id: String(group.id),
|
|
45
58
|
name: group.displayname || String(group.id),
|