summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2021-02-26 15:37:35 +0100
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2021-05-30 10:28:56 +0200
commitd6030761c32d047b2208376a99d75b72c7c57987 (patch)
tree3f8e6bddfaa2f1cb18f81d621979faff78123bd0
parent9acea39cb94222c8d6e4a6c0f7d8dbac5055d1f4 (diff)
Circles listing base
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r--babel.config.js1
-rw-r--r--css/icons.scss1
-rw-r--r--img/circles.svg1
-rw-r--r--package.json3
-rw-r--r--src/components/AppContent/CircleContent.vue164
-rw-r--r--src/components/AppContent/ContactsContent.vue159
-rw-r--r--src/components/AppNavigation/CircleNavigationItem.vue183
-rw-r--r--src/components/AppNavigation/GroupNavigationItem.vue126
-rw-r--r--src/components/AppNavigation/RootNavigation.vue271
-rw-r--r--src/components/AppNavigation/Settings/SettingsAddressbook.vue43
-rw-r--r--src/components/CircleDetails.vue50
-rw-r--r--src/components/EntityPicker/ContactsPicker.vue187
-rw-r--r--src/components/EntityPicker/MembersPicker.vue185
-rw-r--r--src/components/MemberList.vue (renamed from src/components/AppNavigation/GroupsNavigation.vue)59
-rw-r--r--src/components/MemberList/MemberListItem.vue222
-rw-r--r--src/main.js3
-rw-r--r--src/mixins/CopyToClipboardMixin.js65
-rw-r--r--src/mixins/RouterMixin.js (renamed from src/models/groups.js)18
-rw-r--r--src/models/circle.js307
-rw-r--r--src/models/constants.js75
-rw-r--r--src/models/member.js487
-rw-r--r--src/router/index.js5
-rw-r--r--src/services/circles.js101
-rw-r--r--src/store/circles.js165
-rw-r--r--src/utils/fileUtils.js48
-rw-r--r--src/utils/numberUtils.js30
-rw-r--r--src/views/Contacts.vue281
27 files changed, 2332 insertions, 908 deletions
diff --git a/babel.config.js b/babel.config.js
index 5496c985..c8758255 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,6 +1,7 @@
module.exports = {
plugins: [
'@babel/plugin-syntax-dynamic-import',
+ ['@babel/plugin-proposal-class-properties', { loose: true }],
],
presets: [
[
diff --git a/css/icons.scss b/css/icons.scss
index 853289d5..45e5166b 100644
--- a/css/icons.scss
+++ b/css/icons.scss
@@ -32,6 +32,7 @@
@include icon-black-white('clone', 'contacts', 2);
@include icon-black-white('sync', 'contacts', 2);
@include icon-black-white('recent-actors', 'contacts', 1);
+@include icon-black-white('circles', 'contacts', 1);
// social network icons:
@include icon-black-white('facebook', 'contacts', 2); // “facebook (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/facebook?style=brands)
diff --git a/img/circles.svg b/img/circles.svg
new file mode 100644
index 00000000..caf45591
--- /dev/null
+++ b/img/circles.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 57" width="64" height="64"><path d="M7.1 28.5A21.4 21.4 0 0 1 28.5 7.1m10.7 40A21.4 21.4 0 0 1 10 39M39.2 10A21.4 21.4 0 0 1 47 39.2" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><circle cx="28.5" cy="7.1" r="6.5" fill="#fff" /><circle cx="39.2" cy="-10" r="6.5" transform="rotate(90)" fill="#fff" /><circle cx="39.2" cy="-47" r="6.5" transform="rotate(90)" fill="#fff" /></svg> \ No newline at end of file
diff --git a/package.json b/package.json
index 8f9907fd..db5467c3 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.6.0",
"@nextcloud/dialogs": "^3.1.2",
+ "@nextcloud/event-bus": "^1.2.0",
"@nextcloud/initial-state": "^1.2.0",
"@nextcloud/l10n": "^1.4.1",
"@nextcloud/moment": "^1.1.1",
@@ -45,6 +46,7 @@
"@nextcloud/vue": "^3.9.0",
"axios": "^0.21.1",
"b64-to-blob": "^1.2.19",
+ "camelcase": "^5.3.1",
"cdav-library": "git+https://github.com/nextcloud/cdav-library.git",
"core-js": "^3.13.0",
"debounce": "^1.2.1",
@@ -61,6 +63,7 @@
"vue-click-outside": "^1.1.0",
"vue-clipboard2": "^0.3.1",
"vue-masonry": "^0.13.0",
+ "vue-material-design-icons": "^4.11.0",
"vue-router": "^3.5.1",
"vue-virtual-scroll-list": "^2.3.2",
"vue-virtual-scroller": "^1.0.10",
diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue
new file mode 100644
index 00000000..d382ae16
--- /dev/null
+++ b/src/components/AppContent/CircleContent.vue
@@ -0,0 +1,164 @@
+<!--
+ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.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>
+ <AppContent>
+ <div v-if="!circle">
+ <EmptyContent icon="icon-circles">
+ {{ t('contacts', 'Please select a circle') }}
+ </EmptyContent>
+ </div>
+
+ <div v-else id="app-content-wrapper">
+ <!-- loading members -->
+ <AppContentDetails v-if="loading">
+ <EmptyContent icon="icon-loading">
+ {{ t('contacts', 'Loading circle members…') }}
+ </EmptyContent>
+ </AppContentDetails>
+
+ <!-- not a member -->
+ <AppContentDetails v-else-if="!circle.isMember">
+ <EmptyContent v-if="!loadingJoin" icon="icon-circles">
+ {{ t('contacts', 'You are not a member of this circle') }}
+
+ <!-- Only show the join button if the circle is accepting requests -->
+ <template v-if="circle.canJoin" #desc>
+ <button :disabled="loadingJoin" class="primary" @click="requestJoin">
+ {{ t('contacts', 'Request to join') }}
+ </button>
+ </template>
+ </EmptyContent>
+
+ <EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading">
+ {{ t('contacts', 'Joining circle') }}
+ </EmptyContent>
+
+ <EmptyContent v-else icon="icon-loading">
+ {{ t('contacts', 'Joining circle') }}
+ </EmptyContent>
+ </AppContentDetails>
+
+ <template v-else>
+ <!-- member list -->
+ <MemberList :list="members" />
+
+ <!-- main contacts details -->
+ <CircleDetails :circle-id="selectedCircle" />
+ </template>
+ </div>
+ </AppContent>
+</template>
+<script>
+import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails'
+import AppContent from '@nextcloud/vue/dist/Components/AppContent'
+import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
+
+import CircleDetails from '../CircleDetails'
+import MemberList from '../MemberList'
+import RouterMixin from '../../mixins/RouterMixin'
+import { MEMBER_LEVEL_NONE } from '../../models/constants'
+
+export default {
+ name: 'CircleContent',
+
+ components: {
+ AppContent,
+ AppContentDetails,
+ CircleDetails,
+ EmptyContent,
+ MemberList,
+ },
+
+ mixins: [RouterMixin],
+
+ props: {
+ loading: {
+ type: Boolean,
+ default: true,
+ },
+ },
+
+ data() {
+ return {
+ loadingJoin: false,
+ }
+ },
+
+ computed: {
+ // store variables
+ circles() {
+ return this.$store.getters.getCircles
+ },
+ circle() {
+ return this.$store.getters.getCircle(this.selectedCircle)
+ },
+ members() {
+ return Object.values(this.circle?.members || [])
+ },
+
+ /**
+ * Is the current circle empty
+ * @returns {boolean}
+ */
+ isEmptyCircle() {
+ return this.members.length === 0
+ },
+
+ /**
+ * Is the current user member of this circle?
+ * @returns {boolean}
+ */
+ isMemberOfCircle() {
+ return this.circle.initiator?.level > MEMBER_LEVEL_NONE
+ },
+ },
+
+ watch: {
+ circle(newCircle) {
+ if (newCircle?.id) {
+ console.debug('Circles list is done loading, fetching members for', newCircle.id)
+ this.fetchCircleMembers(newCircle.id)
+ }
+ },
+ },
+
+ methods: {
+ fetchCircleMembers(circleId) {
+ this.$store.dispatch('getCircleMembers', circleId)
+ },
+
+ /**
+ * Request to join this circle
+ */
+ requestJoin() {
+ this.loadingJoin = true
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+#app-content-wrapper {
+ display: flex;
+}
+</style>
diff --git a/src/components/AppContent/ContactsContent.vue b/src/components/AppContent/ContactsContent.vue
new file mode 100644
index 00000000..4c84dff1
--- /dev/null
+++ b/src/components/AppContent/ContactsContent.vue
@@ -0,0 +1,159 @@
+<!--
+ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.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>
+ <AppContent>
+ <div v-if="loading">
+ <EmptyContent icon="icon-loading">
+ {{ t('contacts', 'Loading contacts …') }}
+ </EmptyContent>
+ </div>
+
+ <div v-else-if="isEmptyGroup && !isRealGroup">
+ <EmptyContent icon="icon-contacts-dark">
+ {{ t('contacts', 'There are no contacts yet') }}
+ <template #desc>
+ <button class="primary" @click="newContact">
+ {{ t('contacts', 'Create contact') }}
+ </button>
+ </template>
+ </EmptyContent>
+ </div>
+
+ <div v-else-if="isEmptyGroup && isRealGroup">
+ <EmptyContent icon="icon-contacts-dark">
+ {{ t('contacts', 'There are no contacts in this group') }}
+ <template #desc>
+ <button v-if="contacts.length === 0" class="primary" @click="addContactsToGroup(selectedGroup)">
+ {{ t('contacts', 'Create contacts') }}
+ </button>
+ <button v-else class="primary" @click="addContactsToGroup(selectedGroup)">
+ {{ t('contacts', 'Add contacts') }}
+ </button>
+ </template>
+ </EmptyContent>
+ </div>
+
+ <div v-else id="app-content-wrapper">
+ <!-- contacts list -->
+ <ContactsList
+ :list="contactsList"
+ :contacts="contacts"
+ :search-query="searchQuery" />
+
+ <!-- main contacts details -->
+ <ContactDetails :contact-key="selectedContact" />
+ </div>
+ </AppContent>
+</template>
+<script>
+import { emit } from '@nextcloud/event-bus'
+import AppContent from '@nextcloud/vue/dist/Components/AppContent'
+import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
+
+import ContactDetails from '../ContactDetails'
+import ContactsList from '../ContactsList'
+import RouterMixin from '../../mixins/RouterMixin'
+
+export default {
+ name: 'ContactsContent',
+
+ components: {
+ AppContent,
+ ContactDetails,
+ ContactsList,
+ EmptyContent,
+ },
+
+ mixins: [RouterMixin],
+
+ props: {
+ loading: {
+ type: Boolean,
+ default: true,
+ },
+
+ contactsList: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ searchQuery: '',
+ }
+ },
+
+ computed: {
+ // store variables
+ contacts() {
+ return this.$store.getters.getContacts
+ },
+ groups() {
+ return this.$store.getters.getGroups
+ },
+ sortedContacts() {
+ return this.$store.getters.getSortedContacts
+ },
+
+ /**
+ * Is this a real group ?
+ * Aka not a dynamically generated one like `All contacts`
+ * @returns {boolean}
+ */
+ isRealGroup() {
+ return this.groups.findIndex(group => group.name === this.selectedGroup) > -1
+ },
+ /**
+ * Is the current group empty
+ * @returns {boolean}
+ */
+ isEmptyGroup() {
+ return this.contactsList.length === 0
+ },
+ },
+
+ methods: {
+ /**
+ * Forward the addContactsToGroup event to the parent
+ * @param {string} groupName the group name
+ */
+ addContactsToGroup(groupName) {
+ emit('contacts:group:append', groupName)
+ },
+
+ /**
+ * Forward the newContact event to the parent
+ */
+ newContact() {
+ this.$emit('newContact')
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+#app-content-wrapper {
+ display: flex;
+}
+</style>
diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue
new file mode 100644
index 00000000..8e039f43
--- /dev/null
+++ b/src/components/AppNavigation/CircleNavigationItem.vue
@@ -0,0 +1,183 @@
+<!--
+ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.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
+ :key="circle.key"
+ :to="circle.router"
+ :title="circle.displayName"
+ :icon="circle.icon">
+ <template v-if="loading" slot="actions">
+ <ActionText icon="icon-loading-small">
+ {{ t('contacts', 'Loading …') }}
+ </ActionText>
+ </template>
+
+ <template v-else slot="actions">
+ <ActionButton
+ v-if="circle.canManageMembers"
+ icon="icon-add"
+ @click="addMemberToCircle">
+ {{ t('contacts', 'Add member') }}
+ </ActionButton>
+
+ <!-- copy circle link -->
+ <ActionLink
+ :href="circle.url"
+ :icon="copyLoading ? 'icon-loading-small' : 'icon-public'"
+ @click.stop.prevent="copyToClipboard(circleUrl)">
+ {{ copyButtonText }}
+ </ActionLink>
+
+ <!-- leave circle -->
+ <ActionButton
+ v-if="circle.isMember"
+ @click="leaveCircle">
+ {{ t('contacts', 'Leave circle') }}
+ <ExitToApp slot="icon"
+ :size="16"
+ decorative />
+ </ActionButton>
+
+ <!-- join circle -->
+ <ActionButton
+ v-else-if="circle.canJoin"
+ @click="joinCircle">
+ {{ joinButtonTitle }}
+ <LocationEnter slot="icon"
+ :size="16"
+ decorative />
+ </ActionButton>
+
+ <!-- delete circle -->
+ <ActionButton
+ v-if="circle.canDelete"
+ icon="icon-delete"
+ @click="deleteCircle">
+ {{ t('contacts', 'Delete') }}
+ </ActionButton>
+ </template>
+
+ <AppNavigationCounter v-if="circle.members.length > 0" slot="counter">
+ {{ circle.members.length }}
+ </AppNavigationCounter>
+ </AppNavigationItem>
+</template>
+
+<script>
+import { emit } from '@nextcloud/event-bus'
+
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
+import ActionText from '@nextcloud/vue/dist/Components/ActionText'
+import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
+import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
+import ExitToApp from 'vue-material-design-icons/ExitToApp'
+import LocationEnter from 'vue-material-design-icons/LocationEnter'
+
+import CopyToClipboardMixin from '../../mixins/CopyToClipboardMixin'
+import { deleteCircle, joinCircle } from '../../services/circles'
+import { showError } from '@nextcloud/dialogs'
+
+export default {
+ name: 'CircleNavigationItem',
+
+ components: {
+ ActionButton,
+ ActionLink,
+ ActionText,
+ AppNavigationCounter,
+ AppNavigationItem,
+ ExitToApp,
+ LocationEnter,
+ },
+
+ mixins: [CopyToClipboardMixin],
+
+ props: {
+ circle: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ loading: false
+ }
+ },
+
+ computed: {
+ copyButtonText() {
+ if (this.copied) {
+ return this.copySuccess
+ ? t('contacts', 'Copied')
+ : t('contacts', 'Could not copy')
+ }
+ return t('contacts', 'Copy link')
+ },
+
+ circleUrl() {
+ return window.location.origin + this.circle.url
+ },
+
+ joinButtonTitle() {
+ if (this.circle.requireJoinAccept) {
+ return t('contacts', 'Request to join')
+ }
+ return t('contacts', 'Join circle')
+ },
+ },
+
+ methods: {
+ // Trigger the entity picker view
+ addMemberToCircle() {
+ emit('contacts:circles:append', this.circle.id)
+ },
+
+ async joinCircle() {
+ try {
+ await joinCircle(this.circle.id)
+ } catch (error) {
+ showError(t('contacts', 'Unable to join the circle'))
+ }
+
+ },
+
+ leaveCircle() {
+
+ },
+
+ async deleteCircle() {
+ this.loading = true
+
+ try {
+ await deleteCircle(this.circle.id)
+ this.$store.dispatch('deleteCircle', this.circle.id)
+ } catch (error) {
+ showError(t('contacts', 'Unable to delete the circle'))
+ } finally {
+ this.loading = false
+ }
+ },
+ },
+}
+</script>
diff --git a/src/components/AppNavigation/GroupNavigationItem.vue b/src/components/AppNavigation/GroupNavigationItem.vue
new file mode 100644
index 00000000..9545c7b3
--- /dev/null
+++ b/src/components/AppNavigation/GroupNavigationItem.vue
@@ -0,0 +1,126 @@
+<!--
+ - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.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
+ :key="group.key"
+ :to="group.router"
+ :title="group.name"
+ :icon="group.icon">
+ <template slot="actions">
+ <ActionButton
+ icon="icon-add"
+ @click="addContactsToGroup(group)">
+ {{ t('contacts', 'Add contacts') }}
+ </ActionButton>
+ <ActionButton
+ icon="icon-download"
+ @click="downloadGroup(group)">
+ {{ t('contacts', 'Download') }}
+ </ActionButton>
+ </template>
+
+ <AppNavigationCounter v-if="group.contacts.length > 0" slot="counter">
+ {{ group.contacts.length }}
+ </AppNavigationCounter>
+ </AppNavigationItem>
+</template>
+
+<script>
+import { emit } from '@nextcloud/event-bus'
+import download from 'downloadjs'
+import moment from 'moment'
+
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
+import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
+
+export default {
+ name: 'GroupNavigationItem',
+
+ components: {
+ ActionButton,
+ AppNavigationCounter,
+ AppNavigationItem,
+ },
+
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ },
+
+ methods: {
+ // Trigger the entity picker view
+ addContactsToGroup() {
+ emit('contacts:group:append', this.group.name)
+ },
+
+ /**
+ * Download group of contacts
+ *
+ * @param {Object} group of contacts to be downloaded
+ */
+ downloadGroup(group) {
+ // get grouped contacts
+ let groupedContacts = {}
+ group.contacts.forEach(key => {
+ const id = this.contacts[key].addressbook.id
+ groupedContacts = Object.assign({
+ [id]: {
+ addressbook: this.contacts[key].addressbook,
+ contacts: [],
+ },
+ }, groupedContacts)
+ groupedContacts[id].contacts.push(this.contacts[key].url)
+ })
+
+ // create vcard promise with the requested contacts
+ const vcardPromise = Promise.all(
+ Object.keys(groupedContacts).map(key =>
+ groupedContacts[key].addressbook.dav.addressbookMultigetExport(groupedContacts[key].contacts)))
+ .then(response => ({
+ groupName: group.name,
+ data: response.map(data => data.body).join(''),
+ }))
+
+ // download vcard
+ this.downloadVcardPromise(vcardPromise)
+ },
+
+ /**
+ * Download vcard promise as vcard file
+ *
+ * @param {Promise} vcardPromise the full vcf file promise
+ */
+ async downloadVcardPromise(vcardPromise) {
+ vcardPromise.then(response => {
+ const filename = moment().format('YYYY-MM-DD_HH-mm') + '_' + response.groupName + '.vcf'
+ download(response.data, filename, 'text/vcard')
+ })
+ },
+ },
+}
+</script>
diff --git a/src/components/AppNavigation/RootNavigation.vue b/src/components/AppNavigation/RootNavigation.vue
index 6d0b1b49..e80270f4 100644
--- a/src/components/AppNavigation/RootNavigation.vue
+++ b/src/components/AppNavigation/RootNavigation.vue
@@ -1,3 +1,25 @@
+<!--
+ - @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.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>
<AppNavigation>
<slot />
@@ -47,37 +69,11 @@
</AppNavigationCounter>
</AppNavigationItem>
- <AppNavigationSpacer />
-
- <!-- Custom groups -->
- <AppNavigationItem v-for="group in groupsMenu"
- :key="group.key"
- :to="group.router"
- :title="group.name"
- :icon="group.icon">
- <template slot="actions">
- <ActionButton
- ic