Skip to content

Commit 3a7732d

Browse files
committed
Display recurrence summary
1 parent 23bfb0e commit 3a7732d

File tree

10 files changed

+1040
-12
lines changed

10 files changed

+1040
-12
lines changed

package-lock.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@nextcloud/router": "^2.1.2",
4040
"@nextcloud/vue": "^7.12.6",
4141
"@vueuse/components": "^10.4.1",
42+
"calendar-js": "^1.4.4",
4243
"color-convert": "^2.0.1",
4344
"debounce": "^1.2.1",
4445
"ical.js": "^1.5.0",

src/components/AppSidebar/RepeatItem.vue

+14-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Nextcloud - Tasks
33

44
@author Sunik Kupfer
55
@copyright 2023 Sunik Kupfer <mail@sunik.de>
6+
@author Raimund Schlüßler
7+
@copyright 2018 Raimund Schlüßler <[email protected]>
68

79
This library is free software; you can redistribute it and/or
810
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -24,21 +26,24 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
2426
<div class="repeat__icon">
2527
<component :is="icon" :size="20" />
2628
</div>
27-
{{ recurrence }}
29+
<RepeatSummary class="property-repeat__summary__content"
30+
:recurrence-rule="recurrence" />
2831
</div>
2932
</template>
3033

3134
<script>
3235

3336
import { translate as t } from '@nextcloud/l10n'
37+
import RepeatSummary from './RepeatItem/RepeatSummary.vue'
3438

