Merge branch 'main' into feature/recurring-nlp
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
kolaente 2021-12-04 20:26:03 +01:00
commit 903d02be61
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
27 changed files with 1369 additions and 1400 deletions

View File

@ -27,7 +27,7 @@
"camel-case": "4.1.2",
"codemirror": "5.64.0",
"copy-to-clipboard": "3.3.1",
"date-fns": "2.26.0",
"date-fns": "2.27.0",
"dompurify": "2.3.3",
"easymde": "2.15.0",
"flatpickr": "4.6.9",
@ -36,7 +36,7 @@
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.0.5",
"marked": "4.0.6",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"ufo": "0.7.9",
@ -45,11 +45,11 @@
"vue-advanced-cropper": "2.7.0",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5",
"vue-i18n": "9.2.0-beta.22",
"vue-i18n": "9.2.0-beta.23",
"vue-router": "4.0.12",
"vuedraggable": "4.1.0",
"vuex": "4.0.2",
"workbox-precaching": "6.4.1"
"workbox-precaching": "6.4.2"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.1.0",
@ -59,8 +59,8 @@
"@fortawesome/vue-fontawesome": "3.0.0-5",
"@types/flexsearch": "0.7.2",
"@types/jest": "27.0.3",
"@typescript-eslint/eslint-plugin": "5.4.0",
"@typescript-eslint/parser": "5.4.0",
"@typescript-eslint/eslint-plugin": "5.5.0",
"@typescript-eslint/parser": "5.5.0",
"@vitejs/plugin-legacy": "1.6.3",
"@vitejs/plugin-vue": "1.10.1",
"@vue/eslint-config-typescript": "9.1.0",
@ -69,34 +69,35 @@
"browserslist": "4.18.1",
"cypress": "8.7.0",
"cypress-file-upload": "5.0.8",
"esbuild": "0.14.0",
"eslint": "8.3.0",
"esbuild": "0.14.2",
"eslint": "8.4.0",
"eslint-plugin-vue": "8.1.1",
"express": "4.17.1",
"faker": "5.5.3",
"jest": "27.3.1",
"netlify-cli": "8.0.3",
"jest": "27.4.3",
"netlify-cli": "8.0.15",
"postcss": "8.4.4",
"postcss-preset-env": "7.0.1",
"rollup": "2.60.1",
"rollup": "2.60.2",
"rollup-plugin-visualizer": "5.5.2",
"sass": "1.43.5",
"sass": "1.44.0",
"slugify": "1.6.3",
"ts-jest": "27.0.7",
"typescript": "4.5.2",
"vite": "2.6.14",
"vite-plugin-pwa": "0.11.8",
"vite-plugin-pwa": "0.11.10",
"vite-svg-loader": "3.1.0",
"vue-tsc": "0.29.6",
"vue-tsc": "0.29.8",
"wait-on": "6.0.0",
"workbox-cli": "6.4.1"
"workbox-cli": "6.4.2"
},
"eslintConfig": {
"root": true,
"env": {
"browser": true,
"es2021": true,
"node": true
"node": true,
"vue/setup-compiler-macros": true
},
"extends": [
"eslint:recommended",
@ -120,6 +121,7 @@
"error",
"never"
],
"vue/script-setup-uses-vars": "error",
"vue/multi-word-component-names": 0
},
"parser": "vue-eslint-parser",

View File

