1
0

Merge branch 'main' into feature/login-improvements

This commit is contained in:
Dominik Pschenitschni
2022-02-05 18:04:33 +01:00
72 changed files with 2486 additions and 2407 deletions

View File

@ -1,8 +1,12 @@
<template>
<div>
<a @click="$store.commit('menuActive', false)" class="menu-hide-button" v-if="menuActive">
<BaseButton
v-if="menuActive"
@click="$store.commit('menuActive', false)"
class="menu-hide-button"
>
<icon icon="times" />
</a>
</BaseButton>
<div
:class="{'has-background': background}"
:style="{'background-image': background && `url(${background})`}"
@ -16,18 +20,32 @@
]"
class="app-content"
>
<a @click="$store.commit('menuActive', false)" class="mobile-overlay" v-if="menuActive"></a>
<BaseButton
v-if="menuActive"
@click="$store.commit('menuActive', false)"
class="mobile-overlay"
/>
<quick-actions/>
<router-view/>
<router-view name="popup" v-slot="{ Component }">
<transition name="modal">
<router-view :route="routeWithModal" v-slot="{ Component }">
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
<component :is="Component" />
</transition>
</keep-alive>
</router-view>
<transition name="modal">
<modal
v-if="currentModal"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"
>
<component :is="currentModal" />
</modal>
</transition>
<a
class="keyboard-shortcuts-button"
@click="showKeyboardShortcuts()"
@ -41,7 +59,7 @@
</template>
<script lang="ts" setup>
import {watch, computed} from 'vue'
import {watch, computed, shallowRef, watchEffect, VNode, h} from 'vue'
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
@ -49,6 +67,59 @@ import {useEventListener} from '@vueuse/core'
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue'
import BaseButton from '@/components/base/BaseButton.vue'
function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const routeWithModal = computed(() => {
return backdropView.value
? router.resolve(backdropView.value)
: route
})
const currentModal = shallowRef<VNode>()
watchEffect(() => {
if (!backdropView.value) {
currentModal.value = undefined
return
}
// logic from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
currentModal.value = h(
route.matched[0]?.components.default,
routeProps,
)
})
function closeModal() {
const historyState = computed(() => route.fullPath && window.history.state)
if (historyState.value) {
router.back()
} else {
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
router.push(backdropRoute)
}
}
return { routeWithModal, currentModal, closeModal }
}
const { routeWithModal, currentModal, closeModal } = useRouteWithModal()
const store = useStore()
@ -223,4 +294,6 @@ store.dispatch('labels/loadAllLabels')
display: none;
}
}
@include modal-transition();
</style>

View File

