Merge branch 'main' into feat-custom-keyboard-shortcuts

This commit is contained in:
kolaente
2025-11-27 20:15:31 +01:00
12 changed files with 163 additions and 245 deletions

View File

@@ -39,7 +39,7 @@ jobs:
version: latest
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ghcr.io/go-vikunja/vikunja
tags: |

View File

@@ -30,7 +30,7 @@ jobs:
- name: Docker meta version
if: ${{ github.ref_type == 'tag' }}
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: |
vikunja/vikunja

View File

@@ -362,10 +362,10 @@ jobs:
ports:
- 5556:5556
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.57.0-jammy@sha256:6aca677c27a967caf7673d108ac67ffaf8fed134f27e17b27a05464ca0ace831
options: --user 1001
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Download Vikunja Binary
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:

View File

@@ -5,7 +5,7 @@
"main": "main.js",
"repository": "https://code.vikunja.io/desktop",
"license": "GPL-3.0-or-later",
"packageManager": "pnpm@10.22.0",
"packageManager": "pnpm@10.23.0",
"author": {
"email": "maintainers@vikunja.io",
"name": "Vikunja Team"

View File

@@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@10.22.0",
"packageManager": "pnpm@10.23.0",
"keywords": [
"todo",
"productivity",
@@ -93,7 +93,7 @@
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lowlight": "3.3.0",
"marked": "16.4.2",
"marked": "17.0.1",
"pinia": "3.0.4",
"register-service-worker": "1.7.2",
"sortablejs": "1.15.6",
@@ -102,10 +102,10 @@
"vue": "3.5.24",
"vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "11.1.12",
"vue-i18n": "11.2.1",
"vue-router": "4.6.3",
"vuemoji-picker": "0.3.2",
"workbox-precaching": "7.3.0",
"workbox-precaching": "7.4.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {

View File

@@ -36,7 +36,7 @@ importers:
version: 3.1.1(patch_hash=145ab3233cbcd3bc934b4961cd8710e2b15e4ae5dd20862a8d1d6621d7f9d4a8)
'@intlify/unplugin-vue-i18n':
specifier: 11.0.1
version: 11.0.1(@vue/compiler-dom@3.5.24)(eslint@9.39.1(jiti@2.4.2))(rollup@4.53.3)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))
version: 11.0.1(@vue/compiler-dom@3.5.24)(eslint@9.39.1(jiti@2.4.2))(rollup@4.53.3)(typescript@5.9.3)(vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))
'@kyvg/vue3-notification':
specifier: 3.4.2
version: 3.4.2(vue@3.5.24(typescript@5.9.3))
@@ -134,8 +134,8 @@ importers:
specifier: 3.3.0
version: 3.3.0
marked:
specifier: 16.4.2
version: 16.4.2
specifier: 17.0.1
version: 17.0.1
pinia:
specifier: 3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
@@ -161,8 +161,8 @@ importers:
specifier: 11.0.5
version: 11.0.5(vue@3.5.24(typescript@5.9.3))
vue-i18n:
specifier: 11.1.12
version: 11.1.12(vue@3.5.24(typescript@5.9.3))
specifier: 11.2.1
version: 11.2.1(vue@3.5.24(typescript@5.9.3))
vue-router:
specifier: 4.6.3
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
@@ -170,8 +170,8 @@ importers:
specifier: 0.3.2
version: 0.3.2(vue@3.5.24(typescript@5.9.3))
workbox-precaching:
specifier: 7.3.0
version: 7.3.0
specifier: 7.4.0
version: 7.4.0
zhyswan-vuedraggable:
specifier: 4.1.3
version: 4.1.3(vue@3.5.24(typescript@5.9.3))
@@ -1722,18 +1722,26 @@ packages:
vue-i18n:
optional: true
'@intlify/core-base@11.1.12':
resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==}
'@intlify/core-base@11.2.1':
resolution: {integrity: sha512-2V1A4yaN9ElAnQ6ih3HHEc+jZ+sHV6BlQHjCsnIVlOotL5NCUgJElIxgUFiJs6zV4puoAq3hHuQIfWNp+J+8yQ==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.1.12':
resolution: {integrity: sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.2.1':
resolution: {integrity: sha512-J2454D3Agg3Kvgaj14gxTleJU8/H06Sisz7C2BwiHF0/i5Soyfb5ySpwn8GCL6yscDbOGj6xM+lUe6gO6BFQyg==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.12':
resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==}
engines: {node: '>= 16'}
'@intlify/shared@11.2.1':
resolution: {integrity: sha512-O67LZM4dbfr70WCsZLW+g+pIXdgQ66laLVd/FicW7iYgP/RuH0X1FDGSh+Hr9Gou/8TeldUE6KmTGdLwX2ufIA==}
engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@11.0.1':
resolution: {integrity: sha512-nH5NJdNjy/lO6Ne8LDtZzv4SbpVsMhPE+LbvBDmMeIeJDiino8sOJN2QB3MXzTliYTnqe3aB9Fw5+LJ/XVaXCg==}
engines: {node: '>= 20'}
@@ -4836,8 +4844,8 @@ packages:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
marked@16.4.2:
resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
marked@17.0.1:
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
engines: {node: '>= 20'}
hasBin: true
@@ -6838,8 +6846,8 @@ packages:
peerDependencies:
vue: ^3.2.0
vue-i18n@11.1.12:
resolution: {integrity: sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==}
vue-i18n@11.2.1:
resolution: {integrity: sha512-cc3Wx4eJZac9WMS8mxhfYiCipm9PBQ2Dz15piWYm7DwNcCehaKRgpolEdiqrjjT27T3Wijz3xJ7NeIc8ofIWAA==}
engines: {node: '>= 16'}
peerDependencies:
vue: ^3.0.0
@@ -6986,9 +6994,6 @@ packages:
engines: {node: '>=20.0.0'}
hasBin: true
workbox-core@7.3.0:
resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==}
workbox-core@7.4.0:
resolution: {integrity: sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==}
@@ -7001,9 +7006,6 @@ packages:
workbox-navigation-preload@7.4.0:
resolution: {integrity: sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==}
workbox-precaching@7.3.0:
resolution: {integrity: sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==}
workbox-precaching@7.4.0:
resolution: {integrity: sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==}
@@ -7013,15 +7015,9 @@ packages:
workbox-recipes@7.4.0:
resolution: {integrity: sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==}
workbox-routing@7.3.0:
resolution: {integrity: sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==}
workbox-routing@7.4.0:
resolution: {integrity: sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==}
workbox-strategies@7.3.0:
resolution: {integrity: sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==}
workbox-strategies@7.4.0:
resolution: {integrity: sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==}
@@ -8627,7 +8623,7 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {}
'@intlify/bundle-utils@11.0.1(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))':
'@intlify/bundle-utils@11.0.1(vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)))':
dependencies:
'@intlify/message-compiler': 11.1.12
'@intlify/shared': 11.1.12
@@ -8639,26 +8635,33 @@ snapshots:
source-map-js: 1.2.1
yaml-eslint-parser: 1.2.3
optionalDependencies:
vue-i18n: 11.1.12(vue@3.5.24(typescript@5.9.3))
vue-i18n: 11.2.1(vue@3.5.24(typescript@5.9.3))
'@intlify/core-base@11.1.12':
'@intlify/core-base@11.2.1':
dependencies:
'@intlify/message-compiler': 11.1.12
'@intlify/shared': 11.1.12
'@intlify/message-compiler': 11.2.1
'@intlify/shared': 11.2.1
'@intlify/message-compiler@11.1.12':
dependencies:
'@intlify/shared': 11.1.12
source-map-js: 1.2.1
'@intlify/message-compiler@11.2.1':
dependencies:
'@intlify/shared': 11.2.1
source-map-js: 1.2.1
'@intlify/shared@11.1.12': {}
'@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.24)(eslint@9.39.1(jiti@2.4.2))(rollup@4.53.3)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))':
'@intlify/shared@11.2.1': {}
'@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.24)(eslint@9.39.1(jiti@2.4.2))(rollup@4.53.3)(typescript@5.9.3)(vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.39.1(jiti@2.4.2))
'@intlify/bundle-utils': 11.0.1(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))
'@intlify/bundle-utils': 11.0.1(vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)))
'@intlify/shared': 11.1.12
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.24)(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.24)(vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))
'@rollup/pluginutils': 5.1.3(rollup@4.53.3)
'@typescript-eslint/scope-manager': 8.40.0
'@typescript-eslint/typescript-estree': 8.40.0(typescript@5.9.3)
@@ -8669,7 +8672,7 @@ snapshots:
unplugin: 2.3.10
vue: 3.5.24(typescript@5.9.3)
optionalDependencies:
vue-i18n: 11.1.12(vue@3.5.24(typescript@5.9.3))
vue-i18n: 11.2.1(vue@3.5.24(typescript@5.9.3))
transitivePeerDependencies:
- '@vue/compiler-dom'
- eslint
@@ -8677,14 +8680,14 @@ snapshots:
- supports-color
- typescript
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.24)(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))':
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.24)(vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@babel/parser': 7.28.3
optionalDependencies:
'@intlify/shared': 11.1.12
'@vue/compiler-dom': 3.5.24
vue: 3.5.24(typescript@5.9.3)
vue-i18n: 11.1.12(vue@3.5.24(typescript@5.9.3))
vue-i18n: 11.2.1(vue@3.5.24(typescript@5.9.3))
'@isaacs/balanced-match@4.0.1': {}
@@ -12053,7 +12056,7 @@ snapshots:
punycode.js: 2.3.1
uc.micro: 2.1.0
marked@16.4.2: {}
marked@17.0.1: {}
math-intrinsics@1.1.0: {}
@@ -14291,10 +14294,10 @@ snapshots:
flatpickr: 4.6.13
vue: 3.5.24(typescript@5.9.3)
vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)):
vue-i18n@11.2.1(vue@3.5.24(typescript@5.9.3)):
dependencies:
'@intlify/core-base': 11.1.12
'@intlify/shared': 11.1.12
'@intlify/core-base': 11.2.1
'@intlify/shared': 11.2.1
'@vue/devtools-api': 6.6.4
vue: 3.5.24(typescript@5.9.3)
@@ -14505,8 +14508,6 @@ snapshots:
- '@types/babel__core'
- supports-color
workbox-core@7.3.0: {}
workbox-core@7.4.0: {}
workbox-expiration@7.4.0:
@@ -14525,12 +14526,6 @@ snapshots:
dependencies:
workbox-core: 7.4.0
workbox-precaching@7.3.0:
dependencies:
workbox-core: 7.3.0
workbox-routing: 7.3.0
workbox-strategies: 7.3.0
workbox-precaching@7.4.0:
dependencies:
workbox-core: 7.4.0
@@ -14550,18 +14545,10 @@ snapshots:
workbox-routing: 7.4.0
workbox-strategies: 7.4.0
workbox-routing@7.3.0:
dependencies:
workbox-core: 7.3.0
workbox-routing@7.4.0:
dependencies:
workbox-core: 7.4.0
workbox-strategies@7.3.0:
dependencies:
workbox-core: 7.3.0
workbox-strategies@7.4.0:
dependencies:
workbox-core: 7.4.0