3539
export default {
3640
components: {
41+
RepeatSummary,
3742
},
3843
props: {
3944
recurrence: {
40-
type: String,
41-
default: null,
45+
type: Object,
46+
required: true,
4247
},
4348
disabled: {
4449
type: Boolean,
@@ -74,5 +79,11 @@ export default {
7479
flex-basis: 44px;
7580
flex-shrink: 0;
7681
}
82+
83+
.property-repeat__summary__content {
84+
display: flex;
85+
align-items: center;
86+
margin-bottom: 5px;
87+
}
7788
}
7889
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!--
2+
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
3+
-
4+
- @author Georg Ehrke <[email protected]>
5+
- @author Richard Steinmetz <[email protected]>
6+
-
7+
- @license AGPL-3.0-or-later
8+
-
9+
- This program is free software: you can redistribute it and/or modify
10+
- it under the terms of the GNU Affero General Public License as
11+
- published by the Free Software Foundation, either version 3 of the
12+
- License, or (at your option) any later version.
13+
-
14+
- This program is distributed in the hope that it will be useful,
15+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
- GNU Affero General Public License for more details.
18+
-
19+
- You should have received a copy of the GNU Affero General Public License
20+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
-
22+
-->
23+
24+
<template>
25+
<span v-if="display">
26+
{{ recurrenceRule | formatRecurrenceRule }}
27+
</span>
28+
<span v-else>
29+
{{ t('calendar', 'No recurrence') }}
30+
</span>
31+
</template>
32+
33+
<script>
34+
import { translate as t } from '@nextcloud/l10n'
35+
import formatRecurrenceRule from '../../../filters/recurrenceRuleFormat.js'
36+
37+
export default {
38+
name: 'RepeatSummary',
39+
filters: {
40+
formatRecurrenceRule,
41+
},
42+
props: {
43+
/**
44+
* The recurrence-rule object as defined on the eventComponent
45+
*/
46+
recurrenceRule: {
47+
type: Object,
48+
required: true,
49+
},
50+
},
51+
computed: {
52+
/**
53+
* Returns whether to display the summary.
54+
* We do not want to show it if it doesn't repeat
55+
*
56+
* @return {boolean}
57+
*/
58+
display() {
59+
return this.recurrenceRule.frequency !== 'NONE'
60+
},
61+
},
62+
methods: {
63+
t,
64+
},
65+
}
66+
</script>

src/filters/recurrenceRuleFormat.js

+230
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/**
2+
* @copyright Copyright (c) 2019 Georg Ehrke
3+
*
4+
* @author Georg Ehrke <[email protected]>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
import { translate as t, translatePlural as n, getDayNames, getMonthNames } from '@nextcloud/l10n'
23+
import moment from '@nextcloud/moment'
24+
25+
/**
26+
* Formats a recurrence-rule
27+
*
28+
* @param {object} recurrenceRule The recurrence-rule to format
29+
* @return {string}
30+
*/
31+
export default (recurrenceRule) => {
32+
if (recurrenceRule.frequency === 'NONE') {
33+
return t('calendar', 'Does not repeat')
34+
}
35+
36+
let freqPart = ''
37+
if (recurrenceRule.interval === 1) {
38+
switch (recurrenceRule.frequency) {
39+
case 'DAILY':
40+
freqPart = t('calendar', 'Daily')
41+
break
42+
43+
case 'WEEKLY':
44+
freqPart = t('calendar', 'Weekly')
45+
break
46+
47+
case 'MONTHLY':
48+
freqPart = t('calendar', 'Monthly')
49+
break
50+
51+
case 'YEARLY':
52+
freqPart = t('calendar', 'Yearly')
53+
break
54+
}
55+
} else {
56+
switch (recurrenceRule.frequency) {
57+
case 'DAILY':
58+
freqPart = n('calendar', 'Every %n day', 'Every %n days', recurrenceRule.interval)
59+
break
60+
61+
case 'WEEKLY':
62+
freqPart = n('calendar', 'Every %n week', 'Every %n weeks', recurrenceRule.interval)
63+
break
64+
65+
case 'MONTHLY':
66+
freqPart = n('calendar', 'Every %n month', 'Every %n months', recurrenceRule.interval)
67+
break
68+
69+
case 'YEARLY':
70+
freqPart = n('calendar', 'Every %n year', 'Every %n years', recurrenceRule.interval)
71+
break
72+
}
73+
}
74+
75+
let limitPart = ''
76+
if (recurrenceRule.frequency === 'WEEKLY' && recurrenceRule.byDay.length !== 0) {
77+
const formattedDays = getTranslatedByDaySet(recurrenceRule.byDay)
78+
79+
limitPart = n('calendar', 'on {weekday}', 'on {weekdays}', recurrenceRule.byDay.length, {
80+
weekday: formattedDays,
81+
weekdays: formattedDays,
82+
})
83+
} else if (recurrenceRule.frequency === 'MONTHLY') {
84+
if (recurrenceRule.byMonthDay.length !== 0) {
85+
const dayOfMonthList = recurrenceRule.byMonthDay.join(', ')
86+
87+
limitPart = n('calendar', 'on day {dayOfMonthList}', 'on days {dayOfMonthList}', recurrenceRule.byMonthDay.length, {
88+
dayOfMonthList,
89+
})
90+
} else {
91+
const ordinalNumber = getTranslatedOrdinalNumber(recurrenceRule.bySetPosition)
92+
const byDaySet = getTranslatedByDaySet(recurrenceRule.byDay)
93+
94+
limitPart = t('calendar', 'on the {ordinalNumber} {byDaySet}', {
95+
ordinalNumber,
96+
byDaySet,
97+
})
98+
}
99+
} else if (recurrenceRule.frequency === 'YEARLY') {
100+
const monthNames = getTranslatedMonths(recurrenceRule.byMonth)
101+
102+
if (recurrenceRule.byDay.length === 0) {
103+
limitPart = t('calendar', 'in {monthNames}', {
104+
monthNames,
105+
})
106+
} else {
107+
const ordinalNumber = getTranslatedOrdinalNumber(recurrenceRule.bySetPosition)
108+
const byDaySet = getTranslatedByDaySet(recurrenceRule.byDay)
109+
110+
limitPart = t('calendar', 'in {monthNames} on the {ordinalNumber} {byDaySet}', {
111+
monthNames,
112+
ordinalNumber,
113+
byDaySet,
114+
})
115+
}
116+
}
117+
118+
let endPart = ''
119+
if (recurrenceRule.until !== null) {
120+
const untilDate = moment(recurrenceRule.until).format('L')
121+
122+
endPart = t('calendar', 'until {untilDate}', {
123+
untilDate,
124+
})
125+
} else if (recurrenceRule.count !== null) {
126+
endPart = n('calendar', '%n time', '%n times', recurrenceRule.count)
127+
}
128+
129+
return [
130+
freqPart,
131+
limitPart,
132+
endPart,
133+
].join(' ').replace(/\s{2,}/g, ' ').trim()
134+
}
135+
136+
/**
137+
* Gets the byDay list as formatted list of translated weekdays
138+
*
139+
* @param {string[]} byDayList The by-day-list to get formatted
140+
* @return {string}
141+
*/
142+
function getTranslatedByDaySet(byDayList) {
143+
const byDayNames = []
144+
const allByDayNames = getDayNames()
145+
146+
// TODO: This should be sorted by first day of week
147+
// TODO: This should summarise:
148+
// - SA, SU to weekend
149+
// - MO, TU, WE, TH, FR to weekday
150+
// - MO, TU, WE, TH, FR, SA, SU to day
151+
152+
if (byDayList.includes('MO')) {
153+
byDayNames.push(allByDayNames[1])
154+
}
155+
if (byDayList.includes('TU')) {
156+
byDayNames.push(allByDayNames[2])
157+
}
158+
if (byDayList.includes('WE')) {
159+
byDayNames.push(allByDayNames[3])
160+
}
161+
if (byDayList.includes('TH')) {
162+
byDayNames.push(allByDayNames[4])
163+
}
164+
if (byDayList.includes('FR')) {
165+
byDayNames.push(allByDayNames[5])
166+
}
167+
if (byDayList.includes('SA')) {
168+
byDayNames.push(allByDayNames[6])
169+
}
170+
if (byDayList.includes('SU')) {
171+
byDayNames.push(allByDayNames[0])
172+
}
173+
174+
return byDayNames.join(', ')
175+
}
176+
177+
/**
178+
* Gets the byMonth list as formatted list of translated month-names
179+
*
180+
*
181+
* @param {string[]} byMonthList The by-month list to get formatted
182+
* @return {string}
183+
*/
184+
function getTranslatedMonths(byMonthList) {
185+
const sortedByMonth = byMonthList.slice().map((n) => parseInt(n, 10))
186+
sortedByMonth.sort((a, b) => a - b)
187+
188+
const monthNames = []
189+
const allMonthNames = getMonthNames()
190+
191+
for (const month of sortedByMonth) {
192+
monthNames.push(allMonthNames[month - 1])
193+
}
194+
195+
return monthNames.join(', ')
196+
}
197+
198+
/**
199+
* Gets the translated ordinal number for by-set-position
200+
*
201+
* @param {number} bySetPositionNum The by-set-position number to get the translation of
202+
* @return {string}
203+
*/
204+
function getTranslatedOrdinalNumber(bySetPositionNum) {
205+
switch (bySetPositionNum) {
206+
case 1:
207+
return t('calendar', 'first')
208+
209+
case 2:
210+
return t('calendar', 'second')
211+
212+
case 3:
213+
return t('calendar', 'third')
214+
215+
case 4:
216+
return t('calendar', 'fourth')
217+
218+
case 5:
219+
return t('calendar', 'fifth')
220+
221+
case -2:
222+
return t('calendar', 'second to last')
223+
224+
case -1:
225+
return t('calendar', 'last')
226+
227+
default:
228+
return ''
229+
}
230+
}

0 commit comments

Comments
 (0)