@ -90,13 +90,15 @@
v-bind="dragOptions"
:modelValue="activeLists[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)"
:group="`namespace-${n.id}-lists`"
group="namespace-lists"
@start="() => drag = true"
@end="e => saveListPosition(e, nk)"
@end="saveListPosition"
handle=".handle"
:disabled="n.id < 0 || null"
tag="transition-group"
item-key="id"
:data-namespace-id="n.id"
:data-namespace-index="nk"
:component-data="{
type: 'transition',
tag: 'ul',
@ -198,7 +200,7 @@ export default {
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}),
activeLists() {
return this.namespaces.map(({lists}) => lists?.filter(item => !item.isArchived))
return this.namespaces.map(({lists}) => lists?.filter(item => typeof item !== 'undefined' && !item.isArchived))
},
namespaceTitles() {
return this.namespaces.map((namespace) => this.getNamespaceTitle(namespace))
@ -241,15 +243,15 @@ export default {
this.listsVisible[namespaceId] = !this.listsVisible[namespaceId]
},
updateActiveLists(namespace, activeLists) {
// this is a bit hacky: since we do have to filter out the archived items from the list
// This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
const lists = namespace.lists.map((item) => {
if (item.isArchived) {
return item
}
return activeLists.shift()
})
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
...namespace.lists.filter(l => l.isArchived),
]
const newNamespace = {
...namespace,
@ -259,8 +261,11 @@ export default {
this.$store.commit('namespaces/setNamespaceById', newNamespace)
},
async saveListPosition(e, namespaceIndex) {
const listsActive = this.activeLists[namespaceIndex]
async saveListPosition(e) {
const namespaceId = parseInt(e.to.dataset.namespaceId)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex)
const listsActive = this.activeLists[newNamespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
@ -273,6 +278,7 @@ export default {
await this.$store.dispatch('lists/updateList', {
...list,
position,
namespaceId,
})
} finally {
this.listUpdating[list.id] = false

View File

@ -2,21 +2,22 @@
<dropdown>
<template v-if="isSavedFilter">
<dropdown-item
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
icon="trash-alt"
>
{{ $t('misc.delete') }}
</dropdown-item>
</template>
<template v-else-if="list.isArchived">
<dropdown-item
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.unarchive') }}
@ -24,37 +25,38 @@
</template>
<template v-else>
<dropdown-item
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.background`, params: { listId: list.id } }"
v-if="backgroundsEnabled"
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
icon="image"
>
{{ $t('menu.setBackground') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.share`, params: { listId: list.id } }"
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.duplicate`, params: { listId: list.id } }"
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
icon="paste"
>
{{ $t('menu.duplicate') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.archive') }}
</dropdown-item>
<task-subscription
v-if="subscription"
class="dropdown-item has-no-shadow"
:is-button="false"
entity="list"
@ -63,7 +65,7 @@
@change="sub => subscription = sub"
/>
<dropdown-item
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
icon="trash-alt"
class="has-text-danger"
>
@ -73,56 +75,32 @@
</dropdown>
</template>
<script>
<script setup lang="ts">
import {ref, computed, watchEffect} from 'vue'
import {useStore} from 'vuex'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
import ListModel from '@/models/list'
import SubscriptionModel from '@/models/subscription'
export default {
name: 'list-settings-dropdown',
data() {
return {
subscription: null,
}
const props = defineProps({
list: {
type: ListModel,
required: true,
},
components: {
TaskSubscription,
DropdownItem,
Dropdown,
},
props: {
list: {
required: true,
},
},
mounted() {
this.subscription = this.list.subscription
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
},
listRoutePrefix() {
let name = 'list'
})
const subscription = ref<SubscriptionModel>()
watchEffect(() => {
if (props.list.subscription) {
subscription.value = props.list.subscription
}
})
if (this.$route.name !== null && this.$route.name.startsWith('list.')) {
// HACK: we should implement a better routing for the modals
const settingsRoutes = ['edit', 'delete', 'archive', 'background', 'share', 'duplicate']
const suffix = settingsRoutes.find((route) => this.$route.name.endsWith(`.settings.${route}`))
name = this.$route.name.replace(`.settings.${suffix}`,'')
}
if (this.isSavedFilter) {
name = name.replace('list.', 'filter.')
}
return `${name}.settings`
},
isSavedFilter() {
return getSavedFilterIdFromListId(this.list.id) > 0
},
},
}
const store = useStore()
const backgroundsEnabled = computed(() => store.state.config.enabledBackgroundProviders?.length > 0)
const isSavedFilter = computed(() => getSavedFilterIdFromListId(props.list.id) > 0)
</script>

View File

@ -29,9 +29,10 @@
<script>
import Filters from '@/components/list/partials/filters'
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
import Popup from '@/components/misc/popup'
import {getDefaultParams} from '@/composables/taskList'
export default {
name: 'filter-popup',
components: {

View File

@ -191,7 +191,7 @@ import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
import {getDefaultParams} from '@/composables/taskList'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = {

View File

@ -4,9 +4,11 @@
<template v-for="(s, i) in shortcuts" :key="i">
<h3>{{ $t(s.title) }}</h3>
<message class="mb-4">
<message class="mb-4" v-if="s.available">
{{
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
s.available($route)
? $t('keyboardShortcuts.currentPageOnly')
: $t('keyboardShortcuts.allPages')
}}
</message>
@ -17,7 +19,8 @@
class="shortcut-keys"
is="dd"
:keys="sc.keys"
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
:combination="sc.combination && $t(`keyboardShortcuts.${sc.combination}`)"
/>
</template>
</dl>
</template>
@ -25,28 +28,18 @@
</modal>
</template>
<script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message'
import {KEYBOARD_SHORTCUTS} from './shortcuts'
<script lang="ts" setup>
import {useStore} from 'vuex'
export default {
name: 'keyboard-shortcuts',
components: {
Message,
Shortcut,
},
data() {
return {
shortcuts: KEYBOARD_SHORTCUTS,
}
},
methods: {
close() {
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
},
},
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message.vue'
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
const store = useStore()
function close() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
}
</script>

View File

@ -1,11 +1,24 @@
import {RouteLocation} from 'vue-router'
import {isAppleDevice} from '@/helpers/isAppleDevice'
const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
export const KEYBOARD_SHORTCUTS = [
interface Shortcut {
title: string
keys: string[]
combination?: 'then'
}
interface ShortcutGroup {
title: string
available?: (route: RouteLocation) => boolean
shortcuts: Shortcut[]
}
export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
{
title: 'keyboardShortcuts.general',
available: () => null,
shortcuts: [
{
title: 'keyboardShortcuts.toggleMenu',
@ -29,7 +42,7 @@ export const KEYBOARD_SHORTCUTS = [
},
{
title: 'keyboardShortcuts.list.title',
available: (route) => route.name.startsWith('list.'),
available: (route) => (route.name as string)?.startsWith('list.'),
shortcuts: [
{
title: 'keyboardShortcuts.list.switchToListView',
@ -55,13 +68,7 @@ export const KEYBOARD_SHORTCUTS = [
},
{
title: 'keyboardShortcuts.task.title',
available: (route) => [
'task.detail',
'task.list.detail',
'task.gantt.detail',
'task.kanban.detail',
'task.detail',
].includes(route.name),
available: (route) => route.name === 'task.detail',
shortcuts: [
{
title: 'keyboardShortcuts.task.assign',

View File

@ -52,9 +52,15 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline'
import {useRouter, useRoute} from 'vue-router'
import {getAuthForRoute} from '@/router'
const router = useRouter()
const route = useRoute()
const store = useStore()
const ready = computed(() => store.state.vikunjaReady)
const ready = ref(false)
const online = useOnline()
const error = ref('')
@ -63,7 +69,12 @@ const showLoading = computed(() => !ready.value && error.value === '')
async function load() {
try {
await store.dispatch('loadApp')
} catch(e: any) {
const redirectTo = getAuthForRoute(route)
if (typeof redirectTo !== 'undefined') {
await router.push(redirectTo)
}
ready.value = true
} catch (e: any) {
error.value = e
}
}

View File

@ -1,55 +1,47 @@
<template>
<x-button
v-if="isButton"
variant="secondary"
:icon="icon"
:icon="iconName"
v-tooltip="tooltipText"
@click="changeSubscription"
:disabled="disabled || null"
v-if="isButton"
>
{{ buttonText }}
</x-button>
<a
<BaseButton
v-else
v-tooltip="tooltipText"
@click="changeSubscription"
:class="{'is-disabled': disabled}"
v-else
>
<span class="icon">
<icon :icon="icon"/>
<icon :icon="iconName"/>
</span>
{{ buttonText }}
</a>
</BaseButton>
</template>
<script lang="ts" setup>
import {computed, shallowRef} from 'vue'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription'
import {success} from '@/message'
const props = defineProps({
entity: {
required: true,
type: String,
},
subscription: {
required: true,
validator(value) {
return value instanceof SubscriptionModel || value === null
},
},
entityId: {
required: true,
type: Number,
},
isButton: {
type: Boolean,
default: true,
},
interface Props {
entity: string
entityId: number
subscription: SubscriptionModel
isButton?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isButton: true,
})
const subscriptionEntity = computed<string>(() => props.subscription.entity)
@ -73,7 +65,7 @@ const tooltipText = computed(() => {
})
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const icon = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const iconName = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => {
if (props.subscription === null) {
return false

View File

@ -1,4 +1,5 @@
<template>
<!-- FIXME: transition should not be included in the modal -->
<transition name="modal">
<section
v-if="enabled"
@ -21,6 +22,13 @@
'is-wide': wide
}"
>
<BaseButton
@click="emit('close')"
class="close"
>
<icon icon="times"/>
</BaseButton>
<slot>
<div class="header">
<slot name="header"></slot>
@ -53,6 +61,8 @@
</template>
<script>
import BaseButton from '@/components/base/BaseButton.vue'
export const TRANSITION_NAMES = {
MODAL: 'modal',
FADE: 'fade',
@ -70,6 +80,11 @@ function validValue(values) {
export default {
name: 'modal',
components: {
BaseButton,
},
mounted() {
document.addEventListener('keydown', (e) => {
// Close the model when escape is pressed
@ -197,17 +212,22 @@ export default {
}
}
.close {
position: fixed;
top: 5px;
right: 26px;
color: var(--white);
font-size: 2rem;
/* Transitions */
.modal-enter,
.modal-leave-active {
opacity: 0;
@media screen and (max-width: $desktop) {
color: var(--dark);
}
}
</style>
.modal-enter .modal-container,
.modal-leave-active .modal-container {
transform: scale(0.9);
<style lang="scss">
// Close icon SVG uses currentColor, change the color to keep it visible
.dark .task-detail-view-modal .close {
color: var(--grey-900);
}
</style>

View File

@ -16,13 +16,13 @@
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.share', params: { id: namespace.id } }"
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'list.create', params: { id: namespace.id } }"
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }"
icon="plus"
>
{{ $t('menu.newList') }}
@ -34,6 +34,7 @@
{{ $t('menu.archive') }}
</dropdown-item>
<task-subscription
v-if="subscription"
class="dropdown-item has-no-shadow"
:is-button="false"
entity="namespace"

View File

@ -264,4 +264,6 @@ export default {
.sharables-list:not(.card-content) {
overflow-y: auto
}
@include modal-transition();
</style>

View File

@ -365,3 +365,7 @@ export default {
},
}
</script>
<style lang="scss" scoped>
@include modal-transition();
</style>

View File

@ -67,7 +67,7 @@
<router-link
class="mt-2 has-text-centered is-block"
:to="{name: 'task.detail', params: {id: taskEditTask.id}}"
:to="taskDetailRoute"
>
{{ $t('task.openDetail') }}
</router-link>
@ -97,6 +97,15 @@ export default {
taskEditTask: TaskModel,
}
},
computed: {
taskDetailRoute() {
return {
name: 'task.detail',
params: { id: this.taskEditTask.id },
state: { backdropView: this.$router.currentRoute.value.fullPath },
}
},
},
components: {
ColorPicker,
Reminders,

View File

@ -1,101 +0,0 @@
import TaskCollectionService from '@/services/taskCollection'
// FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({
sort_by: ['position', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
})
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export default {
data() {
return {
taskCollectionService: new TaskCollectionService(),
tasks: [],
currentPage: 0,
loadedList: null,
searchTerm: '',
showTaskFilter: false,
params: {...getDefaultParams()},
}
},
watch: {
// Only listen for query path changes
'$route.query': {
handler: 'loadTasksForPage',
immediate: true,
},
'$route.path': 'loadTasksOnSavedFilter',
},
methods: {
async loadTasks(
page,
search = '',
params = null,
forceLoading = false,
) {
// Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks
// FIXME: This is a bit hacky -> Cleanup.
if (
this.$route.name !== 'list.list' &&
this.$route.name !== 'list.table' &&
!forceLoading
) {
return
}
if (params === null) {
params = this.params
}
if (search !== '') {
params.s = search
}
const list = {listId: parseInt(this.$route.params.listId)}
const currentList = {
id: list.listId,
params,
search,
page,
}
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList) && !forceLoading) {
return
}
this.tasks = []
this.tasks = await this.taskCollectionService.getAll(list, params, page)
this.currentPage = page
this.loadedList = JSON.parse(JSON.stringify(currentList))
},
loadTasksForPage(e) {
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
let page = Number(e.page)
if (typeof e.page === 'undefined') {
page = 1
}
let search = e.search
if (typeof e.search === 'undefined') {
search = ''
}
this.initTasks(page, search)
},
loadTasksOnSavedFilter() {
if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
this.loadTasks(1, '', null, true)
}
},
},
}

View File

@ -400,4 +400,6 @@ export default {
transform: translate3d(0, -4px, 0);
}
}
@include modal-transition();
</style>

View File

@ -162,7 +162,7 @@ import {mapState} from 'vuex'
export default {
name: 'comments',
components: {
editor: AsyncEditor,
Editor: AsyncEditor,
},
props: {
taskId: {
@ -339,4 +339,6 @@ export default {
.media-content {
width: calc(100% - 48px - 2rem);
}
@include modal-transition();
</style>

View File

@ -38,7 +38,7 @@ import {mapState} from 'vuex'
export default {
name: 'description',
components: {
editor: AsyncEditor,
Editor: AsyncEditor,
},
data() {
return {

View File

@ -19,7 +19,7 @@
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
class="tag">
<span>{{ props.item.title }}</span>
<a @click="removeLabel(props.item)" class="delete is-small"></a>
<button type="button" v-cy="'taskDetail.removeLabel'" @click="removeLabel(props.item)" class="delete is-small" />
</span>
</template>
<template #searchResult="props">
@ -114,23 +114,17 @@ export default {
},
async removeLabel(label) {
const removeFromState = () => {
for (const l in this.labels) {
if (this.labels[l].id === label.id) {
this.labels.splice(l, 1)
}
if (!this.taskId === 0) {
await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
}
for (const l in this.labels) {
if (this.labels[l].id === label.id) {
this.labels.splice(l, 1)
}
this.$emit('update:modelValue', this.labels)
this.$emit('change', this.labels)
}
if (this.taskId === 0) {
removeFromState()
return
}
await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
removeFromState()
this.$emit('update:modelValue', this.labels)
this.$emit('change', this.labels)
this.$message.success({message: this.$t('task.label.removeSuccess')})
},

View File

@ -7,8 +7,8 @@
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
}"
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
@click.exact="openTaskDetail()"
@click.ctrl="() => toggleTaskDone(task)"
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
@click.meta="() => toggleTaskDone(task)"
>
<span class="task-id">
@ -115,6 +115,13 @@ export default {
this.loadingInternal = false
}
},
openTaskDetail() {
this.$router.push({
name: 'task.detail',
params: { id: this.task.id },
state: { backdropView: this.$router.currentRoute.value.fullPath },
})
},
},
}
</script>

View File

@ -274,10 +274,11 @@ export default {
return tasks
.map(task => {
// by doing this here once we can save a lot of duplicate calls in the template
const listAndNamespace = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
const {
list,
namespace,
} = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
} = listAndNamespace === null ? {list: null, namespace: null} : listAndNamespace
return {
...task,
@ -364,4 +365,6 @@ export default {
:deep(.multiselect .search-results button) {
padding: 0.5rem;
}
@include modal-transition();
</style>

View File

@ -8,7 +8,7 @@
>
</span>
<router-link
:to="{ name: taskDetailRoute, params: { id: task.id } }"
:to="taskDetailRoute"
:class="{ 'done': task.done}"
class="tasktext">
<span>
@ -129,10 +129,6 @@ export default {
type: Boolean,
default: false,
},
taskDetailRoute: {
type: String,
default: 'task.list.detail',
},
showList: {
type: Boolean,
default: false,
@ -170,6 +166,14 @@ export default {
title: '',
} : this.$store.state.currentList
},
taskDetailRoute() {
return {
name: 'task.detail',
params: { id: this.task.id },
// TODO: re-enable opening task detail in modal
// state: { backdropView: this.$router.currentRoute.value.fullPath },
}
},
},
methods: {
async markAsDone(checked) {

View File

@ -1,20 +1,21 @@
<template>
<a @click="$emit('click')">
<BaseButton>
<icon icon="sort-up" v-if="order === 'asc'"/>
<icon icon="sort-up" rotation="180" v-else-if="order === 'desc'"/>
<icon icon="sort-up" v-else-if="order === 'desc'" rotation="180"/>
<icon icon="sort" v-else/>
</a>
</BaseButton>
</template>
<script>
export default {
name: 'sort',
props: {
order: {
type: String,
default: 'none',
},
<script setup lang="ts">
import {PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
type Order = 'asc' | 'desc' | 'none'
defineProps({
order: {
type: String as PropType<Order>,
default: 'none',
},
emits: ['click'],
}
})
</script>