feat: improved types #2547
|
@ -37,6 +37,10 @@ module.exports = {
|
|||
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
||||
|
||||
'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',
|
||||
'parserOptions': {
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001427",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "10.11.0",
|
||||
"esbuild": "0.15.12",
|
||||
"eslint": "8.26.0",
|
||||
|
|
|
@ -41,6 +41,7 @@ specifiers:
|
|||
camel-case: 4.1.2
|
||||
caniuse-lite: 1.0.30001427
|
||||
codemirror: 5.65.9
|
||||
csstype: 3.1.1
|
||||
cypress: 10.11.0
|
||||
date-fns: 2.29.3
|
||||
dayjs: 1.11.6
|
||||
|
@ -157,6 +158,7 @@ devDependencies:
|
|||
autoprefixer: 10.4.13_postcss@8.4.18
|
||||
browserslist: 4.21.4
|
||||
caniuse-lite: 1.0.30001427
|
||||
csstype: 3.1.1
|
||||
cypress: 10.11.0
|
||||
esbuild: 0.15.12
|
||||
eslint: 8.26.0
|
||||
|
@ -5219,6 +5221,10 @@ packages:
|
|||
/csstype/2.6.19:
|
||||
resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==}
|
||||
|
||||
/csstype/3.1.1:
|
||||
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
|
||||
dev: true
|
||||
|
||||
/cyclist/1.0.1:
|
||||
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
|
||||
dev: true
|
||||
|
|
|
@ -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>
|
||||
dpschen marked this conversation as resolved
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
<div
|
||||
v-if="disabled === true && (to !== undefined || href !== undefined)"
|
||||
konrad
commented
Are there any cases where it makes sense for this to be a div? Are there any cases where it makes sense for this to be a div?
dpschen
commented
Yes. Yes.
Out of a semantic perspective a disabled button / link is not a button / link.
|
||||
class="base-button"
|
||||
:class="{ 'base-button--type-button': isButton }"
|
||||
v-bind="elementBindings"
|
||||
:disabled="disabled || undefined"
|
||||
:aria-disabled="disabled || undefined"
|
||||
ref="button"
|
||||
>
|
||||
<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"
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
This looks like it's missing the This looks like it's missing the `href` attribute.
dpschen
commented
Good catch! Fixed Good catch! Fixed
|
||||
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>
|
||||
|
||||
<script lang="ts">
|
||||
export default { inheritAttrs: false }
|
||||
const BASE_BUTTON_TYPES_MAP = {
|
||||
BUTTON: 'button',
|
||||
SUBMIT: 'submit',
|
||||
} as const
|
||||
|
||||
dpschen
commented
We export the types in order to use them in inheriting components We export the types in order to use them in inheriting components
|
||||
export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUTTON_TYPES_MAP] | undefined
|
||||
</script>
|
||||
|
||||
<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
|
||||
// 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
|
||||
// componentNodeName and elementBindings ref for this).
|
||||
// the component tries to heuristically determine what it should be checking the props
|
||||
|
||||
// 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({
|
||||
button: 'button',
|
||||
submit: 'submit',
|
||||
})
|
||||
|
||||
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;
|
||||
export interface BaseButtonProps extends HTMLAttributes {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
As of now I had to add all possible combinations. It might be that with the latest vue-tsc changes this is possible again. As of now I had to add all possible combinations.
I tried to change the props [based on the type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) but vue wouldn't accept that.
It boils down to that fact that vue currently doesn't support dynamic component types.
It might be that with the latest vue-tsc changes this is possible again.
Stricly speaking right now this component allows impossible prop combinations, these should be validated and filtered. But I hoped that I could spare that effort and solve it instead by establishing common knowledge on how this component is supposed to be used.
konrad
commented
I think that's fine. Invalid combinations will show up in PR reviews. I think that's fine. Invalid combinations will show up in PR reviews.
|
||||
type?: BaseButtonTypes
|
||||
disabled?: boolean
|
||||
to?: RouteLocationNamedRaw
|
||||
href?: string
|
||||
}
|
||||
|
||||
const elementBindings = ref({})
|
||||
export interface BaseButtonEmits {
|
||||
(e: 'click', payload: MouseEvent): void
|
||||
}
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
// by default this component is a button element with the attribute of the type "button" (default prop value)
|
||||
let nodeName = 'button'
|
||||
let bindings: ElementBindings = {type: props.type}
|
||||
const {
|
||||
type = BASE_BUTTON_TYPES_MAP.BUTTON,
|
||||
disabled = false,
|
||||
} = defineProps<BaseButtonProps>()
|
||||
|
||||
// if we find a "to" prop we set it as router-link
|
||||
if ('to' in attrs) {
|
||||
nodeName = 'router-link'
|
||||
bindings = {}
|
||||
}
|
||||
const emit = defineEmits<BaseButtonEmits>()
|
||||
|
||||
// if there is a href we assume the user wants an external link via a link element
|
||||
// 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()
|
||||
const button = ref<HTMLElement | null>(null)
|
||||
|
||||
function focus() {
|
||||
button.value.focus()
|
||||
unrefElement(button)?.focus()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -26,7 +26,7 @@ if (navigator && navigator.serviceWorker) {
|
|||
)
|
||||
}
|
||||
|
||||
function showRefreshUI(e) {
|
||||
function showRefreshUI(e: Event) {
|
||||
console.log('recieved refresh event', e)
|
||||
registration.value = e.detail
|
||||
updateAvailable.value = true
|
||||
|
|
|
@ -9,64 +9,61 @@
|
|||
}
|
||||
]"
|
||||
>
|
||||
<template v-if="icon">
|
||||
<icon
|
||||
v-if="showIconOnly"
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
/>
|
||||
<span class="icon is-small" v-else-if="icon !== ''">
|
||||
<span class="icon is-small" v-else>
|
||||
<icon
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<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' }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, useSlots, type PropType} from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import {computed, useSlots} from 'vue'
|
||||
import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const BUTTON_TYPES_MAP = Object.freeze({
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
})
|
||||
// extending the props of the BaseButton
|
||||
export interface ButtonProps extends BaseButtonProps {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
By extending the BaseButtonProps we inherit them and get proper autocomplete! :) By extending the BaseButtonProps we inherit them and get proper autocomplete! :)
|
||||
variant?: ButtonTypes
|
||||
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({
|
||||
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 variantClass = computed(() => BUTTON_TYPES_MAP[variant])
|
||||
|
||||
const slots = useSlots()
|
||||
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
|
||||
const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'undefined')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -193,7 +193,7 @@ function toggleDatePopup() {
|
|||
}
|
||||
|
||||
const datepickerPopup = ref<HTMLElement | null>(null)
|
||||
function hideDatePopup(e) {
|
||||
function hideDatePopup(e: MouseEvent) {
|
||||
if (show.value) {
|
||||
closeWhenClickedOutside(e, datepickerPopup.value, close)
|
||||
}
|
||||
|
|
|
@ -115,6 +115,7 @@ const props = defineProps({
|
|||
default: true,
|
||||
},
|
||||
bottomActions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
emptyText: {
|
||||
|
|
|
@ -100,37 +100,52 @@ function elementInResults(elem: string | any, label: string, query: string): boo
|
|||
}
|
||||
|
||||
const props = defineProps({
|
||||
// When true, shows a loading spinner
|
||||
/**
|
||||
* When true, shows a loading spinner
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// The placeholder of the search input
|
||||
/**
|
||||
* The placeholder of the search input
|
||||
*/
|
||||
placeholder: {
|
||||
type: String,
|
||||
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: {
|
||||
type: Array as PropType<{[id: string]: any}>,
|
||||
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: {
|
||||
type: String,
|
||||
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: {
|
||||
type: [Object] as PropType<{[key: string]: any}>,
|
||||
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: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// The text shown next to the new value option.
|
||||
/**
|
||||
* The text shown next to the new value option.
|
||||
*/
|
||||
createPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
|
@ -138,7 +153,9 @@ const props = defineProps({
|
|||
return t('input.multiselect.createPlaceholder')
|
||||
},
|
||||
},
|
||||
// The text shown next to an option.
|
||||
/**
|
||||
* The text shown next to an option.
|
||||
*/
|
||||
selectPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
|
@ -146,22 +163,30 @@ const props = defineProps({
|
|||
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: {
|
||||
type: Boolean,
|
||||
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: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// If true, shows search results when no query is specified.
|
||||
/**
|
||||
* If true, shows search results when no query is specified.
|
||||
*/
|
||||
showEmpty: {
|
||||
type: Boolean,
|
||||
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: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
|
@ -174,17 +199,25 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits<{
|
||||
(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
|
||||
// @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model.
|
||||
(e: 'select', value: null): void
|
||||
// @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.
|
||||
/**
|
||||
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
|
||||
*/
|
||||
(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
|
||||
// @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
|
||||
}>()
|
||||
|
||||
const query = ref('')
|
||||
const query = ref<string | {[key: string]: any}>('')
|
||||
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const localLoading = ref(false)
|
||||
const showSearchResults = ref(false)
|
||||
|
|
|
@ -70,6 +70,8 @@ import {
|
|||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from '@/types/vue-fontawesome'
|
||||
|
||||
library.add(faAlignLeft)
|
||||
library.add(faAngleRight)
|
||||
library.add(faArchive)
|
||||
|
@ -136,4 +138,5 @@ library.add(faTrashAlt)
|
|||
library.add(faUser)
|
||||
library.add(faUsers)
|
||||
|
||||
export default FontAwesomeIcon
|
||||
// overwriting the wrong types
|
||||
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
|
@ -35,6 +35,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps({
|
||||
|
@ -51,7 +54,7 @@ defineProps({
|
|||
default: false,
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
type: String as PropType<IconProp>,
|
||||
default: 'times',
|
||||
},
|
||||
shadow: {
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Color } from 'csstype'
|
||||
import type { DataType } from 'csstype'
|
||||
|
||||
defineProps< {
|
||||
color: Color,
|
||||
color: DataType.Color,
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
|
@ -55,7 +58,7 @@ defineProps({
|
|||
type: String,
|
||||
},
|
||||
primaryIcon: {
|
||||
type: String,
|
||||
type: String as PropType<IconProp>,
|
||||
default: 'plus',
|
||||
},
|
||||
primaryDisabled: {
|
||||
|
|
|
@ -1,46 +1,24 @@
|
|||
<template>
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
v-bind="elementBindings"
|
||||
:to="to"
|
||||
class="dropdown-item">
|
||||
<BaseButton class="dropdown-item">
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
By reusing the BaseButton the dropdown-item got much simpler :) By reusing the BaseButton the dropdown-item got much simpler :)
konrad
commented
Wow it really looks a lot simpler now. Wow it really looks a lot simpler now.
|
||||
<span class="icon" v-if="icon">
|
||||
<icon :icon="icon"/>
|
||||
<Icon :icon="icon"/>
|
||||
</span>
|
||||
<span>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</span>
|
||||
</component>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<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<{
|
||||
to?: object,
|
||||
icon?: string | string[],
|
||||
}>()
|
||||
export interface DropDownItemProps extends BaseButtonProps {
|
||||
icon?: IconProp,
|
||||
}
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('a')
|
||||
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,
|
||||
}
|
||||
})
|
||||
defineProps<DropDownItemProps>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -91,5 +69,4 @@ button.dropdown-item {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -17,14 +17,15 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {ref, type PropType} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps({
|
||||
triggerIcon: {
|
||||
type: String,
|
||||
type: String as PropType<IconProp>,
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Using Using `IconProp` enables autocomplete for the icons props
|
||||
default: 'ellipsis-h',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<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"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
|
||||
const open = ref(false)
|
||||
const popup = ref(null)
|
||||
|
||||
const toggle = () => {
|
||||
open.value = !open.value
|
||||
}
|
||||
import {ref} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
I wonder if we even need the helper function I built back in the day? I wonder if we even need the helper function I built back in the day?
dpschen
commented
I already try to remove it piece by piece :) I already try to remove it piece by piece :)
|
||||
|
||||
const props = defineProps({
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
close()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
v-else-if="type === 'dropdown'"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:class="{'is-disabled': disabled}"
|
||||
:disabled="disabled"
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Since dropdown inherits from basebutton it automatically inherits props like disabled Since dropdown inherits from basebutton it automatically inherits props like disabled
|
||||
:icon="iconName"
|
||||
>
|
||||
{{ buttonText }}
|
||||
|
@ -44,6 +44,7 @@ import SubscriptionModel from '@/models/subscription'
|
|||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
import {success} from '@/message'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const props = defineProps({
|
||||
entity: String,
|
||||
|
@ -104,7 +105,7 @@ const tooltipText = computed(() => {
|
|||
})
|
||||
|
||||
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)
|
||||
|
||||
function changeSubscription() {
|
||||
|
|
|
@ -76,7 +76,7 @@ const notifications = computed(() => {
|
|||
})
|
||||
const userInfo = computed(() => authStore.info)
|
||||
|
||||
let interval: number
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
|
||||
onMounted(() => {
|
||||
loadNotifications()
|
||||
|
|
|
@ -214,7 +214,7 @@ async function addTask() {
|
|||
return rel
|
||||
})
|
||||
await Promise.all(relations)
|
||||
} catch (e: { message?: string }) {
|
||||
} catch (e: any) {
|
||||
newTaskTitle.value = taskTitleBackup
|
||||
if (e?.message === 'NO_LIST') {
|
||||
errorMessage.value = t('list.create.addListRequired')
|
||||
|
|
|
@ -165,7 +165,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
import type AttachmentModel from '@/models/attachment'
|
||||
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
|
@ -227,9 +226,9 @@ function uploadFilesToTask(files: File[] | FileList) {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -250,7 +249,7 @@ async function deleteAttachment() {
|
|||
|
||||
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))) {
|
||||
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Done class="heading__done" :is-done="task.done"/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="task.getHexColor()"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="mt-1 ml-2"
|
||||
/>
|
||||
<h1
|
||||
|
@ -48,6 +48,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
|||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {getHexColor} from '@/models/task'
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
v-model="list"
|
||||
:select-placeholder="$t('list.searchSelect')"
|
||||
>
|
||||
<template #searchResult="props">
|
||||
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span>
|
||||
{{ props.option.title }}
|
||||
<template #searchResult="{option}">
|
||||
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
|
||||
{{ (option as IList).title }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
|
@ -25,6 +25,7 @@ import type {IList} from '@/modelTypes/IList'
|
|||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import type { INamespace } from '@/modelTypes/INamespace'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -65,7 +66,7 @@ function select(l: IList | null) {
|
|||
emit('update:modelValue', list)
|
||||
}
|
||||
|
||||
function namespace(namespaceId: number) {
|
||||
function namespace(namespaceId: INamespace['id']) {
|
||||
const namespace = namespaceStore.getNamespaceById(namespaceId)
|
||||
return namespace !== null
|
||||
? namespace.title
|
||||
|
|
|
@ -2,7 +2,7 @@ import type {Directive} from 'vue'
|
|||
import {install, uninstall} from '@github/hotkey'
|
||||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
|
||||
const directive: Directive = {
|
||||
const directive = <Directive<HTMLElement,string>>{
|
||||
mounted(el, {value}) {
|
||||
if(value === '') {
|
||||
return
|
||||
|
|
|
@ -3,17 +3,15 @@ import {snakeCase} from 'snake-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
|
||||
if (typeof object !== 'object') {
|
||||
return object
|
||||
}
|
||||
|
||||
const parsedObject = {}
|
||||
const parsedObject: Record<string, any> = {}
|
||||
for (const m in object) {
|
||||
parsedObject[camelCase(m)] = object[m]
|
||||
|
||||
|
@ -25,7 +23,7 @@ export function objectToCamelCase(object) {
|
|||
|
||||
// Call it again for arrays
|
||||
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.
|
||||
continue
|
||||
}
|
||||
|
@ -40,17 +38,15 @@ export function objectToCamelCase(object) {
|
|||
|
||||
/**
|
||||
* 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
|
||||
if (typeof object !== 'object') {
|
||||
return object
|
||||
}
|
||||
|
||||
const parsedObject = {}
|
||||
const parsedObject: Record<string, any> = {}
|
||||
for (const m in object) {
|
||||
parsedObject[snakeCase(m)] = object[m]
|
||||
|
||||
|
@ -65,7 +61,7 @@ export function objectToSnakeCase(object) {
|
|||
|
||||
// Call it again for arrays
|
||||
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.
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* @param 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.
|
||||
// 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.
|
||||
let parent = event.target.parentElement
|
||||
let parent = (event.target as HTMLElement)?.parentElement
|
||||
while (parent !== rootElement) {
|
||||
if (parent === null || parent.parentElement === null) {
|
||||
parent = null
|
||||
|
|
|
@ -35,7 +35,7 @@ export function setupMarkdownRenderer(checkboxId: string) {
|
|||
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'
|
||||
return hljs.highlight(code, {language: validLanguage}).value
|
||||
},
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Make date objects from timestamps
|
||||
*/
|
||||
export function parseDateOrNull(date) {
|
||||
export function parseDateOrNull(date: string | Date) {
|
||||
if (date instanceof Date) {
|
||||
return date
|
||||
}
|
||||
|
||||
if ((typeof date === 'string' || date instanceof String) && !date.startsWith('0001')) {
|
||||
if ((typeof date === 'string') && !date.startsWith('0001')) {
|
||||
dpschen
commented
We should stop using We should stop using `String()`. Regardless date strings normally come from `toISOString()` which produces a type string
konrad
commented
There might be cases though where a date string comes from the api and needs to be parsed here. (or at leat that was the reason why I added this function originally) There might be cases though where a date string comes from the api and needs to be parsed here. (or at leat that was the reason why I added this function originally)
dpschen
commented
Unsure if we talk here about the same. I'm talking about the general use of Unsure if we talk here about the same. I'm talking about the general use of `String()` to convert something to a string. Afaik we don't do that anywhere for the data that's coming from the api. We do use the `parseDateOrNull` function but it should would without the `instanceof String`. If you think it should stay, I'm happy to revert this change.
|
||||
return new Date(date)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
// Save the current list view to local storage
|
||||
// We use local storage and not a store here to make it persistent across reloads.
|
||||
export const saveListView = (listId, routeName) => {
|
||||
import type { IList } from '@/modelTypes/IList'
|
||||
|
||||
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.')) {
|
||||
return
|
||||
}
|
||||
|
@ -9,13 +16,14 @@ export const saveListView = (listId, routeName) => {
|
|||
return
|
||||
}
|
||||
|
||||
// We use local storage and not the store here to make it persistent across reloads.
|
||||
const savedListView = localStorage.getItem('listView')
|
||||
let savedListViewJson = false
|
||||
let savedListViewJson: ListView | false = false
|
||||
if (savedListView !== null) {
|
||||
savedListViewJson = JSON.parse(savedListView)
|
||||
savedListViewJson = JSON.parse(savedListView) as ListView
|
||||
}
|
||||
|
||||
let listView = {}
|
||||
let listView: ListView = {}
|
||||
if (savedListViewJson) {
|
||||
listView = savedListViewJson
|
||||
}
|
||||
|
@ -24,7 +32,7 @@ export const saveListView = (listId, routeName) => {
|
|||
localStorage.setItem('listView', JSON.stringify(listView))
|
||||
}
|
||||
|
||||
export const getListView = listId => {
|
||||
export const getListView = (listId: IList['id']) => {
|
||||
// Remove old stored settings
|
||||
const savedListView = localStorage.getItem('listView')
|
||||
if (savedListView !== null && savedListView.startsWith('list.')) {
|
||||
|
@ -32,13 +40,13 @@ export const getListView = listId => {
|
|||
}
|
||||
|
||||
if (!savedListView) {
|
||||
return 'list.list'
|
||||
return DEFAULT_LIST_VIEW
|
||||
}
|
||||
|
||||
const savedListViewJson = JSON.parse(savedListView)
|
||||
const savedListViewJson: ListView = JSON.parse(savedListView)
|
||||
|
||||
if (!savedListViewJson[listId]) {
|
||||
return 'list.list'
|
||||
return DEFAULT_LIST_VIEW
|
||||
}
|
||||
|
||||
return savedListViewJson[listId]
|
||||
|
|
|
@ -10,7 +10,7 @@ const days = {
|
|||
friday: 5,
|
||||
saturday: 6,
|
||||
sunday: 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in days) {
|
||||
test(`today on a ${n}`, () => {
|
||||
|
@ -32,7 +32,7 @@ const nextMonday = {
|
|||
friday: 3,
|
||||
saturday: 2,
|
||||
sunday: 1,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in nextMonday) {
|
||||
test(`next monday on a ${n}`, () => {
|
||||
|
@ -48,7 +48,7 @@ const thisWeekend = {
|
|||
friday: 1,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in thisWeekend) {
|
||||
test(`this weekend on a ${n}`, () => {
|
||||
|
@ -64,7 +64,7 @@ const laterThisWeek = {
|
|||
friday: 0,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in laterThisWeek) {
|
||||
test(`later this week on a ${n}`, () => {
|
||||
|
@ -80,7 +80,7 @@ const laterNextWeek = {
|
|||
friday: 7 + 0,
|
||||
saturday: 7 + 0,
|
||||
sunday: 7 + 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in laterNextWeek) {
|
||||
test(`later next week on a ${n} (this week)`, () => {
|
||||
|
|
|
@ -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) {
|
||||
case 'today':
|
||||
return 0
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* @param dateString
|
||||
* @returns {Date}
|
||||
*/
|
||||
export const createDateFromString = dateString => {
|
||||
export function createDateFromString(dateString: string | Date) {
|
||||
if (dateString instanceof Date) {
|
||||
return dateString
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {i18n} from '@/i18n'
|
|||
|
||||
const locales = {en: enGB, de, ch: de, fr, ru}
|
||||
|
||||
export function dateIsValid(date) {
|
||||
export function dateIsValid(date: Date | null) {
|
||||
if (date === null) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface ITask extends IAbstract {
|
|||
percentDone: number
|
||||
relatedTasks: Partial<Record<IRelationKind, ITask[]>>
|
||||
attachments: IAttachment[]
|
||||
coverImageAttachmentId: IAttachment['id']
|
||||
coverImageAttachmentId: IAttachment['id'] | null
|
||||
identifier: string
|
||||
index: number
|
||||
isFavorite: boolean
|
||||
|
|
|
@ -20,4 +20,7 @@ export interface IUser extends IAbstract {
|
|||
created: Date
|
||||
updated: Date
|
||||
settings: IUserSettings
|
||||
|
||||
isLocalUser: boolean
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Is it correct to add these properties? They seemed to be missing. Is it correct to add these properties? They seemed to be missing.
konrad
commented
yes, that's correct. yes, that's correct.
|
||||
deletionScheduledAt: string | Date | null
|
||||
}
|
|
@ -8,6 +8,7 @@ export interface IUserSettings extends IAbstract {
|
|||
discoverableByName: boolean
|
||||
discoverableByEmail: boolean
|
||||
overdueTasksRemindersEnabled: boolean
|
||||
overdueTasksRemindersTime: any
|
||||
defaultListId: undefined | IList['id']
|
||||
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
timezone: string
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class EmailUpdateModel extends AbstractModel<IEmailUpdate> implem
|
|||
newEmail = ''
|
||||
password = ''
|
||||
|
||||
constructor(data : Partial<IEmailUpdate>) {
|
||||
constructor(data : Partial<IEmailUpdate> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class PasswordUpdateModel extends AbstractModel<IPasswordUpdate>
|
|||
newPassword = ''
|
||||
oldPassword = ''
|
||||
|
||||
constructor(data: Partial<IPasswordUpdate>) {
|
||||
constructor(data: Partial<IPasswordUpdate> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
}
|
||||
|
|
|
@ -79,10 +79,12 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
|||
percentDone = 0
|
||||
relatedTasks: Partial<Record<IRelationKind, ITask[]>> = {}
|
||||
attachments: IAttachment[] = []
|
||||
coverImageAttachmentId: IAttachment['id'] = null
|
||||
identifier = ''
|
||||
index = 0
|
||||
isFavorite = false
|
||||
subscription: ISubscription = null
|
||||
coverImageAttachmentId: IAttachment['id'] = null
|
||||
|
||||
position = 0
|
||||
kanbanPosition = 0
|
||||
|
|
|
@ -28,6 +28,9 @@ export default class UserModel extends AbstractModel<IUser> implements IUser {
|
|||
updated: Date
|
||||
settings: IUserSettings
|
||||
|
||||
isLocalUser: boolean
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
I assumed that the type here is boolean. I assumed that the type here is boolean.
konrad
commented
That's correct, same as in the interface. That's correct, same as in the interface.
dpschen
commented
I removed the comment. I removed the comment.
|
||||
deletionScheduledAt: null
|
||||
|
||||
constructor(data: Partial<IUser> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
|
|||
discoverableByName = false
|
||||
discoverableByEmail = false
|
||||
overdueTasksRemindersEnabled = true
|
||||
overdueTasksRemindersTime = undefined
|
||||
defaultListId = undefined
|
||||
weekStart = 0 as IUserSettings['weekStart']
|
||||
timezone = ''
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
interface ListHistory {
|
||||
export interface ListHistory {
|
||||
id: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ import {parseTaskText, PrefixMode} from './parseTaskText'
|
|||
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'
|
||||
import {MILLISECONDS_A_DAY} from '@/constants/date'
|
||||
import type {IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
|
||||
describe('Parse Task Text', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -31,9 +32,9 @@ describe('Parse Task Text', () => {
|
|||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label')
|
||||
expect(result.list).toBe('list')
|
||||
|
@ -61,18 +62,18 @@ describe('Parse Task Text', () => {
|
|||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
})
|
||||
it('should recognize today', () => {
|
||||
const result = parseTaskText('Lorem Ipsum today')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
})
|
||||
describe('should recognize today with a time', () => {
|
||||
const cases = {
|
||||
|
@ -93,11 +94,11 @@ describe('Parse Task Text', () => {
|
|||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result.date.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(now.getMonth())
|
||||
expect(result.date.getDate()).toBe(now.getDate())
|
||||
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe(cases[c as keyof typeof cases])
|
||||
expect(result.date.getSeconds()).toBe(0)
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
expect(`${result?.date?.getHours()}:${result?.date?.getMinutes()}`).toBe(cases[c as keyof typeof cases])
|
||||
expect(result?.date?.getSeconds()).toBe(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -107,9 +108,9 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
expect(result.date.getFullYear()).toBe(tomorrow.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result.date.getDate()).toBe(tomorrow.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(tomorrow.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
|
||||
})
|
||||
it('should recognize next monday', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next monday')
|
||||
|
@ -119,9 +120,9 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result.date.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextMonday.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize next monday and ignore casing', () => {
|
||||
const result = parseTaskText('Lorem Ipsum nExt Monday')
|
||||
|
@ -131,9 +132,9 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result.date.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextMonday.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize this weekend', () => {
|
||||
const result = parseTaskText('Lorem Ipsum this weekend')
|
||||
|
@ -143,9 +144,9 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const thisWeekend = new Date()
|
||||
thisWeekend.setDate(thisWeekend.getDate() + untilThisWeekend)
|
||||
expect(result.date.getFullYear()).toBe(thisWeekend.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(thisWeekend.getMonth())
|
||||
expect(result.date.getDate()).toBe(thisWeekend.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(thisWeekend.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(thisWeekend.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(thisWeekend.getDate())
|
||||
})
|
||||
it('should recognize 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')
|
||||
const laterThisWeek = new Date()
|
||||
laterThisWeek.setDate(laterThisWeek.getDate() + untilLaterThisWeek)
|
||||
expect(result.date.getFullYear()).toBe(laterThisWeek.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(laterThisWeek.getMonth())
|
||||
expect(result.date.getDate()).toBe(laterThisWeek.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(laterThisWeek.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(laterThisWeek.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(laterThisWeek.getDate())
|
||||
})
|
||||
it('should recognize 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')
|
||||
const laterNextWeek = new Date()
|
||||
laterNextWeek.setDate(laterNextWeek.getDate() + untilLaterNextWeek)
|
||||
expect(result.date.getFullYear()).toBe(laterNextWeek.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(laterNextWeek.getMonth())
|
||||
expect(result.date.getDate()).toBe(laterNextWeek.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(laterNextWeek.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(laterNextWeek.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(laterNextWeek.getDate())
|
||||
})
|
||||
it('should recognize next week', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next week')
|
||||
|
@ -179,9 +180,9 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextWeek = new Date()
|
||||
nextWeek.setDate(nextWeek.getDate() + untilNextWeek)
|
||||
expect(result.date.getFullYear()).toBe(nextWeek.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextWeek.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextWeek.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(nextWeek.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextWeek.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextWeek.getDate())
|
||||
})
|
||||
it('should recognize next month', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next month')
|
||||
|
@ -190,9 +191,9 @@ describe('Parse Task Text', () => {
|
|||
const nextMonth = new Date()
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1)
|
||||
expect(result.date.getFullYear()).toBe(nextMonth.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(nextMonth.getMonth())
|
||||
expect(result.date.getDate()).toBe(nextMonth.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonth.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonth.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonth.getDate())
|
||||
})
|
||||
it('should recognize a date', () => {
|
||||
const result = parseTaskText('Lorem Ipsum 06/26/2021')
|
||||
|
@ -200,9 +201,9 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const date = new Date()
|
||||
date.setFullYear(2021, 5, 26)
|
||||
expect(result.date.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(date.getMonth())
|
||||
expect(result.date.getDate()).toBe(date.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(date.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(date.getDate())
|
||||
})
|
||||
it('should recognize 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')
|
||||
const curDate = new Date()
|
||||
const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
|
||||
expect(result.date.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(date.getMonth())
|
||||
expect(result.date.getDate()).toBe(date.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(date.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(date.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(date.getDate())
|
||||
})
|
||||
|
||||
const cases = {
|
||||
|
@ -244,7 +245,7 @@ describe('Parse Task Text', () => {
|
|||
'Sunday': 7,
|
||||
'sun': 7,
|
||||
'Sun': 7,
|
||||
}
|
||||
} as Record<string, number>
|
||||
for (const c in cases) {
|
||||
it(`should recognize ${c} as weekday`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c}`)
|
||||
|
@ -252,7 +253,7 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextDate = new Date()
|
||||
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', () => {
|
||||
|
@ -261,8 +262,8 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextThursday = new Date()
|
||||
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.getHours()}:${result.date.getMinutes()}`).toBe('14:0')
|
||||
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')
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month', () => {
|
||||
const time = new Date(2022, 0, 15)
|
||||
|
@ -271,8 +272,8 @@ describe('Parse Task Text', () => {
|
|||
const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(time.getDate() - 1)
|
||||
expect(result.date.getMonth()).toBe(time.getMonth() + 1)
|
||||
expect(result?.date?.getDate()).toBe(time.getDate() - 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', () => {
|
||||
const jan = new Date(2022, 0, 30)
|
||||
|
@ -282,8 +283,8 @@ describe('Parse Task Text', () => {
|
|||
|
||||
const expectedDate = new Date(2022, 2, jan.getDate() - 1)
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(expectedDate.getDate())
|
||||
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', () => {
|
||||
const mar = new Date(2022, 2, 32)
|
||||
|
@ -293,15 +294,15 @@ describe('Parse Task Text', () => {
|
|||
|
||||
const expectedDate = new Date(2022, 4, 31)
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result?.date?.getMonth()).toBe(expectedDate.getMonth())
|
||||
})
|
||||
it('should recognize dates of the month in the future', () => {
|
||||
const nextDay = new Date(+new Date() + MILLISECONDS_A_DAY)
|
||||
const result = parseTaskText(`Lorem Ipsum ${nextDay.getDate()}nd`)
|
||||
|
||||
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', () => {
|
||||
const result = parseTaskText('Lorem Ipsum renewed')
|
||||
|
@ -382,7 +383,7 @@ describe('Parse Task Text', () => {
|
|||
'saturday': 6,
|
||||
'sun': 7,
|
||||
'sunday': 7,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
const prefix = [
|
||||
'next ',
|
||||
|
@ -399,9 +400,9 @@ describe('Parse Task Text', () => {
|
|||
next.setDate(next.getDate() + distance)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getFullYear()).toBe(next.getFullYear())
|
||||
expect(result.date.getMonth()).toBe(next.getMonth())
|
||||
expect(result.date.getDate()).toBe(next.getDate())
|
||||
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||
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 nov 21': '2021-11-21',
|
||||
'dolor sit amet dec 21': '2021-12-21',
|
||||
}
|
||||
} as Record<string, string | null>
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}'`, () => {
|
||||
|
@ -472,7 +473,7 @@ describe('Parse Task Text', () => {
|
|||
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 17th': '2021-7-17 10:0',
|
||||
'Something at 10:00 sep 17th': '2021-9-17 10:0',
|
||||
}
|
||||
} as Record<string, string>
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}'`, () => {
|
||||
|
@ -695,15 +696,15 @@ describe('Parse Task Text', () => {
|
|||
'every eight hours': {type: 'hours', amount: 8},
|
||||
'every nine hours': {type: 'hours', amount: 9},
|
||||
'every ten hours': {type: 'hours', amount: 10},
|
||||
}
|
||||
} as Record<string, IRepeatAfter>
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse ${c} as recurring date every ${cases[c].amount} ${cases[c].type}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c}`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.repeats.type).toBe(cases[c].type)
|
||||
expect(result.repeats.amount).toBe(cases[c].amount)
|
||||
expect(result?.repeats?.type).toBe(cases[c].type)
|
||||
expect(result?.repeats?.amount).toBe(cases[c].amount)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { App } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
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 {Integrations} = await import('@sentry/tracing')
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
|
|||
|
||||
import {downloadBlob} from '@/helpers/downloadBlob'
|
||||
|
||||
export default class AttachmentService extends AbstractService<AttachmentModel> {
|
||||
export default class AttachmentService extends AbstractService<IAttachment> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/attachments',
|
||||
|
|
|
@ -6,7 +6,7 @@ import AbstractService from '../abstractService'
|
|||
export default class AbstractMigrationFileService extends AbstractService {
|
||||
serviceUrlKey = ''
|
||||
|
||||
constructor(serviceUrlKey: '') {
|
||||
constructor(serviceUrlKey: string) {
|
||||
super({
|
||||
create: '/migration/' + serviceUrlKey + '/migrate',
|
||||
})
|
||||
|
|
|
@ -84,7 +84,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
|||
|
||||
const filterService = shallowReactive(new SavedFilterService())
|
||||
|
||||
const filter = ref(new SavedFilterModel())
|
||||
const filter = ref<ISavedFilter>(new SavedFilterModel())
|
||||
const filters = computed({
|
||||
get: () => filter.value.filters,
|
||||
set(value) {
|
||||
|
@ -92,7 +92,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
|||
},
|
||||
})
|
||||
|
||||
// loadSavedFilter
|
||||
// load SavedFilter
|
||||
watch(() => unref(listId), async (watchedListId) => {
|
||||
if (watchedListId === undefined) {
|
||||
return
|
||||
|
|
|
@ -86,7 +86,7 @@ export const useBaseStore = defineStore('base', () => {
|
|||
}
|
||||
|
||||
async function handleSetCurrentList(
|
||||
{list, forceUpdate = false}: {list: IList | null, forceUpdate: boolean},
|
||||
{list, forceUpdate = false}: {list: IList | null, forceUpdate?: boolean},
|
||||
) {
|
||||
if (list === null) {
|
||||
setCurrentList({})
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import type { StoreDefinition } from 'pinia'
|
||||
|
||||
export interface LoadingState {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (loadFunc === null) {
|
||||
store.isLoading = true
|
||||
|
|
|
@ -364,7 +364,7 @@ export const useKanbanStore = defineStore('kanban', {
|
|||
}
|
||||
},
|
||||
|
||||
async updateBucket(updatedBucketData: IBucket) {
|
||||
async updateBucket(updatedBucketData: Partial<IBucket>) {
|
||||
const cancel = setModuleLoading(this)
|
||||
|
||||
const bucketIndex = findIndexById(this.buckets, updatedBucketData.id)
|
||||
|
|
|
@ -180,7 +180,7 @@ export const useListStore = defineStore('list', () => {
|
|||
export function useList(listId: MaybeRef<IList['id']>) {
|
||||
const listService = shallowReactive(new ListService())
|
||||
const {loading: isLoading} = toRefs(listService)
|
||||
const list: ListModel = reactive(new ListModel())
|
||||
const list: IList = reactive(new ListModel())
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
watch(
|
||||
|
|
|
@ -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 {}
|
|
@ -0,0 +1,40 @@
|
|||
// copied and slightly modified from unmerged pull request that corrects types
|
||||
// https://github.com/FortAwesome/vue-fontawesome/pull/355
|
||||
konrad marked this conversation as resolved
Outdated
konrad
commented
We need a reminder to remove this once the PR is merged (is that even possible entirely?) We need a reminder to remove this once the PR is merged (is that even possible entirely?)
dpschen
commented
I subscribed to the thread of that PR. So I should get a notification if there are any changes. I subscribed to the thread of that PR. So I should get a notification if there are any changes.
|
||||
|
||||
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>
|
|
@ -14,7 +14,6 @@
|
|||
</router-link>
|
||||
</message>
|
||||
<add-task
|
||||
:listId="defaultListId"
|
||||
@taskAdded="updateTaskList"
|
||||
class="is-max-width-desktop"
|
||||
/>
|
||||
|
@ -76,6 +75,7 @@ import {useConfigStore} from '@/stores/config'
|
|||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const salutation = useDaytimeSalutation()
|
||||
|
||||
|
@ -94,12 +94,11 @@ const listHistory = computed(() => {
|
|||
|
||||
return getHistory()
|
||||
.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 hasTasks = computed(() => baseStore.hasTasks)
|
||||
const defaultListId = computed(() => authStore.settings.defaultListId)
|
||||
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)
|
||||
const hasLists = computed(() => namespaceStore.namespaces?.[0]?.lists.length > 0)
|
||||
const loading = computed(() => taskStore.isLoading)
|
||||
|
|
|
@ -66,7 +66,7 @@ async function newLabel() {
|
|||
showError.value = false
|
||||
|
||||
const labelStore = useLabelStore()
|
||||
const newLabel = labelStore.createLabel(label.value)
|
||||
const newLabel = await labelStore.createLabel(label.value)
|
||||
router.push({
|
||||
name: 'labels.index',
|
||||
params: {id: newLabel.id},
|
||||
|
|
|
@ -71,11 +71,13 @@ import {useI18n} from 'vue-i18n'
|
|||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const namespaceService = ref(new NamespaceService())
|
||||
const namespace = ref(new NamespaceModel())
|
||||
const namespace = ref<INamespace>(new NamespaceModel())
|
||||
const editorActive = ref(false)
|
||||
const title = ref('')
|
||||
useTitle(() => title.value)
|
||||
|
|
|
@ -558,7 +558,7 @@ const canWrite = computed(() => (
|
|||
const color = computed(() => {
|
||||
const color = task.getHexColor
|
||||
? task.getHexColor()
|
||||
: false
|
||||
: undefined
|
||||
|
||||
return color === TASK_DEFAULT_COLOR
|
||||
? ''
|
||||
|
|
|
@ -50,14 +50,14 @@ async function authenticateWithCode() {
|
|||
if (localStorage.getItem('authenticating')) {
|
||||
return
|
||||
}
|
||||
localStorage.setItem('authenticating', true)
|
||||
localStorage.setItem('authenticating', 'true')
|
||||
|
||||
errorMessage.value = ''
|
||||
|
||||
if (typeof route.query.error !== 'undefined') {
|
||||
localStorage.removeItem('authenticating')
|
||||
errorMessage.value = typeof route.query.message !== 'undefined'
|
||||
? route.query.message
|
||||
? route.query.message as string
|
||||
: t('user.auth.openIdGeneralError')
|
||||
return
|
||||
}
|
||||
|
|
|
@ -130,8 +130,8 @@ async function submit() {
|
|||
|
||||
try {
|
||||
await authStore.register(toRaw(credentials))
|
||||
} catch (e) {
|
||||
errorMessage.value = e.message
|
||||
} catch (e: any) {
|
||||
errorMessage.value = e?.message
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<x-button
|
||||
v-if="!isCropAvatar"
|
||||
:loading="avatarService.loading || loading"
|
||||
@click="$refs.avatarUploadInput.click()"
|
||||
@click="avatarUploadInput.click()"
|
||||
>
|
||||
{{ $t('user.settings.avatar.uploadAvatar') }}
|
||||
</x-button>
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<td>{{ tk.id }}</td>
|
||||
<td>{{ formatDateShort(tk.created) }}</td>
|
||||
<td class="has-text-right">
|
||||
<x-button type="secondary" @click="deleteToken(tk)">
|
||||
<x-button variant="secondary" @click="deleteToken(tk)">
|
||||
{{ $t('misc.delete') }}
|
||||
</x-button>
|
||||
</td>
|
||||
|
|
|
@ -246,7 +246,7 @@ watch(
|
|||
|
||||
const listStore = useListStore()
|
||||
const defaultList = computed({
|
||||
get: () => listStore.getListById(settings.value.defaultListId),
|
||||
get: () => listStore.getListById(settings.value.defaultListId) || undefined,
|
||||
set(l) {
|
||||
settings.value.defaultListId = l ? l.id : DEFAULT_LIST_ID
|
||||
},
|
||||
|
|
|
@ -79,13 +79,14 @@ import {success} from '@/message'
|
|||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import type {ITotp} from '@/modelTypes/ITotp'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => `${t('user.settings.totp.title')} - ${t('user.settings.title')}`)
|
||||
|
||||
|
||||
const totpService = shallowReactive(new TotpService())
|
||||
const totp = ref(new TotpModel())
|
||||
const totp = ref<ITotp>(new TotpModel())
|
||||
const totpQR = ref('')
|
||||
const totpEnrolled = ref(false)
|
||||
const totpConfirmPasscode = ref('')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
@ -18,6 +18,7 @@
|
|||
}
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"strictTemplates": true
|
||||
// "strictTemplates": true
|
||||
dpschen
commented
Unsure about this Unsure about this
dpschen
commented
Biggest reason why I can't judge this: Even after the merge of this branch we still have so many type errors that I don't know which errors appear / disappear when I toggle this option. Biggest reason why I can't judge this: Even after the merge of this branch we still have so many type errors that I don't know which errors appear / disappear when I toggle this option.
konrad
commented
Is there any difference in the number of errors? Is there any difference in the number of errors?
dpschen
commented
Without
With
As said: I do not know which errors these are. It might also be that some disappear while others emerge. Without `"strictTemplates": true` (I guess the default is false ;) )
```
Found 917 errors in 154 files.
```
With `"strictTemplates": true`
```
Found 949 errors in 159 files.
```
As said: I do not know which errors these are. It might also be that some disappear while others emerge.
|
||||
"jsxTemplates": true
|
||||
}
|
||||
}
|
I had to change the way how the BaseButton works for better type support.
It's a bit more repeatition but also very clear now what happens I guess.
Intersting sidenote: I originally placed the comments above inline to the switching elements but the vue renderer had some problems with that. Something about the component being a fragment which it should not be since it has just one root node. But seems like if you add a comment additionally to the root node (in case of a
if / else
that we have here), the comment counts as a node of the component. That makes sense, but is not intuitive, since you normally don't count a comment as content and prevents you from making good comments where they should be :)