View File

@@ -145,7 +145,7 @@ import {eventToHotkeyString} from '@github/hotkey'
import EditorToolbar from './EditorToolbar.vue'
import StarterKit from '@tiptap/starter-kit'
import {Extension, mergeAttributes} from '@tiptap/core'
import {Extension, mergeAttributes, type SetContentOptions} from '@tiptap/core'
import {EditorContent, type Extensions, useEditor, VueNodeViewRenderer} from '@tiptap/vue-3'
import {Plugin, PluginKey} from '@tiptap/pm/state'
import {marked} from 'marked'
@@ -216,6 +216,12 @@ const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
const {t} = useI18n()
const defaultSetContentOptions: SetContentOptions = {
parseOptions: {
preserveWhitespace: true,
},
}
const CustomTableCell = TableCell.extend({
addAttributes() {
return {
@@ -549,6 +555,9 @@ const editor = useEditor({
onUpdate: () => {
bubbleNow()
},
parseOptions: {
preserveWhitespace: true,
},
})
watch(
@@ -606,7 +615,10 @@ function bubbleSave() {
}
function exitEditMode() {
editor.value?.commands.setContent(lastSavedState, {emitUpdate: false})
editor.value?.commands.setContent(lastSavedState, {
...defaultSetContentOptions,
emitUpdate: false,
})
// Clear draft from localStorage when discarding changes
if (props.storageKey) {
@@ -659,7 +671,10 @@ function uploadAndInsertFiles(files: File[] | FileList) {
const html = editor.value?.getHTML().replace(UPLOAD_PLACEHOLDER_ELEMENT, '') ?? ''
editor.value?.commands.setContent(html, {emitUpdate: false})
editor.value?.commands.setContent(html, {
...defaultSetContentOptions,
emitUpdate: false,
})
bubbleNow()
})
@@ -733,7 +748,10 @@ onBeforeUnmount(() => {
function setModeAndValue(value: string) {
internalMode.value = isEditorContentEmpty(value) ? 'edit' : 'preview'
editor.value?.commands.setContent(value, {emitUpdate: false})
editor.value?.commands.setContent(value, {
...defaultSetContentOptions,
emitUpdate: false,
})
}

View File

@@ -126,7 +126,7 @@
<Teleport to="body">
<div
v-if="editEnabled"
:class="{hidden: !isOverDropZone}"
:class="{hidden: !showDropzone}"
class="dropzone"
>
<div class="drop-hint">
@@ -172,8 +172,8 @@
</template>
<script setup lang="ts">
import {ref, shallowReactive, computed} from 'vue'
import {useDropZone} from '@/composables/useDropzone'
import {ref, shallowReactive, computed, watch} from 'vue'
import {useDropZone} from '@vueuse/core'
import User from '@/components/misc/User.vue'
import ProgressBar from '@/components/misc/ProgressBar.vue'
@@ -205,6 +205,28 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
'taskChanged': [ITask],
}>()
const EDITOR_SELECTOR = '.tiptap, .tiptap__editor, [contenteditable]'
function eventTargetsEditor(event: Event | null | undefined): boolean {
if (!event) {
return false
}
const target = event.target
if (target instanceof HTMLElement && target.closest(EDITOR_SELECTOR)) {
return true
}
if (typeof event.composedPath === 'function') {
return event.composedPath().some(element =>
element instanceof HTMLElement && element.matches(EDITOR_SELECTOR),
)
}
return false
}
const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'})
@@ -215,22 +237,67 @@ const attachments = computed(() => attachmentStore.attachments)
const loading = computed(() => attachmentService.loading || taskStore.isLoading)
function onDrop(files: File[] | null) {
if (files && files.length !== 0) {
uploadFilesToTask(files)
}
const isDraggingFiles = ref(false)
const isDragOverEditor = ref(false)
function resetDragState() {
isDraggingFiles.value = false
isDragOverEditor.value = false
}
const {isOverDropZone} = useDropZone(document, {
onDrop,
checkValidity: items => {
for (const item of items) {
if (item.kind === 'file') {
return true
}
onEnter(files, event) {
if (!props.editEnabled) {
return
}
return false
isDraggingFiles.value = true
isDragOverEditor.value = eventTargetsEditor(event)
},
onOver(files, event) {
if (!props.editEnabled) {
return
}
isDragOverEditor.value = eventTargetsEditor(event)
},
onLeave(files, event) {
if (!props.editEnabled) {
return
}
if (!isOverDropZone.value) {
resetDragState()
return
}
isDragOverEditor.value = eventTargetsEditor(event)
},
onDrop(files, event) {
if (!props.editEnabled) {
return
}
const dropOverEditor = eventTargetsEditor(event)
resetDragState()
// Ignore drops over editor - let TipTap handle them
if (dropOverEditor || !files || files.length === 0) {
return
}
uploadFilesToTask(files)
},
})
const showDropzone = computed(() =>
props.editEnabled && isDraggingFiles.value && !isDragOverEditor.value,
)
watch(() => props.editEnabled, enabled => {
if (!enabled) {
resetDragState()
}
})
function downloadAttachment(attachment: IAttachment) {

View File

@@ -1,157 +0,0 @@
import type { MaybeRef, MaybeRefOrGetter, ShallowRef } from 'vue'
import { isClient } from '@vueuse/shared'
import { shallowRef, unref } from 'vue'
import { useEventListener } from '@vueuse/core'
/////////
// Temporary copy until https://github.com/vueuse/vueuse/pull/5169 is merged and released
////////
/* eslint-disable */
export interface UseDropZoneReturn {
files: ShallowRef<File[] | null>
isOverDropZone: ShallowRef<boolean>
}
export interface UseDropZoneOptions {
/**
* Allowed data types, if not set, all data types are allowed.
* Also can be a function to check the data types.
*/
dataTypes?: MaybeRef<readonly string[]> | ((types: readonly string[]) => boolean)
/**
* Similar to dataTypes, but exposes the DataTransferItemList for custom validation.
* If provided, this function takes precedence over dataTypes.
*/
checkValidity?: (items: DataTransferItemList) => boolean
onDrop?: (files: File[] | null, event: DragEvent) => void
onEnter?: (files: File[] | null, event: DragEvent) => void
onLeave?: (files: File[] | null, event: DragEvent) => void
onOver?: (files: File[] | null, event: DragEvent) => void
/**
* Allow multiple files to be dropped. Defaults to true.
*/
multiple?: boolean
/**
* Prevent default behavior for unhandled events. Defaults to false.
*/
preventDefaultForUnhandled?: boolean
}
export function useDropZone(
target: MaybeRefOrGetter<HTMLElement | Document | null | undefined>,
options: UseDropZoneOptions | UseDropZoneOptions['onDrop'] = {},
): UseDropZoneReturn {
const isOverDropZone = shallowRef(false)
const files = shallowRef<File[] | null>(null)
let counter = 0
let isValid = true
if (isClient) {
const _options = typeof options === 'function' ? { onDrop: options } : options
const multiple = _options.multiple ?? true
const preventDefaultForUnhandled = _options.preventDefaultForUnhandled ?? false
const getFiles = (event: DragEvent) => {
const list = Array.from(event.dataTransfer?.files ?? [])
return list.length === 0 ? null : (multiple ? list : [list[0]])
}
const checkDataTypes = (types: string[]) => {
const dataTypes = unref(_options.dataTypes)
if (typeof dataTypes === 'function')
return dataTypes(types)
if (!dataTypes?.length)
return true
if (types.length === 0)
return false
return types.every(type =>
dataTypes.some(allowedType => type.includes(allowedType)),
)
}
const checkValidity = (items: DataTransferItemList) => {
if (_options.checkValidity) {
return _options.checkValidity(items)
}
const types = Array.from(items ?? []).map(item => item.type)
const dataTypesValid = checkDataTypes(types)
const multipleFilesValid = multiple || items.length <= 1
return dataTypesValid && multipleFilesValid
}
const isSafari = () => (
/^(?:(?!chrome|android).)*safari/i.test(navigator.userAgent)
&& !('chrome' in window)
)
const handleDragEvent = (event: DragEvent, eventType: 'enter' | 'over' | 'leave' | 'drop') => {
const dataTransferItemList = event.dataTransfer?.items
isValid = (dataTransferItemList && checkValidity(dataTransferItemList)) ?? false
if (preventDefaultForUnhandled) {
event.preventDefault()
}
if (!isSafari() && !isValid) {
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'none'
}
return
}
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy'
}
const currentFiles = getFiles(event)
switch (eventType) {
case 'enter':
counter += 1
isOverDropZone.value = true
_options.onEnter?.(null, event)
break
case 'over':
_options.onOver?.(null, event)
break
case 'leave':
counter -= 1
if (counter === 0)
isOverDropZone.value = false
_options.onLeave?.(null, event)
break
case 'drop':
counter = 0
isOverDropZone.value = false
if (isValid) {
files.value = currentFiles
_options.onDrop?.(currentFiles, event)
}
break
}
}
useEventListener<DragEvent>(target, 'dragenter', event => handleDragEvent(event, 'enter'))
useEventListener<DragEvent>(target, 'dragover', event => handleDragEvent(event, 'over'))
useEventListener<DragEvent>(target, 'dragleave', event => handleDragEvent(event, 'leave'))
useEventListener<DragEvent>(target, 'drop', event => handleDragEvent(event, 'drop'))
}
return {
files,
isOverDropZone,
}
}

View File

@@ -37,15 +37,14 @@ async function login(page: Page): Promise<void> {
}
test.describe('Login', () => {
test.beforeEach(async ({apiContext}) => {
test.beforeEach(async ({page, apiContext}) => {
await UserFactory.create(1, {username: credentials.username})
await page.clock.setFixedTime(new Date(1625656161057)) // 13:00
})
test('Should log in with the right credentials', async ({page}) => {
await page.goto('/login')
await login(page)
await page.clock.install({time: new Date(1625656161057)}) // 13:00
// Use more specific selector to avoid strict mode violation
await expect(page.locator('main h2')).toContainText(`Hi ${credentials.username}!`)
})

4
go.mod
View File

@@ -33,7 +33,7 @@ require (
github.com/fclairamb/afero-s3 v0.3.1
github.com/gabriel-vasile/mimetype v1.4.11
github.com/ganigeorgiev/fexpr v0.5.0
github.com/getsentry/sentry-go v0.38.0
github.com/getsentry/sentry-go v0.40.0
github.com/getsentry/sentry-go/echo v0.38.0
github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-sql-driver/mysql v1.9.3
@@ -58,7 +58,7 @@ require (
github.com/olekukonko/tablewriter v1.1.1
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/v9 v9.17.0
github.com/redis/go-redis/v9 v9.17.1
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.15.0

4
go.sum
View File

@@ -109,6 +109,8 @@ github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/getsentry/sentry-go v0.38.0 h1:S8Xui7gLeAvXINVLMOaX94HnsDf1GexnfXGSNC4+KQs=
github.com/getsentry/sentry-go v0.38.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/getsentry/sentry-go/echo v0.38.0 h1:ZKvDf3O7jXS+UoeGCBiVVB6J14XWqz+9Dtldstl7FS4=
github.com/getsentry/sentry-go/echo v0.38.0/go.mod h1:iEsS3MBdYoeCMXeG94dhpCca0nOdhB6dMMvbk/XMvvo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
@@ -398,6 +400,8 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/redis/go-redis/v9 v9.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5iWTs=
github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=