Compare commits

..

33 Commits

Author SHA1 Message Date
ddadfb3f26 chore(deps): update pnpm to v8.11.0
All checks were successful
continuous-integration/drone/pr Build is passing
2023-11-24 17:13:40 +00:00
Frederick [Bot]
240906f236 [skip ci] Updated translations via Crowdin 2023-11-24 00:24:04 +00:00
282ec3164b chore(deps): update dev-dependencies (#3829)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3829
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-22 10:20:23 +00:00
Frederick [Bot]
a994264234 [skip ci] Updated translations via Crowdin 2023-11-22 00:25:02 +00:00
4868ac824e
feat(i18n): add Slovene language for selection in the ui
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-21 22:14:15 +01:00
0c58ea1ade
fix(editor): don't crash when the component isn't completely mounted
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-21 13:25:55 +01:00
f45303c2e3
fix(editor): image paste handling 2023-11-21 13:23:05 +01:00
c3e53970de
chore(deps): update lockfile
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-21 13:03:06 +01:00
Frederick [Bot]
0795c0e448 [skip ci] Updated translations via Crowdin 2023-11-21 00:24:01 +00:00
cfd46dc39b fix(deps): update vueuse to v10.6.1 (#3822)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3822
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-20 20:40:34 +00:00
debae2326e
fix(editor): don't create empty "blob" files when pasting images
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-20 12:35:19 +01:00
23d670525d chore(deps): update dessant/repo-lockdown action to v4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-20 05:13:05 +00:00
2967019cd9
feat(editor): mark a checkbox item as done when clicking on its text
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-18 18:01:09 +01:00
d3497c96d7
fix(editor): correctly resolve images in descriptions
Some checks failed
continuous-integration/drone/push Build is failing
Resolves #3808
2023-11-18 17:17:14 +01:00
bd83294ac0
fix(editor): alignment and focus states
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-18 17:03:47 +01:00
6c4f1e1cbf
fix(editor): make initial editor mode (preview/edit) work
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-18 16:54:29 +01:00
fa269f155a
chore(filter): remove debug log
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-18 16:44:51 +01:00
602d15985b
fix(filter): don't immediately re-trigger prepareFilter
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-18 16:40:20 +01:00
cc3c1a9429 chore(deps): update dev-dependencies (#3828)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3828
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-18 14:21:37 +00:00
cfd49864e1 fix(deps): update dependency axios to v1.6.2 (#3820)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3820
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-18 14:21:02 +00:00
6711a08de9 fix(deps): update sentry-javascript monorepo to v7.80.1 (#3819)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3819
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:50:00 +00:00
7fe33c6662 fix(deps): update dependency @types/sortablejs to v1.15.5 (#3818)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3818
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:49:51 +00:00
e61b215dc1 fix(deps): update dependency ufo to v1.3.2 (#3824)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3824
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:03:25 +00:00
3b5cb1ade3 fix(deps): update dependency vue-i18n to v9.7.0 (#3825)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3825
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:01:43 +00:00
89e28cbdf2 chore(deps): update dev-dependencies (#3826)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3826
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 21:59:59 +00:00
e9e836f068 chore(deps): update pnpm to v8.10.5
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-17 21:40:42 +00:00
aa5e11915e
fix(filter): don't prevent entering date math strings
All checks were successful
continuous-integration/drone/push Build is passing
Resolves https://community.vikunja.io/t/filter-setting-s/1791/2
2023-11-17 19:38:55 +01:00
7f279c98e1
fix(tasks): don't use the filter for upcoming when one is set for the home page
All checks were successful
continuous-integration/drone/push Build is passing
Resolves https://github.com/go-vikunja/frontend/issues/132
2023-11-17 19:08:08 +01:00
3c1861eb6a
fix(settings): move overdue remindeer time below
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-17 19:03:58 +01:00
75262b716f
fix(kanban): opening a task from the kanban board and then reloading the page should not crash everything when then navigating back
All checks were successful
continuous-integration/drone/push Build is passing
Before this fix, the following would not work:

1. Open the kanban view of a project
2. Click on a task to open it in a modal
3. Reload the page
4. Using your browser's back button, navigate back

Instead of showing the kanban board with the task modal closed, it would
navigate to `/projects/0/kanban` and crash.
2023-11-15 23:43:39 +01:00
7e623d919e fix(filters): infinite loop when creating filters with dates (#3061)
All checks were successful
continuous-integration/drone/push Build is passing
Rather than putting in a truncated version of the date/time with `startDate.getDate`, use the iso formatted version which includes the timezone data. I have no idea if this has ramifications elsewhere in the app, but it solves the problems I was seeing.

Co-authored-by: Sean Hurley <sean.hurley6@gmail.com>
Reviewed-on: #3061
Reviewed-by: konrad <k@knt.li>
Co-authored-by: ThatHurleyGuy <sean@hurley.io>
Co-committed-by: ThatHurleyGuy <sean@hurley.io>
2023-11-15 12:10:18 +00:00
3f42ce2b34
fix(filter): make other filters are not available for project selection
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-15 12:47:19 +01:00
8b8da40265 chore(deps): update dev-dependencies (#3821)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3821
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-15 11:23:57 +00:00
18 changed files with 1796 additions and 559 deletions

View File

@ -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.

View File

@ -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": {

File diff suppressed because it is too large Load Diff

View File

@ -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}`
} }
}, },

View File

@ -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"

View File

@ -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>

View File

@ -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;
} }
} }

View File

@ -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],
} }
} }

View File

@ -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 {

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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 {

View File

@ -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
} }

View File

@ -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>