feat: improved types (#2547)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2547 Reviewed-by: konrad <k@knt.li>
This commit is contained in:
commit
0ff0d8c5b8
@ -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",
|
||||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -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>
|
||||||
<component
|
<div
|
||||||
:is="componentNodeName"
|
v-if="disabled === true && (to !== undefined || href !== undefined)"
|
||||||
class="base-button"
|
class="base-button"
|
||||||
:class="{ 'base-button--type-button': isButton }"
|
:aria-disabled="disabled || undefined"
|
||||||
v-bind="elementBindings"
|
|
||||||
:disabled="disabled || undefined"
|
|
||||||
ref="button"
|
ref="button"
|
||||||
>
|
>
|
||||||
<slot/>
|
<slot/>
|
||||||
</component>
|
</div>
|
||||||
|
<router-link
|
||||||
|
v-else-if="to !== undefined"
|
||||||
|
:to="to"
|
||||||
|
class="base-button"
|
||||||
|
ref="button"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
</router-link>
|
||||||
|
<a v-else-if="href !== undefined"
|
||||||
|
class="base-button"
|
||||||
|
:href="href"
|
||||||
|
rel="noreferrer noopener nofollow"
|
||||||
|
target="_blank"
|
||||||
|
ref="button"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
:type="type"
|
||||||
|
class="base-button base-button--type-button"
|
||||||
|
:disabled="disabled || undefined"
|
||||||
|
ref="button"
|
||||||
|
@click="(event: MouseEvent) => emit('click', event)"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default { inheritAttrs: false }
|
const BASE_BUTTON_TYPES_MAP = {
|
||||||
|
BUTTON: 'button',
|
||||||
|
SUBMIT: 'submit',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUTTON_TYPES_MAP] | undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -20,77 +55,36 @@ export default { inheritAttrs: false }
|
|||||||
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
|
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
|
||||||
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
|
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
|
||||||
|
|
||||||
// the component tries to heuristically determine what it should be checking the props (see the
|
// the component tries to heuristically determine what it should be checking the props
|
||||||
// componentNodeName and elementBindings ref for this).
|
|
||||||
|
|
||||||
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
|
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
|
||||||
|
|
||||||
import { ref, watchEffect, computed, useAttrs, type PropType } from 'vue'
|
import {unrefElement} from '@vueuse/core'
|
||||||
|
import {ref, type HTMLAttributes} from 'vue'
|
||||||
|
import type {RouteLocationNamedRaw} from 'vue-router'
|
||||||
|
|
||||||
const BASE_BUTTON_TYPES_MAP = Object.freeze({
|
export interface BaseButtonProps extends HTMLAttributes {
|
||||||
button: 'button',
|
type?: BaseButtonTypes
|
||||||
submit: 'submit',
|
disabled?: boolean
|
||||||
})
|
to?: RouteLocationNamedRaw
|
||||||
|
href?: string
|
||||||
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
type: {
|
|
||||||
type: String as PropType<BaseButtonTypes>,
|
|
||||||
default: 'button',
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const componentNodeName = ref<Node['nodeName']>('button')
|
|
||||||
|
|
||||||
interface ElementBindings {
|
|
||||||
type?: string;
|
|
||||||
rel?: string;
|
|
||||||
target?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementBindings = ref({})
|
export interface BaseButtonEmits {
|
||||||
|
(e: 'click', payload: MouseEvent): void
|
||||||
|
}
|
||||||
|
|
||||||
const attrs = useAttrs()
|
const {
|
||||||
watchEffect(() => {
|
type = BASE_BUTTON_TYPES_MAP.BUTTON,
|
||||||
// by default this component is a button element with the attribute of the type "button" (default prop value)
|
disabled = false,
|
||||||
let nodeName = 'button'
|
} = defineProps<BaseButtonProps>()
|
||||||
let bindings: ElementBindings = {type: props.type}
|
|
||||||
|
|
||||||
// if we find a "to" prop we set it as router-link
|
const emit = defineEmits<BaseButtonEmits>()
|
||||||
if ('to' in attrs) {
|
|
||||||
nodeName = 'router-link'
|
|
||||||
bindings = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a href we assume the user wants an external link via a link element
|
const button = ref<HTMLElement | null>(null)
|
||||||
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
|
|
||||||
if ('href' in attrs) {
|
|
||||||
nodeName = 'a'
|
|
||||||
bindings = {
|
|
||||||
rel: 'noreferrer noopener nofollow',
|
|
||||||
target: '_blank',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentNodeName.value = nodeName
|
|
||||||
elementBindings.value = {
|
|
||||||
...bindings,
|
|
||||||
...attrs,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isButton = computed(() => componentNodeName.value === 'button')
|
|
||||||
|
|
||||||
const button = ref()
|
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
button.value.focus()
|
unrefElement(button)?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
@ -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 {
|
||||||
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">
|
||||||
: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>,
|
||||||
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'
|
||||||
|
|
||||||
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"
|
||||||
: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')) {
|
||||||
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
|
||||||
|
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
|
||||||
|
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
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
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
|
||||||
|
|
||||||
|
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
|
||||||
|
"jsxTemplates": true
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user