WIP: feature/single-task-in-list-reworked #3180
164
src/components/misc/ProgressBar.vue
Normal file
164
src/components/misc/ProgressBar.vue
Normal file
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<progress
|
||||
class="progress-bar"
|
||||
:class="{
|
||||
'is-small': isSmall,
|
||||
'is-primary': isPrimary,
|
||||
}"
|
||||
:value="value"
|
||||
max="100"
|
||||
>
|
||||
{{ value }}%
|
||||
</progress>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isPrimary: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.progress-bar {
|
||||
--progress-height: var(--size-normal, #{$size-normal});
|
||||
--progress-bar-background-color: var(--border-light, #{$border-light});
|
||||
--progress-value-background-color: var(--text, #{$text});
|
||||
--progress-border-radius: var(--radius-rounded, #{$radius-rounded});
|
||||
--progress-indeterminate-duration: 1.5s;
|
||||
|
||||
--size-small: #{$size-small};
|
||||
--size-medium: #{$size-medium};
|
||||
--size-large: #{$size-large};
|
||||
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: var(--progress-border-radius);
|
||||
display: block;
|
||||
height: var(--progress-height);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: var(--progress-bar-background-color);
|
||||
}
|
||||
&::-webkit-progress-value {
|
||||
background-color: var(--progress-value-background-color);
|
||||
}
|
||||
&::-moz-progress-bar {
|
||||
background-color: var(--progress-value-background-color);
|
||||
}
|
||||
&::-ms-fill {
|
||||
background-color: var(--progress-value-background-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
// Colors
|
||||
@each $name, $pair in $colors {
|
||||
$color: nth($pair, 1);
|
||||
&.is-#{$name} {
|
||||
&::-webkit-progress-value {
|
||||
--progress-value-background-color: var(--#{$name}, #{$color});
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
--progress-value-background-color: var(--#{$name}, #{$color});
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
--progress-value-background-color: var(--#{$name}, #{$color});
|
||||
}
|
||||
|
||||
&:indeterminate {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--#{$name}, #{$color}) 30%,
|
||||
var(--progress-bar-background-color) 30%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:indeterminate {
|
||||
animation-duration: var(--progress-indeterminate-duration);
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: moveIndeterminate;
|
||||
animation-timing-function: linear;
|
||||
background-color: var(--progress-bar-background-color);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--text, #{$text}) 30%,
|
||||
var(--progress-bar-background-color) 30%
|
||||
);
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 150% 150%;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
animation-name: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&.is-small {
|
||||
--progress-height: var(--size-small, #{$size-small});
|
||||
}
|
||||
&.is-medium {
|
||||
--progress-height: var(--size-medium, #{$size-medium});
|
||||
}
|
||||
&.is-large {
|
||||
--progress-height: var(--size-large, #{$size-large});
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveIndeterminate {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
--progress-height: var(--size-normal, 1rem);
|
||||
|
||||
border-radius: $radius-large;
|
||||
width: inherit; // overwrite bulma
|
||||
min-width: 6vw;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar,
|
||||
&::-webkit-progress-value {
|
||||
background: var(--grey-500);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar.is-small {
|
||||
--progress-height: var(--size-small, 0.75rem);
|
||||
}
|
||||
</style>
|
|
@ -16,14 +16,11 @@
|
|||
type="file"
|
||||
v-if="editEnabled"
|
||||
/>
|
||||
<progress
|
||||
:value="attachmentService.uploadProgress"
|
||||
class="progress is-primary"
|
||||
max="100"
|
||||
<ProgressBar
|
||||
v-if="attachmentService.uploadProgress > 0"
|
||||
>
|
||||
{{ attachmentService.uploadProgress }}%
|
||||
</progress>
|
||||
:value="attachmentService.uploadProgress * 100"
|
||||
is-primary
|
||||
/>
|
||||
|
||||
<div class="files" v-if="attachments.length > 0">
|
||||
<a
|
||||
|
@ -34,7 +31,7 @@
|
|||
>
|
||||
<div class="filename">{{ a.file.name }}</div>
|
||||
<div class="info">
|
||||
<p class="attachment-info-meta">
|
||||
<p class="collapses">
|
||||
<i18n-t keypath="task.attachment.createdBy">
|
||||
<span v-tooltip="formatDate(a.created)">
|
||||
{{ formatDateSince(a.created) }}
|
||||
|
@ -83,7 +80,7 @@
|
|||
@click="$refs.files.click()"
|
||||
class="mb-4"
|
||||
icon="cloud-upload-alt"
|
||||
variant="secondary"
|
||||
type="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
{{ $t('task.attachment.upload') }}
|
||||
|
@ -138,7 +135,8 @@
|
|||
<script>
|
||||
import AttachmentService from '../../../services/attachment'
|
||||
import AttachmentModel from '../../../models/attachment'
|
||||
import User from '../../misc/user'
|
||||
import User from '@/components/misc/user'
|
||||
import ProgressBar from '@/components/misc/ProgressBar'
|
||||
import {mapState} from 'vuex'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
|
@ -148,6 +146,7 @@ export default {
|
|||
name: 'attachments',
|
||||
components: {
|
||||
User,
|
||||
ProgressBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -289,6 +288,21 @@ export default {
|
|||
content: '·';
|
||||
padding: 0 .25rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
&.collapses {
|
||||
flex-direction: column;
|
||||
|
||||
> span:not(:last-child):after,
|
||||
> a:not(:last-child):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user .username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -326,10 +340,6 @@ export default {
|
|||
height: auto;
|
||||
text-shadow: var(--shadow-md);
|
||||
animation: bounce 2s infinite;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
|
@ -346,35 +356,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.attachment-info-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.user) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
:deep(.user) {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
> span:not(:last-child):after,
|
||||
> a:not(:last-child):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user .username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
from,
|
||||
20%,
|
||||
|
@ -400,6 +381,4 @@ export default {
|
|||
transform: translate3d(0, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -28,17 +28,16 @@
|
|||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
<time :datetime="formatISO(task.dueDate)">
|
||||
<span>
|
||||
{{ formatDateSince(task.dueDate) }}
|
||||
</time>
|
||||
</span>
|
||||
</span>
|
||||
<h3>{{ task.title }}</h3>
|
||||
<progress
|
||||
class="progress is-small"
|
||||
<ProgressBar
|
||||
v-if="task.percentDone > 0"
|
||||
:value="task.percentDone * 100" max="100">
|
||||
{{ task.percentDone * 100 }}%
|
||||
</progress>
|
||||
:value="task.percentDone * 100"
|
||||
is-small
|
||||
/>
|
||||
<div class="footer">
|
||||
<labels :labels="task.labels"/>
|
||||
<priority-label :priority="task.priority" :done="task.done"/>
|
||||
|
@ -67,20 +66,20 @@
|
|||
|
||||
<script>
|
||||
import {playPop} from '../../../helpers/playPop'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import User from '../../../components/misc/user'
|
||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
|
||||
import User from '@/components/misc/user'
|
||||
import ProgressBar from '@/components/misc/ProgressBar'
|
||||
import Done from '@/components/misc/Done.vue'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import Labels from '@/components/tasks/partials/labels'
|
||||
import ChecklistSummary from './checklist-summary'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
export default {
|
||||
name: 'kanban-card',
|
||||
components: {
|
||||
ChecklistSummary,
|
||||
Done,
|
||||
PriorityLabel,
|
||||
ProgressBar,
|
||||
User,
|
||||
Labels,
|
||||
},
|
||||
|
@ -100,7 +99,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
colorIsDark,
|
||||
async toggleTaskDone(task) {
|
||||
this.loadingInternal = true
|
||||
try {
|
||||
|
@ -138,6 +136,7 @@ $task-background: var(--white);
|
|||
border: 3px solid transparent;
|
||||
|
||||
font-size: .9rem;
|
||||
margin: .5rem;
|
||||
padding: .4rem;
|
||||
border-radius: $radius;
|
||||
background: $task-background;
|
||||
|
|
|
@ -1,134 +1,159 @@
|
|||
<template>
|
||||
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
||||
<fancycheckbox :disabled="isArchived || disabled" @change="markAsDone" v-model="task.done"/>
|
||||
<div
|
||||
class="task loader-container"
|
||||
:class="{'is-loading': taskService.loading}"
|
||||
@click="$router.push({ name: taskDetailRoute, params: { id: task.id } })"
|
||||
>
|
||||
<fancycheckbox
|
||||
class="fancycheckbox"
|
||||
:disabled="isArchived || disabled" @change="markAsDone"
|
||||
v-model="task.done"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="showListColor && listColor !== ''"
|
||||
:style="{backgroundColor: listColor }"
|
||||
class="color-bubble"
|
||||
>
|
||||
</span>
|
||||
:style="{backgroundColor: listColor }"
|
||||
/>
|
||||
|
||||
<router-link
|
||||
:to="taskDetailRoute"
|
||||
:class="{ 'done': task.done}"
|
||||
class="tasktext">
|
||||
<span>
|
||||
<router-link
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
v-if="showList && list !== null"
|
||||
class="task-list"
|
||||
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
|
||||
{{ $store.getters['lists/getListById'](task.listId).title }}
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: list.title})"
|
||||
>
|
||||
{{ list.title }}
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
class="tasktext"
|
||||
:to="{ name: taskDetailRoute, params: { id: task.id } }"
|
||||
:class="{ 'done': task.done }"
|
||||
>
|
||||
<!-- 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
|
||||
v-if="task.relatedTasks.parenttask"
|
||||
class="parent-tasks"
|
||||
>
|
||||
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
||||
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">, </template>
|
||||
</template>
|
||||
>
|
||||
</span>
|
||||
{{ task.title }}
|
||||
</span>
|
||||
<span class="task-title">{{ task.title }}</span>
|
||||
</router-link>
|
||||
|
||||
<labels class="labels" :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
|
||||
:avatar-size="27"
|
||||
:is-inline="true"
|
||||
v-for="(a, i) in task.assignees"
|
||||
:key="task.id + 'assignee' + a.id + i"
|
||||
:avatar-size="27"
|
||||
:show-username="false"
|
||||
:user="a"
|
||||
v-for="(a, i) in task.assignees"
|
||||
/>
|
||||
<time
|
||||
:datetime="formatISO(task.dueDate)"
|
||||
|
||||
<popup v-if="+new Date(task.dueDate) > 0">
|
||||
<template #trigger="{toggle}">
|
||||
<em
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="is-italic"
|
||||
@click.prevent.stop="showDefer = !showDefer"
|
||||
v-if="+new Date(task.dueDate) > 0"
|
||||
@click.prevent.stop="toggle()"
|
||||
v-tooltip="formatDate(task.dueDate)"
|
||||
:aria-expanded="showDefer ? 'true' : 'false'"
|
||||
>
|
||||
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
|
||||
</time>
|
||||
</em>
|
||||
</template>
|
||||
|
||||
<template #content="{isOpen}">
|
||||
<transition name="fade">
|
||||
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
||||
<defer-task v-if="isOpen" v-model="task" />
|
||||
</transition>
|
||||
</template>
|
||||
</popup>
|
||||
|
||||
<priority-label :priority="task.priority" :done="task.done"/>
|
||||
<span>
|
||||
<span class="list-task-icon" v-if="task.attachments.length > 0">
|
||||
<icon icon="paperclip"/>
|
||||
</span>
|
||||
<span class="list-task-icon" v-if="task.description">
|
||||
<icon icon="align-left"/>
|
||||
</span>
|
||||
<span class="list-task-icon" v-if="task.repeatAfter.amount > 0">
|
||||
<icon icon="history"/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<icon
|
||||
v-if="task.attachments.length > 0"
|
||||
class="list-task-icon"
|
||||
icon="paperclip"
|
||||
/>
|
||||
|
||||
<icon
|
||||
v-if="task.description"
|
||||
class="list-task-icon"
|
||||
icon="align-left"
|
||||
/>
|
||||
|
||||
<icon
|
||||
v-if="task.repeatAfter.amount > 0"
|
||||
class="list-task-icon"
|
||||
icon="history"
|
||||
/>
|
||||
|
||||
<checklist-summary :task="task"/>
|
||||
</router-link>
|
||||
<progress
|
||||
class="progress is-small"
|
||||
|
||||
<ProgressBar
|
||||
v-if="task.percentDone > 0"
|
||||
:value="task.percentDone * 100" max="100">
|
||||
{{ task.percentDone * 100 }}%
|
||||
</progress>
|
||||
:value="task.percentDone * 100"
|
||||
is-small
|
||||
/>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
v-if="!showList && currentList.id !== task.listId && list !== null"
|
||||
class="task-list"
|
||||
v-if="!showList && currentList.id !== task.listId && $store.getters['lists/getListById'](task.listId) !== null"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
|
||||
{{ $store.getters['lists/getListById'](task.listId).title }}
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: list.title})"
|
||||
>
|
||||
{{ list.title }}
|
||||
</router-link>
|
||||
|
||||
<a
|
||||
class="favorite"
|
||||
:class="{'is-favorite': task.isFavorite}"
|
||||
@click="toggleFavorite"
|
||||
class="favorite">
|
||||
<icon icon="star" v-if="task.isFavorite"/>
|
||||
<icon :icon="['far', 'star']" v-else/>
|
||||
>
|
||||
<icon :icon="task.isFavorite ? 'star' : ['far', 'star']" />
|
||||
</a>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TaskModel from '../../../models/task'
|
||||
<script lang="ts" setup>
|
||||
import {reactive, computed, watch, defineProps, defineEmits, PropType } from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useStore} from 'vuex'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
import PriorityLabel from './priorityLabel'
|
||||
import TaskService from '../../../services/task'
|
||||
import Labels from './labels'
|
||||
import User from '../../misc/user'
|
||||
import Fancycheckbox from '../../input/fancycheckbox'
|
||||
import DeferTask from './defer-task'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import Labels from './labels'
|
||||
import ChecklistSummary from './checklist-summary'
|
||||
|
||||
export default {
|
||||
name: 'singleTaskInList',
|
||||
data() {
|
||||
return {
|
||||
taskService: new TaskService(),
|
||||
task: new TaskModel(),
|
||||
showDefer: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ChecklistSummary,
|
||||
DeferTask,
|
||||
Fancycheckbox,
|
||||
User,
|
||||
Labels,
|
||||
PriorityLabel,
|
||||
},
|
||||
props: {
|
||||
import User from '@/components/misc/user'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox'
|
||||
import Popup from '@/components/misc/popup.vue'
|
||||
import ProgressBar from '@/components/misc/ProgressBar'
|
||||
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import {success} from '@/message'
|
||||
|
||||
const props = defineProps({
|
||||
theTask: {
|
||||
type: TaskModel,
|
||||
type: Object as PropType<TaskModel>,
|
||||
required: true,
|
||||
},
|
||||
isArchived: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
taskDetailRoute: {
|
||||
type: String,
|
||||
default: 'task.list.detail',
|
||||
},
|
||||
showList: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -141,144 +166,177 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['task-updated'],
|
||||
watch: {
|
||||
theTask(newVal) {
|
||||
this.task = newVal
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.task = this.theTask
|
||||
document.addEventListener('click', this.hideDeferDueDatePopup)
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.hideDeferDueDatePopup)
|
||||
},
|
||||
computed: {
|
||||
listColor() {
|
||||
const list = this.$store.getters['lists/getListById'](this.task.listId)
|
||||
return list !== null ? list.hexColor : ''
|
||||
},
|
||||
currentList() {
|
||||
return typeof this.$store.state.currentList === 'undefined' ? {
|
||||
})
|
||||
|
||||
const emit = defineEmits(['task-updated'])
|
||||
|
||||
const taskService = reactive(new TaskService())
|
||||
const task = reactive(new TaskModel())
|
||||
|
||||
watch(props.theTask, (newVal) => {
|
||||
Object.assign(task, newVal)
|
||||
}, { immediate: true },
|
||||
)
|
||||
|
||||
const store = useStore()
|
||||
const currentList = computed(() => {
|
||||
return typeof store.state.currentList === 'undefined' ? {
|
||||
id: 0,
|
||||
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) {
|
||||
const updateFunc = async () => {
|
||||
const task = await this.taskService.update(this.task)
|
||||
if (this.task.done) {
|
||||
} : store.state.currentList
|
||||
})
|
||||
|
||||
const list = computed(() => store.getters['lists/getListById'](task.listId))
|
||||
const listColor = computed(() => list.value?.hexColor || '')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
async function markAsDone(checked: boolean) {
|
||||
async function updateFunc() {
|
||||
const newTask = await taskService.update(task)
|
||||
if (task.done) {
|
||||
playPop()
|
||||
}
|
||||
this.task = task
|
||||
this.$emit('task-updated', task)
|
||||
this.$message.success({
|
||||
message: this.task.done ?
|
||||
this.$t('task.doneSuccess') :
|
||||
this.$t('task.undoneSuccess'),
|
||||
Object.assign(task, newTask)
|
||||
emit('task-updated', task)
|
||||
success({
|
||||
message: task.done ?
|
||||
t('task.doneSuccess') :
|
||||
t('task.undoneSuccess'),
|
||||
}, [{
|
||||
title: 'Undo',
|
||||
callback() {
|
||||
this.task.done = !this.task.done
|
||||
this.markAsDone(!checked)
|
||||
task.done = !task.done
|
||||
markAsDone(!checked)
|
||||
},
|
||||
}])
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
|
||||
// Delay it to show the animation when marking a task as done
|
||||
setTimeout(updateFunc, 300)
|
||||
} else {
|
||||
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
// Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
await updateFunc()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async toggleFavorite() {
|
||||
this.task.isFavorite = !this.task.isFavorite
|
||||
this.task = await this.taskService.update(this.task)
|
||||
this.$emit('task-updated', this.task)
|
||||
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
|
||||
},
|
||||
hideDeferDueDatePopup(e) {
|
||||
if (!this.showDefer) {
|
||||
return
|
||||
}
|
||||
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
|
||||
this.showDefer = false
|
||||
})
|
||||
},
|
||||
},
|
||||
async function toggleFavorite() {
|
||||
task.isFavorite = !task.isFavorite
|
||||
const newTask = await taskService.update(task)
|
||||
Object.assign(task, newTask)
|
||||
emit('task-updated', task)
|
||||
store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$line-height: 2.5;
|
||||
.task {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: .4rem;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"checkbox color-bubble list1 text list2";
|
||||
grid-template-columns: min-content max-content fit-content(10ch) minmax(0, 3fr) fit-content(10ch);
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: min-content;
|
||||
grid-template-rows: 1fr;
|
||||
place-items: center start;
|
||||
transition: background-color $transition;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: $radius;
|
||||
border: 2px solid transparent;
|
||||
line-height: $line-height;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--grey-100);
|
||||
}
|
||||
|
||||
.tasktext,
|
||||
&.tasktext {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
flex: 1 0 50%;
|
||||
> * {
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.overdue {
|
||||
color: var(--danger);
|
||||
> * + * {
|
||||
// gap does not work since we have collapsed tracks
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
width: auto;
|
||||
color: var(--grey-400);
|
||||
font-size: .9rem;
|
||||
white-space: nowrap;
|
||||
.task.loader-container.is-loading::after {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
transform: translate(-50%, -50%);
|
||||
border-left-color: var(--grey-300);
|
||||
border-bottom-color: var(--grey-300);
|
||||
}
|
||||
|
||||
.fancycheckbox {
|
||||
grid-area: checkbox;
|
||||
align-self: stretch;
|
||||
padding-left: .25rem;
|
||||
padding-right: .5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.color-bubble {
|
||||
grid-area: color-bubble;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex: 0 0 10px;
|
||||
}
|
||||
|
||||
@mixin overflow-text() {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
grid-area: list1;
|
||||
max-width: 100%;
|
||||
min-width: 0; // this is needed for the overflow text
|
||||
@include overflow-text();
|
||||
font-size: .9rem;
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.tasktext {
|
||||
grid-area: text;
|
||||
max-width: 100%;
|
||||
min-width: 0; // this is needed for the overflow text
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tasktext.done {
|
||||
text-decoration: line-through;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
.parent-tasks {
|
||||
color: var(--grey-500);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
flex: 1 1 0;
|
||||
@include overflow-text();
|
||||
min-width: 6vw;
|
||||
}
|
||||
|
||||
.overdue {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
vertical-align: bottom;
|
||||
margin-left: 5px;
|
||||
height: 27px;
|
||||
width: 27px;
|
||||
}
|
||||
|
||||
.list-task-icon {
|
||||
margin-left: 6px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text);
|
||||
transition: color ease $transition-duration;
|
||||
|
@ -304,62 +362,17 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
&:hover .favorite {
|
||||
.task:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.handle {
|
||||
:deep(.handle) {
|
||||
opacity: 0;
|
||||
transition: opacity $transition;
|
||||
margin-right: .25rem;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&:hover .handle {
|
||||
.task:hover :deep(.handle) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.fancycheckbox) {
|
||||
height: 18px;
|
||||
padding-top: 0;
|
||||
padding-right: .5rem;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tasktext.done {
|
||||
text-decoration: line-through;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
span.parent-tasks {
|
||||
color: var(--grey-500);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.settings {
|
||||
float: right;
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.loader-container.is-loading:after {
|
||||
top: calc(50% - 1rem);
|
||||
left: calc(50% - 1rem);
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-left-color: var(--grey-300);
|
||||
border-bottom-color: var(--grey-300);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -15,11 +15,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
// overwrite bulma
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.noborder {
|
||||
margin: 1rem -0.5rem;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
@import "bulma-css-variables/sass/elements/icon";
|
||||
@import "bulma-css-variables/sass/elements/image";
|
||||
//@import "bulma-css-variables/sass/elements/notification"; // not used
|
||||
@import "bulma-css-variables/sass/elements/progress";
|
||||
// @import "bulma-css-variables/sass/elements/progress"; // not used
|
||||
@import "bulma-css-variables/sass/elements/table";
|
||||
@import "bulma-css-variables/sass/elements/tag";
|
||||
@import "bulma-css-variables/sass/elements/title";
|
||||
|
|
|
@ -38,20 +38,12 @@ h6 {
|
|||
// - kanban-card.vue
|
||||
// - singleTaskInList.vue
|
||||
.progress {
|
||||
border-radius: $radius-large;
|
||||
width: 50px;
|
||||
margin: 0 0.5rem 0 0;
|
||||
flex: 3 1 auto;
|
||||
|
||||
&::-moz-progress-bar,
|
||||
&::-webkit-progress-value {
|
||||
background: var(--grey-500);
|
||||
}
|
||||
// margin: 0 0.5rem 0 0;
|
||||
// flex: 3 1 auto;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0.5rem 0 0 0;
|
||||
order: 1;
|
||||
width: 100%;
|
||||
// margin: 0.5rem 0 0 0;
|
||||
// order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +128,6 @@ button.table {
|
|||
.color-bubble {
|
||||
display: inline-block;
|
||||
border-radius: 100%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-content {
|
||||
|
|
Reference in New Issue
Block a user