feat: improved types #2547
|
@ -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': {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
dpschen marked this conversation as resolved
|
|||||||
<component
|
<div
|
||||||
:is="componentNodeName"
|
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"
|
||||||
: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"
|
||||||
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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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>
|
||||||
|
|
||||||
<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 {
|
||||||
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.
|
|||||||
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({
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
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! :)
|
|||||||
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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,7 @@ const props = defineProps({
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
bottomActions: {
|
bottomActions: {
|
||||||
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -1,46 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<component
|
<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.
|
|||||||
: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>
|
||||||
|
|
|
@ -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>,
|
||||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Using Using `IconProp` enables autocomplete for the icons props
|
|||||||
default: 'ellipsis-h',
|
default: 'ellipsis-h',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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'
|
||||||
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 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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
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"
|
: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() {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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')) {
|
||||||
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)
|
return new Date(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
@ -9,13 +16,14 @@ export const saveListView = (listId, routeName) => {
|
||||||
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]
|
||||||
|
|
|
@ -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)`, () => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -20,4 +20,7 @@ export interface IUser extends IAbstract {
|
||||||
created: Date
|
created: Date
|
||||||
updated: Date
|
updated: Date
|
||||||
settings: IUserSettings
|
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
|
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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -28,6 +28,9 @@ export default class UserModel extends AbstractModel<IUser> implements IUser {
|
||||||
updated: Date
|
updated: Date
|
||||||
settings: IUserSettings
|
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> = {}) {
|
constructor(data: Partial<IUser> = {}) {
|
||||||
super()
|
super()
|
||||||
this.assignData(data)
|
this.assignData(data)
|
||||||
|
|
|
@ -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 = ''
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
interface ListHistory {
|
export interface ListHistory {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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({})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
@ -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 {}
|
40
src/types/vue-fontawesome.ts
Normal file
|
@ -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>
|
</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)
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
? ''
|
? ''
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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('')
|
||||||
|
|
|
@ -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
|
||||||
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 :)