1
0

feat: upgrade to packages for vue 3

- vue3-notification
- vue-advanced-cropper 2
- vuedraggable 4
- vue-shortkey -> moved in repo
This commit is contained in:
Dominik Pschenitschni
2021-08-20 15:46:41 +02:00
parent 7c3c2945f8
commit e779681905
10 changed files with 405 additions and 142 deletions

View File

@ -91,17 +91,20 @@
@end="e => saveListPosition(e, nk)"
handle=".handle"
:disabled="n.id < 0"
:class="{'dragging-disabled': n.id < 0}"
tag="transition-group"
item-key="id"
:component-data="{
type: 'transition',
tag: 'ul',
name: !drag ? 'flip-list' : null,
class: [
'menu-list can-be-hidden',
{ 'dragging-disabled': n.id < 0 }
]
}"
>
<transition-group
type="transition"
:name="!drag ? 'flip-list' : null"
tag="ul"
class="menu-list can-be-hidden"
>
<template #item="{element: l}">
<li
v-for="l in activeLists[nk]"
:key="l.id"
class="loader-container"
:class="{'is-loading': listUpdating[l.id]}"
>
@ -140,7 +143,7 @@
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
<span class="list-setting-spacer" v-else></span>
</li>
</transition-group>
</template>
</draggable>
</div>
</template>

View File

