feat: replace our home-grown gantt implementation with ganttastic #2180
|
@ -64,6 +64,7 @@ import {
|
|||
} from '@infectoone/vue-ganttastic'
|
||||
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
konrad marked this conversation as resolved
Outdated
|
||||
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
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue
picky: use
DATE_FORMAT
to make clear it's a 'config const'But also: 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.