@ -20,64 +20,59 @@
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}"
@click.stop="toggleFavoriteList(list)"
class="favorite">
<icon icon="star" v-if="list.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
</span>
</div>
<div class="title">{{ list.title }}</div>
</router-link>
</template>
<script>
<script lang="ts" setup>
import {ref, watch} from 'vue'
import {useStore} from 'vuex'
import ListService from '@/services/list'
export default {
name: 'list-card',
data() {
return {
background: null,
backgroundLoading: false,
}
},
props: {
list: {
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
},
watch: {
list: {
handler: 'loadBackground',
immediate: true,
},
},
methods: {
async loadBackground() {
if (this.list === null || !this.list.backgroundInformation || this.backgroundLoading) {
return
}
const background = ref(null)
const backgroundLoading = ref(false)
this.backgroundLoading = true
const listService = new ListService()
try {
this.background = await listService.background(this.list)
} finally {
this.backgroundLoading = false
}
},
toggleFavoriteList(list) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
this.$store.dispatch('lists/toggleListFavorite', list)
},
const props = defineProps({
list: {
type: Object,
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
})
watch(props.list, loadBackground, { immediate: true })
async function loadBackground() {
if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) {
return
}
backgroundLoading.value = true
const listService = new ListService()
try {
background.value = await listService.background(props.list)
} finally {
backgroundLoading.value = false
}
}
const store = useStore()
function toggleFavoriteList(list) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
</script>

View File

@ -1,40 +1,27 @@
<template>
<div
v-if="isDone"
class="is-done"
:class="{ 'is-done--small': variant === variants.SMALL }"
>
{{ $t('task.attributes.done') }}
</div>
v-if="isDone"
class="is-done"
:class="{ 'is-done--small': variant === 'small' }"
>
{{ $t('task.attributes.done') }}
</div>
</template>
<script>
const VARIANTS = {
DEFAULT: 'default',
SMALL: 'small',
}
<script lang="ts" setup>
import {PropType} from 'vue'
type Variants = 'default' | 'small'
export default {
name: 'Done',
data() {
return {
variants: VARIANTS,
}
defineProps({
isDone: {
type: Boolean,
default: false,
},
props: {
isDone: {
type: Boolean,
default: false,
},
variant: {
type: String,
default: VARIANTS.DEFAULT,
validator: (variant) => Object.values(VARIANTS).includes(variant),
},
variant: {
type: String as PropType<Variants>,
default: 'default',
},
}
})
</script>

View File

@ -24,41 +24,39 @@
</div>
</template>
<script>
export default {
name: 'card',
props: {
title: {
type: String,
default: '',
},
padding: {
type: Boolean,
default: true,
},
hasClose: {
type: Boolean,
default: false,
},
closeIcon: {
type: String,
default: 'times',
},
shadow: {
type: Boolean,
default: true,
},
hasContent: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
<script setup lang="ts">
defineProps({
title: {
type: String,
default: '',
},
emits: ['close'],
}
padding: {
type: Boolean,
default: true,
},
hasClose: {
type: Boolean,
default: false,
},
closeIcon: {
type: String,
default: 'times',
},
shadow: {
type: Boolean,
default: true,
},
hasContent: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
})
defineEmits(['close'])
</script>
<style lang="scss" scoped>

View File

@ -11,18 +11,15 @@
</router-link>
</template>
<script>
export default {
name: 'dropdown-item',
props: {
to: {
required: true,
},
icon: {
type: String,
required: false,
default: '',
},
<script lang="ts" setup>
defineProps({
to: {
required: true,
},
}
icon: {
type: String,
required: false,
default: '',
},
})
</script>

View File

@ -7,16 +7,10 @@
</message>
</template>
<script>
import Message from '@/components/misc/message'
<script lang="ts" setup>
import Message from '@/components/misc/message.vue'
export default {
name: 'error',
components: {Message},
methods: {
reload() {
window.location.reload()
},
},
function reload() {
window.location.reload()
}
</script>

View File

@ -6,16 +6,14 @@
</div>
</template>
<script>
import {mapState} from 'vuex'
<script lang="ts" setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
export default {
name: 'legal',
computed: mapState({
imprintUrl: state => state.config.legal.imprintUrl,
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
}),
}
const store = useStore()
const imprintUrl = computed(() => store.state.config.legal.imprintUrl)
const privacyPolicyUrl = computed(() => store.state.config.legal.privacyPolicyUrl)
</script>
<style lang="scss" scoped>

View File

@ -2,12 +2,6 @@
<div class="loader-container is-loading"></div>
</template>
<script>
export default {
name: 'loading',
}
</script>
<style scoped>
.loader-container {
height: 100%;

View File

@ -4,7 +4,7 @@
</div>
</template>
<script setup>
<script lang="ts" setup>
defineProps({
variant: {
type: String,

View File

@ -2,17 +2,17 @@
<div class="no-auth-wrapper">
<div class="noauth-container">
<Logo class="logo" width="400" height="117" />
<message v-if="motd !== ''" class="my-2">
<Message v-if="motd !== ''" class="my-2">
{{ motd }}
</message>
</Message>
<slot/>
</div>
</div>
</template>
<script setup>
<script lang="ts" setup>
import Logo from '@/components/home/Logo.vue'
import message from '@/components/misc/message'
import Message from '@/components/misc/message.vue'
import {useStore} from 'vuex'
import {computed} from 'vue'
@ -22,7 +22,7 @@ const motd = computed(() => store.state.config.motd)
<style lang="scss" scoped>
.no-auth-wrapper {
background: url('@/assets/llama.svg') no-repeat bottom left fixed var(--site-background);
background: url('@/assets/llama.svg?url') no-repeat bottom left fixed var(--site-background);
min-height: 100vh;
}

View File

@ -34,8 +34,10 @@
</nav>
</template>
<script>
function createPagination(totalPages, currentPage) {
<script lang="ts" setup>
import {computed} from 'vue'
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
@ -79,30 +81,18 @@ function getRouteForPagination(page = 1, type = 'list') {
}
}
export default {
name: 'Pagination',
props: {
totalPages: {
type: Number,
required: true,
},
currentPage: {
type: Number,
default: 0,
},
const props = defineProps({
totalPages: {
type: Number,
required: true,
},
computed: {
pages() {
return createPagination(this.totalPages, this.currentPage)
},
currentPage: {
type: Number,
default: 0,
},
})
methods: {
getRouteForPagination,
},
}
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>
<style lang="scss" scoped>

View File

@ -13,7 +13,7 @@
<section v-else-if="error !== ''">
<no-auth-wrapper>
<card>
<p v-if="error === errorNoApiUrl">
<p v-if="error === ERROR_NO_API_URL">
{{ $t('ready.noApiUrlConfigured') }}
</p>
<message variant="danger" v-else>
@ -40,51 +40,34 @@
</transition>
</template>
<script>
<script lang="ts" setup>
import {ref, computed} from 'vue'
import {useStore} from 'vuex'
import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config'
import Message from '@/components/misc/message'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
import {mapState} from 'vuex'
import ApiConfig from '@/components/misc/api-config.vue'
import Message from '@/components/misc/message.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
export default {
name: 'ready',
components: {
Message,
Logo,
NoAuthWrapper,
ApiConfig,
},
data() {
return {
error: '',
errorNoApiUrl: ERROR_NO_API_URL,
}
},
created() {
this.load()
},
computed: {
ready() {
return this.$store.state.vikunjaReady
},
showLoading() {
return !this.ready && this.error === ''
},
...mapState([
'online',
]),
},
methods: {
load() {
this.$store.dispatch('loadApp')
.catch(e => {
this.error = e
})
},
},
const store = useStore()
const ready = computed(() => store.state.vikunjaReady)
const online = computed(() => store.state.online)
const error = ref('')
const showLoading = computed(() => !ready.value && error.value === '')
async function load() {
try {
await store.dispatch('loadApp')
} catch(e: any) {
error.value = e
}
}
load()
</script>
<style lang="scss" scoped>

View File

@ -7,24 +7,21 @@
</component>
</template>
<script>
export default {
name: 'shortcut',
props: {
keys: {
type: Array,
required: true,
},
combination: {
type: String,
default: '+',
},
is: {
type: String,
default: 'div',
},
<script lang="ts" setup>
defineProps({
keys: {
type: Array,
required: true,
},
}
combination: {
type: String,
default: '+',
},
is: {
type: String,
default: 'div',
},
})
</script>
<style lang="scss" scoped>

View File

@ -22,91 +22,89 @@
</a>
</template>
<script>
<script lang="ts" setup>
import {computed, shallowRef} from 'vue'
import {useI18n} from 'vue-i18n'
import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription'
export default {
name: 'task-subscription',
data() {
return {
subscriptionService: new SubscriptionService(),
}
},
props: {
entity: {
required: true,
type: String,
},
subscription: {
required: true,
},
entityId: {
required: true,
},
isButton: {
type: Boolean,
default: true,
},
},
emits: ['change'],
computed: {
tooltipText() {
if (this.disabled) {
return this.$t('task.subscription.subscribedThroughParent', {
entity: this.entity,
parent: this.subscription.entity,
})
}
import {success} from '@/message'
return this.subscription !== null ?
this.$t('task.subscription.subscribed', {entity: this.entity}) :
this.$t('task.subscription.notSubscribed', {entity: this.entity})
},
buttonText() {
return this.subscription !== null ? this.$t('task.subscription.unsubscribe') : this.$t('task.subscription.subscribe')
},
icon() {
return this.subscription !== null ? ['far', 'bell-slash'] : 'bell'
},
disabled() {
if (this.subscription === null) {
return false
}
return this.subscription.entity !== this.entity
},
const props = defineProps({
entity: {
required: true,
type: String,
},
methods: {
changeSubscription() {
if (this.disabled) {
return
}
if (this.subscription === null) {
this.subscribe()
} else {
this.unsubscribe()
}
},
async subscribe() {
const subscription = new SubscriptionModel({
entity: this.entity,
entityId: this.entityId,
})
await this.subscriptionService.create(subscription)
this.$emit('change', subscription)
this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})})
},
async unsubscribe() {
const subscription = new SubscriptionModel({
entity: this.entity,
entityId: this.entityId,
})
await this.subscriptionService.delete(subscription)
this.$emit('change', null)
this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})})
},
subscription: {
required: true,
},
entityId: {
required: true,
},
isButton: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['change'])
const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n()
const tooltipText = computed(() => {
if (disabled.value) {
return t('task.subscription.subscribedThroughParent', {
entity: props.entity,
parent: props.subscription.entity,
})
}
return props.subscription !== null ?
t('task.subscription.subscribed', {entity: props.entity}) :
t('task.subscription.notSubscribed', {entity: props.entity})
})
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const icon = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => {
if (props.subscription === null) {
return false
}
return props.subscription.entity !== props.entity
})
function changeSubscription() {
if (disabled.value) {
return
}
if (props.subscription === null) {
subscribe()
} else {
unsubscribe()
}
}
async function subscribe() {
const subscription = new SubscriptionModel({
entity: props.entity,
entityId: props.entityId,
})
await subscriptionService.value.create(subscription)
emit('change', subscription)
success({message: t('task.subscription.subscribeSuccess', {entity: props.entity})})
}
async function unsubscribe() {
const subscription = new SubscriptionModel({
entity: props.entity,
entityId: props.entityId,
})
await subscriptionService.value.delete(subscription)
emit('change', null)
success({message: t('task.subscription.unsubscribeSuccess', {entity: props.entity})})
}
</script>

View File

@ -11,31 +11,28 @@
</div>
</template>
<script>
export default {
name: 'user',
props: {
user: {
required: true,
type: Object,
},
showUsername: {
required: false,
type: Boolean,
default: true,
},
avatarSize: {
required: false,
type: Number,
default: 50,
},
isInline: {
required: false,
type: Boolean,
default: false,
},
<script lang="ts" setup>
defineProps({
user: {
required: true,
type: Object,
},
}
showUsername: {
required: false,
type: Boolean,
default: true,
},
avatarSize: {
required: false,
type: Number,
default: 50,
},
isInline: {
required: false,
type: Boolean,
default: false,
},
})
</script>
<style lang="scss" scoped>

View File

@ -9,32 +9,23 @@
/>
</template>
<script>
<script lang="ts" setup>
import {ref, computed} from 'vue'
import {useStore} from 'vuex'
import Multiselect from '@/components/input/multiselect.vue'
export default {
name: 'namespace-search',
emits: ['selected'],
data() {
return {
query: '',
}
},
components: {
Multiselect,
},
computed: {
namespaces() {
return this.$store.getters['namespaces/searchNamespace'](this.query)
},
},
methods: {
findNamespaces(query) {
this.query = query
},
select(namespace) {
this.$emit('selected', namespace)
},
},
const emit = defineEmits(['selected'])
const query = ref('')
const store = useStore()
const namespaces = computed(() => store.getters['namespaces/searchNamespace'](query.value))
function findNamespaces(newQuery: string) {
query.value = newQuery
}
function select(namespace) {
emit('selected', namespace)
}
</script>

View File

@ -52,30 +52,22 @@
</dropdown>
</template>
<script>
<script setup lang="ts">
import {ref, onMounted} from 'vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
export default {
name: 'namespace-settings-dropdown',
data() {
return {
subscription: null,
}
const props = defineProps({
namespace: {
type: Object, // NamespaceModel
required: true,
},
components: {
DropdownItem,
Dropdown,
TaskSubscription,
},
props: {
namespace: {
required: true,
},
},
mounted() {
this.subscription = this.namespace.subscription
},
}
})
const subscription = ref(null)
onMounted(() => {
subscription.value = props.namespace.subscription
})
</script>

View File

@ -3,17 +3,13 @@
<div class="field is-grouped">
<p class="control has-icons-left is-expanded">
<textarea
:disabled="taskService.loading || null"
class="input"
:disabled="taskService.loading || undefined"
class="add-task-textarea input"
:placeholder="$t('list.list.addPlaceholder')"
cols="1"
rows="1"
v-focus
v-model="newTaskTitle"
ref="newTaskInput"
:style="{
'minHeight': `${initialTextAreaHeight}px`,
'height': `calc(${textAreaHeight}px - 2px + 1rem)`
}"
@keyup="errorMessage = ''"
@keydown.enter="handleEnter"
/>
@ -23,7 +19,8 @@
</p>
<p class="control">
<x-button
:disabled="newTaskTitle === '' || taskService.loading || null"
class="add-task-button"
:disabled="newTaskTitle === '' || taskService.loading || undefined"
@click="addTask()"
icon="plus"
:loading="taskService.loading"
@ -35,121 +32,172 @@
<p class="help is-danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</p>
<quick-add-magic v-if="errorMessage === ''"/>
<quick-add-magic v-else />
</div>
</template>
<script>
<script setup lang="ts">
import {ref, watch, unref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex'
import { tryOnMounted, debouncedWatch, useWindowSize, MaybeRef } from '@vueuse/core'
import TaskService from '../../services/task'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
const INPUT_BORDER_PX = 2
const LINE_HEIGHT = 1.5 // using getComputedStyles().lineHeight returns an (wrong) absolute pixel value, we need the factor to do calculations with it.
const cleanupTitle = title => {
function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
}
export default {
name: 'add-task',
emits: ['taskAdded'],
data() {
return {
newTaskTitle: '',
taskService: new TaskService(),
errorMessage: '',
textAreaHeight: null,
initialTextAreaHeight: null,
function useAutoHeightTextarea(value: MaybeRef<string>) {
const textarea = ref<HTMLInputElement>()
const minHeight = ref(0)
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
function resize(textareaEl: HTMLInputElement|undefined) {
if (!textareaEl) return
let empty
// the value here is the the attribute value
if (!textareaEl.value && textareaEl.placeholder) {
empty = true
textareaEl.value = textareaEl.placeholder
}
},
components: {
QuickAddMagic,
},
props: {
defaultPosition: {
type: Number,
required: false,
const cs = getComputedStyle(textareaEl)
textareaEl.style.minHeight = ''
textareaEl.style.height = '0'
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
const height = textareaEl.scrollHeight + offset + 'px'
textareaEl.style.height = height
// calculate min-height for the first time
if (!minHeight.value) {
minHeight.value = parseFloat(height)
}
textareaEl.style.minHeight = minHeight.value.toString()
if (empty) {
textareaEl.value = ''
}
}
tryOnMounted(() => {
if (textarea.value) {
// we don't want scrollbars
textarea.value.style.overflowY = 'hidden'
}
})
const { width: windowWidth } = useWindowSize()
debouncedWatch(
windowWidth,
() => resize(textarea.value),
{ debounce: 200 },
)
// It is not possible to get notified of a change of the value attribute of a textarea without workarounds (setTimeout)
// So instead we watch the value that we bound to it.
watch(
() => [textarea.value, unref(value)],
() => resize(textarea.value),
{
immediate: true, // calculate initial size
flush: 'post', // resize after value change is rendered to DOM
},
)
return textarea
}
const emit = defineEmits(['taskAdded'])
const props = defineProps({
defaultPosition: {
type: Number,
required: false,
},
watch: {
newTaskTitle(newVal) {
// Calculating the textarea height based on lines of input in it.
// That is more reliable when removing a line from the input.
const numberOfLines = newVal.split(/\r\n|\r|\n/).length
const fontSize = parseFloat(window.getComputedStyle(this.$refs.newTaskInput, null).getPropertyValue('font-size'))
})
this.textAreaHeight = numberOfLines * fontSize * LINE_HEIGHT + INPUT_BORDER_PX
},
},
mounted() {
this.initialTextAreaHeight = this.$refs.newTaskInput.scrollHeight + INPUT_BORDER_PX
},
methods: {
async addTask() {
if (this.newTaskTitle === '') {
this.errorMessage = this.$t('list.create.addTitleRequired')
return
}
this.errorMessage = ''
const taskService = shallowReactive(new TaskService())
const errorMessage = ref('')
if (this.taskService.loading) {
return
}
const newTaskTitle = ref('')
const newTaskInput = useAutoHeightTextarea(newTaskTitle)
const newTasks = this.newTaskTitle.split(/[\r\n]+/).map(async t => {
const title = cleanupTitle(t)
if (title === '') {
return
}
const { t } = useI18n()
const store = useStore()
const task = await this.$store.dispatch('tasks/createNewTask', {
title,
listId: this.$store.state.auth.settings.defaultListId,
position: this.defaultPosition,
})
this.$emit('taskAdded', task)
return task
})
async function addTask() {
if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired')
return
}
errorMessage.value = ''
try {
await Promise.all(newTasks)
this.newTaskTitle = ''
} catch (e) {
if (e.message === 'NO_LIST') {
this.errorMessage = this.$t('list.create.addListRequired')
return
}
throw e
}
},
handleEnter(e) {
// when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create
// the new task(s). The vue event modifier don't allow this, hence this method.
if (e.shiftKey) {
return
}
if (taskService.loading) {
return
}
e.preventDefault()
this.addTask()
},
},
const taskTitleBackup = newTaskTitle.value
const newTasks = newTaskTitle.value.split(/[\r\n]+/).map(async uncleanedTitle => {
const title = cleanupTitle(uncleanedTitle)
if (title === '') {
return
}
const task = await store.dispatch('tasks/createNewTask', {
title,
listId: store.state.auth.settings.defaultListId,
position: props.defaultPosition,
})
emit('taskAdded', task)
return task
})
try {
newTaskTitle.value = ''
await Promise.all(newTasks)
} catch (e: any) {
newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') {
errorMessage.value = t('list.create.addListRequired')
return
}
throw e
}
}
function handleEnter(e: KeyboardEvent) {
// when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create
// the new task(s). The vue event modifier don't allow this, hence this method.
if (e.shiftKey) {
return
}
e.preventDefault()
addTask()
}
</script>
<style lang="scss" scoped>
.task-add {
margin-bottom: 0;
.button {
height: 2.5rem;
}
}
.input, .textarea {
.add-task-button {
height: 2.5rem;
}
.add-task-textarea {
transition: border-color $transition;
}
.input {
resize: vertical;
resize: none;
}
</style>

View File

@ -2,15 +2,7 @@
<div class="gantt-chart">
<div class="filter-container">
<div class="items">
<x-button
@click.prevent.stop="showTaskFilter = !showTaskFilter"
type="secondary"
icon="filter"
>
{{ $t('filters.title') }}
</x-button>
<filter-popup
:visible="showTaskFilter"
v-model="params"
@update:modelValue="loadTasks()"
/>
@ -237,7 +229,6 @@ export default {
newTaskFieldActive: false,
priorities: priorities,
taskCollectionService: new TaskCollectionService(),
showTaskFilter: false,
params: {
sort_by: ['done', 'id'],

View File

@ -1,9 +1,9 @@
import {computed, watch, readonly} from 'vue'
import {useStorage, createSharedComposable, ColorSchemes, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
import {useStorage, createSharedComposable, ColorSchema, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
const STORAGE_KEY = 'color-scheme'
const DEFAULT_COLOR_SCHEME_SETTING: ColorSchemes = 'light'
const DEFAULT_COLOR_SCHEME_SETTING: ColorSchema = 'light'
const CLASS_DARK = 'dark'
const CLASS_LIGHT = 'light'
@ -16,7 +16,7 @@ const CLASS_LIGHT = 'light'
// - value is synced via `createSharedComposable`
// https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts
export const useColorScheme = createSharedComposable(() => {
const store = useStorage<ColorSchemes>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const store = useStorage<ColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const preferredColorScheme = usePreferredColorScheme()

View File

@ -297,7 +297,8 @@ const getDayFromText = (text: string) => {
}
}
const date = new Date()
const now = new Date()
const date = new Date(now)
const day = parseInt(results[0])
date.setDate(day)
@ -309,7 +310,7 @@ const getDayFromText = (text: string) => {
date.setDate(day)
}
if (date < new Date()) {
if (date < now) {
date.setMonth(date.getMonth() + 1)
}

View File

@ -208,7 +208,11 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(date.getDate())
expect(result.date.getMonth()).toBe(result.date.getDate() === 31 ? date.getMonth() + 2 : date.getMonth() + 1)
const nextMonthWithDate = result.date.getDate() === 31
? (date.getMonth() + 2) % 12
: (date.getMonth() + 1) % 12
expect(result.date.getMonth()).toBe(nextMonthWithDate)
})
it('should recognize dates of the month in the future', () => {
const nextDay = new Date(+new Date() + 60 * 60 * 24 * 1000)

View File

@ -19,14 +19,12 @@ $mobile: math.div($tablet, 2);
$family-sans-serif: 'Open Sans', Helvetica, Arial, sans-serif;
$vikunja-font: 'Quicksand', sans-serif;
$thickness: 1px;
$pagination-current-border: var(--primary);
$navbar-item-active-color: var(--primary);
$dropdown-content-shadow: none;
$dropdown-item-hover-background-color: var(--grey-100);
$bulmaswatch-import-font: false !default;
$site-background: var(--grey-100);
$transition-duration: 150ms;

View File

@ -40,6 +40,7 @@
}
.select select {
$thickness: 1px;
border-width: $thickness;
&:not([multiple]) {

View File

@ -1,7 +1,7 @@
/* eslint-disable no-console */
/* eslint-disable no-undef */
const workboxVersion = 'v6.4.1'
const workboxVersion = 'v6.4.2'
importScripts( `/workbox-${workboxVersion}/workbox-sw.js`)
workbox.setConfig({
modulePathPrefix: `/workbox-${workboxVersion}`,

View File

@ -47,7 +47,6 @@
v-for="(l, k) in listHistory"
:key="`l${k}`"
:list="l"
:background-resolver="() => null"
/>
</div>
</div>
@ -55,92 +54,66 @@
</div>
</template>
<script>
import {mapState} from 'vuex'
import Message from '@/components/misc/message'
import ShowTasks from './tasks/ShowTasks.vue'
import {getHistory} from '../modules/listHistory'
<script lang="ts" setup>
import {ref, computed} from 'vue'
import {useStore} from 'vuex'
import {useNow} from '@vueuse/core'
import Message from '@/components/misc/message.vue'
import ShowTasks from '@/views/tasks/ShowTasks.vue'
import ListCard from '@/components/list/partials/list-card.vue'
import AddTask from '../components/tasks/add-task.vue'
import {LOADING, LOADING_MODULE} from '../store/mutation-types'
import {parseDateOrNull} from '../helpers/parseDateOrNull'
import AddTask from '@/components/tasks/add-task.vue'
export default {
name: 'Home',
components: {
Message,
ListCard,
ShowTasks,
AddTask,
},
data() {
return {
currentDate: new Date(),
tasks: [],
showTasksKey: 0,
}
},
computed: {
welcome() {
const now = new Date()
import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
if (now.getHours() < 5) {
return 'Night'
}
const now = useNow()
const welcome = computed(() => {
const hours = new Date(now.value).getHours()
if (now.getHours() < 11) {
return 'Morning'
}
if (hours < 5) {
return 'Night'
}
if (now.getHours() < 18) {
return 'Day'
}
if (hours < 11) {
return 'Morning'
}
if (now.getHours() < 23) {
return 'Evening'
}
if (hours < 18) {
return 'Day'
}
return 'Night'
},
listHistory() {
const history = getHistory()
return history.map(l => {
return this.$store.getters['lists/getListById'](l.id)
}).filter(l => l !== null)
},
...mapState({
migratorsEnabled: state =>
state.config.availableMigrators !== null &&
state.config.availableMigrators.length > 0,
authenticated: state => state.auth.authenticated,
userInfo: state => state.auth.info,
hasTasks: state => state.hasTasks,
defaultListId: state => state.auth.defaultListId,
defaultNamespaceId: state => {
if (state.namespaces.namespaces.length === 0) {
return 0
}
if (hours < 23) {
return 'Evening'
}
return state.namespaces.namespaces[0].id
},
hasLists: state => {
if (state.namespaces.namespaces.length === 0) {
return false
}
return 'Night'
})
return state.namespaces.namespaces[0].lists.length > 0
},
loading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks',
deletionScheduledAt: state => parseDateOrNull(state.auth.info?.deletionScheduledAt),
}),
},
methods: {
// This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?)
updateTaskList() {
this.showTasksKey++
},
},
const store = useStore()
const listHistory = computed(() => {
const history = getHistory()
return history.map(l => {
return store.getters['lists/getListById'](l.id)
}).filter(l => l !== null)
})
const migratorsEnabled = computed(() => store.state.config.availableMigrators?.length > 0)
const userInfo = computed(() => store.state.auth.info)
const hasTasks = computed(() => store.state.hasTasks)
const defaultListId = computed(() => store.state.auth.defaultListId)
const defaultNamespaceId = computed(() => store.state.namespaces.namespaces?.[0]?.id || 0)
const hasLists = computed (() => store.state.namespaces.namespaces?.[0]?.lists.length > 0)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'tasks')
const deletionScheduledAt = computed(() => parseDateOrNull(store.state.auth.info?.deletionScheduledAt))
// This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?)
const showTasksKey = ref(0)
function updateTaskList() {
showTasksKey.value++
}
</script>

1646
yarn.lock

File diff suppressed because it is too large Load Diff