@ -86,9 +86,8 @@
:w="t.durationDays * dayWidth"
:x="t.offsetDays * dayWidth - 6"
:y="0"
@clicked="setTaskDragged(t)"
@dragstop="resizeTask"
@resizestop="resizeTask"
@dragstop="(e) => resizeTask(t, e)"
@resizestop="(e) => resizeTask(t, e)"
axis="x"
class="task"
>
@ -136,9 +135,8 @@
:sticks="['mr', 'ml']"
:x="dayOffsetUntilToday * dayWidth - 6"
:y="0"
@clicked="setTaskDragged(t)"
@dragstop="resizeTask"
@resizestop="resizeTask"
@dragstop="(e) => resizeTask(t, e)"
@resizestop="(e) => resizeTask(t, e)"
axis="x"
class="task nodate"
v-tooltip="$t('list.gantt.noDates')"
@ -233,7 +231,6 @@ export default {
theTasks: [], // Pretty much a copy of the prop, since we cant mutate the prop directly
tasksWithoutDates: [],
taskService: new TaskService(),
taskDragged: null, // Saves to currently dragged task to be able to update it
fullWidth: 0,
now: new Date(),
dayOffsetUntilToday: 0,
@ -361,15 +358,14 @@ export default {
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24)
return t
},
setTaskDragged(t) {
this.taskDragged = t
},
resizeTask(newRect) {
resizeTask(taskDragged, newRect) {
if (this.isTaskEdit) {
return
}
const didntHaveDates = this.taskDragged.startDate === null ? true : false
let newTask = { ...taskDragged }
const didntHaveDates = newTask.startDate === null ? true : false
let startDate = new Date(this.startDate)
startDate.setDate(
@ -379,32 +375,32 @@ export default {
startDate.setUTCMinutes(0)
startDate.setUTCSeconds(0)
startDate.setUTCMilliseconds(0)
this.taskDragged.startDate = startDate
newTask.startDate = startDate
let endDate = new Date(startDate)
endDate.setDate(
startDate.getDate() + newRect.width / this.dayWidth,
)
this.taskDragged.startDate = startDate
this.taskDragged.endDate = endDate
newTask.startDate = startDate
newTask.endDate = endDate
// We take the task from the overall tasks array because the one in it has bad data after it was updated once.
// FIXME: This is a workaround. We should use a better mechanism to get the task or, even better,
// prevent it from containing outdated Data in the first place.
for (const tt in this.theTasks) {
if (this.theTasks[tt].id === this.taskDragged.id) {
this.taskDragged = this.theTasks[tt]
if (this.theTasks[tt].id === newTask.id) {
newTask = this.theTasks[tt]
break
}
}
const ganttData = {
endDate: this.taskDragged.endDate,
durationDays: this.taskDragged.durationDays,
offsetDays: this.taskDragged.offsetDays,
endDate: newTask.endDate,
durationDays: newTask.durationDays,
offsetDays: newTask.offsetDays,
}
this.taskService
.update(this.taskDragged)
.update(newTask)
.then(r => {
r.endDate = ganttData.endDate
r.durationDays = ganttData.durationDays

View File

@ -1,4 +1,11 @@
import { createApp } from 'vue'
import { createApp, configureCompat } from 'vue'
configureCompat({
COMPONENT_V_MODEL: false,
COMPONENT_ASYNC: false,
RENDER_FUNCTION: false,
WATCH_ARRAY: false, // TODO: check this again; this might lead to some problemes
})
import App from './App.vue'
import router from './router'
@ -18,13 +25,13 @@ import {VERSION} from './version.json'
// Add CSS
import './styles/vikunja.scss'
// Notifications
import Notifications from 'vue-notification'
import Notifications from '@kyvg/vue3-notification'
// PWA
import './registerServiceWorker'
// Shortcuts
// @ts-ignore - no types available
import vueShortkey from 'vue-shortkey'
import shortkey from '@/plugins/shortkey'
// Vuex
import {store} from './store'
// i18n
@ -45,19 +52,11 @@ if (window.API_URL.substr(window.API_URL.length - 1, window.API_URL.length) ===
const app = createApp(App)
Vue.use(Notifications)
app.use(Notifications)
Vue.use(vueShortkey, {prevent: ['input', 'textarea', '.input', '[contenteditable]']})
app.config.globalProperties.$message = {
error(e, actions = []) {
return error(e, Vue.prototype, actions)
},
success(s, actions = []) {
return success(s, Vue.prototype, actions)
},
}
app.use(shortkey, {prevent: ['input', 'textarea', '.input', '[contenteditable]']})
// directives
import focus from './directives/focus'
@ -92,6 +91,15 @@ app.mixin({
},
})
app.config.errorHandler = (err, vm, info) => {
error(err)
}
app.config.globalProperties.$message = {
error,
success,
}
app.use(router)
app.use(store)
app.use(i18n)

View File

@ -1,4 +1,5 @@
import {i18n} from '@/i18n'
import { notify } from '@kyvg/vue3-notification'
export const getErrorText = (r) => {
@ -27,8 +28,8 @@ export const getErrorText = (r) => {
return [r.message]
}
export function error(e, context, actions = []) {
context.$notify({
export function error(e, actions = []) {
notify({
type: 'error',
title: i18n.global.t('error.error'),
text: getErrorText(e),
@ -37,8 +38,8 @@ export function error(e, context, actions = []) {
console.error(e, actions)
}
export function success(e, context, actions = []) {
context.$notify({
export function success(e, actions = []) {
notify({
type: 'success',
title: i18n.global.t('error.success'),
text: getErrorText(e),

View File

@ -0,0 +1,78 @@
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
const MODIFIER_KEYS = ['shift', 'ctrl', 'meta', 'alt']
const SHORT_CUT_INDEX = [
{ key: 'ArrowUp', value: 'arrowup' },
{ key: 'ArrowLeft', value: 'arrowlef' },
{ key: 'ArrowRight', value: 'arrowright' },
{ key: 'ArrowDown', value: 'arrowdown' },
{ key: 'AltGraph', value: 'altgraph' },
{ key: 'Escape', value: 'esc' },
{ key: 'Enter', value: 'enter' },
{ key: 'Tab', value: 'tab' },
{ key: ' ', value: 'space' },
{ key: 'PageUp', value: 'pagup' },
{ key: 'PageDown', value: 'pagedow' },
{ key: 'Home', value: 'home' },
{ key: 'End', value: 'end' },
{ key: 'Delete', value: 'del' },
{ key: 'Backspace', value: 'bacspace' },
{ key: 'Insert', value: 'insert' },
{ key: 'NumLock', value: 'numlock' },
{ key: 'CapsLock', value: 'capslock' },
{ key: 'Pause', value: 'pause' },
{ key: 'ContextMenu', value: 'cotextmenu' },
{ key: 'ScrollLock', value: 'scrolllock' },
{ key: 'BrowserHome', value: 'browserhome' },
{ key: 'MediaSelect', value: 'mediaselect' },
]
export function encodeKey(pKey) {
const shortKey = {}
MODIFIER_KEYS.forEach((key) => {
shortKey[`${key}Key`] = pKey.includes(key)
})
let indexedKeys = createShortcutIndex(shortKey)
const vKey = pKey.filter(
(item) => !MODIFIER_KEYS.includes(item),
)
indexedKeys += vKey.join('')
return indexedKeys
}
function createShortcutIndex(pKey) {
let k = ''
MODIFIER_KEYS.forEach((key) => {
if (pKey.key === capitalizeFirstLetter(key) || pKey[`${key}Key`]) {
k += key
}
})
SHORT_CUT_INDEX.forEach(({ key, value }) => {
if (pKey.key === key) {
k += value
}
})
if (
(pKey.key && pKey.key !== ' ' && pKey.key.length === 1) ||
/F\d{1,2}|\//g.test(pKey.key)
) {
k += pKey.key.toLowerCase()
}
return k
}
export { createShortcutIndex as decodeKey }
export function parseValue(value) {
value = typeof value === 'string' ? JSON.parse(value.replace(/'/gi, '"')) : value
return value instanceof Array ? { '': value } : value
}

View File

@ -0,0 +1,186 @@
import { parseValue, decodeKey, encodeKey } from './helpers'
let mapFunctions = {}
let objAvoided = []
let elementAvoided = []
let keyPressed = false
function dispatchShortkeyEvent(pKey) {
const e = new CustomEvent('shortkey', { bubbles: false })
if (mapFunctions[pKey].key) {
e.srcKey = mapFunctions[pKey].key
}
const elm = mapFunctions[pKey].el
if (!mapFunctions[pKey].propagte) {
elm[elm.length - 1].dispatchEvent(e)
} else {
elm.forEach((elmItem) => elmItem.dispatchEvent(e))
}
}
function keyDown(pKey) {
if (
(!mapFunctions[pKey].once && !mapFunctions[pKey].push) ||
(mapFunctions[pKey].push && !keyPressed)
) {
dispatchShortkeyEvent(pKey)
}
}
function fillMappingFunctions(
mappingFunctions,
{ b, push, once, focus, propagte, el },
) {
for (let key in b) {
const k = encodeKey(b[key])
const propagated = mappingFunctions[k] && mappingFunctions[k].propagte
const elm =
mappingFunctions[k] && mappingFunctions[k].el
? mappingFunctions[k].el
: []
elm.push(el)
mappingFunctions[k] = {
push,
once,
focus,
key,
propagte: propagated || propagte,
el: elm,
}
}
}
function bindValue(value, el, binding, vnode) {
const { modifiers } = binding
const push = !!modifiers.push
const avoid = !!modifiers.avoid
const focus = !modifiers.focus
const once = !!modifiers.once
const propagte = !!modifiers.propagte
if (avoid) {
objAvoided = objAvoided.filter((itm) => !itm === el)
objAvoided.push(el)
} else {
fillMappingFunctions(mapFunctions, {
b: value,
push,
once,
focus,
propagte,
el: vnode.el,
})
}
}
function unbindValue(value, el) {
for (let key in value) {
const k = encodeKey(value[key])
const idxElm = mapFunctions[k].el.indexOf(el)
if (mapFunctions[k].el.length > 1 && idxElm > -1) {
mapFunctions[k].el.splice(idxElm, 1)
} else {
delete mapFunctions[k]
}
}
}
function availableElement(decodedKey) {
const objectIsAvoided = !!objAvoided.find(
(r) => r === document.activeElement,
)
const filterAvoided = !!elementAvoided.find(
(selector) =>
document.activeElement && document.activeElement.matches(selector),
)
return !!mapFunctions[decodedKey] && !(objectIsAvoided || filterAvoided)
}
function keyDownListener(pKey) {
const decodedKey = decodeKey(pKey)
// Check avoidable elements
if (!availableElement(decodedKey)) {
return
}
if (!mapFunctions[decodedKey].propagte) {
pKey.preventDefault()
pKey.stopPropagation()
}
if (mapFunctions[decodedKey].focus) {
keyDown(decodedKey)
keyPressed = true
} else if (!keyPressed) {
const elm = mapFunctions[decodedKey].el
elm[elm.length - 1].focus()
keyPressed = true
}
}
function keyUpListener(pKey) {
const decodedKey = decodeKey(pKey)
if (!availableElement(decodedKey)) {
keyPressed = false
return
}
if (!mapFunctions[decodedKey].propagte) {
pKey.preventDefault()
pKey.stopPropagation()
}
if (mapFunctions[decodedKey].once || mapFunctions[decodedKey].push) {
dispatchShortkeyEvent(decodedKey)
}
keyPressed = false
}
// register key presses that happen before mounting of directive
// if (process?.env?.NODE_ENV !== 'test') {
// (() => {
document.addEventListener('keydown', keyDownListener, true)
document.addEventListener('keyup', keyUpListener, true)
// })()
// }
function install(app, options) {
elementAvoided = [...(options && options.prevent ? options.prevent : [])]
app.directive('shortkey', {
beforeMount(el, binding, vnode) {
// Mapping the commands
const value = parseValue(binding.value)
bindValue(value, el, binding, vnode)
},
updated(el, binding, vnode) {
const oldValue = parseValue(binding.oldValue)
unbindValue(oldValue, el)
const newValue = parseValue(binding.value)
bindValue(newValue, el, binding, vnode)
},
unmounted(el, binding) {
const value = parseValue(binding.value)
unbindValue(value, el)
},
})
}
export default {
install,
encodeKey,
decodeKey,
keyDown,
}

View File

@ -27,17 +27,14 @@
group="buckets"
:disabled="!canWrite"
:class="{'dragging-disabled': !canWrite}"
tag="transition-group"
:item-key="({id}) => `bucket${id}`"
:component-data="bucketDraggableComponentData"
>
<transition-group
type="transition"
:name="!dragBucket ? 'move-bucket': null"
tag="div"
class="kanban-bucket-container">
<template #item="{element: bucket, index: bucketIndex }">
<div
:key="`bucket${bucket.id}`"
class="bucket"
:class="{'is-collapsed': collapsedBuckets[bucket.id]}"
v-for="(bucket, k) in buckets"
>
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
<span
@ -136,16 +133,15 @@
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}"
:disabled="!canWrite"
:class="{'dragging-disabled': !canWrite}"
:data-bucket-index="k"
:data-bucket-index="bucketIndex"
class="dropper"
tag="transition-group"
:item-key="(task) => `bucket${bucket.id}-task${task.id}`"
:component-data="taskDraggableTaskComponentData"
>
<transition-group type="transition" :name="!drag ? 'move-card': null" tag="div">
<kanban-card
:key="`bucket${bucket.id}-task${task.id}`"
v-for="task in bucket.tasks"
:task="task"
/>
</transition-group>
<template #item="{element: task}">
<kanban-card :task="task" />
</template>
</draggable>
</div>
<div class="bucket-footer" v-if="canWrite">
@ -181,7 +177,7 @@
</x-button>
</div>
</div>
</transition-group>
</template>
</draggable>
<div class="bucket new-bucket" v-if="canWrite && !loading && buckets.length > 0">
@ -251,6 +247,15 @@ import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveC
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
import KanbanCard from '../../../components/tasks/partials/kanban-card'
const DRAG_OPTIONS = {
// sortable options
animation: 150,
ghostClass: 'ghost',
dragClass: 'task-dragging',
delayOnTouchOnly: true,
delay: 150,
}
export default {
name: 'Kanban',
components: {
@ -261,6 +266,8 @@ export default {
},
data() {
return {
dragOptions: DRAG_OPTIONS,
drag: false,
dragBucket: false,
sourceBucket: 0,
@ -305,6 +312,21 @@ export default {
'$route.params.listId': 'loadBuckets',
},
computed: {
bucketDraggableComponentData() {
return {
type: 'transition',
tag: 'div',
name: !this.dragBucket ? 'move-bucket': null,
class: 'kanban-bucket-container',
}
},
taskDraggableTaskComponentData() {
return {
type: 'transition',
tag: 'div',
name: !this.drag ? 'move-card': null,
}
},
buckets: {
get() {
return this.$store.state.kanban.buckets
@ -313,17 +335,6 @@ export default {
this.$store.commit('kanban/setBuckets', value)
},
},
dragOptions() {
const options = {
animation: 150,
ghostClass: 'ghost',
dragClass: 'task-dragging',
delay: 150,
delayOnTouchOnly: true,
}
return options
},
...mapState({
loadedListId: state => state.kanban.listId,
loading: state => state[LOADING] && state[LOADING_MODULE] === 'kanban',

View File

@ -89,27 +89,28 @@
handle=".handle"
:disabled="!canWrite"
:class="{'dragging-disabled': !canWrite}"
item-key="id"
>
<single-task-in-list
:show-list-color="false"
:disabled="!canWrite"
:key="t.id"
:the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
v-for="t in tasks"
>
<span class="icon handle">
<icon icon="grip-lines"/>
</span>
<div
@click="editTask(t.id)"
class="icon settings"
v-if="!list.isArchived && canWrite"
<template #item="{element: t}">
<single-task-in-list
:show-list-color="false"
:disabled="!canWrite"
:the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
>
<icon icon="pencil-alt"/>
</div>
</single-task-in-list>
<span class="icon handle">
<icon icon="grip-lines"/>
</span>
<div
@click="editTask(t.id)"
class="icon settings"
v-if="!list.isArchived && canWrite"
>
<icon icon="pencil-alt"/>
</div>
</single-task-in-list>
</template>
</draggable>
</div>
<card