Compare commits
29 Commits
1dee14a868
...
76dbf612f6
Author | SHA1 | Date |
---|---|---|
renovate | 76dbf612f6 | |
kolaente | c8809d899a | |
renovate | 203041ae36 | |
renovate | 11d11012e7 | |
renovate | 30046c7ff5 | |
renovate | 668ff753b3 | |
renovate | 0b68ab93e1 | |
renovate | af22d2e88a | |
kolaente | 611e9feb6d | |
kolaente | e770496524 | |
kolaente | 1cbb93ea9b | |
Frederick [Bot] | f7c06e53b7 | |
Frederick [Bot] | 240906f236 | |
renovate | 282ec3164b | |
Frederick [Bot] | a994264234 | |
kolaente | 4868ac824e | |
kolaente | 0c58ea1ade | |
kolaente | f45303c2e3 | |
kolaente | c3e53970de | |
Frederick [Bot] | 0795c0e448 | |
renovate | cfd46dc39b | |
kolaente | debae2326e | |
renovate | 23d670525d | |
kolaente | 2967019cd9 | |
kolaente | d3497c96d7 | |
kolaente | bd83294ac0 | |
kolaente | 6c4f1e1cbf | |
kolaente | fa269f155a | |
kolaente | 602d15985b |
16
.drone.yml
16
.drone.yml
|
@ -42,7 +42,7 @@ steps:
|
|||
# - .cache
|
||||
|
||||
- name: dependencies
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -55,7 +55,7 @@ steps:
|
|||
# - restore-cache
|
||||
|
||||
- name: lint
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -66,7 +66,7 @@ steps:
|
|||
- dependencies
|
||||
|
||||
- name: build-prod
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -77,7 +77,7 @@ steps:
|
|||
- dependencies
|
||||
|
||||
- name: test-unit
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
|
@ -87,7 +87,7 @@ steps:
|
|||
|
||||
- name: typecheck
|
||||
failure: ignore
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -202,7 +202,7 @@ steps:
|
|||
# - .cache
|
||||
|
||||
- name: build
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -285,7 +285,7 @@ steps:
|
|||
# - .cache
|
||||
|
||||
- name: build
|
||||
image: node:20.9-alpine
|
||||
image: node:20.10-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -532,6 +532,6 @@ steps:
|
|||
src/i18n/lang/en.json: en.json
|
||||
---
|
||||
kind: signature
|
||||
hmac: dab902060979f246df77641c995c843ea39f86dba2de9003da7e593ce6f6f08a
|
||||
hmac: 3380c4283256eea047e6228817161991d23457d09abe9d99f06e018b1eb047f4
|
||||
|
||||
...
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/repo-lockdown@v3
|
||||
- uses: dessant/repo-lockdown@v4
|
||||
with:
|
||||
pr-comment: 'Hi! Thank you for your contribution.
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:20.9-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:20.10-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1697730408,
|
||||
"narHash": "sha256-Ww//zzukdTrwTrCUkaJA/NsaLEfUfQpWZXBdXBYfhak=",
|
||||
"lastModified": 1701336116,
|
||||
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ff0a5a776b56e0ca32d47a4a47695452ec7f7d80",
|
||||
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
121
package.json
121
package.json
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.10.5",
|
||||
"packageManager": "pnpm@8.11.0",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -45,9 +45,9 @@
|
|||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.5",
|
||||
"@github/hotkey": "2.3.0",
|
||||
"@infectoone/vue-ganttastic": "2.2.0",
|
||||
|
@ -55,44 +55,43 @@
|
|||
"@kyvg/vue3-notification": "3.0.2",
|
||||
"@sentry/tracing": "7.80.1",
|
||||
"@sentry/vue": "7.80.1",
|
||||
"@tiptap/core": "2.1.12",
|
||||
"@tiptap/extension-blockquote": "2.1.12",
|
||||
"@tiptap/extension-bold": "2.1.12",
|
||||
"@tiptap/extension-bullet-list": "2.1.12",
|
||||
"@tiptap/extension-code": "2.1.12",
|
||||
"@tiptap/extension-code-block-lowlight": "2.1.12",
|
||||
"@tiptap/extension-document": "2.1.12",
|
||||
"@tiptap/extension-dropcursor": "2.1.12",
|
||||
"@tiptap/extension-gapcursor": "2.1.12",
|
||||
"@tiptap/extension-hard-break": "2.1.12",
|
||||
"@tiptap/extension-heading": "2.1.12",
|
||||
"@tiptap/extension-history": "2.1.12",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.12",
|
||||
"@tiptap/extension-image": "2.1.12",
|
||||
"@tiptap/extension-italic": "2.1.12",
|
||||
"@tiptap/extension-link": "2.1.12",
|
||||
"@tiptap/extension-list-item": "2.1.12",
|
||||
"@tiptap/extension-ordered-list": "2.1.12",
|
||||
"@tiptap/extension-paragraph": "2.1.12",
|
||||
"@tiptap/extension-placeholder": "2.1.12",
|
||||
"@tiptap/extension-strike": "2.1.12",
|
||||
"@tiptap/extension-table": "2.1.12",
|
||||
"@tiptap/extension-table-cell": "2.1.12",
|
||||
"@tiptap/extension-table-header": "2.1.12",
|
||||
"@tiptap/extension-table-row": "2.1.12",
|
||||
"@tiptap/extension-task-item": "2.1.12",
|
||||
"@tiptap/extension-task-list": "2.1.12",
|
||||
"@tiptap/extension-text": "2.1.12",
|
||||
"@tiptap/extension-typography": "2.1.12",
|
||||
"@tiptap/extension-underline": "2.1.12",
|
||||
"@tiptap/pm": "2.1.12",
|
||||
"@tiptap/suggestion": "2.1.12",
|
||||
"@tiptap/vue-3": "2.1.12",
|
||||
"@tiptap/core": "2.1.13",
|
||||
"@tiptap/extension-blockquote": "2.1.13",
|
||||
"@tiptap/extension-bold": "2.1.13",
|
||||
"@tiptap/extension-bullet-list": "2.1.13",
|
||||
"@tiptap/extension-code": "2.1.13",
|
||||
"@tiptap/extension-code-block-lowlight": "2.1.13",
|
||||
"@tiptap/extension-document": "2.1.13",
|
||||
"@tiptap/extension-dropcursor": "2.1.13",
|
||||
"@tiptap/extension-gapcursor": "2.1.13",
|
||||
"@tiptap/extension-hard-break": "2.1.13",
|
||||
"@tiptap/extension-heading": "2.1.13",
|
||||
"@tiptap/extension-history": "2.1.13",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.13",
|
||||
"@tiptap/extension-image": "2.1.13",
|
||||
"@tiptap/extension-italic": "2.1.13",
|
||||
"@tiptap/extension-link": "2.1.13",
|
||||
"@tiptap/extension-list-item": "2.1.13",
|
||||
"@tiptap/extension-ordered-list": "2.1.13",
|
||||
"@tiptap/extension-paragraph": "2.1.13",
|
||||
"@tiptap/extension-placeholder": "2.1.13",
|
||||
"@tiptap/extension-strike": "2.1.13",
|
||||
"@tiptap/extension-table": "2.1.13",
|
||||
"@tiptap/extension-table-cell": "2.1.13",
|
||||
"@tiptap/extension-table-header": "2.1.13",
|
||||
"@tiptap/extension-table-row": "2.1.13",
|
||||
"@tiptap/extension-task-item": "2.1.13",
|
||||
"@tiptap/extension-task-list": "2.1.13",
|
||||
"@tiptap/extension-text": "2.1.13",
|
||||
"@tiptap/extension-typography": "2.1.13",
|
||||
"@tiptap/extension-underline": "2.1.13",
|
||||
"@tiptap/pm": "2.1.13",
|
||||
"@tiptap/suggestion": "2.1.13",
|
||||
"@tiptap/vue-3": "2.1.13",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@types/sortablejs": "1.15.5",
|
||||
"@vueuse/core": "10.6.1",
|
||||
"@vueuse/router": "10.6.1",
|
||||
"@vueuse/core": "10.7.0",
|
||||
"@vueuse/router": "10.7.0",
|
||||
"axios": "1.6.2",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
|
@ -114,7 +113,7 @@
|
|||
"sortablejs": "1.15.0",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.3.2",
|
||||
"vue": "3.3.8",
|
||||
"vue": "3.3.9",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.7.0",
|
||||
|
@ -127,54 +126,54 @@
|
|||
"@cypress/vite-dev-server": "5.0.6",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@histoire/plugin-screenshot": "0.17.0",
|
||||
"@histoire/plugin-vue": "0.17.5",
|
||||
"@rushstack/eslint-patch": "1.5.1",
|
||||
"@histoire/plugin-screenshot": "0.17.6",
|
||||
"@histoire/plugin-vue": "0.17.6",
|
||||
"@rushstack/eslint-patch": "1.6.0",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@types/codemirror": "5.60.13",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/flexsearch": "0.7.6",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.9.1",
|
||||
"@types/node": "20.10.1",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.13.1",
|
||||
"@typescript-eslint/parser": "6.13.1",
|
||||
"@vitejs/plugin-legacy": "4.1.1",
|
||||
"@vitejs/plugin-vue": "4.5.0",
|
||||
"@vitejs/plugin-vue": "4.5.1",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
"@vue/test-utils": "2.4.2",
|
||||
"@vue/test-utils": "2.4.3",
|
||||
"@vue/tsconfig": "0.4.0",
|
||||
"autoprefixer": "10.4.16",
|
||||
"browserslist": "4.22.1",
|
||||
"caniuse-lite": "1.0.30001563",
|
||||
"caniuse-lite": "1.0.30001565",
|
||||
"css-has-pseudo": "6.0.0",
|
||||
"csstype": "3.1.2",
|
||||
"cypress": "13.5.1",
|
||||
"esbuild": "0.19.5",
|
||||
"cypress": "13.6.0",
|
||||
"esbuild": "0.19.8",
|
||||
"eslint": "8.54.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"eslint-plugin-vue": "9.19.2",
|
||||
"happy-dom": "12.10.3",
|
||||
"histoire": "0.17.5",
|
||||
"histoire": "0.17.6",
|
||||
"postcss": "8.4.31",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.0",
|
||||
"postcss-preset-env": "9.3.0",
|
||||
"rollup": "4.5.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"rollup": "4.6.1",
|
||||
"rollup-plugin-visualizer": "5.9.3",
|
||||
"sass": "1.69.5",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "5.3.2",
|
||||
"vite": "4.5.0",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.16.7",
|
||||
"vite-plugin-pwa": "0.17.2",
|
||||
"vite-plugin-sentry": "1.3.0",
|
||||
"vite-svg-loader": "4.0.0",
|
||||
"vitest": "0.34.6",
|
||||
"vue-tsc": "1.8.22",
|
||||
"vue-tsc": "1.8.24",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
},
|
||||
|
|
1659
pnpm-lock.yaml
1659
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="tiptap">
|
||||
<div class="tiptap" ref="tiptapInstanceRef">
|
||||
<EditorToolbar
|
||||
v-if="editor && isEditing"
|
||||
:editor="editor"
|
||||
|
@ -62,7 +62,7 @@
|
|||
|
||||
<editor-content
|
||||
class="tiptap__editor"
|
||||
:class="{'tiptap__editor-is-empty': isEmpty, 'tiptap__editor-is-edit-enabled': isEditing}"
|
||||
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
|
||||
:editor="editor"
|
||||
/>
|
||||
|
||||
|
@ -117,7 +117,7 @@
|
|||
</template>
|
||||
|
||||
<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 EditorToolbar from './EditorToolbar.vue'
|
||||
|
@ -173,7 +173,8 @@ import {Placeholder} from '@tiptap/extension-placeholder'
|
|||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {mergeAttributes} from '@tiptap/core'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
|
||||
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
|
@ -202,9 +203,28 @@ type CacheKey = `${ITask['id']}-${IAttachment['id']}`
|
|||
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
|
||||
|
||||
const CustomImage = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
id: {
|
||||
default: null,
|
||||
},
|
||||
'data-src': {
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
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()
|
||||
nextTick(async () => {
|
||||
|
||||
|
@ -213,7 +233,7 @@ const CustomImage = Image.extend({
|
|||
if (!img) return
|
||||
|
||||
// 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 attachmentId = Number(parts[3])
|
||||
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
|
||||
|
@ -223,15 +243,14 @@ const CustomImage = Image.extend({
|
|||
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
|
||||
|
||||
const attachmentService = new AttachmentService()
|
||||
const url = await attachmentService.getBlobUrl(attachment)
|
||||
loadedAttachments.value[cacheKey] = url
|
||||
loadedAttachments.value[cacheKey] = await attachmentService.getBlobUrl(attachment)
|
||||
}
|
||||
|
||||
img.src = loadedAttachments.value[cacheKey]
|
||||
})
|
||||
|
||||
return ['img', mergeAttributes(this.options.HTMLAttributes, {
|
||||
'data-src': HTMLAttributes.src,
|
||||
'data-src': imageUrl,
|
||||
src: '#',
|
||||
alt: HTMLAttributes.alt,
|
||||
title: HTMLAttributes.title,
|
||||
|
@ -268,7 +287,6 @@ const {
|
|||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const inputHTML = ref('')
|
||||
const isEmpty = computed(() => isEditorContentEmpty(inputHTML.value))
|
||||
const internalMode = ref<Mode>(initialMode)
|
||||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
|
||||
|
@ -475,26 +493,37 @@ function setLink() {
|
|||
|
||||
onMounted(() => {
|
||||
internalMode.value = initialMode
|
||||
document.addEventListener('paste', handleImagePaste)
|
||||
nextTick(() => {
|
||||
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
|
||||
input?.addEventListener('paste', handleImagePaste)
|
||||
})
|
||||
if (editShortcut !== '') {
|
||||
document.addEventListener('keydown', setFocusToEditor)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('paste', handleImagePaste)
|
||||
nextTick(() => {
|
||||
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
|
||||
input?.removeEventListener('paste', handleImagePaste)
|
||||
})
|
||||
if (editShortcut !== '') {
|
||||
document.removeEventListener('keydown', setFocusToEditor)
|
||||
}
|
||||
})
|
||||
|
||||
function handleImagePaste(event) {
|
||||
if (event?.clipboardData?.items?.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event?.clipboardData?.items?.forEach(i => {
|
||||
if (i.kind === 'file' && i.type.startsWith('image/')) {
|
||||
uploadAndInsertFiles([i.getAsFile()])
|
||||
}
|
||||
})
|
||||
|
||||
const image = event.clipboardData.items[0]
|
||||
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
|
||||
|
@ -515,23 +544,78 @@ function setFocusToEditor(event) {
|
|||
|
||||
editor.value?.commands.focus()
|
||||
}
|
||||
|
||||
function clickTasklistCheckbox(event) {
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
if (event.target.localName !== 'p') {
|
||||
return
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
.tiptap__editor {
|
||||
&.tiptap__editor-is-edit-enabled {
|
||||
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;
|
||||
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);
|
||||
color: var(--grey-400);
|
||||
pointer-events: none;
|
||||
|
@ -541,7 +625,7 @@ function setFocusToEditor(event) {
|
|||
|
||||
// Basic editor styles
|
||||
.ProseMirror {
|
||||
padding: .5rem;
|
||||
padding: .5rem .5rem .5rem 0;
|
||||
|
||||
&:focus-within, &:focus {
|
||||
box-shadow: none;
|
||||
|
@ -774,6 +858,7 @@ ul[data-type='taskList'] {
|
|||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -177,8 +177,9 @@ export const ALPHABETICAL_SORT = 'title'
|
|||
</script>
|
||||
|
||||
<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 {watchDebounced} from '@vueuse/core'
|
||||
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
|
@ -274,15 +275,16 @@ onMounted(() => {
|
|||
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,
|
||||
(value) => {
|
||||
// FIXME: filters should only be converted to snake case in
|
||||
// the last moment
|
||||
// FIXME: filters should only be converted to snake case in the last moment
|
||||
params.value = objectToSnakeCase(value)
|
||||
prepareFilters()
|
||||
},
|
||||
{immediate: true},
|
||||
{immediate: true, debounce: 500, maxWait: 1000},
|
||||
)
|
||||
|
||||
const sortAlphabetically = computed({
|
||||
|
@ -311,7 +313,7 @@ function prepareFilters() {
|
|||
prepareDate('end_date', 'endDate')
|
||||
prepareSingleValue('priority', 'priority', 'usePriority', true)
|
||||
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||
prepareDate('reminders')
|
||||
prepareDate('reminders', 'reminders')
|
||||
prepareRelatedObjectFilter('users', 'assignees')
|
||||
prepareProjectsFilter()
|
||||
|
||||
|
@ -387,13 +389,13 @@ function setDateFilter(filterName, {dateFrom, dateTo}) {
|
|||
change()
|
||||
}
|
||||
|
||||
function prepareDate(filterName, variableName) {
|
||||
function prepareDate(filterName: string, variableName: 'dueDate' | 'startDate' | 'endDate' | 'reminders') {
|
||||
if (typeof params.value.filter_by === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
let foundDateStart = false
|
||||
let foundDateEnd = false
|
||||
let foundDateStart: boolean | string = false
|
||||
let foundDateEnd: boolean | string = false
|
||||
for (const i in params.value.filter_by) {
|
||||
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'greater_equals') {
|
||||
foundDateStart = i
|
||||
|
|
|
@ -218,13 +218,19 @@ const actions = computed(() => {
|
|||
])))
|
||||
})
|
||||
|
||||
function attachmentUpload(
|
||||
file: File,
|
||||
onSuccess: (url: string) => void,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onError: (error: string) => void,
|
||||
) {
|
||||
return uploadFile(props.taskId, file, onSuccess)
|
||||
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
|
||||
|
||||
const uploadPromises: Promise<string>[] = []
|
||||
|
||||
files.forEach((file: File) => {
|
||||
const promise = new Promise<string>((resolve) => {
|
||||
uploadFile(props.taskId, file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
|
||||
})
|
||||
|
||||
uploadPromises.push(promise)
|
||||
})
|
||||
|
||||
return await Promise.all(uploadPromises)
|
||||
}
|
||||
|
||||
const taskCommentService = shallowReactive(new TaskCommentService())
|
||||
|
@ -299,7 +305,7 @@ async function editComment() {
|
|||
if (commentEdit.comment === '') {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (changeTimeout.value !== null) {
|
||||
clearTimeout(changeTimeout.value)
|
||||
}
|
||||
|
@ -368,7 +374,7 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
|||
}
|
||||
|
||||
.image.is-avatar {
|
||||
border-radius: 100%;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.media-content {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
</CustomTransition>
|
||||
</h3>
|
||||
<editor
|
||||
class="tiptap__task-description"
|
||||
:is-edit-enabled="canWrite"
|
||||
:upload-callback="uploadCallback"
|
||||
:placeholder="$t('task.description.placeholder')"
|
||||
|
@ -25,7 +26,7 @@
|
|||
v-model="description"
|
||||
@update:model-value="saveWithDelay"
|
||||
@save="save"
|
||||
:initial-mode="isEditorContentEmpty(description) ? 'preview' : 'edit'"
|
||||
:initial-mode="isEditorContentEmpty(description) ? 'edit' : 'preview'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -123,3 +124,10 @@ async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
|
|||
}
|
||||
</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>
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<div
|
||||
:class="{'is-loading': taskService.loading}"
|
||||
class="task loader-container"
|
||||
@click.stop.self="openTaskDetail"
|
||||
@mouseup.stop.self="openTaskDetail"
|
||||
@mousedown.stop.self="focusTaskLink"
|
||||
ref="taskContainerRef"
|
||||
tabindex="-1"
|
||||
>
|
||||
<fancycheckbox
|
||||
:disabled="(isArchived || disabled) && !canMarkAsDone"
|
||||
|
@ -20,6 +23,8 @@
|
|||
<div
|
||||
:class="{ 'done': task.done, 'show-project': showProject && project}"
|
||||
class="tasktext"
|
||||
@mouseup.stop.self="openTaskDetail"
|
||||
@mousedown.stop.self="focusTaskLink"
|
||||
>
|
||||
<span>
|
||||
<router-link
|
||||
|
@ -313,13 +318,24 @@ function hideDeferDueDatePopup(e) {
|
|||
}
|
||||
|
||||
const taskLink = ref<HTMLElement | null>(null)
|
||||
const taskContainerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function hasTextSelected() {
|
||||
const isTextSelected = window.getSelection().toString()
|
||||
return !(typeof isTextSelected === 'undefined' || isTextSelected === '' || isTextSelected === '\n')
|
||||
}
|
||||
|
||||
function openTaskDetail() {
|
||||
const isTextSelected = window.getSelection().toString()
|
||||
if (!isTextSelected) {
|
||||
if (!hasTextSelected()) {
|
||||
taskLink.value.$el.click()
|
||||
}
|
||||
}
|
||||
|
||||
function focusTaskLink() {
|
||||
if (!hasTextSelected()) {
|
||||
taskContainerRef.value.focus()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -20,6 +20,7 @@ export const SUPPORTED_LOCALES = {
|
|||
'ja-JP': '日本語',
|
||||
'hu-HU': 'Magyar',
|
||||
'ar-SA': 'اَلْعَرَبِيَّةُ',
|
||||
'sl-SI': 'Slovenščina',
|
||||
} as const
|
||||
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"welcomeEvening": "こんばんは、{username}さん",
|
||||
"lastViewed": "最近の表示",
|
||||
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
|
||||
"goToOverview": "Go to overview",
|
||||
"goToOverview": "概要に移動",
|
||||
"project": {
|
||||
"importText": "他のサービスからVikunjaにプロジェクトやタスクをインポートします:",
|
||||
"import": "Vikunjaへのデータのインポート"
|
||||
|
@ -150,8 +150,8 @@
|
|||
"title": "APIトークン",
|
||||
"general": "API tokens allow you to use Vikunja's API without user credentials.",
|
||||
"apiDocs": "Check out the api docs",
|
||||
"createAToken": "Create a token",
|
||||
"createToken": "Create token",
|
||||
"createAToken": "トークンの生成",
|
||||
"createToken": "トークンの生成",
|
||||
"30d": "30日",
|
||||
"60d": "60日",
|
||||
"90d": "90日",
|
||||
|
@ -161,7 +161,7 @@
|
|||
"tokenCreatedSuccess": "Here is your new api token: {token}",
|
||||
"tokenCreatedNotSeeAgain": "このトークンは二度と表示されません。安全な場所に保管してください。",
|
||||
"delete": {
|
||||
"header": "Delete this token",
|
||||
"header": "トークンの削除",
|
||||
"text1": "Are you sure you want to delete the token \"{token}\"?",
|
||||
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
|
||||
},
|
||||
|
@ -343,7 +343,7 @@
|
|||
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
|
||||
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
|
||||
"deleteLast": "You cannot remove the last bucket.",
|
||||
"addTaskPlaceholder": "Enter the new task title…",
|
||||
"addTaskPlaceholder": "新しいタスク名を入力…",
|
||||
"addTask": "タスクの追加",
|
||||
"addAnotherTask": "他のタスクを追加",
|
||||
"addBucket": "新しいバケットの作成",
|
||||
|
@ -351,7 +351,7 @@
|
|||
"deleteHeaderBucket": "バケットの削除",
|
||||
"deleteBucketText1": "このバケットを削除して本当によろしいですか?",
|
||||
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.",
|
||||
"deleteBucketSuccess": "The bucket has been deleted successfully.",
|
||||
"deleteBucketSuccess": "バケットは正常に削除されました。",
|
||||
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
|
||||
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
|
||||
"collapse": "Collapse this bucket"
|
||||
|
@ -362,15 +362,15 @@
|
|||
}
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
"title": "Webhook",
|
||||
"targetUrl": "Target URL",
|
||||
"targetUrlInvalid": "Please provide a valid URL.",
|
||||
"events": "Events",
|
||||
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
|
||||
"mustSelectEvents": "You must select at least one event.",
|
||||
"delete": "Delete this webhook",
|
||||
"delete": "Webhookの削除",
|
||||
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
|
||||
"deleteSuccess": "The webhook was successfully deleted.",
|
||||
"deleteSuccess": "Webhookは正常に削除されました。",
|
||||
"create": "Create webhook",
|
||||
"secret": "Secret",
|
||||
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
|
||||
|
@ -527,48 +527,48 @@
|
|||
"bold": "太字",
|
||||
"italic": "斜体",
|
||||
"strikethrough": "打ち消し線",
|
||||
"underline": "Underline",
|
||||
"underline": "下線",
|
||||
"code": "コード",
|
||||
"codeTooltip": "Capture a code snippet.",
|
||||
"quote": "引用",
|
||||
"quoteTooltip": "Capture a quote.",
|
||||
"bulletList": "Bullet list",
|
||||
"bulletList": "順序なしリスト",
|
||||
"bulletListTooltip": "Create a simple bullet list.",
|
||||
"unorderedList": "Unordered list",
|
||||
"orderedList": "Ordered list",
|
||||
"unorderedList": "順序なしリスト",
|
||||
"orderedList": "順序付きリスト",
|
||||
"orderedListTooltip": "Create a list with numbering.",
|
||||
"cleanBlock": "Clean Block",
|
||||
"link": "リンク",
|
||||
"image": "画像",
|
||||
"imageTooltip": "Upload an image from your computer.",
|
||||
"table": {
|
||||
"title": "Table",
|
||||
"insert": "Insert table",
|
||||
"title": "テーブル",
|
||||
"insert": "テーブルの挿入",
|
||||
"addColumnBefore": "Add column before",
|
||||
"addColumnAfter": "Add column after",
|
||||
"deleteColumn": "Delete column",
|
||||
"deleteColumn": "列の削除",
|
||||
"addRowBefore": "Add row before",
|
||||
"addRowAfter": "Add row after",
|
||||
"deleteRow": "Delete row",
|
||||
"deleteTable": "Delete table",
|
||||
"mergeCells": "Merge cells",
|
||||
"splitCell": "Split cell",
|
||||
"deleteRow": "行の削除",
|
||||
"deleteTable": "テーブルの削除",
|
||||
"mergeCells": "セルの統合",
|
||||
"splitCell": "セルの分割",
|
||||
"toggleHeaderColumn": "Toggle header column",
|
||||
"toggleHeaderRow": "Toggle header row",
|
||||
"toggleHeaderCell": "Toggle header cell",
|
||||
"mergeOrSplit": "Merge or split",
|
||||
"fixTables": "Fix tables"
|
||||
"fixTables": "テーブルの修正"
|
||||
},
|
||||
"horizontalRule": "横罫",
|
||||
"horizontalRuleTooltip": "Divide a section.",
|
||||
"sideBySide": "Side By Side",
|
||||
"guide": "説明書",
|
||||
"text": "Text",
|
||||
"text": "テキスト",
|
||||
"textTooltip": "Just start typing with plain text.",
|
||||
"taskList": "Task list",
|
||||
"taskList": "タスクリスト",
|
||||
"taskListTooltip": "Track tasks with a to-do list.",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"undo": "元に戻す",
|
||||
"redo": "やり直す",
|
||||
"placeholder": "Type some text or hit '/' to see more options…"
|
||||
},
|
||||
"multiselect": {
|
||||
|
@ -797,7 +797,7 @@
|
|||
"delete": "関連タスクの削除",
|
||||
"deleteText1": "この関連タスクを削除して本当によろしいですか?",
|
||||
"select": "Select a relation kind",
|
||||
"taskRequired": "Please select a task or enter a new task title.",
|
||||
"taskRequired": "タスクを選択するか、新しいタスク名を入力してください。",
|
||||
"kinds": {
|
||||
"subtask": "サブタスク",
|
||||
"parenttask": "親タスク",
|
||||
|
@ -844,7 +844,7 @@
|
|||
"title": "Quick Add Magic",
|
||||
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
|
||||
"multiple": "You can use this multiple times.",
|
||||
"label1": "To add a label, simply prefix the name of the label with {prefix}.",
|
||||
"label1": "ラベルを付けるには、ラベル名の前に {prefix} を入力します。",
|
||||
"label2": "Vikunja will first check if the label already exist and create it if not.",
|
||||
"label3": "To use spaces, simply add a \" or ' around the label name.",
|
||||
"label4": "For example: {prefix}\"Label with spaces\".",
|
||||
|
@ -894,7 +894,7 @@
|
|||
"header": "Remove a user from the team",
|
||||
"text1": "Are you sure you want to remove this user from the team?",
|
||||
"text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
|
||||
"success": "The user was successfully deleted from the team."
|
||||
"success": "ユーザーは正常にチームから削除されました。"
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
|
@ -998,7 +998,7 @@
|
|||
"tasks": "タスク",
|
||||
"projects": "プロジェクト",
|
||||
"teams": "チーム",
|
||||
"labels": "Labels",
|
||||
"labels": "ラベル",
|
||||
"newProject": "新しいプロジェクト名を入力…",
|
||||
"newTask": "新しいタスク名を入力…",
|
||||
"newTeam": "新しいチーム名を入力…",
|
||||
|
@ -1074,12 +1074,12 @@
|
|||
"8002": "そのラベルは存在しません。",
|
||||
"8003": "You do not have access to this label.",
|
||||
"9001": "The right is invalid.",
|
||||
"10001": "The bucket does not exist.",
|
||||
"10001": "そのバケットは存在しません。",
|
||||
"10002": "The bucket does not belong to that project.",
|
||||
"10003": "You cannot remove the last bucket on a project.",
|
||||
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
|
||||
"10005": "There can be only one done bucket per project.",
|
||||
"11001": "The saved filter does not exist.",
|
||||
"11001": "その絞り込み条件は存在しません。",
|
||||
"11002": "絞り込み条件はリンクの共有には使用できません。",
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -37,7 +37,7 @@
|
|||
>
|
||||
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
|
||||
<span
|
||||
v-if="project.doneBucketId === bucket.id"
|
||||
v-if="project?.doneBucketId === bucket.id"
|
||||
class="icon is-small has-text-success mr-2"
|
||||
v-tooltip="$t('project.kanban.doneBucketHint')"
|
||||
>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<dropdown-item
|
||||
@click.stop="toggleDoneBucket(bucket)"
|
||||
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
|
||||
:icon-class="{'has-text-success': bucket.id === project.doneBucketId}"
|
||||
:icon-class="{'has-text-success': bucket.id === project?.doneBucketId}"
|
||||
icon="check-double"
|
||||
>
|
||||
{{ $t('project.kanban.doneBucket') }}
|
||||
|
@ -436,7 +436,7 @@ async function updateTaskPosition(e) {
|
|||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
||||
newBucket.id !== oldBucket.id
|
||||
) {
|
||||
newTask.done = project.value.doneBucketId === newBucket.id
|
||||
newTask.done = project.value?.doneBucketId === newBucket.id
|
||||
}
|
||||
if (
|
||||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
||||
|
@ -620,7 +620,7 @@ async function toggleDefaultBucket(bucket: IBucket) {
|
|||
}
|
||||
|
||||
async function toggleDoneBucket(bucket: IBucket) {
|
||||
const doneBucketId = project.value.doneBucketId === bucket.id
|
||||
const doneBucketId = project.value?.doneBucketId === bucket.id
|
||||
? 0
|
||||
: bucket.id
|
||||
|
||||
|
|
Reference in New Issue