Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion css/import.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

.import-modal-file-item {
display: flex;
padding-top: 10px;
margin-top: calc(var(--default-grid-baseline) * 2);
margin-bottom: calc(var(--default-grid-baseline) * 2);

&--header {
font-weight: bold;
Expand All @@ -34,6 +35,10 @@
&__calendar-select {
flex: 1 1 0;
}

&__calendar-disabled-hint {
color: var(--color-text-maxcontrast);
}
}
}
}
110 changes: 74 additions & 36 deletions src/components/AppNavigation/Settings/ImportScreenRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@
<template>
<li class="import-modal-file-item">
<div class="import-modal-file-item__filename">
{{ file.name }}
<div>{{ file.name }}</div>
<div
Comment thread
odzhychko marked this conversation as resolved.
v-if="disabledHint"
class="import-modal-file-item__calendar-disabled-hint">
{{ disabledHint }}
</div>
</div>
<CalendarPicker
class="import-modal-file-item__calendar-select"
:value="calendar"
:calendars="calendars"
:isCalendarSelectable="isCalendarSelectable"
@selectCalendar="selectCalendar" />
</li>
</template>

<script>
import { getLanguage } from '@nextcloud/l10n'
import { mapStores } from 'pinia'
import CalendarPicker from '../../Shared/CalendarPicker.vue'
import useCalendarsStore from '../../../store/calendars.js'
Expand All @@ -39,59 +46,90 @@ export default {

computed: {
...mapStores(usePrincipalsStore, useImportFilesStore, useCalendarsStore),
calendar() {
let calendarId = this.importFilesStore.importCalendarRelation[this.file.id]
if (!calendarId) {
this.setDefaultCalendarId()
calendarId = this.importFilesStore.importCalendarRelation[this.file.id]
newCalendar() {
return {
id: 'new',
displayName: this.$t('calendar', 'New calendar'),
isSharedWithMe: false,
color: uidToHexColor(this.$t('calendar', 'New calendar')),
owner: this.principalsStore.getCurrentUserPrincipal.url,
}
},

if (calendarId === 'new') {
return {
id: 'new',
displayName: this.$t('calendar', 'New calendar'),
isSharedWithMe: false,
color: uidToHexColor(this.$t('calendar', 'New calendar')),
owner: this.principalsStore.getCurrentUserPrincipal.url,
}
calendar() {
const calendarId = this.importFilesStore.importCalendarRelation[this.file.id]
if (calendarId === this.newCalendar.id) {
return this.newCalendar
}

return this.calendarsStore.getCalendarById(calendarId)
},

calendars() {
// TODO: remove once the false positive is fixed upstream
const existingCalendars = this.calendarsStore.sortedWritableCalendarsEvenWithoutSupportForEvents
return [...existingCalendars, this.newCalendar]
},

const calendars = this.calendarsStore.sortedCalendarFilteredByComponents(
this.file.parser.containsVEvents(),
this.file.parser.containsVJournals(),
this.file.parser.containsVTodos(),
)
/**
* Returns a hint explaining why some calendars cannot be selected.
*
* @return {string|undefined} A message, or undefined if all calendars can be selected.
*/
disabledHint() {
const disalbedBecauseOfEvents = this.file.parser.containsVEvents() && this.calendars.some((calendar) => !calendar.supportsEvents)
const disalbedBecauseOfTasks = this.file.parser.containsVTodos() && this.calendars.some((calendar) => !calendar.supportsTasks)
const disalbedBecauseOfJournalEntries = this.file.parser.containsVJournals() && this.calendars.some((calendar) => !calendar.supportsJournals)

calendars.push({
id: 'new',
displayName: this.$t('calendar', 'New calendar'),
isSharedWithMe: false,
color: uidToHexColor(this.$t('calendar', 'New calendar')),
owner: this.principalsStore.getCurrentUserPrincipal.url,
})
const disablingTypes = []
if (disalbedBecauseOfEvents) {
disablingTypes.push(this.$t('calendar', 'events'))
}
if (disalbedBecauseOfTasks) {
disablingTypes.push(this.$t('calendar', 'tasks'))
}
if (disalbedBecauseOfJournalEntries) {
disablingTypes.push(this.$t('calendar', 'journal entries'))
}

return calendars
if (disablingTypes.lenght === 0) {
return undefined
}

const formatter = new Intl.ListFormat(getLanguage(), { type: 'conjunction' })
const localizedTypes = formatter.format(disablingTypes)
return this.$t('calendar', 'Some calendars are disabled because this file contains {types}.', { types: localizedTypes })
},
},

created() {
const preselectedCalendar = this.calendars.find((calendar) => this.isCalendarSelectable(calendar))
if (!preselectedCalendar) {
// If no other calendar is selectable, at least `this.newCalendar` should be selectable and be preselected.
throw new Error('Encountered illegal state. At least one calendar that can be selected should exist.')
}
this.selectCalendar(preselectedCalendar)
},

methods: {
selectCalendar(newCalendar) {
this.importFilesStore.setCalendarForFileId({
fileId: this.file.id,
calendarId: newCalendar.id,
})
isCalendarSelectable(calendar) {
if (calendar.id === this.newCalendar.id) {
return true
}
if (this.file.parser.containsVEvents() && !calendar.supportsEvents) {
return false
}
if (this.file.parser.containsVTodos() && !calendar.supportsTasks) {
return false
}
if (this.file.parser.containsVJournals() && !calendar.supportsJournals) {
return false
}
return true
},

setDefaultCalendarId() {
selectCalendar(newCalendar) {
this.importFilesStore.setCalendarForFileId({
fileId: this.file.id,
calendarId: this.calendars[0].id,
calendarId: newCalendar.id,
})
},
},
Expand Down
24 changes: 24 additions & 0 deletions src/components/Shared/CalendarPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
:filterBy="selectFilterBy"
:inputLabel="inputLabel"
:labelOutside="inputLabel === ''"
:selectable="selectable"
@update:modelValue="handleSelectionUpdate">
<template #option="{ id }">
<CalendarPickerOption
Expand Down Expand Up @@ -87,6 +88,18 @@ export default {
type: String,
default: '',
},

/**
* Decides whether a calendar is selectable or not.
* Non-selectable calendars are displayed but cannot be selected.
*
* @param {object} calendar
* @return {boolean}
*/
isCalendarSelectable: {
type: Function,
default: (calendar) => true,
},
},

computed: {
Expand Down Expand Up @@ -177,6 +190,17 @@ export default {
selectFilterBy(option, label, search) {
return option.displayName.toLowerCase().indexOf(search) !== -1
},

/**
* Decide whether the given option can be selected
*
* @param {object} option The calendar option
* @return {boolean} True if the option can be selected
*/
selectable(option) {
const calendar = this.getCalendarById(option.id)
return this.isCalendarSelectable(calendar)
},
},
}
</script>
Expand Down
38 changes: 15 additions & 23 deletions src/store/calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ export default defineStore('calendars', {
.sort((a, b) => a.order - b.order)
},

/**
* List of sorted writable calendars.
*
* Even including ones without support for events.
Comment thread
odzhychko marked this conversation as resolved.
* Those are usually excluded by all other getters.
*
* @param {object} state the store data
* @return {Array}
*/
sortedWritableCalendarsEvenWithoutSupportForEvents(state) {
Comment thread
odzhychko marked this conversation as resolved.
return state.calendars
.filter((calendar) => !calendar.readOnly)
.sort((a, b) => a.order - b.order)
},

/**
* List of sorted calendars owned by the principal
*
Expand Down Expand Up @@ -221,29 +236,6 @@ export default defineStore('calendars', {
return null
},

/**
* @return {function({Boolean}, {Boolean}, {Boolean}): {Object}[]}
*/
sortedCalendarFilteredByComponents() {
return (vevent, vjournal, vtodo) => {
return this.sortedCalendars.filter((calendar) => {
if (vevent && !calendar.supportsEvents) {
return false
}

if (vjournal && !calendar.supportsJournals) {
return false
}

if (vtodo && !calendar.supportsTasks) {
return false
}

return true
})
}
},

/**
* Get the current sync token of a calendar or undefined it the calendar is not present
*
Expand Down
34 changes: 32 additions & 2 deletions tests/javascript/unit/store/calendars.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,41 @@
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import useCalendarsStore from '../../../../src/store/calendars.js'

import { setActivePinia, createPinia } from 'pinia'

describe('store/calendars test suite', () => {

beforeEach(() => {
setActivePinia(createPinia())
})

it('should provide a getter for all writable calendars sorted', () => {
const calendarsStore = useCalendarsStore()
const calendarOrderLast = {
id: "1",
order: 2,
supportsEvents: false,
supportsJournals: true
}
const calendarReadOnly = {
id: "2",
readOnly: true,
supportsEvents: true,
}
const calendarOrderFirst = {
id: "3",
order: 1,
supportsEvents: true,
supportsJournals: false
}
calendarsStore.addCalendarMutation({ calendar: calendarOrderLast })
calendarsStore.addCalendarMutation({ calendar: calendarReadOnly })
calendarsStore.addCalendarMutation({ calendar: calendarOrderFirst })

it('should be true', () => {
expect(true).toEqual(true)
writableCalendars = calendarsStore.sortedWritableCalendarsEvenWithoutSupportForEvents
expect(writableCalendars).toMatchObject([calendarOrderFirst, calendarOrderLast])
})

})