Compare commits
33 Commits
eb21d17120
...
ddadfb3f26
Author | SHA1 | Date | |
---|---|---|---|
ddadfb3f26 | |||
|
240906f236 | ||
282ec3164b | |||
|
a994264234 | ||
4868ac824e | |||
0c58ea1ade | |||
f45303c2e3 | |||
c3e53970de | |||
|
0795c0e448 | ||
cfd46dc39b | |||
debae2326e | |||
23d670525d | |||
2967019cd9 | |||
d3497c96d7 | |||
bd83294ac0 | |||
6c4f1e1cbf | |||
fa269f155a | |||
602d15985b | |||
cc3c1a9429 | |||
cfd49864e1 | |||
6711a08de9 | |||
7fe33c6662 | |||
e61b215dc1 | |||
3b5cb1ade3 | |||
89e28cbdf2 | |||
e9e836f068 | |||
aa5e11915e | |||
7f279c98e1 | |||
3c1861eb6a | |||
75262b716f | |||
7e623d919e | |||
3f42ce2b34 | |||
8b8da40265 |
2
.github/workflows/lockdown.yml
vendored
2
.github/workflows/lockdown.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
action:
|
action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/repo-lockdown@v3
|
- uses: dessant/repo-lockdown@v4
|
||||||
with:
|
with:
|
||||||
pr-comment: 'Hi! Thank you for your contribution.
|
pr-comment: 'Hi! Thank you for your contribution.
|
||||||
|
|
||||||
|
|
57
package.json
57
package.json
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://vikunja.io/",
|
"homepage": "https://vikunja.io/",
|
||||||
"funding": "https://opencollective.com/vikunja",
|
"funding": "https://opencollective.com/vikunja",
|
||||||
"packageManager": "pnpm@8.10.5",
|
"packageManager": "pnpm@8.11.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"todo",
|
"todo",
|
||||||
"productivity",
|
"productivity",
|
||||||
|
@ -53,8 +53,8 @@
|
||||||
"@infectoone/vue-ganttastic": "2.2.0",
|
"@infectoone/vue-ganttastic": "2.2.0",
|
||||||
"@intlify/unplugin-vue-i18n": "1.5.0",
|
"@intlify/unplugin-vue-i18n": "1.5.0",
|
||||||
"@kyvg/vue3-notification": "3.0.2",
|
"@kyvg/vue3-notification": "3.0.2",
|
||||||
"@sentry/tracing": "7.77.0",
|
"@sentry/tracing": "7.80.1",
|
||||||
"@sentry/vue": "7.77.0",
|
"@sentry/vue": "7.80.1",
|
||||||
"@tiptap/core": "2.1.12",
|
"@tiptap/core": "2.1.12",
|
||||||
"@tiptap/extension-blockquote": "2.1.12",
|
"@tiptap/extension-blockquote": "2.1.12",
|
||||||
"@tiptap/extension-bold": "2.1.12",
|
"@tiptap/extension-bold": "2.1.12",
|
||||||
|
@ -90,10 +90,9 @@
|
||||||
"@tiptap/vue-3": "2.1.12",
|
"@tiptap/vue-3": "2.1.12",
|
||||||
"@types/is-touch-device": "1.0.2",
|
"@types/is-touch-device": "1.0.2",
|
||||||
"@types/lodash.clonedeep": "4.5.9",
|
"@types/lodash.clonedeep": "4.5.9",
|
||||||
"@types/sortablejs": "1.15.4",
|
"@vueuse/core": "10.6.1",
|
||||||
"@vueuse/core": "10.5.0",
|
"@vueuse/router": "10.6.1",
|
||||||
"@vueuse/router": "10.5.0",
|
"axios": "1.6.2",
|
||||||
"axios": "1.6.0",
|
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
"camel-case": "4.1.2",
|
"camel-case": "4.1.2",
|
||||||
|
@ -113,11 +112,11 @@
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"ufo": "1.3.1",
|
"ufo": "1.3.2",
|
||||||
"vue": "3.3.8",
|
"vue": "3.3.8",
|
||||||
"vue-advanced-cropper": "2.8.8",
|
"vue-advanced-cropper": "2.8.8",
|
||||||
"vue-flatpickr-component": "11.0.3",
|
"vue-flatpickr-component": "11.0.3",
|
||||||
"vue-i18n": "9.6.5",
|
"vue-i18n": "9.7.0",
|
||||||
"vue-router": "4.2.5",
|
"vue-router": "4.2.5",
|
||||||
"workbox-precaching": "7.0.0",
|
"workbox-precaching": "7.0.0",
|
||||||
"zhyswan-vuedraggable": "4.1.3"
|
"zhyswan-vuedraggable": "4.1.3"
|
||||||
|
@ -126,56 +125,56 @@
|
||||||
"@4tw/cypress-drag-drop": "2.2.5",
|
"@4tw/cypress-drag-drop": "2.2.5",
|
||||||
"@cypress/vite-dev-server": "5.0.6",
|
"@cypress/vite-dev-server": "5.0.6",
|
||||||
"@cypress/vue": "6.0.0",
|
"@cypress/vue": "6.0.0",
|
||||||
"@faker-js/faker": "8.2.0",
|
"@faker-js/faker": "8.3.1",
|
||||||
"@histoire/plugin-screenshot": "0.17.0",
|
"@histoire/plugin-screenshot": "0.17.0",
|
||||||
"@histoire/plugin-vue": "0.17.4",
|
"@histoire/plugin-vue": "0.17.5",
|
||||||
"@rushstack/eslint-patch": "1.5.1",
|
"@rushstack/eslint-patch": "1.6.0",
|
||||||
"@tsconfig/node18": "18.2.2",
|
"@tsconfig/node18": "18.2.2",
|
||||||
"@types/codemirror": "5.60.13",
|
"@types/codemirror": "5.60.15",
|
||||||
"@types/dompurify": "3.0.5",
|
"@types/dompurify": "3.0.5",
|
||||||
"@types/flexsearch": "0.7.6",
|
"@types/flexsearch": "0.7.6",
|
||||||
"@types/is-touch-device": "1.0.2",
|
"@types/is-touch-device": "1.0.2",
|
||||||
"@types/lodash.debounce": "4.0.9",
|
"@types/lodash.debounce": "4.0.9",
|
||||||
"@types/marked": "5.0.2",
|
"@types/marked": "5.0.2",
|
||||||
"@types/node": "20.8.10",
|
"@types/node": "20.9.4",
|
||||||
"@types/postcss-preset-env": "7.7.0",
|
"@types/postcss-preset-env": "7.7.0",
|
||||||
"@types/sortablejs": "1.15.5",
|
"@types/sortablejs": "1.15.7",
|
||||||
"@typescript-eslint/eslint-plugin": "6.10.0",
|
"@typescript-eslint/eslint-plugin": "6.12.0",
|
||||||
"@typescript-eslint/parser": "6.10.0",
|
"@typescript-eslint/parser": "6.12.0",
|
||||||
"@vitejs/plugin-legacy": "4.1.1",
|
"@vitejs/plugin-legacy": "4.1.1",
|
||||||
"@vitejs/plugin-vue": "4.4.0",
|
"@vitejs/plugin-vue": "4.5.0",
|
||||||
"@vue/eslint-config-typescript": "12.0.0",
|
"@vue/eslint-config-typescript": "12.0.0",
|
||||||
"@vue/test-utils": "2.4.1",
|
"@vue/test-utils": "2.4.2",
|
||||||
"@vue/tsconfig": "0.4.0",
|
"@vue/tsconfig": "0.4.0",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
"browserslist": "4.22.1",
|
"browserslist": "4.22.1",
|
||||||
"caniuse-lite": "1.0.30001561",
|
"caniuse-lite": "1.0.30001564",
|
||||||
"css-has-pseudo": "6.0.0",
|
"css-has-pseudo": "6.0.0",
|
||||||
"csstype": "3.1.2",
|
"csstype": "3.1.2",
|
||||||
"cypress": "13.4.0",
|
"cypress": "13.6.0",
|
||||||
"esbuild": "0.19.5",
|
"esbuild": "0.19.7",
|
||||||
"eslint": "8.53.0",
|
"eslint": "8.54.0",
|
||||||
"eslint-plugin-vue": "9.18.1",
|
"eslint-plugin-vue": "9.18.1",
|
||||||
"happy-dom": "12.10.3",
|
"happy-dom": "12.10.3",
|
||||||
"histoire": "0.17.4",
|
"histoire": "0.17.5",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"postcss-easing-gradients": "3.0.1",
|
"postcss-easing-gradients": "3.0.1",
|
||||||
"postcss-easings": "4.0.0",
|
"postcss-easings": "4.0.0",
|
||||||
"postcss-focus-within": "8.0.0",
|
"postcss-focus-within": "8.0.0",
|
||||||
"postcss-preset-env": "9.3.0",
|
"postcss-preset-env": "9.3.0",
|
||||||
"rollup": "4.3.0",
|
"rollup": "4.5.1",
|
||||||
"rollup-plugin-visualizer": "5.9.2",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
"start-server-and-test": "2.0.2",
|
"start-server-and-test": "2.0.3",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.3.2",
|
||||||
"vite": "4.5.0",
|
"vite": "4.5.0",
|
||||||
"vite-plugin-inject-preload": "1.3.3",
|
"vite-plugin-inject-preload": "1.3.3",
|
||||||
"vite-plugin-pwa": "0.16.7",
|
"vite-plugin-pwa": "0.17.0",
|
||||||
"vite-plugin-sentry": "1.3.0",
|
"vite-plugin-sentry": "1.3.0",
|
||||||
"vite-svg-loader": "4.0.0",
|
"vite-svg-loader": "4.0.0",
|
||||||
"vitest": "0.34.6",
|
"vitest": "0.34.6",
|
||||||
"vue-tsc": "1.8.22",
|
"vue-tsc": "1.8.22",
|
||||||
"wait-on": "7.1.0",
|
"wait-on": "7.2.0",
|
||||||
"workbox-cli": "7.0.0"
|
"workbox-cli": "7.0.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|
886
pnpm-lock.yaml
886
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -75,6 +75,7 @@ import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import flatPickr from 'vue-flatpickr-component'
|
import flatPickr from 'vue-flatpickr-component'
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
|
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||||
|
|
||||||
import Popup from '@/components/misc/popup.vue'
|
import Popup from '@/components/misc/popup.vue'
|
||||||
import {DATE_RANGES} from '@/components/date/dateRanges'
|
import {DATE_RANGES} from '@/components/date/dateRanges'
|
||||||
|
@ -120,9 +121,9 @@ watch(
|
||||||
to.value = newValue.dateTo
|
to.value = newValue.dateTo
|
||||||
// Only set the date back to flatpickr when it's an actual date.
|
// Only set the date back to flatpickr when it's an actual date.
|
||||||
// Otherwise flatpickr runs in an endless loop and slows down the browser.
|
// Otherwise flatpickr runs in an endless loop and slows down the browser.
|
||||||
const dateFrom = new Date(from.value)
|
const dateFrom = parseDateOrString(from.value, false)
|
||||||
const dateTo = new Date(to.value)
|
const dateTo = parseDateOrString(to.value, false)
|
||||||
if (dateTo.getTime() === dateTo.getTime() && dateFrom.getTime() === dateFrom.getTime()) {
|
if (dateFrom instanceof Date && dateTo instanceof Date) {
|
||||||
flatpickrRange.value = `${from.value} to ${to.value}`
|
flatpickrRange.value = `${from.value} to ${to.value}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
:enabled="Boolean(currentModal)"
|
:enabled="typeof currentModal !== 'undefined'"
|
||||||
@close="closeModal()"
|
@close="closeModal()"
|
||||||
variant="scrolling"
|
variant="scrolling"
|
||||||
class="task-detail-view-modal"
|
class="task-detail-view-modal"
|
||||||
|
|
|
@ -20,11 +20,20 @@ import type {IProject} from '@/modelTypes/IProject'
|
||||||
import ProjectService from '@/services/project'
|
import ProjectService from '@/services/project'
|
||||||
import {includesById} from '@/helpers/utils'
|
import {includesById} from '@/helpers/utils'
|
||||||
|
|
||||||
|
type ProjectFilterFunc = (p: IProject) => boolean
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array as PropType<IProject[]>,
|
type: Array as PropType<IProject[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
projectFilter: {
|
||||||
|
type: Function as PropType<ProjectFilterFunc>,
|
||||||
|
default: () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
return (_: IProject) => true
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: IProject[]): void
|
(e: 'update:modelValue', value: IProject[]): void
|
||||||
|
@ -58,6 +67,8 @@ async function findProjects(query: string) {
|
||||||
const response = await projectService.getAll({}, {s: query}) as IProject[]
|
const response = await projectService.getAll({}, {s: query}) as IProject[]
|
||||||
|
|
||||||
// Filter selected items from the results
|
// Filter selected items from the results
|
||||||
foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
|
foundProjects.value = response
|
||||||
|
.filter(({id}) => !includesById(projects.value, id))
|
||||||
|
.filter(props.projectFilter)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tiptap">
|
<div class="tiptap" ref="tiptapInstanceRef">
|
||||||
<EditorToolbar
|
<EditorToolbar
|
||||||
v-if="editor && isEditing"
|
v-if="editor && isEditing"
|
||||||
:editor="editor"
|
:editor="editor"
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
<editor-content
|
<editor-content
|
||||||
class="tiptap__editor"
|
class="tiptap__editor"
|
||||||
:class="{'tiptap__editor-is-empty': isEmpty, 'tiptap__editor-is-edit-enabled': isEditing}"
|
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
|
||||||
:editor="editor"
|
:editor="editor"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watch, onBeforeUnmount, nextTick, onMounted, computed} from 'vue'
|
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
|
||||||
import {refDebounced} from '@vueuse/core'
|
import {refDebounced} from '@vueuse/core'
|
||||||
|
|
||||||
import EditorToolbar from './EditorToolbar.vue'
|
import EditorToolbar from './EditorToolbar.vue'
|
||||||
|
@ -173,7 +173,8 @@ import {Placeholder} from '@tiptap/extension-placeholder'
|
||||||
import {eventToHotkeyString} from '@github/hotkey'
|
import {eventToHotkeyString} from '@github/hotkey'
|
||||||
import {mergeAttributes} from '@tiptap/core'
|
import {mergeAttributes} from '@tiptap/core'
|
||||||
import {createRandomID} from '@/helpers/randomId'
|
import {createRandomID} from '@/helpers/randomId'
|
||||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
|
||||||
|
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
|
||||||
|
@ -202,9 +203,28 @@ type CacheKey = `${ITask['id']}-${IAttachment['id']}`
|
||||||
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
|
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
|
||||||
|
|
||||||
const CustomImage = Image.extend({
|
const CustomImage = Image.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
'data-src': {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
renderHTML({HTMLAttributes}) {
|
renderHTML({HTMLAttributes}) {
|
||||||
if (HTMLAttributes.src?.startsWith(window.API_URL)) {
|
if (HTMLAttributes.src?.startsWith(window.API_URL) || HTMLAttributes['data-src']?.startsWith(window.API_URL)) {
|
||||||
|
const imageUrl = HTMLAttributes['data-src'] ?? HTMLAttributes.src
|
||||||
const id = 'tiptap-image-' + createRandomID()
|
const id = 'tiptap-image-' + createRandomID()
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
|
|
||||||
|
@ -213,7 +233,7 @@ const CustomImage = Image.extend({
|
||||||
if (!img) return
|
if (!img) return
|
||||||
|
|
||||||
// The url is something like /tasks/<id>/attachments/<id>
|
// The url is something like /tasks/<id>/attachments/<id>
|
||||||
const parts = img.dataset?.src.slice(window.API_URL.length + 1).split('/')
|
const parts = imageUrl.slice(window.API_URL.length + 1).split('/')
|
||||||
const taskId = Number(parts[1])
|
const taskId = Number(parts[1])
|
||||||
const attachmentId = Number(parts[3])
|
const attachmentId = Number(parts[3])
|
||||||
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
|
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
|
||||||
|
@ -223,15 +243,14 @@ const CustomImage = Image.extend({
|
||||||
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
|
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
|
||||||
|
|
||||||
const attachmentService = new AttachmentService()
|
const attachmentService = new AttachmentService()
|
||||||
const url = await attachmentService.getBlobUrl(attachment)
|
loadedAttachments.value[cacheKey] = await attachmentService.getBlobUrl(attachment)
|
||||||
loadedAttachments.value[cacheKey] = url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img.src = loadedAttachments.value[cacheKey]
|
img.src = loadedAttachments.value[cacheKey]
|
||||||
})
|
})
|
||||||
|
|
||||||
return ['img', mergeAttributes(this.options.HTMLAttributes, {
|
return ['img', mergeAttributes(this.options.HTMLAttributes, {
|
||||||
'data-src': HTMLAttributes.src,
|
'data-src': imageUrl,
|
||||||
src: '#',
|
src: '#',
|
||||||
alt: HTMLAttributes.alt,
|
alt: HTMLAttributes.alt,
|
||||||
title: HTMLAttributes.title,
|
title: HTMLAttributes.title,
|
||||||
|
@ -268,7 +287,6 @@ const {
|
||||||
const emit = defineEmits(['update:modelValue', 'save'])
|
const emit = defineEmits(['update:modelValue', 'save'])
|
||||||
|
|
||||||
const inputHTML = ref('')
|
const inputHTML = ref('')
|
||||||
const isEmpty = computed(() => isEditorContentEmpty(inputHTML.value))
|
|
||||||
const internalMode = ref<Mode>(initialMode)
|
const internalMode = ref<Mode>(initialMode)
|
||||||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||||
|
|
||||||
|
@ -475,26 +493,37 @@ function setLink() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
internalMode.value = initialMode
|
internalMode.value = initialMode
|
||||||
document.addEventListener('paste', handleImagePaste)
|
nextTick(() => {
|
||||||
|
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
|
||||||
|
input?.addEventListener('paste', handleImagePaste)
|
||||||
|
})
|
||||||
if (editShortcut !== '') {
|
if (editShortcut !== '') {
|
||||||
document.addEventListener('keydown', setFocusToEditor)
|
document.addEventListener('keydown', setFocusToEditor)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('paste', handleImagePaste)
|
nextTick(() => {
|
||||||
|
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
|
||||||
|
input?.removeEventListener('paste', handleImagePaste)
|
||||||
|
})
|
||||||
if (editShortcut !== '') {
|
if (editShortcut !== '') {
|
||||||
document.removeEventListener('keydown', setFocusToEditor)
|
document.removeEventListener('keydown', setFocusToEditor)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleImagePaste(event) {
|
function handleImagePaste(event) {
|
||||||
|
if (event?.clipboardData?.items?.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event?.clipboardData?.items?.forEach(i => {
|
|
||||||
if (i.kind === 'file' && i.type.startsWith('image/')) {
|
const image = event.clipboardData.items[0]
|
||||||
uploadAndInsertFiles([i.getAsFile()])
|
if (image.kind === 'file' && image.type.startsWith('image/')) {
|
||||||
}
|
console.log('img', image.getAsFile())
|
||||||
})
|
uploadAndInsertFiles([image.getAsFile()])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
||||||
|
@ -515,23 +544,71 @@ function setFocusToEditor(event) {
|
||||||
|
|
||||||
editor.value?.commands.focus()
|
editor.value?.commands.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clickTasklistCheckbox(event) {
|
||||||
|
// Needs to be a separate function to be able to remove the event listener
|
||||||
|
event.target.parentNode.parentNode.firstChild.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isEditing.value,
|
||||||
|
editing => {
|
||||||
|
nextTick(() => {
|
||||||
|
const checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||||
|
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (editing) {
|
||||||
|
checkboxes.forEach(check => {
|
||||||
|
if (check.children.length < 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||||
|
// When the actual label is clicked, we forward that click to the checkbox.
|
||||||
|
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checkboxes.forEach(check => {
|
||||||
|
if (check.children.length < 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||||
|
// When the actual label is clicked, we forward that click to the checkbox.
|
||||||
|
check.children[1].addEventListener('click', clickTasklistCheckbox)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{immediate: true},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tiptap__editor {
|
.tiptap__editor {
|
||||||
&.tiptap__editor-is-edit-enabled {
|
&.tiptap__editor-is-edit-enabled {
|
||||||
min-height: 10rem;
|
min-height: 10rem;
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within, &:focus {
|
||||||
|
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type='taskList'] li > div {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: box-shadow $transition;
|
transition: box-shadow $transition;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
|
|
||||||
&:focus-within, &:focus {
|
|
||||||
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap p.is-empty::before {
|
.tiptap p::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
color: var(--grey-400);
|
color: var(--grey-400);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -541,7 +618,7 @@ function setFocusToEditor(event) {
|
||||||
|
|
||||||
// Basic editor styles
|
// Basic editor styles
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
padding: .5rem;
|
padding: .5rem .5rem .5rem 0;
|
||||||
|
|
||||||
&:focus-within, &:focus {
|
&:focus-within, &:focus {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -774,6 +851,7 @@ ul[data-type='taskList'] {
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,7 @@
|
||||||
v-model="entities.projects"
|
v-model="entities.projects"
|
||||||
@select="changeMultiselectFilter('projects', 'project_id')"
|
@select="changeMultiselectFilter('projects', 'project_id')"
|
||||||
@remove="changeMultiselectFilter('projects', 'project_id')"
|
@remove="changeMultiselectFilter('projects', 'project_id')"
|
||||||
|
:project-filter="p => p.id > 0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,8 +177,9 @@ export const ALPHABETICAL_SORT = 'title'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs, watch} from 'vue'
|
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs} from 'vue'
|
||||||
import {camelCase} from 'camel-case'
|
import {camelCase} from 'camel-case'
|
||||||
|
import {watchDebounced} from '@vueuse/core'
|
||||||
|
|
||||||
import type {ILabel} from '@/modelTypes/ILabel'
|
import type {ILabel} from '@/modelTypes/ILabel'
|
||||||
import type {IUser} from '@/modelTypes/IUser'
|
import type {IUser} from '@/modelTypes/IUser'
|
||||||
|
@ -273,15 +275,16 @@ onMounted(() => {
|
||||||
filters.value.requireAllFilters = params.value.filter_concat === 'and'
|
filters.value.requireAllFilters = params.value.filter_concat === 'and'
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
// Using watchDebounced to prevent the filter re-triggering itself.
|
||||||
|
// FIXME: Only here until this whole component changes a lot with the new filter syntax.
|
||||||
|
watchDebounced(
|
||||||
modelValue,
|
modelValue,
|
||||||
(value) => {
|
(value) => {
|
||||||
// FIXME: filters should only be converted to snake case in
|
// FIXME: filters should only be converted to snake case in the last moment
|
||||||
// the last moment
|
|
||||||
params.value = objectToSnakeCase(value)
|
params.value = objectToSnakeCase(value)
|
||||||
prepareFilters()
|
prepareFilters()
|
||||||
},
|
},
|
||||||
{immediate: true},
|
{immediate: true, debounce: 500, maxWait: 1000},
|
||||||
)
|
)
|
||||||
|
|
||||||
const sortAlphabetically = computed({
|
const sortAlphabetically = computed({
|
||||||
|
@ -310,7 +313,7 @@ function prepareFilters() {
|
||||||
prepareDate('end_date', 'endDate')
|
prepareDate('end_date', 'endDate')
|
||||||
prepareSingleValue('priority', 'priority', 'usePriority', true)
|
prepareSingleValue('priority', 'priority', 'usePriority', true)
|
||||||
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||||
prepareDate('reminders')
|
prepareDate('reminders', 'reminders')
|
||||||
prepareRelatedObjectFilter('users', 'assignees')
|
prepareRelatedObjectFilter('users', 'assignees')
|
||||||
prepareProjectsFilter()
|
prepareProjectsFilter()
|
||||||
|
|
||||||
|
@ -386,13 +389,13 @@ function setDateFilter(filterName, {dateFrom, dateTo}) {
|
||||||
change()
|
change()
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareDate(filterName, variableName) {
|
function prepareDate(filterName: string, variableName: 'dueDate' | 'startDate' | 'endDate' | 'reminders') {
|
||||||
if (typeof params.value.filter_by === 'undefined') {
|
if (typeof params.value.filter_by === 'undefined') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let foundDateStart = false
|
let foundDateStart: boolean | string = false
|
||||||
let foundDateEnd = false
|
let foundDateEnd: boolean | string = false
|
||||||
for (const i in params.value.filter_by) {
|
for (const i in params.value.filter_by) {
|
||||||
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'greater_equals') {
|
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'greater_equals') {
|
||||||
foundDateStart = i
|
foundDateStart = i
|
||||||
|
@ -411,10 +414,10 @@ function prepareDate(filterName, variableName) {
|
||||||
const endDate = new Date(params.value.filter_value[foundDateEnd])
|
const endDate = new Date(params.value.filter_value[foundDateEnd])
|
||||||
filters.value[variableName] = {
|
filters.value[variableName] = {
|
||||||
dateFrom: !isNaN(startDate)
|
dateFrom: !isNaN(startDate)
|
||||||
? `${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}`
|
? `${startDate.getUTCFullYear()}-${startDate.getUTCMonth() + 1}-${startDate.getUTCDate()}`
|
||||||
: params.value.filter_value[foundDateStart],
|
: params.value.filter_value[foundDateStart],
|
||||||
dateTo: !isNaN(endDate)
|
dateTo: !isNaN(endDate)
|
||||||
? `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}`
|
? `${endDate.getUTCFullYear()}-${endDate.getUTCMonth() + 1}-${endDate.getUTCDate()}`
|
||||||
: params.value.filter_value[foundDateEnd],
|
: params.value.filter_value[foundDateEnd],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,13 +218,19 @@ const actions = computed(() => {
|
||||||
])))
|
])))
|
||||||
})
|
})
|
||||||
|
|
||||||
function attachmentUpload(
|
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
|
||||||
file: File,
|
|
||||||
onSuccess: (url: string) => void,
|
const uploadPromises: Promise<string>[] = []
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
onError: (error: string) => void,
|
files.forEach((file: File) => {
|
||||||
) {
|
const promise = new Promise<string>((resolve) => {
|
||||||
return uploadFile(props.taskId, file, onSuccess)
|
uploadFile(props.taskId, file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadPromises.push(promise)
|
||||||
|
})
|
||||||
|
|
||||||
|
return await Promise.all(uploadPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskCommentService = shallowReactive(new TaskCommentService())
|
const taskCommentService = shallowReactive(new TaskCommentService())
|
||||||
|
@ -299,7 +305,7 @@ async function editComment() {
|
||||||
if (commentEdit.comment === '') {
|
if (commentEdit.comment === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeTimeout.value !== null) {
|
if (changeTimeout.value !== null) {
|
||||||
clearTimeout(changeTimeout.value)
|
clearTimeout(changeTimeout.value)
|
||||||
}
|
}
|
||||||
|
@ -368,7 +374,7 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.image.is-avatar {
|
.image.is-avatar {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-content {
|
.media-content {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
</CustomTransition>
|
</CustomTransition>
|
||||||
</h3>
|
</h3>
|
||||||
<editor
|
<editor
|
||||||
|
class="tiptap__task-description"
|
||||||
:is-edit-enabled="canWrite"
|
:is-edit-enabled="canWrite"
|
||||||
:upload-callback="uploadCallback"
|
:upload-callback="uploadCallback"
|
||||||
:placeholder="$t('task.description.placeholder')"
|
:placeholder="$t('task.description.placeholder')"
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
v-model="description"
|
v-model="description"
|
||||||
@update:model-value="saveWithDelay"
|
@update:model-value="saveWithDelay"
|
||||||
@save="save"
|
@save="save"
|
||||||
:initial-mode="isEditorContentEmpty(description) ? 'preview' : 'edit'"
|
:initial-mode="isEditorContentEmpty(description) ? 'edit' : 'preview'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -123,3 +124,10 @@ async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tiptap__task-description {
|
||||||
|
// The exact amount of pixels we need to make the description icon align with the buttons and the form inside the editor.
|
||||||
|
// The icon is not exactly the same length on all sides so we need to hack our way around it.
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -24,13 +24,18 @@ export function useRouteWithModal() {
|
||||||
// this is adapted from vue-router
|
// this is adapted from vue-router
|
||||||
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
||||||
const routePropsOption = route.matched[0]?.props.default
|
const routePropsOption = route.matched[0]?.props.default
|
||||||
const routeProps = routePropsOption
|
let routeProps = undefined
|
||||||
? routePropsOption === true
|
if (routePropsOption) {
|
||||||
? route.params
|
if (routePropsOption === true) {
|
||||||
: typeof routePropsOption === 'function'
|
routeProps = route.params
|
||||||
? routePropsOption(route)
|
} else {
|
||||||
: routePropsOption
|
if(typeof routePropsOption === 'function') {
|
||||||
: {}
|
routeProps = routePropsOption(route)
|
||||||
|
} else {
|
||||||
|
routeProps = routePropsOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof routeProps === 'undefined') {
|
if (typeof routeProps === 'undefined') {
|
||||||
currentModal.value = undefined
|
currentModal.value = undefined
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
export function parseDateOrString(rawValue: string | undefined, fallback: unknown) {
|
export function parseDateOrString(rawValue: string | undefined | null, fallback: unknown): (unknown | string | Date) {
|
||||||
if (typeof rawValue === 'undefined') {
|
if (rawValue === null || typeof rawValue === 'undefined') {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rawValue.toLowerCase().includes('now') || rawValue.toLowerCase().includes('||')) {
|
||||||
|
return rawValue
|
||||||
|
}
|
||||||
|
|
||||||
const d = new Date(rawValue)
|
const d = new Date(rawValue)
|
||||||
|
|
||||||
return !isNaN(+d)
|
return !isNaN(+d)
|
||||||
|
|
|
@ -20,6 +20,7 @@ export const SUPPORTED_LOCALES = {
|
||||||
'ja-JP': '日本語',
|
'ja-JP': '日本語',
|
||||||
'hu-HU': 'Magyar',
|
'hu-HU': 'Magyar',
|
||||||
'ar-SA': 'اَلْعَرَبِيَّةُ',
|
'ar-SA': 'اَلْعَرَبِيَّةُ',
|
||||||
|
'sl-SI': 'Slovenščina',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||||
|
|
1105
src/i18n/lang/sl-SI.json
Normal file
1105
src/i18n/lang/sl-SI.json
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -37,7 +37,11 @@ const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler
|
||||||
const ProjectList = () => import('@/views/project/ProjectList.vue')
|
const ProjectList = () => import('@/views/project/ProjectList.vue')
|
||||||
const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
|
const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
|
||||||
const ProjectTable = () => import('@/views/project/ProjectTable.vue')
|
const ProjectTable = () => import('@/views/project/ProjectTable.vue')
|
||||||
const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
|
// If we load the component async, using it as a backdrop view will not work. Instead, everything explodes
|
||||||
|
// with an error from the core saying "Cannot read properties of undefined (reading 'parentNode')"
|
||||||
|
// Of course, with no clear indicator of where the problem comes from.
|
||||||
|
// const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
|
||||||
|
import ProjectKanban from '@/views/project/ProjectKanban.vue'
|
||||||
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
|
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
|
||||||
|
|
||||||
// Project Settings
|
// Project Settings
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<ProjectWrapper
|
<ProjectWrapper
|
||||||
class="project-kanban"
|
class="project-kanban"
|
||||||
:project-id="project.id"
|
:project-id="projectId"
|
||||||
viewName="kanban"
|
viewName="kanban"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
@ -330,9 +330,14 @@ const bucketDraggableComponentData = computed(() => ({
|
||||||
{'dragging-disabled': !canWrite.value},
|
{'dragging-disabled': !canWrite.value},
|
||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
const {
|
||||||
|
projectId = undefined,
|
||||||
|
} = defineProps<{
|
||||||
|
projectId: number,
|
||||||
|
}>()
|
||||||
|
|
||||||
const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
|
const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
|
||||||
const project = computed(() => baseStore.currentProject)
|
const project = computed(() => projectId ? projectStore.projects[projectId]: null)
|
||||||
|
|
||||||
const buckets = computed(() => kanbanStore.buckets)
|
const buckets = computed(() => kanbanStore.buckets)
|
||||||
const loading = computed(() => kanbanStore.isLoading)
|
const loading = computed(() => kanbanStore.isLoading)
|
||||||
|
@ -342,10 +347,9 @@ const taskLoading = computed(() => taskStore.isLoading)
|
||||||
watch(
|
watch(
|
||||||
() => ({
|
() => ({
|
||||||
params: params.value,
|
params: params.value,
|
||||||
project: project.value,
|
projectId,
|
||||||
}),
|
}),
|
||||||
({params, project}) => {
|
({params}) => {
|
||||||
const projectId = project.id
|
|
||||||
if (projectId === undefined || Number(projectId) === 0) {
|
if (projectId === undefined || Number(projectId) === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -396,7 +400,7 @@ function updateTasks(bucketId: IBucket['id'], tasks: IBucket['tasks']) {
|
||||||
async function updateTaskPosition(e) {
|
async function updateTaskPosition(e) {
|
||||||
drag.value = false
|
drag.value = false
|
||||||
|
|
||||||
// While we could just pass the bucket index in through the function call, this would not give us the
|
// While we could just pass the bucket index in through the function call, this would not give us the
|
||||||
// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
|
// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
|
||||||
// of the drop target works all the time.
|
// of the drop target works all the time.
|
||||||
const bucketIndex = parseInt(e.to.dataset.bucketIndex)
|
const bucketIndex = parseInt(e.to.dataset.bucketIndex)
|
||||||
|
@ -450,7 +454,7 @@ async function updateTaskPosition(e) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await taskStore.update(newTask)
|
await taskStore.update(newTask)
|
||||||
|
|
||||||
// Make sure the first and second task don't both get position 0 assigned
|
// Make sure the first and second task don't both get position 0 assigned
|
||||||
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
||||||
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
|
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
|
||||||
|
@ -480,7 +484,7 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newTaskError.value[bucketId] = false
|
newTaskError.value[bucketId] = false
|
||||||
|
|
||||||
const task = await taskStore.createNewTask({
|
const task = await taskStore.createNewTask({
|
||||||
title: newTaskText.value,
|
title: newTaskText.value,
|
||||||
bucketId,
|
bucketId,
|
||||||
|
@ -619,7 +623,7 @@ async function toggleDoneBucket(bucket: IBucket) {
|
||||||
const doneBucketId = project.value.doneBucketId === bucket.id
|
const doneBucketId = project.value.doneBucketId === bucket.id
|
||||||
? 0
|
? 0
|
||||||
: bucket.id
|
: bucket.id
|
||||||
|
|
||||||
await projectStore.updateProject({
|
await projectStore.updateProject({
|
||||||
...project.value,
|
...project.value,
|
||||||
doneBucketId,
|
doneBucketId,
|
||||||
|
@ -722,7 +726,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||||
}
|
}
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
padding-bottom: .5rem;
|
padding-bottom: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-move {
|
.no-move {
|
||||||
|
|
|
@ -182,7 +182,7 @@ async function loadPendingTasks(from: string, to: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authStore.settings.frontendSettings.filterIdUsedOnOverview && typeof projectStore.projects[authStore.settings.frontendSettings.filterIdUsedOnOverview] !== 'undefined') {
|
if (showAll.value && authStore.settings.frontendSettings.filterIdUsedOnOverview && typeof projectStore.projects[authStore.settings.frontendSettings.filterIdUsedOnOverview] !== 'undefined') {
|
||||||
tasks.value = await taskStore.loadTasks(params, authStore.settings.frontendSettings.filterIdUsedOnOverview)
|
tasks.value = await taskStore.loadTasks(params, authStore.settings.frontendSettings.filterIdUsedOnOverview)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,25 +24,6 @@
|
||||||
</label>
|
</label>
|
||||||
<project-search v-model="filterUsedInOverview" :saved-filters-only="true"/>
|
<project-search v-model="filterUsedInOverview" :saved-filters-only="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox">
|
|
||||||
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
|
|
||||||
{{ $t('user.settings.general.overdueReminders') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field" v-if="settings.overdueTasksRemindersEnabled">
|
|
||||||
<label class="label" for="overdueTasksReminderTime">
|
|
||||||
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
@keyup.enter="updateSettings"
|
|
||||||
class="input"
|
|
||||||
id="overdueTasksReminderTime"
|
|
||||||
type="time"
|
|
||||||
v-model="settings.overdueTasksRemindersTime"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
|
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
|
||||||
|
@ -67,6 +48,25 @@
|
||||||
{{ $t('user.settings.general.playSoundWhenDone') }}
|
{{ $t('user.settings.general.playSoundWhenDone') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
|
||||||
|
{{ $t('user.settings.general.overdueReminders') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field" v-if="settings.overdueTasksRemindersEnabled">
|
||||||
|
<label class="label" for="overdueTasksReminderTime">
|
||||||
|
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
@keyup.enter="updateSettings"
|
||||||
|
class="input"
|
||||||
|
id="overdueTasksReminderTime"
|
||||||
|
type="time"
|
||||||
|
v-model="settings.overdueTasksRemindersTime"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="is-flex is-align-items-center">
|
<label class="is-flex is-align-items-center">
|
||||||
<span>
|
<span>
|
||||||
|
|
Reference in New Issue
Block a user