Subscriptions and notifications for namespaces, tasks and lists (#410)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/410 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
b384c8ecde
commit
3f20ae89a8
10 changed files with 223 additions and 1 deletions
|
@ -54,6 +54,14 @@
|
||||||
>
|
>
|
||||||
Archive
|
Archive
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
|
<task-subscription
|
||||||
|
class="dropdown-item has-no-shadow"
|
||||||
|
:is-button="false"
|
||||||
|
entity="list"
|
||||||
|
:entity-id="list.id"
|
||||||
|
:subscription="subscription"
|
||||||
|
@change="sub => subscription = sub"
|
||||||
|
/>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
|
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
|
@ -69,10 +77,17 @@
|
||||||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||||
import Dropdown from '@/components/misc/dropdown'
|
import Dropdown from '@/components/misc/dropdown'
|
||||||
import DropdownItem from '@/components/misc/dropdown-item'
|
import DropdownItem from '@/components/misc/dropdown-item'
|
||||||
|
import TaskSubscription from '@/components/misc/subscription'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'list-settings-dropdown',
|
name: 'list-settings-dropdown',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
subscription: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
|
TaskSubscription,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
|
@ -81,6 +96,9 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.subscription = this.list.subscription
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
backgroundsEnabled() {
|
backgroundsEnabled() {
|
||||||
return this.$store.state.config.enabledBackgroundProviders.length > 0
|
return this.$store.state.config.enabledBackgroundProviders.length > 0
|
||||||
|
|
121
src/components/misc/subscription.vue
Normal file
121
src/components/misc/subscription.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<x-button
|
||||||
|
type="secondary"
|
||||||
|
:icon="icon"
|
||||||
|
v-tooltip="tooltipText"
|
||||||
|
@click="changeSubscription"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-if="isButton"
|
||||||
|
>
|
||||||
|
{{ buttonText }}
|
||||||
|
</x-button>
|
||||||
|
<a
|
||||||
|
v-tooltip="tooltipText"
|
||||||
|
@click="changeSubscription"
|
||||||
|
:class="{'is-disabled': disabled}"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<icon :icon="icon"/>
|
||||||
|
</span>
|
||||||
|
{{ buttonText }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SubscriptionService from '@/services/subscription'
|
||||||
|
import SubscriptionModel from '@/models/subscription'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'task-subscription',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
subscriptionService: SubscriptionService,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
entity: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
subscription: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
entityId: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.subscriptionService = new SubscriptionService()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tooltipText() {
|
||||||
|
if(this.disabled) {
|
||||||
|
return `You can't unsubscribe here because you are subscribed to this ${this.entity} through its ${this.subscription.entity}.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.subscription !== null ?
|
||||||
|
`You are currently subscribed to this ${this.entity} and will receive notifications for changes.` :
|
||||||
|
`You are not subscribed to this ${this.entity} and won't receive notifications for changes.`
|
||||||
|
},
|
||||||
|
buttonText() {
|
||||||
|
return this.subscription !== null ? 'Unsubscribe' : 'Subscribe'
|
||||||
|
},
|
||||||
|
icon() {
|
||||||
|
return this.subscription !== null ? ['far', 'bell-slash'] : 'bell'
|
||||||
|
},
|
||||||
|
disabled() {
|
||||||
|
if (this.subscription === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.subscription.entity !== this.entity
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeSubscription() {
|
||||||
|
if(this.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.subscription === null) {
|
||||||
|
this.subscribe()
|
||||||
|
} else {
|
||||||
|
this.unsubscribe()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subscribe() {
|
||||||
|
const subscription = new SubscriptionModel({
|
||||||
|
entity: this.entity,
|
||||||
|
entityId: this.entityId,
|
||||||
|
})
|
||||||
|
this.subscriptionService.create(subscription)
|
||||||
|
.then(() => {
|
||||||
|
this.$emit('change', subscription)
|
||||||
|
this.success({message: `You are now subscribed to this ${this.entity}`}, this)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.error(e, this)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unsubscribe() {
|
||||||
|
const subscription = new SubscriptionModel({
|
||||||
|
entity: this.entity,
|
||||||
|
entityId: this.entityId,
|
||||||
|
})
|
||||||
|
this.subscriptionService.delete(subscription)
|
||||||
|
.then(() => {
|
||||||
|
this.$emit('change', null)
|
||||||
|
this.success({message: `You are now unsubscribed to this ${this.entity}`}, this)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.error(e, this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -33,6 +33,14 @@
|
||||||
>
|
>
|
||||||
Archive
|
Archive
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
|
<task-subscription
|
||||||
|
class="dropdown-item has-no-shadow"
|
||||||
|
:is-button="false"
|
||||||
|
entity="namespace"
|
||||||
|
:entity-id="namespace.id"
|
||||||
|
:subscription="subscription"
|
||||||
|
@change="sub => subscription = sub"
|
||||||
|
/>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
|
@ -47,17 +55,27 @@
|
||||||
<script>
|
<script>
|
||||||
import Dropdown from '@/components/misc/dropdown'
|
import Dropdown from '@/components/misc/dropdown'
|
||||||
import DropdownItem from '@/components/misc/dropdown-item'
|
import DropdownItem from '@/components/misc/dropdown-item'
|
||||||
|
import TaskSubscription from '@/components/misc/subscription'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'namespace-settings-dropdown',
|
name: 'namespace-settings-dropdown',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
subscription: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
TaskSubscription,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
namespace: {
|
namespace: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.subscription = this.namespace.subscription
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -61,8 +61,9 @@ import {
|
||||||
faArchive,
|
faArchive,
|
||||||
faShareAlt,
|
faShareAlt,
|
||||||
faImage,
|
faImage,
|
||||||
|
faBell,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
|
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun, faBellSlash} from '@fortawesome/free-regular-svg-icons'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||||
// PWA
|
// PWA
|
||||||
import './registerServiceWorker'
|
import './registerServiceWorker'
|
||||||
|
@ -152,6 +153,8 @@ library.add(faEllipsisH)
|
||||||
library.add(faArchive)
|
library.add(faArchive)
|
||||||
library.add(faShareAlt)
|
library.add(faShareAlt)
|
||||||
library.add(faImage)
|
library.add(faImage)
|
||||||
|
library.add(faBell)
|
||||||
|
library.add(faBellSlash)
|
||||||
|
|
||||||
Vue.component('icon', FontAwesomeIcon)
|
Vue.component('icon', FontAwesomeIcon)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
|
||||||
import TaskModel from './task'
|
import TaskModel from './task'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||||
|
import SubscriptionModel from '@/models/subscription'
|
||||||
|
|
||||||
export default class ListModel extends AbstractModel {
|
export default class ListModel extends AbstractModel {
|
||||||
|
|
||||||
|
@ -19,6 +20,10 @@ export default class ListModel extends AbstractModel {
|
||||||
|
|
||||||
this.owner = new UserModel(this.owner)
|
this.owner = new UserModel(this.owner)
|
||||||
|
|
||||||
|
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||||
|
this.subscription = new SubscriptionModel(this.subscription)
|
||||||
|
}
|
||||||
|
|
||||||
this.created = new Date(this.created)
|
this.created = new Date(this.created)
|
||||||
this.updated = new Date(this.updated)
|
this.updated = new Date(this.updated)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +42,7 @@ export default class ListModel extends AbstractModel {
|
||||||
identifier: '',
|
identifier: '',
|
||||||
backgroundInformation: null,
|
backgroundInformation: null,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
|
subscription: null,
|
||||||
|
|
||||||
created: null,
|
created: null,
|
||||||
updated: null,
|
updated: null,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import AbstractModel from './abstractModel'
|
import AbstractModel from './abstractModel'
|
||||||
import ListModel from './list'
|
import ListModel from './list'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
|
import SubscriptionModel from '@/models/subscription'
|
||||||
|
|
||||||
export default class NamespaceModel extends AbstractModel {
|
export default class NamespaceModel extends AbstractModel {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
@ -13,8 +14,13 @@ export default class NamespaceModel extends AbstractModel {
|
||||||
this.lists = this.lists.map(l => {
|
this.lists = this.lists.map(l => {
|
||||||
return new ListModel(l)
|
return new ListModel(l)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.owner = new UserModel(this.owner)
|
this.owner = new UserModel(this.owner)
|
||||||
|
|
||||||
|
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||||
|
this.subscription = new SubscriptionModel(this.subscription)
|
||||||
|
}
|
||||||
|
|
||||||
this.created = new Date(this.created)
|
this.created = new Date(this.created)
|
||||||
this.updated = new Date(this.updated)
|
this.updated = new Date(this.updated)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +35,7 @@ export default class NamespaceModel extends AbstractModel {
|
||||||
lists: [],
|
lists: [],
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
hexColor: '',
|
hexColor: '',
|
||||||
|
subscription: null,
|
||||||
|
|
||||||
created: null,
|
created: null,
|
||||||
updated: null,
|
updated: null,
|
||||||
|
|
20
src/models/subscription.js
Normal file
20
src/models/subscription.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import AbstractModel from '@/models/abstractModel'
|
||||||
|
import UserModel from '@/models/user'
|
||||||
|
|
||||||
|
export default class SubscriptionModel extends AbstractModel {
|
||||||
|
constructor(data) {
|
||||||
|
super(data)
|
||||||
|
this.user = new UserModel(this.user)
|
||||||
|
this.created = new Date(this.created)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults() {
|
||||||
|
return {
|
||||||
|
id: 0,
|
||||||
|
entity: '',
|
||||||
|
entityId: 0,
|
||||||
|
created: null,
|
||||||
|
user: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
import LabelModel from './label'
|
import LabelModel from './label'
|
||||||
import AttachmentModel from './attachment'
|
import AttachmentModel from './attachment'
|
||||||
|
import SubscriptionModel from '@/models/subscription'
|
||||||
|
|
||||||
const parseDate = date => {
|
const parseDate = date => {
|
||||||
if (date && !date.startsWith('0001')) {
|
if (date && !date.startsWith('0001')) {
|
||||||
|
@ -75,6 +76,10 @@ export default class TaskModel extends AbstractModel {
|
||||||
this.identifier = ''
|
this.identifier = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||||
|
this.subscription = new SubscriptionModel(this.subscription)
|
||||||
|
}
|
||||||
|
|
||||||
this.created = new Date(this.created)
|
this.created = new Date(this.created)
|
||||||
this.updated = new Date(this.updated)
|
this.updated = new Date(this.updated)
|
||||||
}
|
}
|
||||||
|
@ -104,6 +109,7 @@ export default class TaskModel extends AbstractModel {
|
||||||
identifier: '',
|
identifier: '',
|
||||||
index: 0,
|
index: 0,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
|
subscription: null,
|
||||||
|
|
||||||
createdBy: UserModel,
|
createdBy: UserModel,
|
||||||
created: null,
|
created: null,
|
||||||
|
|
15
src/services/subscription.js
Normal file
15
src/services/subscription.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
import SubscriptionModel from '@/models/subscription'
|
||||||
|
|
||||||
|
export default class SubscriptionService extends AbstractService {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
create: '/subscriptions/{entity}/{entityId}',
|
||||||
|
delete: '/subscriptions/{entity}/{entityId}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
modelFactory(data) {
|
||||||
|
return new SubscriptionModel(data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -255,6 +255,12 @@
|
||||||
>
|
>
|
||||||
{{ task.done ? 'Mark as undone' : 'Done!' }}
|
{{ task.done ? 'Mark as undone' : 'Done!' }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
<task-subscription
|
||||||
|
entity="task"
|
||||||
|
:entity-id="task.id"
|
||||||
|
:subscription="task.subscription"
|
||||||
|
@change="sub => task.subscription = sub"
|
||||||
|
/>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('assignees')"
|
@click="setFieldActive('assignees')"
|
||||||
@shortkey="setFieldActive('assignees')"
|
@shortkey="setFieldActive('assignees')"
|
||||||
|
@ -422,10 +428,12 @@ import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
|
||||||
import heading from '@/components/tasks/partials/heading'
|
import heading from '@/components/tasks/partials/heading'
|
||||||
import Datepicker from '@/components/input/datepicker'
|
import Datepicker from '@/components/input/datepicker'
|
||||||
import {playPop} from '@/helpers/playPop'
|
import {playPop} from '@/helpers/playPop'
|
||||||
|
import TaskSubscription from '@/components/misc/subscription'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TaskDetailView',
|
name: 'TaskDetailView',
|
||||||
components: {
|
components: {
|
||||||
|
TaskSubscription,
|
||||||
Datepicker,
|
Datepicker,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
ListSearch,
|
ListSearch,
|
||||||
|
|
Loading…
Reference in a new issue