feat: replace our home-grown gantt implementation with ganttastic #2180

Merged
konrad merged 78 commits from feature/ganttastic into main 2022-10-27 16:03:27 +00:00
9 changed files with 96 additions and 77 deletions
Showing only changes of commit a70a2e3ba6 - Show all commits

View File

@ -64,6 +64,7 @@ import {
} from '@infectoone/vue-ganttastic'
import Loading from '@/components/misc/loading.vue'
konrad marked this conversation as resolved Outdated

picky: use DATE_FORMAT to make clear it's a 'config const'

picky: use `DATE_FORMAT` to make clear it's a 'config const'

But also: shouldn't this depend on the user setting / language?

But also: shouldn't this depend on the user setting / language?

shouldn't this depend on the user setting / language?

It's only used to pass the date in the correct format to the gantt chart libaray so it will always be the same. Not sure why they only take strings as input instead of Date objects but that's how it is.

> shouldn't this depend on the user setting / language? It's only used to pass the date in the correct format to the gantt chart libaray so it will always be the same. Not sure why they only take strings as input instead of `Date` objects but that's how it is.
import {MILLISECONDS_A_DAY} from '@/constants/date'
export interface GanttChartProps {
isLoading: boolean,
@ -94,7 +95,7 @@ const dateToDate = computed(() => new Date(new Date(filters.value.dateTo).setHou
const DAY_WIDTH_PIXELS = 30
const ganttChartWidth = computed(() => {
const dateDiff = Math.floor((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / (1000 * 60 * 60 * 24))
const dateDiff = Math.floor((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY)
return dateDiff * DAY_WIDTH_PIXELS
})

View File

@ -3,6 +3,9 @@ import {useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
import {MILLISECONDS_A_HOUR, SECONDS_A_HOUR} from '@/constants/date'
const SECONDS_TOKEN_VALID = 60 * SECONDS_A_HOUR
export function useRenewTokenOnFocus() {
const router = useRouter()
@ -21,7 +24,7 @@ export function useRenewTokenOnFocus() {
return
}
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - new Date().valueOf() / MILLISECONDS_A_HOUR
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
@ -32,7 +35,7 @@ export function useRenewTokenOnFocus() {
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
if (expiresIn < SECONDS_TOKEN_VALID) {
authStore.renewToken()
console.debug('renewed token')
}

View File

@ -1 +1,14 @@
export const DATEFNS_DATE_FORMAT_KEBAB = 'yyyy-LL-dd'
export const SECONDS_A_MINUTE = 60
export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60
export const SECONDS_A_DAY = SECONDS_A_HOUR * 24
export const SECONDS_A_WEEK = SECONDS_A_DAY * 7
export const SECONDS_A_MONTH = SECONDS_A_DAY * 30
export const SECONDS_A_YEAR = SECONDS_A_DAY * 365
export const MILLISECONDS_A_SECOND = 1000
export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND
export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND
export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND
export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND

View File

@ -1,3 +1,5 @@
import {MILLISECONDS_A_WEEK} from "@/constants/date";
export function getNextWeekDate(): Date {
return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
return new Date((new Date()).getTime() + MILLISECONDS_A_WEEK)
}

View File

@ -1,6 +1,28 @@
import {SECONDS_A_HOUR} from '@/constants/date'
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
import { nativeEnum, number, object, preprocess } from 'zod'
/**
* 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
}
export const RepeatsSchema = preprocess(
(repeats: unknown) => {
// Parses the "repeat after x seconds" from the task into a usable js object inside the task.
@ -9,32 +31,7 @@ export const RepeatsSchema = preprocess(
return repeats
}
const repeatAfterHours = (repeats / 60) / 60
const repeatAfter : IRepeatAfter = {
type: 'hours',
amount: repeatAfterHours,
}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterHours % 24 === 0) {
const repeatAfterDays = repeatAfterHours / 24
if (repeatAfterDays % 7 === 0) {
repeatAfter.type = 'weeks'
repeatAfter.amount = repeatAfterDays / 7
} else if (repeatAfterDays % 30 === 0) {
repeatAfter.type = 'months'
repeatAfter.amount = repeatAfterDays / 30
} else if (repeatAfterDays % 365 === 0) {
repeatAfter.type = 'years'
repeatAfter.amount = repeatAfterDays / 365
} else {
repeatAfter.type = 'days'
repeatAfter.amount = repeatAfterDays
}
}
return repeatAfter
return parseRepeatAfter(repeats)
},
object({
type: nativeEnum(REPEAT_TYPES),

View File

@ -1,5 +1,5 @@
import { PRIORITIES, type Priority } from '@/constants/priorities'
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'
import type {ITask} from '@/modelTypes/ITask'
import type {ILabel} from '@/modelTypes/ILabel'
@ -10,10 +10,10 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
import type {IBucket} from '@/modelTypes/IBucket'
import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {IRelationKind} from '@/types/IRelationKind'
import {TASK_REPEAT_MODES, type IRepeatMode} from '@/types/IRepeatMode'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import type { IRelationKind } from '@/types/IRelationKind'
import AbstractModel from './abstractModel'
import LabelModel from './label'
@ -36,6 +36,27 @@ export function getHexColor(hexColor: string): string {
return hexColor
}
/**
* 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
}
export default class TaskModel extends AbstractModel<ITask> implements ITask {
id = 0
title = ''
@ -95,7 +116,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
this.endDate = parseDateOrNull(this.endDate)
// Parse the repeat after into something usable
this.parseRepeatAfter()
this.repeatAfter = parseRepeatAfter(this.repeatAfter as number)
this.reminderDates = this.reminderDates.map(d => new Date(d))
@ -151,33 +172,6 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
// Helper functions
///////////////
/**
* Parses the "repeat after x seconds" from the task into a usable js object inside the task.
* This function should only be called from the constructor.
*/
parseRepeatAfter() {
const repeatAfterHours = (this.repeatAfter as number / 60) / 60
this.repeatAfter = {type: 'hours', amount: repeatAfterHours}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterHours % 24 === 0) {
const repeatAfterDays = repeatAfterHours / 24
if (repeatAfterDays % 7 === 0) {
this.repeatAfter.type = 'weeks'
this.repeatAfter.amount = repeatAfterDays / 7
} else if (repeatAfterDays % 30 === 0) {
this.repeatAfter.type = 'months'
this.repeatAfter.amount = repeatAfterDays / 30
} else if (repeatAfterDays % 365 === 0) {
this.repeatAfter.type = 'years'
this.repeatAfter.amount = repeatAfterDays / 365
} else {
this.repeatAfter.type = 'days'
this.repeatAfter.amount = repeatAfterDays
}
}
}
async cancelScheduledNotifications() {
if (!SUPPORTS_TRIGGERED_NOTIFICATION) {
return

View File

@ -1,9 +1,10 @@
import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
import {parseTaskText, PrefixMode} from './parseTaskText'
import {getDateFromText, getDateFromTextIn, parseDate} from '../helpers/time/parseDate'
import {getDateFromText, parseDate} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import {PRIORITIES} from '@/constants/priorities'
import { MILLISECONDS_A_DAY } from '@/constants/date'
describe('Parse Task Text', () => {
beforeEach(() => {
@ -296,7 +297,7 @@ describe('Parse Task Text', () => {
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
})
it('should recognize dates of the month in the future', () => {
const nextDay = new Date(+new Date() + 60 * 60 * 24 * 1000)
const nextDay = new Date(+new Date() + MILLISECONDS_A_DAY)
const result = parseTaskText(`Lorem Ipsum ${nextDay.getDate()}nd`)
expect(result.text).toBe('Lorem Ipsum')

View File

@ -6,6 +6,7 @@ import LabelService from './label'
import {formatISO} from 'date-fns'
import {colorFromHex} from '@/helpers/color/colorFromHex'
import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_WEEK, SECONDS_A_MONTH, SECONDS_A_YEAR} from '@/constants/date'
const parseDate = date => {
if (date) {
@ -73,19 +74,19 @@ export default class TaskService extends AbstractService<ITask> {
if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) {
switch (model.repeatAfter.type) {
case 'hours':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_HOUR
break
case 'days':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_DAY
break
case 'weeks':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24 * 7
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_WEEK
break
case 'months':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24 * 30
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_MONTH
break
case 'years':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24 * 365
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_YEAR
break
}
}

View File

@ -14,6 +14,7 @@ import type {IUserSettings} from '@/modelTypes/IUserSettings'
import router from '@/router'
import {useConfigStore} from '@/stores/config'
import UserSettingsModel from '@/models/userSettings'
import {MILLISECONDS_A_SECOND} from '@/constants/date'
export interface AuthState {
authenticated: boolean,
@ -133,8 +134,10 @@ export const useAuthStore = defineStore('auth', {
}
},
// Registers a new user and logs them in.
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
/**
* Registers a new user and logs them in.
* Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
*/
async register(credentials) {
const HTTP = HTTPFactory()
this.setIsLoading(true)
@ -184,14 +187,17 @@ export const useAuthStore = defineStore('auth', {
return response.data
},
// Populates user information from jwt token saved in local storage in store
/**
* Populates user information from jwt token saved in local storage in store
*/
async checkAuth() {
const now = new Date()
const inOneMinute = new Date(new Date().setMinutes(now.getMinutes() + 1))
// This function can be called from multiple places at the same time and shortly after one another.
// To prevent hitting the api too frequently or race conditions, we check at most once per minute.
if (
this.lastUserInfoRefresh !== null &&
this.lastUserInfoRefresh > (new Date()).setMinutes((new Date()).getMinutes() + 1)
this.lastUserInfoRefresh > inOneMinute
) {
return
}
@ -204,7 +210,7 @@ export const useAuthStore = defineStore('auth', {
.replace('-', '+')
.replace('_', '/')
const info = new UserModel(JSON.parse(atob(base64)))
const ts = Math.round((new Date()).getTime() / 1000)
const ts = Math.round((new Date()).getTime() / MILLISECONDS_A_SECOND)
authenticated = info.exp >= ts
this.setUser(info)
@ -282,9 +288,8 @@ export const useAuthStore = defineStore('auth', {
/**
* Try to verify the email
* @returns {Promise<boolean>} if the email was successfully confirmed
*/
async verifyEmail() {
async verifyEmail(): Promise<boolean> {
const emailVerifyToken = localStorage.getItem('emailConfirmToken')
if (emailVerifyToken) {
const stopLoading = setModuleLoading(this)
@ -325,7 +330,9 @@ export const useAuthStore = defineStore('auth', {
}
},
// Renews the api token and saves it to local storage
/**
* Renews the api token and saves it to local storage
*/
renewToken() {
// FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
// link share in another tab. Without the timeout both the token renew and link share auth are executed at