feat: improved types (#2547)

Reviewed-on: vikunja/frontend#2547
Reviewed-by: konrad <k@knt.li>
This commit is contained in:
konrad 2022-11-02 16:06:55 +00:00
commit 0ff0d8c5b8
63 changed files with 444 additions and 340 deletions

View File

@ -37,6 +37,10 @@ module.exports = {
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }], '@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
'vue/multi-word-component-names': 0, 'vue/multi-word-component-names': 0,
// disabled until we have support for reactivityTransform
// See https://github.com/vuejs/eslint-plugin-vue/issues/1948
// see also setting in `vite.config`
'vue/no-setup-props-destructure': 0,
}, },
'parser': 'vue-eslint-parser', 'parser': 'vue-eslint-parser',
'parserOptions': { 'parserOptions': {

View File

@ -86,6 +86,7 @@
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"browserslist": "4.21.4", "browserslist": "4.21.4",
"caniuse-lite": "1.0.30001427", "caniuse-lite": "1.0.30001427",
"csstype": "3.1.1",
"cypress": "10.11.0", "cypress": "10.11.0",
"esbuild": "0.15.12", "esbuild": "0.15.12",
"eslint": "8.26.0", "eslint": "8.26.0",

View File

@ -41,6 +41,7 @@ specifiers:
camel-case: 4.1.2 camel-case: 4.1.2
caniuse-lite: 1.0.30001427 caniuse-lite: 1.0.30001427
codemirror: 5.65.9 codemirror: 5.65.9
csstype: 3.1.1
cypress: 10.11.0 cypress: 10.11.0
date-fns: 2.29.3 date-fns: 2.29.3
dayjs: 1.11.6 dayjs: 1.11.6
@ -157,6 +158,7 @@ devDependencies:
autoprefixer: 10.4.13_postcss@8.4.18 autoprefixer: 10.4.13_postcss@8.4.18
browserslist: 4.21.4 browserslist: 4.21.4
caniuse-lite: 1.0.30001427 caniuse-lite: 1.0.30001427
csstype: 3.1.1
cypress: 10.11.0 cypress: 10.11.0
esbuild: 0.15.12 esbuild: 0.15.12
eslint: 8.26.0 eslint: 8.26.0
@ -5219,6 +5221,10 @@ packages:
/csstype/2.6.19: /csstype/2.6.19:
resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==} resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==}
/csstype/3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
dev: true
/cyclist/1.0.1: /cyclist/1.0.1:
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==} resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
dev: true dev: true

View File

