This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/models/task.ts

252 lines
7.4 KiB
TypeScript
Raw Normal View History

2022-10-19 13:18:34 +00:00
import {PRIORITIES, type Priority} from '@/constants/priorities'
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_MONTH, SECONDS_A_WEEK, SECONDS_A_YEAR} from '@/constants/date'
2022-07-20 22:42:36 +00:00
2022-08-04 18:57:43 +00:00
import type {ITask} from '@/modelTypes/ITask'
import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IList} from '@/modelTypes/IList'
import type {ISubscription} from '@/modelTypes/ISubscription'
import type {IBucket} from '@/modelTypes/IBucket'
import type {IRepeatAfter} from '@/types/IRepeatAfter'
2022-10-19 13:18:34 +00:00
import type {IRelationKind} from '@/types/IRelationKind'
2022-08-04 18:57:43 +00:00
import {TASK_REPEAT_MODES, type IRepeatMode} from '@/types/IRepeatMode'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
2022-08-04 18:57:43 +00:00
import AbstractModel from './abstractModel'
import LabelModel from './label'
import UserModel from './user'
import AttachmentModel from './attachment'
import SubscriptionModel from './subscription'
2020-12-08 14:43:51 +00:00
export const TASK_DEFAULT_COLOR = '#1973ff'
const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
console.debug('This browser does not support triggered notifications')
}
export function getHexColor(hexColor: string): string {
2022-08-04 18:57:43 +00:00
if (hexColor === '' || hexColor === '#') {
return TASK_DEFAULT_COLOR
}
return hexColor
2022-07-20 22:42:36 +00:00
}
2022-10-19 13:18:34 +00:00
/**
* Parses `repeatAfterSeconds` into a usable js object.
*/
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
}
return repeatAfter
}
2022-09-06 09:36:01 +00:00
export default class TaskModel extends AbstractModel<ITask> implements ITask {
id = 0
title = ''
description = ''
done = false
doneAt: Date | null = null
priority: Priority = PRIORITIES.UNSET
labels: ILabel[] = []
assignees: IUser[] = []
dueDate: Date | null = 0
startDate: Date | null = 0
endDate: Date | null = 0
2022-08-04 18:57:43 +00:00
repeatAfter: number | IRepeatAfter = 0
repeatFromCurrentDate = false
2022-08-04 18:57:43 +00:00
repeatMode: IRepeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
reminderDates: Date[] = []
parentTaskId: ITask['id'] = 0
hexColor = ''
percentDone = 0
2022-09-15 10:52:38 +00:00
relatedTasks: Partial<Record<IRelationKind, ITask[]>> = {}
attachments: IAttachment[] = []
2022-10-17 11:14:07 +00:00
coverImageAttachmentId: IAttachment['id'] = null
identifier = ''
index = 0
isFavorite = false
subscription: ISubscription = null
2022-11-01 13:27:35 +00:00
coverImageAttachmentId: IAttachment['id'] = null
position = 0
kanbanPosition = 0
createdBy: IUser = UserModel
created: Date = null
updated: Date = null
listId: IList['id'] = 0
bucketId: IBucket['id'] = 0
2022-06-23 01:22:21 +00:00
2022-09-15 10:52:38 +00:00
constructor(data: Partial<ITask> = {}) {
super()
this.assignData(data)
2019-11-24 13:16:24 +00:00
this.id = Number(this.id)
this.title = this.title?.trim()
this.doneAt = parseDateOrNull(this.doneAt)
this.labels = this.labels
.map(l => new LabelModel(l))
.sort((f, s) => f.title > s.title ? 1 : -1)
// Parse the assignees into user models
this.assignees = this.assignees.map(a => {
return new UserModel(a)
})
this.dueDate = parseDateOrNull(this.dueDate)
this.startDate = parseDateOrNull(this.startDate)
this.endDate = parseDateOrNull(this.endDate)
// Parse the repeat after into something usable
2022-10-19 13:18:34 +00:00
this.repeatAfter = parseRepeatAfter(this.repeatAfter as number)
this.reminderDates = this.reminderDates.map(d => new Date(d))
// Cancel all scheduled notifications for this task to be sure to only have available notifications
this.cancelScheduledNotifications().then(() => {
// Every time we see a reminder, we schedule a notification for it
this.reminderDates.forEach(d => this.scheduleNotification(d))
})
if (this.hexColor !== '' && this.hexColor.substring(0, 1) !== '#') {
2019-04-30 20:18:06 +00:00
this.hexColor = '#' + this.hexColor
}
2022-09-15 10:52:38 +00:00
// Convert all subtasks to task models
Object.keys(this.relatedTasks).forEach(relationKind => {
this.relatedTasks[relationKind] = this.relatedTasks[relationKind].map(t => {
return new TaskModel(t)
})
})
2019-11-24 13:16:24 +00:00
// Make all attachments to attachment models
this.attachments = this.attachments.map(a => new AttachmentModel(a))
2020-05-16 11:14:57 +00:00
// Set the task identifier to empty if the list does not have one
if (this.identifier === `-${this.index}`) {
2020-05-16 11:14:57 +00:00
this.identifier = ''
}
if (typeof this.subscription !== 'undefined' && this.subscription !== null) {
this.subscription = new SubscriptionModel(this.subscription)
}
this.createdBy = new UserModel(this.createdBy)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
this.listId = Number(this.listId)
}
getTextIdentifier() {
if (this.identifier === '') {
return `#${this.index}`
}
return this.identifier
}
getHexColor() {
2022-08-04 18:57:43 +00:00
return getHexColor(this.hexColor)
}
/////////////////
// Helper functions
///////////////
async cancelScheduledNotifications() {
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
return
}
if (typeof navigator.serviceWorker === 'undefined') {
console.debug('Service Worker not available')
return
}
const registration = await navigator.serviceWorker.getRegistration()
if (typeof registration === 'undefined') {
return
}
// Get all scheduled notifications for this task and cancel them
const scheduledNotifications = await registration.getNotifications({
tag: `vikunja-task-${this.id}`,
includeTriggered: true,
})
console.debug('Already scheduled notifications:', scheduledNotifications)
scheduledNotifications.forEach(n => n.close())
}
async scheduleNotification(date) {
if (typeof navigator.serviceWorker === 'undefined') {
console.debug('Service Worker not available')
return
}
if (date < new Date()) {
console.debug('Date is in the past, not scheduling a notification. Date is ', date)
return
}
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
return
}
const {state} = await navigator.permissions.request({name: 'notifications'})
if (state !== 'granted') {
console.debug('Notification permission not granted, not showing notifications')
return
}
const registration = await navigator.serviceWorker.getRegistration()
if (typeof registration === 'undefined') {
console.error('No service worker registration available')
return
}
// Register the actual notification
try {
registration.showNotification('Vikunja Reminder', {
tag: `vikunja-task-${this.id}`, // Group notifications by task id so we're only showing one notification per task
body: this.title,
// eslint-disable-next-line no-undef
showTrigger: new TimestampTrigger(date),
badge: '/images/icons/badge-monochrome.png',
icon: '/images/icons/android-chrome-512x512.png',
data: {taskId: this.id},
actions: [
{
action: 'show-task',
title: 'Show task',
},
],
})
console.debug('Notification scheduled for ' + date)
} catch (e) {
throw new Error('Error scheduling notification', e)
}
}
}