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
|
||||
</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
|
||||
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
|
||||
icon="trash-alt"
|
||||
|
@ -69,10 +77,17 @@
|
|||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import DropdownItem from '@/components/misc/dropdown-item'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'list-settings-dropdown',
|
||||
data() {
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TaskSubscription,
|
||||
DropdownItem,
|
||||
Dropdown,
|
||||
},
|
||||
|
@ -81,6 +96,9 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.subscription = this.list.subscription
|
||||
},
|
||||
computed: {
|
||||
backgroundsEnabled() {
|
||||
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
|
||||
</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
|
||||
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
||||
icon="trash-alt"
|
||||
|
@ -47,17 +55,27 @@
|
|||
<script>
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import DropdownItem from '@/components/misc/dropdown-item'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'namespace-settings-dropdown',
|
||||
data() {
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DropdownItem,
|
||||
Dropdown,
|
||||
TaskSubscription,
|
||||
},
|
||||
props: {
|
||||
namespace: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.subscription = this.namespace.subscription
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -61,8 +61,9 @@ import {
|
|||
faArchive,
|
||||
faShareAlt,
|
||||
faImage,
|
||||
faBell,
|
||||
} 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'
|
||||
// PWA
|
||||
import './registerServiceWorker'
|
||||
|
@ -152,6 +153,8 @@ library.add(faEllipsisH)
|
|||
library.add(faArchive)
|
||||
library.add(faShareAlt)
|
||||
library.add(faImage)
|
||||
library.add(faBell)
|
||||
library.add(faBellSlash)
|
||||
|
||||
Vue.component('icon', FontAwesomeIcon)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
|
|||
import TaskModel from './task'
|
||||
import UserModel from './user'
|
||||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default class ListModel extends AbstractModel {
|
||||
|
||||
|
@ -19,6 +20,10 @@ export default class ListModel extends AbstractModel {
|
|||
|
||||
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.updated = new Date(this.updated)
|
||||
}
|
||||
|
@ -37,6 +42,7 @@ export default class ListModel extends AbstractModel {
|
|||
identifier: '',
|
||||
backgroundInformation: null,
|
||||
isFavorite: false,
|
||||
subscription: null,
|
||||
|
||||
created: null,
|
||||
updated: null,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import ListModel from './list'
|
||||
import UserModel from './user'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default class NamespaceModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
|
@ -13,8 +14,13 @@ export default class NamespaceModel extends AbstractModel {
|
|||
this.lists = this.lists.map(l => {
|
||||
return new ListModel(l)
|
||||
})
|
||||
|
||||
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.updated = new Date(this.updated)
|
||||
}
|
||||
|
@ -29,6 +35,7 @@ export default class NamespaceModel extends AbstractModel {
|
|||
lists: [],
|
||||
isArchived: false,
|
||||
hexColor: '',
|
||||
subscription: null,
|
||||
|
||||
created: 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 LabelModel from './label'
|
||||
import AttachmentModel from './attachment'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
const parseDate = date => {
|
||||
if (date && !date.startsWith('0001')) {
|
||||
|
@ -75,6 +76,10 @@ export default class TaskModel extends AbstractModel {
|
|||
this.identifier = ''
|
||||
}
|
||||
|
||||
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||
this.subscription = new SubscriptionModel(this.subscription)
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
}
|
||||
|
@ -104,6 +109,7 @@ export default class TaskModel extends AbstractModel {
|
|||
identifier: '',
|
||||
index: 0,
|
||||
isFavorite: false,
|
||||
subscription: null,
|
||||
|
||||
createdBy: UserModel,
|
||||
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!' }}
|
||||
</x-button>
|
||||
<task-subscription
|
||||
entity="task"
|
||||
:entity-id="task.id"
|
||||
:subscription="task.subscription"
|
||||
@change="sub => task.subscription = sub"
|
||||
/>
|
||||
<x-button
|
||||
@click="setFieldActive('assignees')"
|
||||
@shortkey="setFieldActive('assignees')"
|
||||
|
@ -422,10 +428,12 @@ import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
|
|||
import heading from '@/components/tasks/partials/heading'
|
||||
import Datepicker from '@/components/input/datepicker'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'TaskDetailView',
|
||||
components: {
|
||||
TaskSubscription,
|
||||
Datepicker,
|
||||
ColorPicker,
|
||||
ListSearch,
|
||||
|
|
Loading…
Reference in a new issue