@ -1,18 +1,53 @@
<!-- a disabled link of any kind is not a link -->
<!-- we have a router link -->
<!-- just a normal link -->
<!-- a button it shall be -->
<!-- note that we only pass the click listener here -->
<template> <template>
<component <div
:is="componentNodeName" v-if="disabled === true && (to !== undefined || href !== undefined)"
class="base-button" class="base-button"
:class="{ 'base-button--type-button': isButton }" :aria-disabled="disabled || undefined"
v-bind="elementBindings"
:disabled="disabled || undefined"
ref="button" ref="button"
> >
<slot/> <slot/>
</component> </div>
<router-link
v-else-if="to !== undefined"
:to="to"
class="base-button"
ref="button"
>
<slot/>
</router-link>
<a v-else-if="href !== undefined"
class="base-button"
:href="href"
rel="noreferrer noopener nofollow"
target="_blank"
ref="button"
>
<slot/>
</a>
<button
v-else
:type="type"
class="base-button base-button--type-button"
:disabled="disabled || undefined"
ref="button"
@click="(event: MouseEvent) => emit('click', event)"
>
<slot/>
</button>
</template> </template>
<script lang="ts"> <script lang="ts">
export default { inheritAttrs: false } const BASE_BUTTON_TYPES_MAP = {
BUTTON: 'button',
SUBMIT: 'submit',
} as const
export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUTTON_TYPES_MAP] | undefined
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
@ -20,77 +55,36 @@ export default { inheritAttrs: false }
// by doing so we make it easy abstract the functionality from style and enable easier and semantic // by doing so we make it easy abstract the functionality from style and enable easier and semantic
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations // correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
// the component tries to heuristically determine what it should be checking the props (see the // the component tries to heuristically determine what it should be checking the props
// componentNodeName and elementBindings ref for this).
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead! // NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
import { ref, watchEffect, computed, useAttrs, type PropType } from 'vue' import {unrefElement} from '@vueuse/core'
import {ref, type HTMLAttributes} from 'vue'
import type {RouteLocationNamedRaw} from 'vue-router'
const BASE_BUTTON_TYPES_MAP = Object.freeze({ export interface BaseButtonProps extends HTMLAttributes {
button: 'button', type?: BaseButtonTypes
submit: 'submit', disabled?: boolean
}) to?: RouteLocationNamedRaw
href?: string
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
const props = defineProps({
type: {
type: String as PropType<BaseButtonTypes>,
default: 'button',
},
disabled: {
type: Boolean,
default: false,
},
})
const componentNodeName = ref<Node['nodeName']>('button')
interface ElementBindings {
type?: string;
rel?: string;
target?: string;
} }
const elementBindings = ref({}) export interface BaseButtonEmits {
(e: 'click', payload: MouseEvent): void
}
const attrs = useAttrs() const {
watchEffect(() => { type = BASE_BUTTON_TYPES_MAP.BUTTON,
// by default this component is a button element with the attribute of the type "button" (default prop value) disabled = false,
let nodeName = 'button' } = defineProps<BaseButtonProps>()
let bindings: ElementBindings = {type: props.type}
// if we find a "to" prop we set it as router-link const emit = defineEmits<BaseButtonEmits>()
if ('to' in attrs) {
nodeName = 'router-link'
bindings = {}
}
// if there is a href we assume the user wants an external link via a link element const button = ref<HTMLElement | null>(null)
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
if ('href' in attrs) {
nodeName = 'a'
bindings = {
rel: 'noreferrer noopener nofollow',
target: '_blank',
}
}
componentNodeName.value = nodeName
elementBindings.value = {
...bindings,
...attrs,
}
})
const isButton = computed(() => componentNodeName.value === 'button')
const button = ref()
function focus() { function focus() {
button.value.focus() unrefElement(button)?.focus()
} }
defineExpose({ defineExpose({

View File

@ -26,7 +26,7 @@ if (navigator && navigator.serviceWorker) {
) )
} }
function showRefreshUI(e) { function showRefreshUI(e: Event) {
console.log('recieved refresh event', e) console.log('recieved refresh event', e)
registration.value = e.detail registration.value = e.detail
updateAvailable.value = true updateAvailable.value = true

View File

@ -9,64 +9,61 @@
} }
]" ]"
> >
<icon <template v-if="icon">
v-if="showIconOnly"
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
<span class="icon is-small" v-else-if="icon !== ''">
<icon <icon
v-if="showIconOnly"
:icon="icon" :icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}" :style="{'color': iconColor !== '' ? iconColor : false}"
/> />
</span> <span class="icon is-small" v-else>
<icon
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
</span>
</template>
<slot /> <slot />
</BaseButton> </BaseButton>
</template> </template>
<script lang="ts"> <script lang="ts">
const BUTTON_TYPES_MAP = {
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
} as const
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
export default { name: 'x-button' } export default { name: 'x-button' }
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {computed, useSlots, type PropType} from 'vue' import {computed, useSlots} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const BUTTON_TYPES_MAP = Object.freeze({ // extending the props of the BaseButton
primary: 'is-primary', export interface ButtonProps extends BaseButtonProps {
secondary: 'is-outlined', variant?: ButtonTypes
tertiary: 'is-text is-inverted underline-none', icon?: IconProp
}) iconColor?: string
loading?: boolean
shadow?: boolean
}
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP const {
variant = 'primary',
icon = '',
iconColor = '',
loading = false,
shadow = true,
} = defineProps<ButtonProps>()
const props = defineProps({ const variantClass = computed(() => BUTTON_TYPES_MAP[variant])
variant: {
type: String as PropType<ButtonTypes>,
default: 'primary',
},
icon: {
type: [String, Array],
default: '',
},
iconColor: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
})
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
const slots = useSlots() const slots = useSlots()
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined') const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'undefined')
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -193,7 +193,7 @@ function toggleDatePopup() {
} }
const datepickerPopup = ref<HTMLElement | null>(null) const datepickerPopup = ref<HTMLElement | null>(null)
function hideDatePopup(e) { function hideDatePopup(e: MouseEvent) {
if (show.value) { if (show.value) {
closeWhenClickedOutside(e, datepickerPopup.value, close) closeWhenClickedOutside(e, datepickerPopup.value, close)
} }

View File

@ -115,6 +115,7 @@ const props = defineProps({
default: true, default: true,
}, },
bottomActions: { bottomActions: {
type: Array,
default: () => [], default: () => [],
}, },
emptyText: { emptyText: {

View File

@ -100,37 +100,52 @@ function elementInResults(elem: string | any, label: string, query: string): boo
} }
const props = defineProps({ const props = defineProps({
// When true, shows a loading spinner /**
* When true, shows a loading spinner
*/
loading: { loading: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// The placeholder of the search input /**
* The placeholder of the search input
*/
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',
}, },
// The search results where the @search listener needs to put the results into /**
* The search results where the @search listener needs to put the results into
*/
searchResults: { searchResults: {
type: Array as PropType<{[id: string]: any}>, type: Array as PropType<{[id: string]: any}>,
default: () => [], default: () => [],
}, },
// The name of the property of the searched object to show the user. /**
// If empty the component will show all raw data of an entry. * The name of the property of the searched object to show the user.
* If empty the component will show all raw data of an entry.
*/
label: { label: {
type: String, type: String,
default: '', default: '',
}, },
// The object with the value, updated every time an entry is selected. /**
* The object with the value, updated every time an entry is selected.
*/
modelValue: { modelValue: {
type: [Object] as PropType<{[key: string]: any}>,
default: null, default: null,
}, },
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it. /**
* If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
*/
creatable: { creatable: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// The text shown next to the new value option. /**
* The text shown next to the new value option.
*/
createPlaceholder: { createPlaceholder: {
type: String, type: String,
default() { default() {
@ -138,7 +153,9 @@ const props = defineProps({
return t('input.multiselect.createPlaceholder') return t('input.multiselect.createPlaceholder')
}, },
}, },
// The text shown next to an option. /**
* The text shown next to an option.
*/
selectPlaceholder: { selectPlaceholder: {
type: String, type: String,
default() { default() {
@ -146,22 +163,30 @@ const props = defineProps({
return t('input.multiselect.selectPlaceholder') return t('input.multiselect.selectPlaceholder')
}, },
}, },
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case. /**
* If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
*/
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// If true, displays the search results inline instead of using a dropdown. /**
* If true, displays the search results inline instead of using a dropdown.
*/
inline: { inline: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// If true, shows search results when no query is specified. /**
* If true, shows search results when no query is specified.
*/
showEmpty: { showEmpty: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
// The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke. /**
* The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
*/
searchDelay: { searchDelay: {
type: Number, type: Number,
default: 200, default: 200,
@ -174,17 +199,25 @@ const props = defineProps({
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: null): void (e: 'update:modelValue', value: null): void
// @search: Triggered every time the search query input changes /**
* Triggered every time the search query input changes
*/
(e: 'search', query: string): void (e: 'search', query: string): void
// @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model. /**
(e: 'select', value: null): void * Triggered every time an option from the search results is selected. Also triggers a change in v-model.
// @create: If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query. */
(e: 'select', value: {[key: string]: any}): void
/**
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
*/
(e: 'create', query: string): void (e: 'create', query: string): void
// @remove: If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items. /**
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
*/
(e: 'remove', value: null): void (e: 'remove', value: null): void
}>() }>()
const query = ref('') const query = ref<string | {[key: string]: any}>('')
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null) const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const localLoading = ref(false) const localLoading = ref(false)
const showSearchResults = ref(false) const showSearchResults = ref(false)

View File

@ -70,6 +70,8 @@ import {
} from '@fortawesome/free-regular-svg-icons' } from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from '@/types/vue-fontawesome'
library.add(faAlignLeft) library.add(faAlignLeft)
library.add(faAngleRight) library.add(faAngleRight)
library.add(faArchive) library.add(faArchive)
@ -136,4 +138,5 @@ library.add(faTrashAlt)
library.add(faUser) library.add(faUser)
library.add(faUsers) library.add(faUsers)
export default FontAwesomeIcon // overwriting the wrong types
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes

View File

@ -35,6 +35,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {PropType} from 'vue'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
defineProps({ defineProps({
@ -51,7 +54,7 @@ defineProps({
default: false, default: false,
}, },
closeIcon: { closeIcon: {
type: String, type: String as PropType<IconProp>,
default: 'times', default: 'times',
}, },
shadow: { shadow: {

View File

@ -6,10 +6,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Color } from 'csstype' import type { DataType } from 'csstype'
defineProps< { defineProps< {
color: Color, color: DataType.Color,
}>() }>()
</script> </script>

View File

@ -46,6 +46,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {PropType} from 'vue'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
defineProps({ defineProps({
title: { title: {
type: String, type: String,
@ -55,7 +58,7 @@ defineProps({
type: String, type: String,
}, },
primaryIcon: { primaryIcon: {
type: String, type: String as PropType<IconProp>,
default: 'plus', default: 'plus',
}, },
primaryDisabled: { primaryDisabled: {

View File

@ -1,46 +1,24 @@
<template> <template>
<component <BaseButton class="dropdown-item">
:is="componentNodeName"
v-bind="elementBindings"
:to="to"
class="dropdown-item">
<span class="icon" v-if="icon"> <span class="icon" v-if="icon">
<icon :icon="icon"/> <Icon :icon="icon"/>
</span> </span>
<span> <span>
<slot></slot> <slot />
</span> </span>
</component> </BaseButton>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref, useAttrs, watchEffect} from 'vue' import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.vue'
import Icon from '@/components/misc/Icon'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = defineProps<{ export interface DropDownItemProps extends BaseButtonProps {
to?: object, icon?: IconProp,
icon?: string | string[], }
}>()
const componentNodeName = ref<Node['nodeName']>('a') defineProps<DropDownItemProps>()
const elementBindings = ref({})
const attrs = useAttrs()
watchEffect(() => {
let nodeName = 'a'
if (props.to) {
nodeName = 'router-link'
}
if ('href' in attrs) {
nodeName = 'BaseButton'
}
componentNodeName.value = nodeName
elementBindings.value = {
...attrs,
}
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -91,5 +69,4 @@ button.dropdown-item {
} }
} }
} }
</style> </style>

View File

@ -17,14 +17,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import {ref, type PropType} from 'vue'
import {onClickOutside} from '@vueuse/core' import {onClickOutside} from '@vueuse/core'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
defineProps({ defineProps({
triggerIcon: { triggerIcon: {
type: String, type: String as PropType<IconProp>,
default: 'ellipsis-h', default: 'ellipsis-h',
}, },
}) })

View File

@ -1,20 +1,20 @@
<template> <template>
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot> <slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
<div class="popup" :class="{'is-open': open, 'has-overflow': props.hasOverflow && open}" ref="popup"> <div
class="popup"
:class="{
'is-open': open,
'has-overflow': props.hasOverflow && open
}"
ref="popup"
>
<slot name="content" :isOpen="open"/> <slot name="content" :isOpen="open"/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {ref} from 'vue'
import {onBeforeUnmount, onMounted, ref} from 'vue' import {onClickOutside} from '@vueuse/core'
const open = ref(false)
const popup = ref(null)
const toggle = () => {
open.value = !open.value
}
const props = defineProps({ const props = defineProps({
hasOverflow: { hasOverflow: {
@ -23,24 +23,22 @@ const props = defineProps({
}, },
}) })
function hidePopup(e) { const open = ref(false)
const popup = ref<HTMLElement | null>(null)
function close() {
open.value = false
}
function toggle() {
open.value = !open.value
}
onClickOutside(popup, () => {
if (!open.value) { if (!open.value) {
return return
} }
close()
// we actually want to use popup.$el, not its value.
// eslint-disable-next-line vue/no-ref-as-operand
closeWhenClickedOutside(e, popup.value, () => {
open.value = false
})
}
onMounted(() => {
document.addEventListener('click', hidePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hidePopup)
}) })
</script> </script>

View File

@ -13,7 +13,7 @@
v-else-if="type === 'dropdown'" v-else-if="type === 'dropdown'"
v-tooltip="tooltipText" v-tooltip="tooltipText"
@click="changeSubscription" @click="changeSubscription"
:class="{'is-disabled': disabled}" :disabled="disabled"
:icon="iconName" :icon="iconName"
> >
{{ buttonText }} {{ buttonText }}
@ -44,6 +44,7 @@ import SubscriptionModel from '@/models/subscription'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {success} from '@/message' import {success} from '@/message'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = defineProps({ const props = defineProps({
entity: String, entity: String,
@ -104,7 +105,7 @@ const tooltipText = computed(() => {
}) })
const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe')) const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const iconName = computed(() => props.modelValue ? ['far', 'bell-slash'] : 'bell') const iconName = computed<IconProp>(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity) const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity)
function changeSubscription() { function changeSubscription() {

View File

@ -76,7 +76,7 @@ const notifications = computed(() => {
}) })
const userInfo = computed(() => authStore.info) const userInfo = computed(() => authStore.info)
let interval: number let interval: ReturnType<typeof setInterval>
onMounted(() => { onMounted(() => {
loadNotifications() loadNotifications()

View File

@ -214,7 +214,7 @@ async function addTask() {
return rel return rel
}) })
await Promise.all(relations) await Promise.all(relations)
} catch (e: { message?: string }) { } catch (e: any) {
newTaskTitle.value = taskTitleBackup newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') { if (e?.message === 'NO_LIST') {
errorMessage.value = t('list.create.addListRequired') errorMessage.value = t('list.create.addListRequired')

View File

@ -165,7 +165,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment' import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
@ -227,9 +226,9 @@ function uploadFilesToTask(files: File[] | FileList) {
uploadFiles(attachmentService, props.task.id, files) uploadFiles(attachmentService, props.task.id, files)
} }
const attachmentToDelete = ref<AttachmentModel | null>(null) const attachmentToDelete = ref<IAttachment | null>(null)
function setAttachmentToDelete(attachment: AttachmentModel | null) { function setAttachmentToDelete(attachment: IAttachment | null) {
attachmentToDelete.value = attachment attachmentToDelete.value = attachment
} }
@ -250,7 +249,7 @@ async function deleteAttachment() {
const attachmentImageBlobUrl = ref<string | null>(null) const attachmentImageBlobUrl = ref<string | null>(null)
async function viewOrDownload(attachment: AttachmentModel) { async function viewOrDownload(attachment: IAttachment) {
if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) { if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment) attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else { } else {

View File

@ -4,7 +4,7 @@
<Done class="heading__done" :is-done="task.done"/> <Done class="heading__done" :is-done="task.done"/>
<ColorBubble <ColorBubble
v-if="task.hexColor !== ''" v-if="task.hexColor !== ''"
:color="task.getHexColor()" :color="getHexColor(task.hexColor)"
class="mt-1 ml-2" class="mt-1 ml-2"
/> />
<h1 <h1
@ -48,6 +48,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {getHexColor} from '@/models/task'
const props = defineProps({ const props = defineProps({
task: { task: {

View File

@ -9,9 +9,9 @@
v-model="list" v-model="list"
:select-placeholder="$t('list.searchSelect')" :select-placeholder="$t('list.searchSelect')"
> >
<template #searchResult="props"> <template #searchResult="{option}">
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span> <span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
{{ props.option.title }} {{ (option as IList).title }}
</template> </template>
</Multiselect> </Multiselect>
</template> </template>
@ -25,6 +25,7 @@ import type {IList} from '@/modelTypes/IList'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import type { INamespace } from '@/modelTypes/INamespace'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -65,7 +66,7 @@ function select(l: IList | null) {
emit('update:modelValue', list) emit('update:modelValue', list)
} }
function namespace(namespaceId: number) { function namespace(namespaceId: INamespace['id']) {
const namespace = namespaceStore.getNamespaceById(namespaceId) const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null return namespace !== null
? namespace.title ? namespace.title

View File

@ -2,7 +2,7 @@ import type {Directive} from 'vue'
import {install, uninstall} from '@github/hotkey' import {install, uninstall} from '@github/hotkey'
import {isAppleDevice} from '@/helpers/isAppleDevice' import {isAppleDevice} from '@/helpers/isAppleDevice'
const directive: Directive = { const directive = <Directive<HTMLElement,string>>{
mounted(el, {value}) { mounted(el, {value}) {
if(value === '') { if(value === '') {
return return

View File

@ -3,17 +3,15 @@ import {snakeCase} from 'snake-case'
/** /**
* Transforms field names to camel case. * Transforms field names to camel case.
* @param object
* @returns {*}
*/ */
export function objectToCamelCase(object) { export function objectToCamelCase(object: Record<string, any>) {
// When calling recursively, this can be called without being and object or array in which case we just return the value // When calling recursively, this can be called without being and object or array in which case we just return the value
if (typeof object !== 'object') { if (typeof object !== 'object') {
return object return object
} }
const parsedObject = {} const parsedObject: Record<string, any> = {}
for (const m in object) { for (const m in object) {
parsedObject[camelCase(m)] = object[m] parsedObject[camelCase(m)] = object[m]
@ -25,7 +23,7 @@ export function objectToCamelCase(object) {
// Call it again for arrays // Call it again for arrays
if (Array.isArray(object[m])) { if (Array.isArray(object[m])) {
parsedObject[camelCase(m)] = object[m].map(o => objectToCamelCase(o)) parsedObject[camelCase(m)] = object[m].map((o: Record<string, any>) => objectToCamelCase(o))
// Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects. // Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects.
continue continue
} }
@ -40,17 +38,15 @@ export function objectToCamelCase(object) {
/** /**
* Transforms field names to snake case - used before making an api request. * Transforms field names to snake case - used before making an api request.
* @param object
* @returns {*}
*/ */
export function objectToSnakeCase(object) { export function objectToSnakeCase(object: Record<string, any>) {
// When calling recursively, this can be called without being and object or array in which case we just return the value // When calling recursively, this can be called without being and object or array in which case we just return the value
if (typeof object !== 'object') { if (typeof object !== 'object') {
return object return object
} }
const parsedObject = {} const parsedObject: Record<string, any> = {}
for (const m in object) { for (const m in object) {
parsedObject[snakeCase(m)] = object[m] parsedObject[snakeCase(m)] = object[m]
@ -65,7 +61,7 @@ export function objectToSnakeCase(object) {
// Call it again for arrays // Call it again for arrays
if (Array.isArray(object[m])) { if (Array.isArray(object[m])) {
parsedObject[snakeCase(m)] = object[m].map(o => objectToSnakeCase(o)) parsedObject[snakeCase(m)] = object[m].map((o: Record<string, any>) => objectToSnakeCase(o))
// Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects. // Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects.
continue continue
} }

View File

@ -5,11 +5,11 @@
* @param rootElement * @param rootElement
* @param closeCallback A closure function to call when the click event happened outside of the rootElement. * @param closeCallback A closure function to call when the click event happened outside of the rootElement.
*/ */
export const closeWhenClickedOutside = (event, rootElement, closeCallback) => { export const closeWhenClickedOutside = (event: MouseEvent, rootElement: HTMLElement, closeCallback: () => void) => {
// We walk up the tree to see if any parent of the clicked element is the root element. // We walk up the tree to see if any parent of the clicked element is the root element.
// If it is not, we call the close callback. We're doing all this hassle to only call the // If it is not, we call the close callback. We're doing all this hassle to only call the
// closing callback when a click happens outside of the rootElement. // closing callback when a click happens outside of the rootElement.
let parent = event.target.parentElement let parent = (event.target as HTMLElement)?.parentElement
while (parent !== rootElement) { while (parent !== rootElement) {
if (parent === null || parent.parentElement === null) { if (parent === null || parent.parentElement === null) {
parent = null parent = null

View File

@ -35,7 +35,7 @@ export function setupMarkdownRenderer(checkboxId: string) {
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ') return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
}, },
}, },
highlight(code, language) { highlight(code: string, language: string) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext' const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(code, {language: validLanguage}).value return hljs.highlight(code, {language: validLanguage}).value
}, },

View File

@ -1,12 +1,12 @@
/** /**
* Make date objects from timestamps * Make date objects from timestamps
*/ */
export function parseDateOrNull(date) { export function parseDateOrNull(date: string | Date) {
if (date instanceof Date) { if (date instanceof Date) {
return date return date
} }
if ((typeof date === 'string' || date instanceof String) && !date.startsWith('0001')) { if ((typeof date === 'string') && !date.startsWith('0001')) {
return new Date(date) return new Date(date)
} }

View File

@ -1,21 +1,29 @@
// Save the current list view to local storage import type { IList } from '@/modelTypes/IList'
// We use local storage and not a store here to make it persistent across reloads.
export const saveListView = (listId, routeName) => { type ListView = Record<IList['id'], string>
const DEFAULT_LIST_VIEW = 'list.list' as const
/**
* Save the current list view to local storage
*/
export function saveListView(listId: IList['id'], routeName: string) {
if (routeName.includes('settings.')) { if (routeName.includes('settings.')) {
return return
} }
if (!listId) { if (!listId) {
return return
} }
// We use local storage and not the store here to make it persistent across reloads.
const savedListView = localStorage.getItem('listView') const savedListView = localStorage.getItem('listView')
let savedListViewJson = false let savedListViewJson: ListView | false = false
if (savedListView !== null) { if (savedListView !== null) {
savedListViewJson = JSON.parse(savedListView) savedListViewJson = JSON.parse(savedListView) as ListView
} }
let listView = {} let listView: ListView = {}
if (savedListViewJson) { if (savedListViewJson) {
listView = savedListViewJson listView = savedListViewJson
} }
@ -24,7 +32,7 @@ export const saveListView = (listId, routeName) => {
localStorage.setItem('listView', JSON.stringify(listView)) localStorage.setItem('listView', JSON.stringify(listView))
} }
export const getListView = listId => { export const getListView = (listId: IList['id']) => {
// Remove old stored settings // Remove old stored settings
const savedListView = localStorage.getItem('listView') const savedListView = localStorage.getItem('listView')
if (savedListView !== null && savedListView.startsWith('list.')) { if (savedListView !== null && savedListView.startsWith('list.')) {
@ -32,13 +40,13 @@ export const getListView = listId => {
} }
if (!savedListView) { if (!savedListView) {
return 'list.list' return DEFAULT_LIST_VIEW
} }
const savedListViewJson = JSON.parse(savedListView) const savedListViewJson: ListView = JSON.parse(savedListView)
if (!savedListViewJson[listId]) { if (!savedListViewJson[listId]) {
return 'list.list' return DEFAULT_LIST_VIEW
} }
return savedListViewJson[listId] return savedListViewJson[listId]

View File

@ -10,7 +10,7 @@ const days = {
friday: 5, friday: 5,
saturday: 6, saturday: 6,
sunday: 0, sunday: 0,
} } as Record<string, number>
for (const n in days) { for (const n in days) {
test(`today on a ${n}`, () => { test(`today on a ${n}`, () => {
@ -32,7 +32,7 @@ const nextMonday = {
friday: 3, friday: 3,
saturday: 2, saturday: 2,
sunday: 1, sunday: 1,
} } as Record<string, number>
for (const n in nextMonday) { for (const n in nextMonday) {
test(`next monday on a ${n}`, () => { test(`next monday on a ${n}`, () => {
@ -48,7 +48,7 @@ const thisWeekend = {
friday: 1, friday: 1,
saturday: 0, saturday: 0,
sunday: 0, sunday: 0,
} } as Record<string, number>
for (const n in thisWeekend) { for (const n in thisWeekend) {
test(`this weekend on a ${n}`, () => { test(`this weekend on a ${n}`, () => {
@ -64,7 +64,7 @@ const laterThisWeek = {
friday: 0, friday: 0,
saturday: 0, saturday: 0,
sunday: 0, sunday: 0,
} } as Record<string, number>
for (const n in laterThisWeek) { for (const n in laterThisWeek) {
test(`later this week on a ${n}`, () => { test(`later this week on a ${n}`, () => {
@ -80,7 +80,7 @@ const laterNextWeek = {
friday: 7 + 0, friday: 7 + 0,
saturday: 7 + 0, saturday: 7 + 0,
sunday: 7 + 0, sunday: 7 + 0,
} } as Record<string, number>
for (const n in laterNextWeek) { for (const n in laterNextWeek) {
test(`later next week on a ${n} (this week)`, () => { test(`later next week on a ${n} (this week)`, () => {

View File

@ -1,4 +1,6 @@
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())) { type Day<T extends number = number> = T
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())): Day {
switch (dateString) { switch (dateString) {
case 'today': case 'today':
return 0 return 0

View File

@ -6,7 +6,7 @@
* @param dateString * @param dateString
* @returns {Date} * @returns {Date}
*/ */
export const createDateFromString = dateString => { export function createDateFromString(dateString: string | Date) {
if (dateString instanceof Date) { if (dateString instanceof Date) {
return dateString return dateString
} }

View File

@ -8,7 +8,7 @@ import {i18n} from '@/i18n'
const locales = {en: enGB, de, ch: de, fr, ru} const locales = {en: enGB, de, ch: de, fr, ru}
export function dateIsValid(date) { export function dateIsValid(date: Date | null) {
if (date === null) { if (date === null) {
return false return false
} }

View File

@ -36,7 +36,7 @@ export interface ITask extends IAbstract {
percentDone: number percentDone: number
relatedTasks: Partial<Record<IRelationKind, ITask[]>> relatedTasks: Partial<Record<IRelationKind, ITask[]>>
attachments: IAttachment[] attachments: IAttachment[]
coverImageAttachmentId: IAttachment['id'] coverImageAttachmentId: IAttachment['id'] | null
identifier: string identifier: string
index: number index: number
isFavorite: boolean isFavorite: boolean

View File

@ -20,4 +20,7 @@ export interface IUser extends IAbstract {
created: Date created: Date
updated: Date updated: Date
settings: IUserSettings settings: IUserSettings
isLocalUser: boolean
deletionScheduledAt: string | Date | null
} }

View File

@ -8,6 +8,7 @@ export interface IUserSettings extends IAbstract {
discoverableByName: boolean discoverableByName: boolean
discoverableByEmail: boolean discoverableByEmail: boolean
overdueTasksRemindersEnabled: boolean overdueTasksRemindersEnabled: boolean
overdueTasksRemindersTime: any
defaultListId: undefined | IList['id'] defaultListId: undefined | IList['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6 weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string timezone: string

View File

@ -6,7 +6,7 @@ export default class EmailUpdateModel extends AbstractModel<IEmailUpdate> implem
newEmail = '' newEmail = ''
password = '' password = ''
constructor(data : Partial<IEmailUpdate>) { constructor(data : Partial<IEmailUpdate> = {}) {
super() super()
this.assignData(data) this.assignData(data)
} }

View File

@ -6,7 +6,7 @@ export default class PasswordUpdateModel extends AbstractModel<IPasswordUpdate>
newPassword = '' newPassword = ''
oldPassword = '' oldPassword = ''
constructor(data: Partial<IPasswordUpdate>) { constructor(data: Partial<IPasswordUpdate> = {}) {
super() super()
this.assignData(data) this.assignData(data)
} }

View File

@ -79,10 +79,12 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
percentDone = 0 percentDone = 0
relatedTasks: Partial<Record<IRelationKind, ITask[]>> = {} relatedTasks: Partial<Record<IRelationKind, ITask[]>> = {}
attachments: IAttachment[] = [] attachments: IAttachment[] = []
coverImageAttachmentId: IAttachment['id'] = null
identifier = '' identifier = ''
index = 0 index = 0
isFavorite = false isFavorite = false
subscription: ISubscription = null subscription: ISubscription = null
coverImageAttachmentId: IAttachment['id'] = null
position = 0 position = 0
kanbanPosition = 0 kanbanPosition = 0

View File

@ -28,6 +28,9 @@ export default class UserModel extends AbstractModel<IUser> implements IUser {
updated: Date updated: Date
settings: IUserSettings settings: IUserSettings
isLocalUser: boolean
deletionScheduledAt: null
constructor(data: Partial<IUser> = {}) { constructor(data: Partial<IUser> = {}) {
super() super()
this.assignData(data) this.assignData(data)

View File

@ -9,6 +9,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
discoverableByName = false discoverableByName = false
discoverableByEmail = false discoverableByEmail = false
overdueTasksRemindersEnabled = true overdueTasksRemindersEnabled = true
overdueTasksRemindersTime = undefined
defaultListId = undefined defaultListId = undefined
weekStart = 0 as IUserSettings['weekStart'] weekStart = 0 as IUserSettings['weekStart']
timezone = '' timezone = ''

View File

@ -1,4 +1,4 @@
interface ListHistory { export interface ListHistory {
id: number; id: number;
} }

View File

@ -4,7 +4,8 @@ import {parseTaskText, PrefixMode} from './parseTaskText'
import {getDateFromText, parseDate} from '../helpers/time/parseDate' import {getDateFromText, parseDate} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval' import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import {PRIORITIES} from '@/constants/priorities' import {PRIORITIES} from '@/constants/priorities'
import { MILLISECONDS_A_DAY } from '@/constants/date' import {MILLISECONDS_A_DAY} from '@/constants/date'
import type {IRepeatAfter} from '@/types/IRepeatAfter'
describe('Parse Task Text', () => { describe('Parse Task Text', () => {
beforeEach(() => { beforeEach(() => {
@ -31,9 +32,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const now = new Date() const now = new Date()
expect(result.date.getFullYear()).toBe(now.getFullYear()) expect(result?.date?.getFullYear()).toBe(now.getFullYear())
expect(result.date.getMonth()).toBe(now.getMonth()) expect(result?.date?.getMonth()).toBe(now.getMonth())
expect(result.date.getDate()).toBe(now.getDate()) expect(result?.date?.getDate()).toBe(now.getDate())
expect(result.labels).toHaveLength(1) expect(result.labels).toHaveLength(1)
expect(result.labels[0]).toBe('label') expect(result.labels[0]).toBe('label')
expect(result.list).toBe('list') expect(result.list).toBe('list')
@ -61,18 +62,18 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const now = new Date() const now = new Date()
expect(result.date.getFullYear()).toBe(now.getFullYear()) expect(result?.date?.getFullYear()).toBe(now.getFullYear())
expect(result.date.getMonth()).toBe(now.getMonth()) expect(result?.date?.getMonth()).toBe(now.getMonth())
expect(result.date.getDate()).toBe(now.getDate()) expect(result?.date?.getDate()).toBe(now.getDate())
}) })
it('should recognize today', () => { it('should recognize today', () => {
const result = parseTaskText('Lorem Ipsum today') const result = parseTaskText('Lorem Ipsum today')
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const now = new Date() const now = new Date()
expect(result.date.getFullYear()).toBe(now.getFullYear()) expect(result?.date?.getFullYear()).toBe(now.getFullYear())
expect(result.date.getMonth()).toBe(now.getMonth()) expect(result?.date?.getMonth()).toBe(now.getMonth())
expect(result.date.getDate()).toBe(now.getDate()) expect(result?.date?.getDate()).toBe(now.getDate())
}) })
describe('should recognize today with a time', () => { describe('should recognize today with a time', () => {
const cases = { const cases = {
@ -93,11 +94,11 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const now = new Date() const now = new Date()
expect(result.date.getFullYear()).toBe(now.getFullYear()) expect(result?.date?.getFullYear()).toBe(now.getFullYear())
expect(result.date.getMonth()).toBe(now.getMonth()) expect(result?.date?.getMonth()).toBe(now.getMonth())
expect(result.date.getDate()).toBe(now.getDate()) expect(result?.date?.getDate()).toBe(now.getDate())
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe(cases[c as keyof typeof cases]) expect(`${result?.date?.getHours()}:${result?.date?.getMinutes()}`).toBe(cases[c as keyof typeof cases])
expect(result.date.getSeconds()).toBe(0) expect(result?.date?.getSeconds()).toBe(0)
}) })
} }
}) })
@ -107,9 +108,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const tomorrow = new Date() const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1) tomorrow.setDate(tomorrow.getDate() + 1)
expect(result.date.getFullYear()).toBe(tomorrow.getFullYear()) expect(result?.date?.getFullYear()).toBe(tomorrow.getFullYear())
expect(result.date.getMonth()).toBe(tomorrow.getMonth()) expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
expect(result.date.getDate()).toBe(tomorrow.getDate()) expect(result?.date?.getDate()).toBe(tomorrow.getDate())
}) })
it('should recognize next monday', () => { it('should recognize next monday', () => {
const result = parseTaskText('Lorem Ipsum next monday') const result = parseTaskText('Lorem Ipsum next monday')
@ -119,9 +120,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const nextMonday = new Date() const nextMonday = new Date()
nextMonday.setDate(nextMonday.getDate() + untilNextMonday) nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
expect(result.date.getFullYear()).toBe(nextMonday.getFullYear()) expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
expect(result.date.getMonth()).toBe(nextMonday.getMonth()) expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
expect(result.date.getDate()).toBe(nextMonday.getDate()) expect(result?.date?.getDate()).toBe(nextMonday.getDate())
}) })
it('should recognize next monday and ignore casing', () => { it('should recognize next monday and ignore casing', () => {
const result = parseTaskText('Lorem Ipsum nExt Monday') const result = parseTaskText('Lorem Ipsum nExt Monday')
@ -131,9 +132,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const nextMonday = new Date() const nextMonday = new Date()
nextMonday.setDate(nextMonday.getDate() + untilNextMonday) nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
expect(result.date.getFullYear()).toBe(nextMonday.getFullYear()) expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
expect(result.date.getMonth()).toBe(nextMonday.getMonth()) expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
expect(result.date.getDate()).toBe(nextMonday.getDate()) expect(result?.date?.getDate()).toBe(nextMonday.getDate())
}) })
it('should recognize this weekend', () => { it('should recognize this weekend', () => {
const result = parseTaskText('Lorem Ipsum this weekend') const result = parseTaskText('Lorem Ipsum this weekend')
@ -143,9 +144,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const thisWeekend = new Date() const thisWeekend = new Date()
thisWeekend.setDate(thisWeekend.getDate() + untilThisWeekend) thisWeekend.setDate(thisWeekend.getDate() + untilThisWeekend)
expect(result.date.getFullYear()).toBe(thisWeekend.getFullYear()) expect(result?.date?.getFullYear()).toBe(thisWeekend.getFullYear())
expect(result.date.getMonth()).toBe(thisWeekend.getMonth()) expect(result?.date?.getMonth()).toBe(thisWeekend.getMonth())
expect(result.date.getDate()).toBe(thisWeekend.getDate()) expect(result?.date?.getDate()).toBe(thisWeekend.getDate())
}) })
it('should recognize later this week', () => { it('should recognize later this week', () => {
const result = parseTaskText('Lorem Ipsum later this week') const result = parseTaskText('Lorem Ipsum later this week')
@ -155,9 +156,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const laterThisWeek = new Date() const laterThisWeek = new Date()
laterThisWeek.setDate(laterThisWeek.getDate() + untilLaterThisWeek) laterThisWeek.setDate(laterThisWeek.getDate() + untilLaterThisWeek)
expect(result.date.getFullYear()).toBe(laterThisWeek.getFullYear()) expect(result?.date?.getFullYear()).toBe(laterThisWeek.getFullYear())
expect(result.date.getMonth()).toBe(laterThisWeek.getMonth()) expect(result?.date?.getMonth()).toBe(laterThisWeek.getMonth())
expect(result.date.getDate()).toBe(laterThisWeek.getDate()) expect(result?.date?.getDate()).toBe(laterThisWeek.getDate())
}) })
it('should recognize later next week', () => { it('should recognize later next week', () => {
const result = parseTaskText('Lorem Ipsum later next week') const result = parseTaskText('Lorem Ipsum later next week')
@ -167,9 +168,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const laterNextWeek = new Date() const laterNextWeek = new Date()
laterNextWeek.setDate(laterNextWeek.getDate() + untilLaterNextWeek) laterNextWeek.setDate(laterNextWeek.getDate() + untilLaterNextWeek)
expect(result.date.getFullYear()).toBe(laterNextWeek.getFullYear()) expect(result?.date?.getFullYear()).toBe(laterNextWeek.getFullYear())
expect(result.date.getMonth()).toBe(laterNextWeek.getMonth()) expect(result?.date?.getMonth()).toBe(laterNextWeek.getMonth())
expect(result.date.getDate()).toBe(laterNextWeek.getDate()) expect(result?.date?.getDate()).toBe(laterNextWeek.getDate())
}) })
it('should recognize next week', () => { it('should recognize next week', () => {
const result = parseTaskText('Lorem Ipsum next week') const result = parseTaskText('Lorem Ipsum next week')
@ -179,9 +180,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const nextWeek = new Date() const nextWeek = new Date()
nextWeek.setDate(nextWeek.getDate() + untilNextWeek) nextWeek.setDate(nextWeek.getDate() + untilNextWeek)
expect(result.date.getFullYear()).toBe(nextWeek.getFullYear()) expect(result?.date?.getFullYear()).toBe(nextWeek.getFullYear())
expect(result.date.getMonth()).toBe(nextWeek.getMonth()) expect(result?.date?.getMonth()).toBe(nextWeek.getMonth())
expect(result.date.getDate()).toBe(nextWeek.getDate()) expect(result?.date?.getDate()).toBe(nextWeek.getDate())
}) })
it('should recognize next month', () => { it('should recognize next month', () => {
const result = parseTaskText('Lorem Ipsum next month') const result = parseTaskText('Lorem Ipsum next month')
@ -190,9 +191,9 @@ describe('Parse Task Text', () => {
const nextMonth = new Date() const nextMonth = new Date()
nextMonth.setDate(1) nextMonth.setDate(1)
nextMonth.setMonth(nextMonth.getMonth() + 1) nextMonth.setMonth(nextMonth.getMonth() + 1)
expect(result.date.getFullYear()).toBe(nextMonth.getFullYear()) expect(result?.date?.getFullYear()).toBe(nextMonth.getFullYear())
expect(result.date.getMonth()).toBe(nextMonth.getMonth()) expect(result?.date?.getMonth()).toBe(nextMonth.getMonth())
expect(result.date.getDate()).toBe(nextMonth.getDate()) expect(result?.date?.getDate()).toBe(nextMonth.getDate())
}) })
it('should recognize a date', () => { it('should recognize a date', () => {
const result = parseTaskText('Lorem Ipsum 06/26/2021') const result = parseTaskText('Lorem Ipsum 06/26/2021')
@ -200,9 +201,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const date = new Date() const date = new Date()
date.setFullYear(2021, 5, 26) date.setFullYear(2021, 5, 26)
expect(result.date.getFullYear()).toBe(date.getFullYear()) expect(result?.date?.getFullYear()).toBe(date.getFullYear())
expect(result.date.getMonth()).toBe(date.getMonth()) expect(result?.date?.getMonth()).toBe(date.getMonth())
expect(result.date.getDate()).toBe(date.getDate()) expect(result?.date?.getDate()).toBe(date.getDate())
}) })
it('should recognize end of month', () => { it('should recognize end of month', () => {
const result = parseTaskText('Lorem Ipsum end of month') const result = parseTaskText('Lorem Ipsum end of month')
@ -210,9 +211,9 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const curDate = new Date() const curDate = new Date()
const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0) const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
expect(result.date.getFullYear()).toBe(date.getFullYear()) expect(result?.date?.getFullYear()).toBe(date.getFullYear())
expect(result.date.getMonth()).toBe(date.getMonth()) expect(result?.date?.getMonth()).toBe(date.getMonth())
expect(result.date.getDate()).toBe(date.getDate()) expect(result?.date?.getDate()).toBe(date.getDate())
}) })
const cases = { const cases = {
@ -244,7 +245,7 @@ describe('Parse Task Text', () => {
'Sunday': 7, 'Sunday': 7,
'sun': 7, 'sun': 7,
'Sun': 7, 'Sun': 7,
} } as Record<string, number>
for (const c in cases) { for (const c in cases) {
it(`should recognize ${c} as weekday`, () => { it(`should recognize ${c} as weekday`, () => {
const result = parseTaskText(`Lorem Ipsum ${c}`) const result = parseTaskText(`Lorem Ipsum ${c}`)
@ -252,7 +253,7 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const nextDate = new Date() const nextDate = new Date()
nextDate.setDate(nextDate.getDate() + ((cases[c] + 7 - nextDate.getDay()) % 7)) nextDate.setDate(nextDate.getDate() + ((cases[c] + 7 - nextDate.getDay()) % 7))
expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`) expect(`${result?.date?.getFullYear()}-${result?.date?.getMonth()}-${result?.date?.getDate()}`).toBe(`${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`)
}) })
} }
it('should recognize weekdays with time', () => { it('should recognize weekdays with time', () => {
@ -261,8 +262,8 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
const nextThursday = new Date() const nextThursday = new Date()
nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7)) nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7))
expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`) expect(`${result?.date?.getFullYear()}-${result?.date?.getMonth()}-${result?.date?.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`)
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe('14:0') expect(`${result?.date?.getHours()}:${result?.date?.getMinutes()}`).toBe('14:0')
}) })
it('should recognize dates of the month in the past but next month', () => { it('should recognize dates of the month in the past but next month', () => {
const time = new Date(2022, 0, 15) const time = new Date(2022, 0, 15)
@ -271,8 +272,8 @@ describe('Parse Task Text', () => {
const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`) const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(time.getDate() - 1) expect(result?.date?.getDate()).toBe(time.getDate() - 1)
expect(result.date.getMonth()).toBe(time.getMonth() + 1) expect(result?.date?.getMonth()).toBe(time.getMonth() + 1)
}) })
it('should recognize dates of the month in the past but next month when february is the next month', () => { it('should recognize dates of the month in the past but next month when february is the next month', () => {
const jan = new Date(2022, 0, 30) const jan = new Date(2022, 0, 30)
@ -282,8 +283,8 @@ describe('Parse Task Text', () => {
const expectedDate = new Date(2022, 2, jan.getDate() - 1) const expectedDate = new Date(2022, 2, jan.getDate() - 1)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(expectedDate.getDate()) expect(result?.date?.getDate()).toBe(expectedDate.getDate())
expect(result.date.getMonth()).toBe(expectedDate.getMonth()) expect(result?.date?.getMonth()).toBe(expectedDate.getMonth())
}) })
it('should recognize dates of the month in the past but next month when the next month has less days than this one', () => { it('should recognize dates of the month in the past but next month when the next month has less days than this one', () => {
const mar = new Date(2022, 2, 32) const mar = new Date(2022, 2, 32)
@ -293,15 +294,15 @@ describe('Parse Task Text', () => {
const expectedDate = new Date(2022, 4, 31) const expectedDate = new Date(2022, 4, 31)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(expectedDate.getDate()) expect(result?.date?.getDate()).toBe(expectedDate.getDate())
expect(result.date.getMonth()).toBe(expectedDate.getMonth()) expect(result?.date?.getMonth()).toBe(expectedDate.getMonth())
}) })
it('should recognize dates of the month in the future', () => { it('should recognize dates of the month in the future', () => {
const nextDay = new Date(+new Date() + MILLISECONDS_A_DAY) const nextDay = new Date(+new Date() + MILLISECONDS_A_DAY)
const result = parseTaskText(`Lorem Ipsum ${nextDay.getDate()}nd`) const result = parseTaskText(`Lorem Ipsum ${nextDay.getDate()}nd`)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(nextDay.getDate()) expect(result?.date?.getDate()).toBe(nextDay.getDate())
}) })
it('should only recognize weekdays with a space before or after them 1', () => { it('should only recognize weekdays with a space before or after them 1', () => {
const result = parseTaskText('Lorem Ipsum renewed') const result = parseTaskText('Lorem Ipsum renewed')
@ -382,7 +383,7 @@ describe('Parse Task Text', () => {
'saturday': 6, 'saturday': 6,
'sun': 7, 'sun': 7,
'sunday': 7, 'sunday': 7,
} } as Record<string, number>
const prefix = [ const prefix = [
'next ', 'next ',
@ -399,9 +400,9 @@ describe('Parse Task Text', () => {
next.setDate(next.getDate() + distance) next.setDate(next.getDate() + distance)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getFullYear()).toBe(next.getFullYear()) expect(result?.date?.getFullYear()).toBe(next.getFullYear())
expect(result.date.getMonth()).toBe(next.getMonth()) expect(result?.date?.getMonth()).toBe(next.getMonth())
expect(result.date.getDate()).toBe(next.getDate()) expect(result?.date?.getDate()).toBe(next.getDate())
}) })
} }
}) })
@ -462,7 +463,7 @@ describe('Parse Task Text', () => {
'dolor sit amet oct 21': '2021-10-21', 'dolor sit amet oct 21': '2021-10-21',
'dolor sit amet nov 21': '2021-11-21', 'dolor sit amet nov 21': '2021-11-21',
'dolor sit amet dec 21': '2021-12-21', 'dolor sit amet dec 21': '2021-12-21',
} } as Record<string, string | null>
for (const c in cases) { for (const c in cases) {
it(`should parse '${c}' as '${cases[c]}'`, () => { it(`should parse '${c}' as '${cases[c]}'`, () => {
@ -472,7 +473,7 @@ describe('Parse Task Text', () => {
return return
} }
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`).toBe(cases[c]) expect(`${date?.getFullYear()}-${date.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
}) })
} }
}) })
@ -510,7 +511,7 @@ describe('Parse Task Text', () => {
'Something at 10:00 in 5 days': '2021-6-29 10:0', 'Something at 10:00 in 5 days': '2021-6-29 10:0',
'Something at 10:00 17th': '2021-7-17 10:0', 'Something at 10:00 17th': '2021-7-17 10:0',
'Something at 10:00 sep 17th': '2021-9-17 10:0', 'Something at 10:00 sep 17th': '2021-9-17 10:0',
} } as Record<string, string>
for (const c in cases) { for (const c in cases) {
it(`should parse '${c}' as '${cases[c]}'`, () => { it(`should parse '${c}' as '${cases[c]}'`, () => {
@ -695,15 +696,15 @@ describe('Parse Task Text', () => {
'every eight hours': {type: 'hours', amount: 8}, 'every eight hours': {type: 'hours', amount: 8},
'every nine hours': {type: 'hours', amount: 9}, 'every nine hours': {type: 'hours', amount: 9},
'every ten hours': {type: 'hours', amount: 10}, 'every ten hours': {type: 'hours', amount: 10},
} } as Record<string, IRepeatAfter>
for (const c in cases) { for (const c in cases) {
it(`should parse ${c} as recurring date every ${cases[c].amount} ${cases[c].type}`, () => { it(`should parse ${c} as recurring date every ${cases[c].amount} ${cases[c].type}`, () => {
const result = parseTaskText(`Lorem Ipsum ${c}`) const result = parseTaskText(`Lorem Ipsum ${c}`)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.repeats.type).toBe(cases[c].type) expect(result?.repeats?.type).toBe(cases[c].type)
expect(result.repeats.amount).toBe(cases[c].amount) expect(result?.repeats?.amount).toBe(cases[c].amount)
}) })
} }
}) })

View File

@ -1,6 +1,8 @@
import type { App } from 'vue'
import type { Router } from 'vue-router'
import {VERSION} from './version.json' import {VERSION} from './version.json'
export default async function setupSentry(app, router) { export default async function setupSentry(app: App, router: Router) {
const Sentry = await import('@sentry/vue') const Sentry = await import('@sentry/vue')
const {Integrations} = await import('@sentry/tracing') const {Integrations} = await import('@sentry/tracing')

View File

@ -7,7 +7,7 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
import {downloadBlob} from '@/helpers/downloadBlob' import {downloadBlob} from '@/helpers/downloadBlob'
export default class AttachmentService extends AbstractService<AttachmentModel> { export default class AttachmentService extends AbstractService<IAttachment> {
constructor() { constructor() {
super({ super({
create: '/tasks/{taskId}/attachments', create: '/tasks/{taskId}/attachments',

View File

@ -6,7 +6,7 @@ import AbstractService from '../abstractService'
export default class AbstractMigrationFileService extends AbstractService { export default class AbstractMigrationFileService extends AbstractService {
serviceUrlKey = '' serviceUrlKey = ''
constructor(serviceUrlKey: '') { constructor(serviceUrlKey: string) {
super({ super({
create: '/migration/' + serviceUrlKey + '/migrate', create: '/migration/' + serviceUrlKey + '/migrate',
}) })

View File

@ -84,7 +84,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
const filterService = shallowReactive(new SavedFilterService()) const filterService = shallowReactive(new SavedFilterService())
const filter = ref(new SavedFilterModel()) const filter = ref<ISavedFilter>(new SavedFilterModel())
const filters = computed({ const filters = computed({
get: () => filter.value.filters, get: () => filter.value.filters,
set(value) { set(value) {
@ -92,7 +92,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
}, },
}) })
// loadSavedFilter // load SavedFilter
watch(() => unref(listId), async (watchedListId) => { watch(() => unref(listId), async (watchedListId) => {
if (watchedListId === undefined) { if (watchedListId === undefined) {
return return

View File

@ -86,7 +86,7 @@ export const useBaseStore = defineStore('base', () => {
} }
async function handleSetCurrentList( async function handleSetCurrentList(
{list, forceUpdate = false}: {list: IList | null, forceUpdate: boolean}, {list, forceUpdate = false}: {list: IList | null, forceUpdate?: boolean},
) { ) {
if (list === null) { if (list === null) {
setCurrentList({}) setCurrentList({})

View File

@ -1,12 +1,10 @@
import type { StoreDefinition } from 'pinia'
export interface LoadingState { export interface LoadingState {
isLoading: boolean isLoading: boolean
} }
const LOADING_TIMEOUT = 100 const LOADING_TIMEOUT = 100
export const setModuleLoading = <LoadingStore extends StoreDefinition<string, LoadingState>>(store: LoadingStore, loadFunc : ((isLoading: boolean) => void) | null = null) => { export const setModuleLoading = <Store extends LoadingState>(store: Store, loadFunc : ((isLoading: boolean) => void) | null = null) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (loadFunc === null) { if (loadFunc === null) {
store.isLoading = true store.isLoading = true

View File

@ -364,7 +364,7 @@ export const useKanbanStore = defineStore('kanban', {
} }
}, },
async updateBucket(updatedBucketData: IBucket) { async updateBucket(updatedBucketData: Partial<IBucket>) {
const cancel = setModuleLoading(this) const cancel = setModuleLoading(this)
const bucketIndex = findIndexById(this.buckets, updatedBucketData.id) const bucketIndex = findIndexById(this.buckets, updatedBucketData.id)

View File

@ -180,7 +180,7 @@ export const useListStore = defineStore('list', () => {
export function useList(listId: MaybeRef<IList['id']>) { export function useList(listId: MaybeRef<IList['id']>) {
const listService = shallowReactive(new ListService()) const listService = shallowReactive(new ListService())
const {loading: isLoading} = toRefs(listService) const {loading: isLoading} = toRefs(listService)
const list: ListModel = reactive(new ListModel()) const list: IList = reactive(new ListModel())
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
watch( watch(

21
src/types/global-components.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
// import FontAwesomeIcon from '@/components/misc/Icon'
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from './vue-fontawesome'
import type XButton from '@/components/input/button.vue'
import type Modal from '@/components/misc/modal.vue'
import type Card from '@/components/misc/card.vue'
// Here we define globally imported components
// See:
// https://github.com/johnsoncodehk/volar/blob/2ca8fd3434423c7bea1c8e08132df3b9ce84eea7/extensions/vscode-vue-language-features/README.md#usage
// Under the hidden collapsible "Define Global Components"
declare module '@vue/runtime-core' {
export interface GlobalComponents {
Icon: FontAwesomeIconFixedTypes
XButton: typeof XButton,
Modal: typeof Modal,
Card: typeof Card,
}
}
export {}

View File

@ -0,0 +1,40 @@
// copied and slightly modified from unmerged pull request that corrects types
// https://github.com/FortAwesome/vue-fontawesome/pull/355
import type { FaSymbol, FlipProp, IconLookup, IconProp, PullProp, SizeProp, Transform } from '@fortawesome/fontawesome-svg-core'
import type { DefineComponent } from 'vue'
interface FontAwesomeIconProps {
border?: boolean
fixedWidth?: boolean
flip?: FlipProp
icon: IconProp
mask?: IconLookup
listItem?: boolean
pull?: PullProp
pulse?: boolean
rotation?: 90 | 180 | 270 | '90' | '180' | '270'
swapOpacity?: boolean
size?: SizeProp
spin?: boolean
transform?: Transform
symbol?: FaSymbol
title?: string | string[]
inverse?: boolean
}
interface FontAwesomeLayersProps {
fixedWidth?: boolean
}
interface FontAwesomeLayersTextProps {
value: string | number
transform?: object | string
counter?: boolean
position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'
}
export type FontAwesomeIcon = DefineComponent<FontAwesomeIconProps>
export type FontAwesomeLayers = DefineComponent<FontAwesomeLayersProps>
export type FontAwesomeLayersText = DefineComponent<FontAwesomeLayersTextProps>

View File

@ -14,7 +14,6 @@
</router-link> </router-link>
</message> </message>
<add-task <add-task
:listId="defaultListId"
@taskAdded="updateTaskList" @taskAdded="updateTaskList"
class="is-max-width-desktop" class="is-max-width-desktop"
/> />
@ -76,6 +75,7 @@ import {useConfigStore} from '@/stores/config'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import type {IList} from '@/modelTypes/IList'
const salutation = useDaytimeSalutation() const salutation = useDaytimeSalutation()
@ -94,12 +94,11 @@ const listHistory = computed(() => {
return getHistory() return getHistory()
.map(l => listStore.getListById(l.id)) .map(l => listStore.getListById(l.id))
.filter(l => l !== null) .filter((l): l is IList => l !== null)
}) })
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0) const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
const hasTasks = computed(() => baseStore.hasTasks) const hasTasks = computed(() => baseStore.hasTasks)
const defaultListId = computed(() => authStore.settings.defaultListId)
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0) const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)
const hasLists = computed(() => namespaceStore.namespaces?.[0]?.lists.length > 0) const hasLists = computed(() => namespaceStore.namespaces?.[0]?.lists.length > 0)
const loading = computed(() => taskStore.isLoading) const loading = computed(() => taskStore.isLoading)

View File

@ -66,7 +66,7 @@ async function newLabel() {
showError.value = false showError.value = false
const labelStore = useLabelStore() const labelStore = useLabelStore()
const newLabel = labelStore.createLabel(label.value) const newLabel = await labelStore.createLabel(label.value)
router.push({ router.push({
name: 'labels.index', name: 'labels.index',
params: {id: newLabel.id}, params: {id: newLabel.id},

View File

@ -71,11 +71,13 @@ import {useI18n} from 'vue-i18n'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import type {INamespace} from '@/modelTypes/INamespace'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const namespaceService = ref(new NamespaceService()) const namespaceService = ref(new NamespaceService())
const namespace = ref(new NamespaceModel()) const namespace = ref<INamespace>(new NamespaceModel())
const editorActive = ref(false) const editorActive = ref(false)
const title = ref('') const title = ref('')
useTitle(() => title.value) useTitle(() => title.value)

View File

@ -558,7 +558,7 @@ const canWrite = computed(() => (
const color = computed(() => { const color = computed(() => {
const color = task.getHexColor const color = task.getHexColor
? task.getHexColor() ? task.getHexColor()
: false : undefined
return color === TASK_DEFAULT_COLOR return color === TASK_DEFAULT_COLOR
? '' ? ''

View File

@ -50,14 +50,14 @@ async function authenticateWithCode() {
if (localStorage.getItem('authenticating')) { if (localStorage.getItem('authenticating')) {
return return
} }
localStorage.setItem('authenticating', true) localStorage.setItem('authenticating', 'true')
errorMessage.value = '' errorMessage.value = ''
if (typeof route.query.error !== 'undefined') { if (typeof route.query.error !== 'undefined') {
localStorage.removeItem('authenticating') localStorage.removeItem('authenticating')
errorMessage.value = typeof route.query.message !== 'undefined' errorMessage.value = typeof route.query.message !== 'undefined'
? route.query.message ? route.query.message as string
: t('user.auth.openIdGeneralError') : t('user.auth.openIdGeneralError')
return return
} }

View File

@ -130,8 +130,8 @@ async function submit() {
try { try {
await authStore.register(toRaw(credentials)) await authStore.register(toRaw(credentials))
} catch (e) { } catch (e: any) {
errorMessage.value = e.message errorMessage.value = e?.message
} }
} }
</script> </script>

View File

@ -23,7 +23,7 @@
<x-button <x-button
v-if="!isCropAvatar" v-if="!isCropAvatar"
:loading="avatarService.loading || loading" :loading="avatarService.loading || loading"
@click="$refs.avatarUploadInput.click()" @click="avatarUploadInput.click()"
> >
{{ $t('user.settings.avatar.uploadAvatar') }} {{ $t('user.settings.avatar.uploadAvatar') }}
</x-button> </x-button>

View File

@ -41,7 +41,7 @@
<td>{{ tk.id }}</td> <td>{{ tk.id }}</td>
<td>{{ formatDateShort(tk.created) }}</td> <td>{{ formatDateShort(tk.created) }}</td>
<td class="has-text-right"> <td class="has-text-right">
<x-button type="secondary" @click="deleteToken(tk)"> <x-button variant="secondary" @click="deleteToken(tk)">
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</x-button> </x-button>
</td> </td>

View File

@ -246,7 +246,7 @@ watch(
const listStore = useListStore() const listStore = useListStore()
const defaultList = computed({ const defaultList = computed({
get: () => listStore.getListById(settings.value.defaultListId), get: () => listStore.getListById(settings.value.defaultListId) || undefined,
set(l) { set(l) {
settings.value.defaultListId = l ? l.id : DEFAULT_LIST_ID settings.value.defaultListId = l ? l.id : DEFAULT_LIST_ID
}, },

View File

@ -79,13 +79,14 @@ import {success} from '@/message'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import type {ITotp} from '@/modelTypes/ITotp'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(() => `${t('user.settings.totp.title')} - ${t('user.settings.title')}`) useTitle(() => `${t('user.settings.totp.title')} - ${t('user.settings.title')}`)
const totpService = shallowReactive(new TotpService()) const totpService = shallowReactive(new TotpService())
const totp = ref(new TotpModel()) const totp = ref<ITotp>(new TotpModel())
const totpQR = ref('') const totpQR = ref('')
const totpEnrolled = ref(false) const totpEnrolled = ref(false)
const totpConfirmPasscode = ref('') const totpConfirmPasscode = ref('')

View File

@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json", "extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/i18n/lang/*.json"], "include": ["env.d.ts", "src/**/*.d.ts", "src/**/*", "src/**/*.vue", "src/i18n/lang/*.json"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
@ -18,6 +18,7 @@
} }
}, },
"vueCompilerOptions": { "vueCompilerOptions": {
"strictTemplates": true // "strictTemplates": true
"jsxTemplates": true
} }
} }