Merge calendar and subscription list

Signed-off-by: Georg Ehrke <developer@georgehrke.com>

Alternative design with calendar-icon

Signed-off-by: Georg Ehrke <developer@georgehrke.com>

Move new calendar to bottom of list

Signed-off-by: Georg Ehrke <developer@georgehrke.com>

Reset new calendar menu on close

Signed-off-by: Georg Ehrke <developer@georgehrke.com>

use actions instead of counter

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
Georg Ehrke 2019-10-21 13:34:53 +02:00
parent a203bd6150
commit 578d3b36a4
No known key found for this signature in database
GPG Key ID: 9D98FD9380A1CB43
8 changed files with 275 additions and 223 deletions

View File

@ -206,6 +206,27 @@
}
}
.app-navigation-entry-new-calendar {
.app-navigation-entry__title {
color: var(--color-text-maxcontrast) !important;
}
&:hover,
&--open {
.app-navigation-entry__title {
color: var(--color-text-light) !important;
}
}
.action-item:not(.action-item--open) {
.action-item__menutoggle:not(:hover):not(:focus):not(:active) {
opacity: .5;
}
}
}
ul {
// Calendar list items / Subscription list items

View File

@ -26,6 +26,8 @@
@include icon-black-white('eye', 'calendar', 4);
@include icon-black-white('invitees-no-response', 'calendar', 5);
@include icon-black-white('leftarrow', 'calendar', 2);
@include icon-black-white('new-calendar', 'calendar', 2);
@include icon-black-white('new-calendar-with-task-list', 'calendar', 2);
@include icon-black-white('random', 'calendar', 1);
@include icon-black-white('reminder', 'calendar', 4);
@include icon-black-white('reminder-audio', 'calendar', 1);

View File

@ -39,3 +39,13 @@
- Created by: Google
- License: Apache License version 2.0
- Link: https://material.io/resources/icons/?search=view_&icon=view_week&style=baseline
## new-calendar.svg
- Created by: Austin Andrews
- License: Apache License version 2.0
- Link: https://materialdesignicons.com/icon/calendar-blank
## new-calendar-with-task-list.svg
- Created by: Google
- License: Apache License version 2.0
- Link: https://materialdesignicons.com/icon/calendar-check

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 19H5V8h14m0-5h-1V1h-2v2H8V1H6v2H5L3 5v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2m-2 8l-2-1-4 5-3-2-1 1 4 3 6-6z"/></svg>

After

Width:  |  Height:  |  Size: 196 B

1
img/new-calendar.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 19H5V8h14m-3-7v2H8V1H6v2H5L3 5v14a2 2 0 002 2h14a2 2 0 002-2V5l-2-2h-1V1"/></svg>

After

Width:  |  Height:  |  Size: 155 B

View File

@ -24,30 +24,22 @@
id="calendars-list"
name="list"
tag="ul">
<CalendarListNew
:key="newCalendarKey"
:disabled="loadingCalendars" />
<AppNavigationSpacer :key="spacerKey" />
<CalendarListItemLoadingPlaceholder v-if="loadingCalendars" :key="loadingKeyCalendars" />
<CalendarListItem
v-for="calendar in calendars"
:key="calendar.id"
:calendar="calendar" />
<AppNavigationSpacer
:key="spacerKey" />
<SubscriptionListNew :key="newSubscriptionKey" :disabled="loadingCalendars" />
<CalendarListItemLoadingPlaceholder v-if="loadingCalendars" :key="loadingKeySubscriptions" />
<CalendarListItem
v-for="calendar in subscriptions"
v-for="calendar in allCalendars"
:key="calendar.id"
:calendar="calendar" />
<CalendarListNew
v-if="!loadingCalendars"
:key="newCalendarKey"
:disabled="loadingCalendars" />
</transition-group>
<transition-group v-else
id="calendars-list"
name="list"
tag="ul">
<CalendarListItemLoadingPlaceholder v-if="loadingCalendars" :key="loadingKeySubscriptions" />
<CalendarListItemLoadingPlaceholder v-if="loadingCalendars" :key="loadingKeyCalendars" />
<PublicCalendarListItem
v-for="calendar in subscriptions"
:key="calendar.id"
@ -56,26 +48,22 @@
</template>
<script>
import {
AppNavigationSpacer
} from '@nextcloud/vue'
import {
mapGetters
} from 'vuex'
import { AppNavigationSpacer } from '@nextcloud/vue'
import CalendarListNew from './CalendarList/CalendarListNew.vue'
import CalendarListItem from './CalendarList/CalendarListItem.vue'
import PublicCalendarListItem from './CalendarList/PublicCalendarListItem.vue'
import CalendarListItemLoadingPlaceholder from './CalendarList/CalendarListItemLoadingPlaceholder.vue'
import CalendarListNew from './CalendarList/CalendarListNew.vue'
import SubscriptionListNew from './CalendarList/SubscriptionListNew.vue'
export default {
name: 'CalendarList',
components: {
AppNavigationSpacer,
CalendarListNew,
CalendarListItem,
CalendarListItemLoadingPlaceholder,
CalendarListNew,
SubscriptionListNew,
PublicCalendarListItem
},
props: {
@ -90,7 +78,7 @@ export default {
},
computed: {
...mapGetters({
calendars: 'sortedCalendars',
allCalendars: 'sortedCalendarsSubscriptions',
subscriptions: 'sortedSubscriptions'
}),
newCalendarKey() {
@ -99,12 +87,6 @@ export default {
loadingKeyCalendars() {
return this._uid + '-loading-placeholder-calendars'
},
loadingKeySubscriptions() {
return this._uid + '-loading-placeholder-subscriptions'
},
newSubscriptionKey() {
return this._uid + '-new-subscription'
},
spacerKey() {
return this._uid + '-spacer'
}

View File

@ -21,95 +21,259 @@
<template>
<AppNavigationItem
v-if="!showForm"
icon="icon-add"
:class="{disabled: disabled}"
:title="$t('calendar', 'New calendar')"
@click.prevent.stop="openDialog" />
class="app-navigation-entry-new-calendar"
:class="{'app-navigation-entry-new-calendar--open': isOpen}"
:title="$t('calendar', '+ New calendar')"
:menu-open.sync="isOpen"
menu-icon="icon-add"
@click.prevent.stop="toggleDialog">
<template slot="actions">
<ActionButton
v-if="showCreateCalendarLabel"
icon="icon-new-calendar"
@click.prevent.stop="openCreateCalendarInput">
{{ $t('calendar', 'New calendar') }}
</ActionButton>
<ActionInput
v-if="showCreateCalendarInput"
icon="icon-new-calendar"
@submit.prevent.stop="createNewCalendar" />
<ActionText
v-if="showCreateCalendarSaving"
icon="icon-loading-small">
{{ $t('calendar', 'Creating calendar ...') }}
</ActionText>
<ActionInput
v-else
v-click-outside="closeNewCalendarForm"
:icon="inputIcon"
:value="name"
:disabled="isCreating"
@submit.prevent.stop="addCalendar">
{{ $t('calendar', 'Name of calendar') }}
</ActionInput>
<ActionButton
v-if="showCreateCalendarTaskListLabel"
icon="icon-new-calendar-with-task-list"
@click.prevent.stop="openCreateCalendarTaskListInput">
{{ $t('calendar', 'New calendar with task list') }}
</ActionButton>
<ActionInput
v-if="showCreateCalendarTaskListInput"
icon="icon-new-calendar-with-task-list"
@submit.prevent.stop="createNewCalendarTaskList" />
<ActionText
v-if="showCreateCalendarTaskListSaving"
icon="icon-loading-small">
{{ $t('calendar', 'Creating calendar ...') }}
</ActionText>
<ActionButton
v-if="showCreateSubscriptionLabel"
icon="icon-public"
@click.prevent.stop="openCreateSubscriptionInput">
{{ $t('calendar', 'New subscription from link') }}
</ActionButton>
<ActionInput
v-if="showCreateSubscriptionInput"
icon="icon-public"
@submit.prevent.stop="createNewSubscription" />
<ActionText
v-if="showCreateSubscriptionSaving"
icon="icon-loading-small">
{{ $t('calendar', 'Creating subscription ...') }}
</ActionText>
</template>
</AppNavigationItem>
</template>
<script>
import {
AppNavigationItem,
ActionInput
} from '@nextcloud/vue'
import ClickOutside from 'vue-click-outside'
import { ActionButton } from '@nextcloud/vue/dist/Components/ActionButton'
import { ActionInput } from '@nextcloud/vue/dist/Components/ActionInput'
import { ActionText } from '@nextcloud/vue/dist/Components/ActionText'
import { AppNavigationItem } from '@nextcloud/vue/dist/Components/AppNavigationItem'
import { getRandomColor } from '../../../utils/color.js'
export default {
name: 'CalendarListNew',
components: {
AppNavigationItem,
ActionInput
},
directives: {
ClickOutside
},
props: {
disabled: {
type: Boolean,
default: false
}
ActionButton,
ActionInput,
ActionText,
AppNavigationItem
},
data: function() {
return {
isCreating: false,
showForm: false,
name: ''
// Open state
isOpen: false,
// New calendar
showCreateCalendarLabel: true,
showCreateCalendarInput: false,
showCreateCalendarSaving: false,
// New calendar with task list
showCreateCalendarTaskListLabel: true,
showCreateCalendarTaskListInput: false,
showCreateCalendarTaskListSaving: false,
// New subscription
showCreateSubscriptionLabel: true,
showCreateSubscriptionInput: false,
showCreateSubscriptionSaving: false
}
},
computed: {
inputIcon() {
if (this.isCreating) {
return 'icon-loading-small'
watch: {
isOpen() {
if (this.isOpen) {
return
}
return 'icon-add'
this.closeMenu()
}
},
methods: {
openDialog() {
if (this.disabled) {
return false
}
this.showForm = true
this.$nextTick(() => {
this.$el.querySelector('input[type=text]').focus()
})
/**
* Opens the Actions menu when clicking on the main item label
*/
toggleDialog() {
this.isOpen = !this.isOpen
},
addCalendar(event) {
/**
* Opens the create calendar input
*/
openCreateCalendarInput() {
this.showCreateCalendarLabel = false
this.showCreateCalendarInput = true
this.showCreateCalendarSaving = false
this.showCreateCalendarTaskListLabel = true
this.showCreateCalendarTaskListInput = false
this.showCreateSubscriptionLabel = true
this.showCreateSubscriptionInput = false
},
/**
* Opens the create calendar with task list input
*/
openCreateCalendarTaskListInput() {
this.showCreateCalendarTaskListLabel = false
this.showCreateCalendarTaskListInput = true
this.showCreateCalendarTaskListSaving = false
this.showCreateCalendarLabel = true
this.showCreateCalendarInput = false
this.showCreateSubscriptionLabel = true
this.showCreateSubscriptionInput = false
},
/**
* Opens the create subscription input
*/
openCreateSubscriptionInput() {
this.showCreateSubscriptionLabel = false
this.showCreateSubscriptionInput = true
this.showCreateSubscriptionSaving = false
this.showCreateCalendarLabel = true
this.showCreateCalendarInput = false
this.showCreateCalendarTaskListLabel = true
this.showCreateCalendarTaskListInput = false
},
/**
* Creates a new calendar
*
* @param {Event} event The submit event
*/
async createNewCalendar(event) {
this.showCreateCalendarInput = false
this.showCreateCalendarSaving = true
const displayName = event.target.querySelector('input[type=text]').value
// Keep displayname visible while saving
this.name = displayName
this.isCreating = true
this.$store.dispatch('appendCalendar', { displayName, color: getRandomColor() }) // TODO - use uid2color
.then(() => {
this.showForm = false
this.isCreating = false
this.name = ''
})
.catch((error) => {
console.error(error)
this.$toast.error(this.$t('calendar', 'An error occurred, unable to create the calendar.'))
this.isCreating = false
try {
await this.$store.dispatch('appendCalendar', {
displayName,
color: getRandomColor() // TODO - use uid2color
})
} catch (error) {
console.debug(error)
this.$toast.error(this.$t('calendar', 'An error occurred, unable to create the calendar.'))
} finally {
this.showCreateCalendarSaving = false
this.showCreateCalendarLabel = true
this.isOpen = false
this.closeMenu()
}
},
closeNewCalendarForm() {
this.showForm = false
this.name = ''
/**
* Creates a new calendar with task list
*
* @param {Event} event The submit event
*/
async createNewCalendarTaskList(event) {
this.showCreateCalendarTaskListInput = false
this.showCreateCalendarTaskListSaving = true
const displayName = event.target.querySelector('input[type=text]').value
try {
await this.$store.dispatch('appendCalendar', {
displayName,
color: getRandomColor(), // TODO - uid2color
components: ['VEVENT', 'VTODO']
})
} catch (error) {
console.debug(error)
this.$toast.error(this.$t('calendar', 'An error occurred, unable to create the calendar.'))
} finally {
this.showCreateCalendarTaskListSaving = false
this.showCreateCalendarTaskListLabel = true
this.isOpen = false
this.closeMenu()
}
},
/**
* Creates a new subscription
*
* @param {Event} event The submit event
*/
async createNewSubscription(event) {
this.showCreateSubscriptionInput = false
this.showCreateSubscriptionSaving = true
const link = event.target.querySelector('input[type=text]').value
let url
let hostname
try {
url = new URL(link)
hostname = url.hostname
} catch (error) {
console.error(error)
this.$toast.error(this.$t('calendar', 'Please enter a valid link (starting with http://, https://, webcal://, or webcals://)'))
return
}
try {
await this.$store.dispatch('appendSubscription', {
displayName: hostname,
color: getRandomColor(), // TODO - uid2color
source: link
})
} catch (error) {
console.debug(error)
this.$toast.error(this.$t('calendar', 'An error occurred, unable to create the calendar.'))
} finally {
this.showCreateSubscriptionSaving = false
this.showCreateSubscriptionLabel = true
this.isOpen = false
this.closeMenu()
}
},
/**
* This resets the actions on close of menu
*/
closeMenu() {
this.showCreateCalendarLabel = true
this.showCreateCalendarInput = false
this.showCreateCalendarSaving = false
this.showCreateCalendarTaskListLabel = true
this.showCreateCalendarTaskListInput = false
this.showCreateCalendarTaskListSaving = false
this.showCreateSubscriptionLabel = true
this.showCreateSubscriptionInput = false
this.showCreateSubscriptionSaving = false
}
}
}

View File

@ -1,129 +0,0 @@
<!--
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
- @author Georg Ehrke <oc.list@georgehrke.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<AppNavigationItem
v-if="!showForm"
icon="icon-add"
:class="{disabled: disabled}"
:title="$t('calendar', 'New subscription')"
@click.prevent.stop="openDialog" />
<ActionInput
v-else
v-click-outside="closeNewCalendarForm"
:icon="inputIcon"
:value="link"
:disabled="isCreating"
@submit.prevent.stop="addCalendar">
{{ $t('calendar', 'Link to iCal') }}
</ActionInput>
</template>
<script>
import {
AppNavigationItem,
ActionInput
} from '@nextcloud/vue'
import ClickOutside from 'vue-click-outside'
import { getRandomColor } from '../../../utils/color.js'
export default {
name: 'SubscriptionListNew',
components: {
AppNavigationItem,
ActionInput
},
directives: {
ClickOutside
},
props: {
disabled: {
type: Boolean,
default: false
}
},
data: function() {
return {
link: '',
isCreating: false,
showForm: false
}
},
computed: {
inputIcon() {
if (this.isCreating) {
return 'icon-loading-small'
}
return 'icon-add'
}
},
methods: {
openDialog() {
if (this.disabled) {
return false
}
this.showForm = true
this.$nextTick(() => {
this.$el.querySelector('input[type=text]').focus()
})
},
addCalendar(event) {
const link = event.target.querySelector('input[type=text]').value
// Keep link visible while saving
this.link = link
this.isCreating = true
let url
let hostname
try {
url = new URL(link)
hostname = url.hostname
} catch (error) {
console.error(error)
this.$toast.error(this.$t('calendar', 'Please enter a valid link (starting with http://, https://, webcal://, or webcals://)'))
this.link = ''
this.isCreating = false
return
}
this.$store.dispatch('appendSubscription', { displayName: hostname, color: getRandomColor(), source: link }) // TODO - use uid2color
.then(() => {
this.displayName = ''
this.showForm = false
this.isCreating = false
})
.catch((error) => {
console.error(error)
this.$toast.error(this.$t('calendar', 'An error occurred, unable to create the calendar.'))
this.isCreating = false
})
},
closeNewCalendarForm() {
this.showForm = false
this.link = ''
}
}
}
</script>