Skip to content

Commit 10a57f7

Browse files
committed
feat(importing): show all writable calendars in picker
Mark unsupported calendars as non-selectable. Previously they were fully hidden. Show a message about why some calendars are not selectable. Also show calendars that do not support events (and only support tasks and/or journal entries). Previously, calendars that did not support events were hidden altogether. Resolves: #2572 Signed-off-by: Oleksandr Dzhychko <hey@oleks.dev>
1 parent 26fa154 commit 10a57f7

4 files changed

Lines changed: 124 additions & 61 deletions

File tree

css/import.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
.import-modal-file-item {
2323
display: flex;
24-
padding-top: 10px;
24+
margin-top: 10px;
25+
margin-bottom: 10px;
2526

2627
&--header {
2728
font-weight: bold;
@@ -34,6 +35,10 @@
3435
&__calendar-select {
3536
flex: 1 1 0;
3637
}
38+
39+
&__calendar-disabled-hint {
40+
color: var(--color-text-maxcontrast);
41+
}
3742
}
3843
}
3944
}

src/components/AppNavigation/Settings/ImportScreenRow.vue

Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
<template>
77
<li class="import-modal-file-item">
88
<div class="import-modal-file-item__filename">
9-
{{ file.name }}
9+
<div>{{ file.name }}</div>
10+
<div
11+
v-if="disabledHint"
12+
class="import-modal-file-item__calendar-disabled-hint">
13+
{{ disabledHint }}
14+
</div>
1015
</div>
1116
<CalendarPicker
1217
class="import-modal-file-item__calendar-select"
1318
:value="calendar"
1419
:calendars="calendars"
20+
:is-calendar-selectable="isCalendarSelectable"
1521
@select-calendar="selectCalendar" />
1622
</li>
1723
</template>
@@ -39,59 +45,94 @@ export default {
3945
4046
computed: {
4147
...mapStores(usePrincipalsStore, useImportFilesStore, useCalendarsStore),
42-
calendar() {
43-
let calendarId = this.importFilesStore.importCalendarRelation[this.file.id]
44-
if (!calendarId) {
45-
this.setDefaultCalendarId()
46-
calendarId = this.importFilesStore.importCalendarRelation[this.file.id]
48+
newCalendar() {
49+
return {
50+
id: 'new',
51+
displayName: this.$t('calendar', 'New calendar'),
52+
isSharedWithMe: false,
53+
color: uidToHexColor(this.$t('calendar', 'New calendar')),
54+
owner: this.principalsStore.getCurrentUserPrincipal.url,
4755
}
56+
},
4857
49-
if (calendarId === 'new') {
50-
return {
51-
id: 'new',
52-
displayName: this.$t('calendar', 'New calendar'),
53-
isSharedWithMe: false,
54-
color: uidToHexColor(this.$t('calendar', 'New calendar')),
55-
owner: this.principalsStore.getCurrentUserPrincipal.url,
56-
}
58+
calendar() {
59+
const calendarId = this.importFilesStore.importCalendarRelation[this.file.id]
60+
if (calendarId === this.newCalendar.id) {
61+
return this.newCalendar
5762
}
58-
5963
return this.calendarsStore.getCalendarById(calendarId)
6064
},
6165
6266
calendars() {
63-
// TODO: remove once the false positive is fixed upstream
64-
65-
const calendars = this.calendarsStore.sortedCalendarFilteredByComponents(
66-
this.file.parser.containsVEvents(),
67-
this.file.parser.containsVJournals(),
68-
this.file.parser.containsVTodos(),
69-
)
67+
const existingCalendars = this.calendarsStore.sortedWritableCalendarsEvenWithoutSupportForEvents
68+
return [...existingCalendars, this.newCalendar]
69+
},
7070
71-
calendars.push({
72-
id: 'new',
73-
displayName: this.$t('calendar', 'New calendar'),
74-
isSharedWithMe: false,
75-
color: uidToHexColor(this.$t('calendar', 'New calendar')),
76-
owner: this.principalsStore.getCurrentUserPrincipal.url,
77-
})
71+
/**
72+
* Returns a hint explaining why some calendars cannot be selected.
73+
*
74+
* @return {string|undefined} A message, or undefined if all calendars can be selected.
75+
*/
76+
disabledHint() {
77+
const disalbedBecauseOfEvents = this.file.parser.containsVEvents() && this.calendars.some((calendar) => !calendar.supportsEvents)
78+
const disalbedBecauseOfTasks = this.file.parser.containsVTodos() && this.calendars.some((calendar) => !calendar.supportsTasks)
79+
const disalbedBecauseOfJournalEntries = this.file.parser.containsVJournals() && this.calendars.some((calendar) => !calendar.supportsJournals)
7880
79-
return calendars
81+
if (disalbedBecauseOfEvents && disalbedBecauseOfTasks && disalbedBecauseOfJournalEntries) {
82+
return this.$t('calendar', 'Some calendars are disabled because this file contains events, tasks and journal entries.')
83+
}
84+
if (disalbedBecauseOfEvents && disalbedBecauseOfTasks && !disalbedBecauseOfJournalEntries) {
85+
return this.$t('calendar', 'Some calendars are disabled because this file contains events and tasks.')
86+
}
87+
if (disalbedBecauseOfEvents && !disalbedBecauseOfTasks && disalbedBecauseOfJournalEntries) {
88+
return this.$t('calendar', 'Some calendars are disabled because this file contains events and journal entries.')
89+
}
90+
if (disalbedBecauseOfEvents && !disalbedBecauseOfTasks && !disalbedBecauseOfJournalEntries) {
91+
return this.$t('calendar', 'Some calendars are disabled because this file contains events.')
92+
}
93+
if (!disalbedBecauseOfEvents && disalbedBecauseOfTasks && disalbedBecauseOfJournalEntries) {
94+
return this.$t('calendar', 'Some calendars are disabled because this file contains tasks and journal entries.')
95+
}
96+
if (!disalbedBecauseOfEvents && disalbedBecauseOfTasks && !disalbedBecauseOfJournalEntries) {
97+
return this.$t('calendar', 'Some calendars are disabled because this file contains tasks.')
98+
}
99+
if (!disalbedBecauseOfEvents && !disalbedBecauseOfTasks && disalbedBecauseOfJournalEntries) {
100+
return this.$t('calendar', 'Some calendars are disabled because this file contains journal entries.')
101+
}
102+
return undefined
80103
},
81104
},
82105
106+
created() {
107+
const preselectedCalendar = this.calendars.find((calendar) => this.isCalendarSelectable(calendar))
108+
if (!preselectedCalendar) {
109+
// If no other calendar is selectable, at least `this.newCalendar` should be selectable and be preselected.
110+
throw new Error('Encountered illegal state. At least one calendar that can be selected should exist.')
111+
}
112+
this.selectCalendar(preselectedCalendar)
113+
},
114+
83115
methods: {
84-
selectCalendar(newCalendar) {
85-
this.importFilesStore.setCalendarForFileId({
86-
fileId: this.file.id,
87-
calendarId: newCalendar.id,
88-
})
116+
isCalendarSelectable(calendar) {
117+
if (calendar.id === this.newCalendar.id) {
118+
return true
119+
}
120+
if (this.file.parser.containsVEvents() && !calendar.supportsEvents) {
121+
return false
122+
}
123+
if (this.file.parser.containsVTodos() && !calendar.supportsTasks) {
124+
return false
125+
}
126+
if (this.file.parser.containsVJournals() && !calendar.supportsJournals) {
127+
return false
128+
}
129+
return true
89130
},
90131
91-
setDefaultCalendarId() {
132+
selectCalendar(newCalendar) {
92133
this.importFilesStore.setCalendarForFileId({
93134
fileId: this.file.id,
94-
calendarId: this.calendars[0].id,
135+
calendarId: newCalendar.id,
95136
})
96137
},
97138
},

src/components/Shared/CalendarPicker.vue

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
:filter-by="selectFilterBy"
1515
:input-label="inputLabel"
1616
:label-outside="inputLabel === ''"
17+
:selectable="selectable"
1718
@option:selected="change"
1819
@option:deselected="remove">
1920
<template #option="{ id }">
@@ -87,6 +88,19 @@ export default {
8788
type: String,
8889
default: '',
8990
},
91+
92+
/**
93+
* Decides whether a calendar is selectable or not.
94+
* Non-selectable calendars are displayed but cannot be selected.
95+
*
96+
* @type {Function}
97+
* @param {object} calendar
98+
* @return {boolean}
99+
*/
100+
isCalendarSelectable: {
101+
type: Function,
102+
default: (calendar) => true,
103+
},
90104
},
91105
92106
computed: {
@@ -162,6 +176,17 @@ export default {
162176
selectFilterBy(option, label, search) {
163177
return option.displayName.toLowerCase().indexOf(search) !== -1
164178
},
179+
180+
/**
181+
* Decide whether the given option can be selected
182+
*
183+
* @param {object} option The calendar option
184+
* @return {boolean} True if the option can be selected
185+
*/
186+
selectable(option) {
187+
const calendar = this.getCalendarById(option.id)
188+
return this.isCalendarSelectable(calendar)
189+
},
165190
},
166191
}
167192
</script>

src/store/calendars.js

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ export default defineStore('calendars', {
9090
.sort((a, b) => a.order - b.order)
9191
},
9292

93+
/**
94+
* List of sorted writable calendars.
95+
*
96+
* Even including ones without support for events.
97+
* Those are usually excluded by all other getters.
98+
*
99+
* @param {object} state the store data
100+
* @return {Array}
101+
*/
102+
sortedWritableCalendarsEvenWithoutSupportForEvents(state) {
103+
return state.calendars
104+
.filter((calendar) => !calendar.readOnly)
105+
.sort((a, b) => a.order - b.order)
106+
},
107+
93108
/**
94109
* List of sorted calendars owned by the principal
95110
*
@@ -221,29 +236,6 @@ export default defineStore('calendars', {
221236
return null
222237
},
223238

224-
/**
225-
* @return {function({Boolean}, {Boolean}, {Boolean}): {Object}[]}
226-
*/
227-
sortedCalendarFilteredByComponents() {
228-
return (vevent, vjournal, vtodo) => {
229-
return this.sortedCalendars.filter((calendar) => {
230-
if (vevent && !calendar.supportsEvents) {
231-
return false
232-
}
233-
234-
if (vjournal && !calendar.supportsJournals) {
235-
return false
236-
}
237-
238-
if (vtodo && !calendar.supportsTasks) {
239-
return false
240-
}
241-
242-
return true
243-
})
244-
}
245-
},
246-
247239
/**
248240
* Get the current sync token of a calendar or undefined it the calendar is not present
249241
*

0 commit comments

Comments
 (0)