@conduction/nextcloud-vue 2.143.0 → 2.144.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.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/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/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 +3 -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/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/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/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/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/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 +6 -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 +815 -363
- package/dist/esm/utils/fetchFilterCounts.js +5 -2
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/nextcloudDirectory.js +19 -6
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1347 -758
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +815 -363
- package/l10n/en.json +6 -0
- package/l10n/nl.json +6 -0
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +28 -3
- package/src/components/CnConversationThread/CnConversationThread.vue +5 -2
- package/src/components/CnEventsCard/CnEventsCard.vue +27 -5
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +15 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +3 -1
- package/src/components/CnNotesCard/CnNotesCard.vue +8 -0
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +114 -46
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +26 -1
- package/src/components/CnTasksWidget/CnTasksWidget.vue +170 -13
- package/src/css/dialog.css +7 -1
- package/src/css/kpi-card.css +3 -0
- package/src/css/look-board-detail.css +317 -0
- package/src/css/look-board-index.css +14 -0
- package/src/css/look-board.css +25 -0
- 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/fetchFilterCounts.js +5 -2
- package/src/utils/nextcloudDirectory.js +19 -6
|
@@ -83,22 +83,42 @@
|
|
|
83
83
|
v-for="ev in displayedEvents"
|
|
84
84
|
:key="rowKey(ev)"
|
|
85
85
|
class="cn-calendar-card__row">
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<div class="cn-calendar-card__meta">
|
|
93
|
-
<span v-if="ev.dtstart" class="cn-calendar-card__when">{{ formatShort(ev.dtstart) }}</span>
|
|
94
|
-
<span v-if="ev.location" class="cn-calendar-card__location">· {{ ev.location }}</span>
|
|
86
|
+
<!-- The board look draws a meeting as a ruled row: its title, one
|
|
87
|
+
meta line (when, where, who) and its status at the end. -->
|
|
88
|
+
<template v-if="boardRows">
|
|
89
|
+
<div class="cn-calendar-card__row-main">
|
|
90
|
+
<span class="cn-calendar-card__title">{{ ev.summary || untitledLabel }}</span>
|
|
91
|
+
<span class="cn-calendar-card__meta" data-testid="cn-calendar-card-meta">{{ boardMetaOf(ev) }}</span>
|
|
95
92
|
</div>
|
|
96
|
-
|
|
93
|
+
<span v-if="statusOf(ev)"
|
|
94
|
+
class="cn-status-badge"
|
|
95
|
+
:class="`cn-status-badge--${statusVariantOf(ev)}`"
|
|
96
|
+
data-testid="cn-calendar-card-status">{{ statusOf(ev) }}</span>
|
|
97
|
+
</template>
|
|
98
|
+
<template v-else>
|
|
99
|
+
<div class="cn-calendar-card__row-icon">
|
|
100
|
+
<CalendarClock v-if="isUpcoming(ev)" :size="20" class="cn-calendar-card__icon--upcoming" />
|
|
101
|
+
<CalendarCheck v-else :size="20" class="cn-calendar-card__icon--past" />
|
|
102
|
+
</div>
|
|
103
|
+
<div class="cn-calendar-card__row-main">
|
|
104
|
+
<span class="cn-calendar-card__title">{{ ev.summary || untitledLabel }}</span>
|
|
105
|
+
<div class="cn-calendar-card__meta">
|
|
106
|
+
<span v-if="ev.dtstart" class="cn-calendar-card__when">{{ formatShort(ev.dtstart) }}</span>
|
|
107
|
+
<span v-if="ev.location" class="cn-calendar-card__location">· {{ ev.location }}</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
97
111
|
</div>
|
|
112
|
+
<a v-if="boardRows"
|
|
113
|
+
class="cn-calendar-card__open"
|
|
114
|
+
:href="calendarAppUrl"
|
|
115
|
+
target="_blank"
|
|
116
|
+
rel="noopener"
|
|
117
|
+
data-testid="cn-calendar-card-open">{{ openInCalendarLabel }}</a>
|
|
98
118
|
</div>
|
|
99
119
|
|
|
100
120
|
<!-- Footer: detail surface gets "Open in NC Calendar" CTA -->
|
|
101
|
-
<template v-if="surface === 'detail-page'" #footer>
|
|
121
|
+
<template v-if="surface === 'detail-page' && !isBoard" #footer>
|
|
102
122
|
<a
|
|
103
123
|
class="cn-calendar-card__cta"
|
|
104
124
|
:href="calendarAppUrl"
|
|
@@ -144,9 +164,12 @@ import CnCalendarEventCreate from '../../../components/CnCalendarEventCreate/CnC
|
|
|
144
164
|
import CnCalendarEventPicker from '../../../components/CnCalendarEventPicker/CnCalendarEventPicker.vue'
|
|
145
165
|
import CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'
|
|
146
166
|
import CnIcon from '../../../components/CnIcon/CnIcon.vue'
|
|
167
|
+
import { normalizeLook } from '../../../composables/useLook.js'
|
|
168
|
+
import { formatBoardDate, formatTimeOfDay } from '../../../utils/boardDate.js'
|
|
147
169
|
import { buildHeaders, prefixUrl } from '../../../utils/index.js'
|
|
148
170
|
import { userLocale } from '../../../utils/userLocale.js'
|
|
149
171
|
import { linkCalendarEvent } from './calendarEventLink.js'
|
|
172
|
+
import { eventAttendeeNames, eventStatusLabel, eventStatusVariant } from './eventStatus.js'
|
|
150
173
|
|
|
151
174
|
/**
|
|
152
175
|
* CnCalendarCard — surface-aware compact widget for the calendar
|
|
@@ -158,6 +181,10 @@ export default {
|
|
|
158
181
|
|
|
159
182
|
components: { CnCalendarEventCreate, CnCalendarEventPicker, CnDetailCard, CnIcon, LinkVariant, NcButton, NcLoadingIcon, Plus, CalendarClock, CalendarCheck },
|
|
160
183
|
|
|
184
|
+
inject: {
|
|
185
|
+
cnLook: { default: 'nextcloud' },
|
|
186
|
+
},
|
|
187
|
+
|
|
161
188
|
props: {
|
|
162
189
|
/** OpenRegister register id (slug or uuid). Optional for user-dashboard. */
|
|
163
190
|
register: { type: String, default: '' },
|
|
@@ -233,6 +260,16 @@ export default {
|
|
|
233
260
|
*
|
|
234
261
|
* @return {Array<string>}
|
|
235
262
|
*/
|
|
263
|
+
/** @return {boolean} The board look draws ruled rows. */
|
|
264
|
+
isBoard() {
|
|
265
|
+
return normalizeLook(this.cnLook) === 'board'
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/** @return {boolean} The meetings are ruled rows: the board look on a detail page. */
|
|
269
|
+
boardRows() {
|
|
270
|
+
return this.isBoard && this.surface === 'detail-page'
|
|
271
|
+
},
|
|
272
|
+
|
|
236
273
|
shownHeaderActions() {
|
|
237
274
|
if (this.surface !== 'detail-page' || !this.objectId || !Array.isArray(this.headerActions)) {
|
|
238
275
|
return []
|
|
@@ -292,6 +329,34 @@ export default {
|
|
|
292
329
|
},
|
|
293
330
|
|
|
294
331
|
methods: {
|
|
332
|
+
/**
|
|
333
|
+
* A meeting's meta line in the board look: "14 okt, 10:00 tot 10:30",
|
|
334
|
+
* its place and its attendees, separated by middots.
|
|
335
|
+
*
|
|
336
|
+
* @param {object} ev The event.
|
|
337
|
+
* @return {string}
|
|
338
|
+
*/
|
|
339
|
+
boardMetaOf(ev) {
|
|
340
|
+
const start = ev.dtstart ? new Date(ev.dtstart) : null
|
|
341
|
+
const end = ev.dtend ? new Date(ev.dtend) : null
|
|
342
|
+
let when = ''
|
|
343
|
+
if (start && !Number.isNaN(start.getTime())) {
|
|
344
|
+
when = `${formatBoardDate(start)}, ${formatTimeOfDay(start)}`
|
|
345
|
+
if (end && !Number.isNaN(end.getTime())) {
|
|
346
|
+
when = t('nextcloud-vue', '{start} until {end}', { start: when, end: formatTimeOfDay(end) })
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return [when, ev.location, ...eventAttendeeNames(ev)].filter(Boolean).join(' · ')
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
statusOf(ev) {
|
|
353
|
+
return eventStatusLabel(ev)
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
statusVariantOf(ev) {
|
|
357
|
+
return eventStatusVariant(ev)
|
|
358
|
+
},
|
|
359
|
+
|
|
295
360
|
/**
|
|
296
361
|
* A meeting was made on this object: close the form, read the list again.
|
|
297
362
|
*
|
|
@@ -242,6 +242,7 @@ import CnStatusBadge from '../../../components/CnStatusBadge/CnStatusBadge.vue'
|
|
|
242
242
|
import { buildHeaders, prefixUrl } from '../../../utils/index.js'
|
|
243
243
|
import { userLocale } from '../../../utils/userLocale.js'
|
|
244
244
|
import { linkCalendarEvent } from './calendarEventLink.js'
|
|
245
|
+
import { eventStatusLabel, eventStatusVariant } from './eventStatus.js'
|
|
245
246
|
|
|
246
247
|
const MAX_AVATARS = 3
|
|
247
248
|
|
|
@@ -542,31 +543,11 @@ export default {
|
|
|
542
543
|
},
|
|
543
544
|
|
|
544
545
|
statusLabel(ev) {
|
|
545
|
-
|
|
546
|
-
if (status === 'confirmed') {
|
|
547
|
-
return t('nextcloud-vue', 'Confirmed')
|
|
548
|
-
}
|
|
549
|
-
if (status === 'tentative') {
|
|
550
|
-
return t('nextcloud-vue', 'Tentative')
|
|
551
|
-
}
|
|
552
|
-
if (status === 'cancelled') {
|
|
553
|
-
return t('nextcloud-vue', 'Cancelled')
|
|
554
|
-
}
|
|
555
|
-
return ''
|
|
546
|
+
return eventStatusLabel(ev)
|
|
556
547
|
},
|
|
557
548
|
|
|
558
549
|
statusVariant(ev) {
|
|
559
|
-
|
|
560
|
-
if (status === 'confirmed') {
|
|
561
|
-
return 'success'
|
|
562
|
-
}
|
|
563
|
-
if (status === 'tentative') {
|
|
564
|
-
return 'warning'
|
|
565
|
-
}
|
|
566
|
-
if (status === 'cancelled') {
|
|
567
|
-
return 'error'
|
|
568
|
-
}
|
|
569
|
-
return 'default'
|
|
550
|
+
return eventStatusVariant(ev)
|
|
570
551
|
},
|
|
571
552
|
|
|
572
553
|
attendeeList(ev) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CnCalendarCard in the board look on a detail page (DqZaakTaken
|
|
3
|
+
* "Afspraken"): each meeting is a ruled row with its title, one meta line
|
|
4
|
+
* (when, where, who) and its status at the end, and "Open in Calendar" is a
|
|
5
|
+
* link under the rows rather than a card footer.
|
|
6
|
+
*
|
|
7
|
+
* @spec openspec/changes/board-deck-calendar-rows/specs/integrations/spec.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { flushPromises, mount } = require('@vue/test-utils')
|
|
11
|
+
|
|
12
|
+
jest.mock('@nextcloud/router', () => ({ generateUrl: (path) => path }))
|
|
13
|
+
|
|
14
|
+
const CnCalendarCard = require('../CnCalendarCard.vue').default
|
|
15
|
+
|
|
16
|
+
const EVENT = { id: 'e1', summary: 'Toelichting stukken', dtstart: '2026-10-14T10:00:00', dtend: '2026-10-14T10:30:00', location: 'stadskantoor, balie 3', attendees: [{ name: 'Sanne de Vries' }], status: 'TENTATIVE' }
|
|
17
|
+
|
|
18
|
+
function mountCard(look) {
|
|
19
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ results: [EVENT] }) })
|
|
20
|
+
return mount(CnCalendarCard, {
|
|
21
|
+
props: { register: 'reg', schema: 'case', objectId: 'c-1', surface: 'detail-page' },
|
|
22
|
+
global: { provide: { cnLook: look }, stubs: { NcLoadingIcon: true } },
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('CnCalendarCard board rows', () => {
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
delete global.fetch
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('draws the meeting as a row with its meta line and status, and the link under the rows', async () => {
|
|
32
|
+
const w = mountCard('board')
|
|
33
|
+
await flushPromises()
|
|
34
|
+
const row = w.find('.cn-calendar-card__row')
|
|
35
|
+
expect(row.find('.cn-calendar-card__row-icon').exists()).toBe(false)
|
|
36
|
+
const meta = row.find('[data-testid="cn-calendar-card-meta"]').text()
|
|
37
|
+
expect(meta).toContain('stadskantoor, balie 3')
|
|
38
|
+
expect(meta).toContain('Sanne de Vries')
|
|
39
|
+
expect(row.find('[data-testid="cn-calendar-card-status"]').text()).toBe('Tentative')
|
|
40
|
+
expect(w.find('[data-testid="cn-calendar-card-open"]').exists()).toBe(true)
|
|
41
|
+
expect(w.find('.cn-detail-card__footer').exists()).toBe(false)
|
|
42
|
+
w.unmount()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('keeps the icon rows and the footer in the Nextcloud look', async () => {
|
|
46
|
+
const w = mountCard('nextcloud')
|
|
47
|
+
await flushPromises()
|
|
48
|
+
expect(w.find('.cn-calendar-card__row-icon').exists()).toBe(true)
|
|
49
|
+
expect(w.find('.cn-detail-card__footer').exists()).toBe(true)
|
|
50
|
+
w.unmount()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* A calendar event's status as a label and a badge tone, shared by the
|
|
6
|
+
* calendar tab and the calendar card.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The event's status in words, or '' when it has none the reader needs.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ev The event.
|
|
15
|
+
* @return {string}
|
|
16
|
+
*/
|
|
17
|
+
export function eventStatusLabel(ev) {
|
|
18
|
+
const status = String(ev?.status || '').toLowerCase()
|
|
19
|
+
if (status === 'confirmed') {
|
|
20
|
+
return t('nextcloud-vue', 'Confirmed')
|
|
21
|
+
}
|
|
22
|
+
if (status === 'tentative') {
|
|
23
|
+
return t('nextcloud-vue', 'Tentative')
|
|
24
|
+
}
|
|
25
|
+
if (status === 'cancelled') {
|
|
26
|
+
return t('nextcloud-vue', 'Cancelled')
|
|
27
|
+
}
|
|
28
|
+
return ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The badge tone of the event's status.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} ev The event.
|
|
35
|
+
* @return {string} `success`, `warning`, `error` or `default`.
|
|
36
|
+
*/
|
|
37
|
+
export function eventStatusVariant(ev) {
|
|
38
|
+
const status = String(ev?.status || '').toLowerCase()
|
|
39
|
+
if (status === 'confirmed') {
|
|
40
|
+
return 'success'
|
|
41
|
+
}
|
|
42
|
+
if (status === 'tentative') {
|
|
43
|
+
return 'warning'
|
|
44
|
+
}
|
|
45
|
+
if (status === 'cancelled') {
|
|
46
|
+
return 'error'
|
|
47
|
+
}
|
|
48
|
+
return 'default'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The names of the event's attendees.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} ev The event.
|
|
55
|
+
* @return {Array<string>}
|
|
56
|
+
*/
|
|
57
|
+
export function eventAttendeeNames(ev) {
|
|
58
|
+
const raw = ev?.attendees || ev?.participants || []
|
|
59
|
+
if (!Array.isArray(raw)) {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
return raw.map((att) => (typeof att === 'string' ? att : (att.name || att.displayName || att.cn || att.email || ''))).filter(Boolean)
|
|
63
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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 = []
|
|
@@ -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),
|