feat: color the task color button when the task has a color set (#2331)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2331 Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
commit
f70b1d2902
6 changed files with 96 additions and 28 deletions
|
@ -56,10 +56,10 @@
|
||||||
class="menu-label"
|
class="menu-label"
|
||||||
v-tooltip="namespaceTitles[nk]"
|
v-tooltip="namespaceTitles[nk]"
|
||||||
>
|
>
|
||||||
<span
|
<ColorBubble
|
||||||
v-if="n.hexColor !== ''"
|
v-if="n.hexColor !== ''"
|
||||||
:style="{ backgroundColor: n.hexColor }"
|
:color="n.hexColor"
|
||||||
class="color-bubble"
|
class="mr-1"
|
||||||
/>
|
/>
|
||||||
<span class="name">{{ namespaceTitles[nk] }}</span>
|
<span class="name">{{ namespaceTitles[nk] }}</span>
|
||||||
<div
|
<div
|
||||||
|
@ -114,11 +114,11 @@
|
||||||
<span class="icon handle">
|
<span class="icon handle">
|
||||||
<icon icon="grip-lines"/>
|
<icon icon="grip-lines"/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<ColorBubble
|
||||||
:style="{ backgroundColor: l.hexColor }"
|
v-if="l.hexColor !== ''"
|
||||||
class="color-bubble"
|
:color="l.hexColor"
|
||||||
v-if="l.hexColor !== ''">
|
class="mr-1"
|
||||||
</span>
|
/>
|
||||||
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
@ -158,6 +158,7 @@ import {getListTitle} from '@/helpers/getListTitle'
|
||||||
import {useEventListener} from '@vueuse/core'
|
import {useEventListener} from '@vueuse/core'
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
|
||||||
const drag = ref(false)
|
const drag = ref(false)
|
||||||
const dragOptions = {
|
const dragOptions = {
|
||||||
|
|
|
@ -9,9 +9,16 @@
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<icon :icon="icon" v-if="showIconOnly"/>
|
<icon
|
||||||
|
v-if="showIconOnly"
|
||||||
|
:icon="icon"
|
||||||
|
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||||
|
/>
|
||||||
<span class="icon is-small" v-else-if="icon !== ''">
|
<span class="icon is-small" v-else-if="icon !== ''">
|
||||||
<icon :icon="icon"/>
|
<icon
|
||||||
|
:icon="icon"
|
||||||
|
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<slot />
|
<slot />
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
@ -42,6 +49,10 @@ const props = defineProps({
|
||||||
type: [String, Array],
|
type: [String, Array],
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
iconColor: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
|
24
src/components/misc/colorBubble.vue
Normal file
24
src/components/misc/colorBubble.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:style="{backgroundColor: color }"
|
||||||
|
class="color-bubble"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Color } from 'csstype'
|
||||||
|
|
||||||
|
defineProps< {
|
||||||
|
color: Color,
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.color-bubble {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="heading">
|
<div class="heading">
|
||||||
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
|
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
|
||||||
<Done class="heading__done" :is-done="task.done" />
|
<Done class="heading__done" :is-done="task.done"/>
|
||||||
|
<ColorBubble
|
||||||
|
v-if="task.hexColor !== ''"
|
||||||
|
:color="task.getHexColor()"
|
||||||
|
class="mt-1 ml-2"
|
||||||
|
/>
|
||||||
<h1
|
<h1
|
||||||
class="title input"
|
class="title input"
|
||||||
:class="{'disabled': !canWrite}"
|
:class="{'disabled': !canWrite}"
|
||||||
|
@ -42,6 +47,7 @@ import Done from '@/components/misc/Done.vue'
|
||||||
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
||||||
|
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
task: {
|
task: {
|
||||||
|
@ -58,8 +64,9 @@ const emit = defineEmits(['update:task'])
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const copy = useCopyToClipboard()
|
const copy = useCopyToClipboard()
|
||||||
|
|
||||||
async function copyUrl() {
|
async function copyUrl() {
|
||||||
const route = router.resolve({ name: 'task.detail', query: { taskId: props.task.id}})
|
const route = router.resolve({name: 'task.detail', query: {taskId: props.task.id}})
|
||||||
const absoluteURL = new URL(route.href, window.location.href).href
|
const absoluteURL = new URL(route.href, window.location.href).href
|
||||||
|
|
||||||
await copy(absoluteURL)
|
await copy(absoluteURL)
|
||||||
|
@ -95,8 +102,7 @@ async function save(title: string) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showSavedMessage.value = false
|
showSavedMessage.value = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,4 +112,9 @@ async function save(title: string) {
|
||||||
.heading__done {
|
.heading__done {
|
||||||
margin-left: .5rem;
|
margin-left: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-bubble {
|
||||||
|
height: .75rem;
|
||||||
|
width: .75rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,12 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
||||||
<fancycheckbox :disabled="(isArchived || disabled) && !canMarkAsDone" @change="markAsDone" v-model="task.done"/>
|
<fancycheckbox :disabled="(isArchived || disabled) && !canMarkAsDone" @change="markAsDone" v-model="task.done"/>
|
||||||
<span
|
<ColorBubble
|
||||||
v-if="showListColor && listColor !== ''"
|
v-if="showListColor && listColor !== ''"
|
||||||
:style="{backgroundColor: listColor }"
|
:color="listColor"
|
||||||
class="color-bubble"
|
class="mr-1"
|
||||||
>
|
/>
|
||||||
</span>
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="taskDetailRoute"
|
:to="taskDetailRoute"
|
||||||
:class="{ 'done': task.done}"
|
:class="{ 'done': task.done}"
|
||||||
|
@ -15,11 +14,17 @@
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||||
class="task-list"
|
class="task-list"
|
||||||
|
:class="{'mr-2': task.hexColor !== ''}"
|
||||||
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null"
|
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null"
|
||||||
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
|
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
|
||||||
{{ $store.getters['lists/getListById'](task.listId).title }}
|
{{ $store.getters['lists/getListById'](task.listId).title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<ColorBubble
|
||||||
|
v-if="task.hexColor !== ''"
|
||||||
|
:color="task.getHexColor()"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
||||||
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
|
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
|
||||||
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
||||||
|
@ -30,7 +35,7 @@
|
||||||
{{ task.title }}
|
{{ task.title }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0" />
|
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0"/>
|
||||||
<user
|
<user
|
||||||
:avatar-size="27"
|
:avatar-size="27"
|
||||||
:is-inline="true"
|
:is-inline="true"
|
||||||
|
@ -111,6 +116,7 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
import {playPop} from '@/helpers/playPop'
|
import {playPop} from '@/helpers/playPop'
|
||||||
import ChecklistSummary from './checklist-summary.vue'
|
import ChecklistSummary from './checklist-summary.vue'
|
||||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||||
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'singleTaskInList',
|
name: 'singleTaskInList',
|
||||||
|
@ -122,6 +128,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
ColorBubble,
|
||||||
BaseButton,
|
BaseButton,
|
||||||
ChecklistSummary,
|
ChecklistSummary,
|
||||||
DeferTask,
|
DeferTask,
|
||||||
|
@ -282,11 +289,6 @@ export default defineComponent({
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-bubble {
|
|
||||||
height: 10px;
|
|
||||||
flex: 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
|
@ -378,6 +378,7 @@
|
||||||
@click="setFieldActive('color')"
|
@click="setFieldActive('color')"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="fill-drip"
|
icon="fill-drip"
|
||||||
|
:icon-color="color"
|
||||||
v-shortcut="'c'"
|
v-shortcut="'c'"
|
||||||
>
|
>
|
||||||
{{ $t('task.detail.actions.color') }}
|
{{ $t('task.detail.actions.color') }}
|
||||||
|
@ -429,7 +430,7 @@ import {defineComponent} from 'vue'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
import TaskService from '../../services/task'
|
import TaskService from '../../services/task'
|
||||||
import TaskModel from '@/models/task'
|
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
|
||||||
import { PRIORITIES as priorites } from '@/constants/priorities'
|
import { PRIORITIES as priorites } from '@/constants/priorities'
|
||||||
|
@ -461,6 +462,7 @@ import { setTitle } from '@/helpers/setTitle'
|
||||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||||
import {getListTitle} from '@/helpers/getListTitle'
|
import {getListTitle} from '@/helpers/getListTitle'
|
||||||
import type { IList } from '@/modelTypes/IList'
|
import type { IList } from '@/modelTypes/IList'
|
||||||
|
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||||
|
|
||||||
function scrollIntoView(el) {
|
function scrollIntoView(el) {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
@ -528,6 +530,8 @@ export default defineComponent({
|
||||||
// Used to avoid flashing of empty elements if the task content is not yet loaded.
|
// Used to avoid flashing of empty elements if the task content is not yet loaded.
|
||||||
visible: false,
|
visible: false,
|
||||||
|
|
||||||
|
TASK_DEFAULT_COLOR,
|
||||||
|
|
||||||
activeFields: {
|
activeFields: {
|
||||||
assignees: false,
|
assignees: false,
|
||||||
priority: false,
|
priority: false,
|
||||||
|
@ -594,6 +598,15 @@ export default defineComponent({
|
||||||
shouldShowClosePopup() {
|
shouldShowClosePopup() {
|
||||||
return this.$route.name.includes('kanban')
|
return this.$route.name.includes('kanban')
|
||||||
},
|
},
|
||||||
|
color() {
|
||||||
|
const color = this.task.getHexColor
|
||||||
|
? this.task.getHexColor()
|
||||||
|
: false
|
||||||
|
|
||||||
|
return color === TASK_DEFAULT_COLOR
|
||||||
|
? ''
|
||||||
|
: color
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getNamespaceTitle,
|
getNamespaceTitle,
|
||||||
|
@ -745,6 +758,8 @@ export default defineComponent({
|
||||||
this.task = await this.taskService.update(this.task)
|
this.task = await this.taskService.update(this.task)
|
||||||
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
|
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
colorIsDark,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -933,6 +948,10 @@ $flash-background-duration: 750ms;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
|
|
||||||
|
&.has-light-text {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue