feat: Kanban Buckets
# Conflicts: # src/features/kanban/TaskCard.vue # src/helpers/saveCollapsedBucketState.ts
This commit is contained in:
parent
bef0142db5
commit
193212ad05
|
@ -3,15 +3,15 @@ import {ref, computed, nextTick} from 'vue'
|
||||||
import {useStore} from 'vuex'
|
import {useStore} from 'vuex'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
import { useVModels } from '@vueuse/core'
|
||||||
|
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import BucketModel from '@/models/bucket'
|
import BucketModel from '@/models/bucket'
|
||||||
import TaskModel from '@/models/task'
|
import TaskModel from '@/models/task'
|
||||||
|
|
||||||
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
|
||||||
|
|
||||||
import Dropdown from '@/components/misc/dropdown.vue'
|
import Dropdown from '@/components/misc/dropdown.vue'
|
||||||
import KanbanCard from '@/components/tasks/partials/KanbanCard.vue'
|
import KanbanCard from '@/feature/kanban/KanbanCard.vue'
|
||||||
|
import KanbanTaskNew from '@/feature/kanban/KanbanTaskNew.vue'
|
||||||
|
|
||||||
const MIN_SCROLL_HEIGHT_PERCENT = 0.25
|
const MIN_SCROLL_HEIGHT_PERCENT = 0.25
|
||||||
|
|
||||||
|
@ -25,33 +25,19 @@ const props = defineProps<{
|
||||||
params: Object
|
params: Object
|
||||||
shouldAcceptDrop: boolean
|
shouldAcceptDrop: boolean
|
||||||
isDraggingTask: boolean
|
isDraggingTask: boolean
|
||||||
|
tasksUpdating: { [taskId: TaskModel['id']]: boolean },
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits(['openDeleteBucketModal', 'dragstart', 'updateTaskPosition'])
|
const emit = defineEmits([
|
||||||
|
'openDeleteBucketModal',
|
||||||
|
'dragstart',
|
||||||
|
'dragend',
|
||||||
|
'update:isCollapsed',
|
||||||
|
])
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const loading = computed(() =>
|
const { isCollapsed } = useVModels(props, emit)
|
||||||
store.state.loading && store.state.loadingModule === 'kanban',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const isCollapsed = ref(false)
|
|
||||||
function collapseBucket() {
|
|
||||||
isCollapsed.value = true
|
|
||||||
// TODO:
|
|
||||||
// saveCollapsedBucketState(this.listId, this.collapsedBuckets)
|
|
||||||
}
|
|
||||||
function unCollapseBucket() {
|
|
||||||
if (!isCollapsed.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isCollapsed.value = false
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// saveCollapsedBucketState(this.listId, this.collapsedBuckets)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bucketTitleEditable = ref(true)
|
const bucketTitleEditable = ref(true)
|
||||||
async function saveBucketTitle(bucketTitle: string) {
|
async function saveBucketTitle(bucketTitle: string) {
|
||||||
|
@ -138,30 +124,7 @@ function scrollTaskContainerToBottom() {
|
||||||
taskContainer.value.scrollTop = taskContainer.value.scrollHeight
|
taskContainer.value.scrollTop = taskContainer.value.scrollHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNewTaskInput = ref(false)
|
|
||||||
function toggleShowNewTaskInput() {
|
|
||||||
hasNewTaskInput.value = !hasNewTaskInput.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTaskText = ref('')
|
|
||||||
const newTaskError = ref(false)
|
|
||||||
|
|
||||||
async function addTaskToBucket() {
|
|
||||||
if (newTaskText.value === '') {
|
|
||||||
newTaskError.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newTaskError.value = false
|
|
||||||
|
|
||||||
const task = await store.dispatch('tasks/createNewTask', {
|
|
||||||
title: newTaskText.value,
|
|
||||||
bucketId: props.bucket.id,
|
|
||||||
listId: props.bucket.listId,
|
|
||||||
})
|
|
||||||
newTaskText.value = ''
|
|
||||||
store.commit('kanban/addTaskToBucket', task)
|
|
||||||
scrollTaskContainerToBottom()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -169,7 +132,7 @@ async function addTaskToBucket() {
|
||||||
class="bucket"
|
class="bucket"
|
||||||
:class="{'is-collapsed': isCollapsed}"
|
:class="{'is-collapsed': isCollapsed}"
|
||||||
>
|
>
|
||||||
<div class="bucket-header" @click="unCollapseBucket()">
|
<div class="bucket-header" @click="isCollapsed = false">
|
||||||
<span
|
<span
|
||||||
v-if="bucket.isDoneBucket"
|
v-if="bucket.isDoneBucket"
|
||||||
class="icon is-small has-text-success mr-2"
|
class="icon is-small has-text-success mr-2"
|
||||||
|
@ -249,7 +212,7 @@ async function addTaskToBucket() {
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click.stop="collapseBucket()"
|
@click.stop="isCollapsed = true"
|
||||||
>
|
>
|
||||||
{{ $t('list.kanban.collapse') }}
|
{{ $t('list.kanban.collapse') }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -272,7 +235,7 @@ async function addTaskToBucket() {
|
||||||
:modelValue="bucket.tasks"
|
:modelValue="bucket.tasks"
|
||||||
@update:modelValue="updateTasks"
|
@update:modelValue="updateTasks"
|
||||||
@start="emit('dragstart', bucket.id)"
|
@start="emit('dragstart', bucket.id)"
|
||||||
@end="emit('updateTaskPosition', $event)"
|
@end="emit('dragend', $event)"
|
||||||
:group="{name: 'tasks', put: shouldAcceptDrop}"
|
:group="{name: 'tasks', put: shouldAcceptDrop}"
|
||||||
:disabled="!canWrite"
|
:disabled="!canWrite"
|
||||||
:data-bucket-index="bucketIndex"
|
:data-bucket-index="bucketIndex"
|
||||||
|
@ -280,43 +243,24 @@ async function addTaskToBucket() {
|
||||||
:item-key="(task: TaskModel) => `bucket${bucket.id}-task${task.id}`"
|
:item-key="(task: TaskModel) => `bucket${bucket.id}-task${task.id}`"
|
||||||
:component-data="taskDraggableTaskComponentData"
|
:component-data="taskDraggableTaskComponentData"
|
||||||
>
|
>
|
||||||
<template #footer>
|
<template #item="{element: task}">
|
||||||
<div class="bucket-footer" v-if="canWrite">
|
<div class="task-item">
|
||||||
<div class="field" v-if="hasNewTaskInput">
|
<kanban-card
|
||||||
<div class="control" :class="{'is-loading': loading}">
|
class="kanban-card"
|
||||||
<input
|
:task="task"
|
||||||
class="input"
|
:loading="tasksUpdating[task.id]"
|
||||||
:disabled="loading || undefined"
|
/>
|
||||||
@focusout="toggleShowNewTaskInput()"
|
|
||||||
@keyup.esc="toggleShowNewTaskInput()"
|
|
||||||
@keyup.enter="addTaskToBucket()"
|
|
||||||
:placeholder="$t('list.kanban.addTaskPlaceholder')"
|
|
||||||
type="text"
|
|
||||||
v-focus.always
|
|
||||||
v-model="newTaskText"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="help is-danger" v-if="newTaskError && newTaskText === ''">
|
|
||||||
{{ $t('list.create.addTitleRequired') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<x-button
|
|
||||||
@click="toggleShowNewTaskInput()"
|
|
||||||
class="is-fullwidth has-text-centered"
|
|
||||||
:shadow="false"
|
|
||||||
v-else
|
|
||||||
icon="plus"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{{ bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask') }}
|
|
||||||
</x-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #item="{element: task}">
|
<template #footer>
|
||||||
<div class="task-item">
|
<KanbanTaskNew
|
||||||
<kanban-card class="kanban-card" :task="task"/>
|
v-if="canWrite"
|
||||||
</div>
|
:bucketId="bucket.id"
|
||||||
|
:listId="bucket.listId"
|
||||||
|
:bucketIsEmpty="bucket.tasks.length === 0"
|
||||||
|
@scrollTaskContainerToBottom="scrollTaskContainerToBottom"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
</div>
|
</div>
|
||||||
|
@ -331,52 +275,54 @@ async function addTaskToBucket() {
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.tasks {
|
.is-collapsed {
|
||||||
overflow: hidden auto;
|
align-self: flex-start;
|
||||||
height: 100%;
|
transform: rotate(90deg) translateY(-100%);
|
||||||
}
|
transform-origin: top left;
|
||||||
|
// Using negative margins instead of translateY here to make all other buckets fill the empty space
|
||||||
|
margin-right: calc((var(--bucket-width) - var(--bucket-header-height) - var(--bucket-right-margin)) * -1);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.task-item {
|
.tasks, .bucket-footer {
|
||||||
background-color: var(--grey-100);
|
display: none;
|
||||||
padding: .25rem .5rem;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
padding-top: .5rem;
|
|
||||||
}
|
|
||||||
&:last-of-type {
|
|
||||||
padding-bottom: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-move {
|
|
||||||
transition: transform 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.dropdown-item {
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-collapsed {
|
|
||||||
align-self: flex-start;
|
|
||||||
transform: rotate(90deg) translateY(-100%);
|
|
||||||
transform-origin: top left;
|
|
||||||
// Using negative margins instead of translateY here to make all other buckets fill the empty space
|
|
||||||
margin-right: calc((var(--bucket-width) - var(--bucket-header-height) - var(--bucket-right-margin)) * -1);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.tasks, .bucket-footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tasks {
|
||||||
|
overflow: hidden auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
padding-top: .5rem;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-move {
|
||||||
|
transition: transform 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.dropdown-item {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.bucket-header {
|
.bucket-header {
|
||||||
background-color: var(--grey-100);
|
background-color: var(--grey-100);
|
||||||
height: min-content;
|
height: min-content;
|
||||||
|
@ -385,22 +331,22 @@ async function addTaskToBucket() {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
height: var(--bucket-header-height);
|
height: var(--bucket-header-height);
|
||||||
|
}
|
||||||
|
|
||||||
.limit {
|
.limit {
|
||||||
padding: 0 .5rem;
|
padding: 0 .5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
&.is-max {
|
&.is-max {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title.input {
|
.title.input {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: .4rem .5rem;
|
padding: .4rem .5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.dropdown-trigger) {
|
:deep(.dropdown-trigger) {
|
||||||
|
@ -412,18 +358,6 @@ async function addTaskToBucket() {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
padding: .5rem;
|
|
||||||
background-color: var(--grey-100);
|
|
||||||
border-bottom-left-radius: $radius;
|
|
||||||
border-bottom-right-radius: $radius;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.move-card-move {
|
.move-card-move {
|
73
src/features/kanban/BucketNew.vue
Normal file
73
src/features/kanban/BucketNew.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="new-bucket">
|
||||||
|
<input
|
||||||
|
v-if="hasNewBucketInput"
|
||||||
|
:class="{'is-loading': loading}"
|
||||||
|
:disabled="loading || undefined"
|
||||||
|
@blur="hasNewBucketInput = false"
|
||||||
|
@keyup.enter="createNewBucket"
|
||||||
|
@keyup.esc="($event.target as HTMLInputElement).blur()"
|
||||||
|
class="input"
|
||||||
|
:placeholder="$t('list.kanban.addBucketPlaceholder')"
|
||||||
|
type="text"
|
||||||
|
v-focus.always
|
||||||
|
v-model="newBucketTitle"
|
||||||
|
/>
|
||||||
|
<x-button
|
||||||
|
v-else
|
||||||
|
@click="hasNewBucketInput = true"
|
||||||
|
:shadow="false"
|
||||||
|
class="is-transparent is-fullwidth has-text-centered"
|
||||||
|
variant="secondary"
|
||||||
|
icon="plus"
|
||||||
|
>
|
||||||
|
{{ $t('list.kanban.addBucket') }}
|
||||||
|
</x-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
import BucketModel from '@/models/bucket'
|
||||||
|
import {useStore} from 'vuex'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
listId: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const loading = computed(() => store.state.loading && store.state.loadingModule === 'kanban')
|
||||||
|
|
||||||
|
const hasNewBucketInput = ref(false)
|
||||||
|
|
||||||
|
const newBucketTitle = ref('')
|
||||||
|
async function createNewBucket() {
|
||||||
|
if (newBucketTitle.value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.dispatch('kanban/createBucket', new BucketModel({
|
||||||
|
title: newBucketTitle.value,
|
||||||
|
listId: props.listId,
|
||||||
|
}))
|
||||||
|
newBucketTitle.value = ''
|
||||||
|
hasNewBucketInput.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.new-bucket {
|
||||||
|
// Because of reasons, this button ignores the margin we gave it to the right.
|
||||||
|
// To make it still look like it has some, we modify the container to have a padding of 1rem,
|
||||||
|
// which is the same as the margin it should have. Then we make the container itself bigger
|
||||||
|
// to hide the fact we just made the button smaller.
|
||||||
|
min-width: calc(var(--bucket-width) + 1rem);
|
||||||
|
background: transparent;
|
||||||
|
padding-right: 1rem;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: var(--grey-100);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -81,7 +81,7 @@ import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||||
import User from '@/components/misc/user.vue'
|
import User from '@/components/misc/user.vue'
|
||||||
import Done from '@/components/misc/Done.vue'
|
import Done from '@/components/misc/Done.vue'
|
||||||
import Labels from '@/components/tasks/partials/labels.vue'
|
import Labels from '@/components/tasks/partials/labels.vue'
|
||||||
import ChecklistSummary from './checklist-summary.vue'
|
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
|
||||||
|
|
||||||
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
|
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
94
src/features/kanban/TaskNew.vue
Normal file
94
src/features/kanban/TaskNew.vue
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div class="bucket-footer">
|
||||||
|
<div class="field" v-if="hasNewTaskInput">
|
||||||
|
<div class="control" :class="{'is-loading': loading}">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
:disabled="loading || undefined"
|
||||||
|
@focusout="toggleShowNewTaskInput()"
|
||||||
|
@keyup.esc="toggleShowNewTaskInput()"
|
||||||
|
@keyup.enter="addTaskToBucket()"
|
||||||
|
:placeholder="$t('list.kanban.addTaskPlaceholder')"
|
||||||
|
type="text"
|
||||||
|
v-focus.always
|
||||||
|
v-model="newTaskText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" v-if="newTaskError && newTaskText === ''">
|
||||||
|
{{ $t('list.create.addTitleRequired') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<x-button
|
||||||
|
@click="toggleShowNewTaskInput()"
|
||||||
|
class="is-fullwidth has-text-centered"
|
||||||
|
:shadow="false"
|
||||||
|
v-else
|
||||||
|
icon="plus"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{{ bucketIsEmpty ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask') }}
|
||||||
|
</x-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, computed} from 'vue'
|
||||||
|
import {useStore} from 'vuex'
|
||||||
|
|
||||||
|
import BucketModel from '@/models/bucket'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
bucketId: BucketModel['id']
|
||||||
|
listId: BucketModel['listId']
|
||||||
|
bucketIsEmpty: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['scrollTaskContainerToBottom'])
|
||||||
|
|
||||||
|
const loading = computed(() =>
|
||||||
|
store.state.loading && store.state.loadingModule === 'kanban',
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasNewTaskInput = ref(false)
|
||||||
|
function toggleShowNewTaskInput() {
|
||||||
|
hasNewTaskInput.value = !hasNewTaskInput.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTaskText = ref('')
|
||||||
|
const newTaskError = ref(false)
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
async function addTaskToBucket() {
|
||||||
|
if (newTaskText.value === '') {
|
||||||
|
newTaskError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newTaskError.value = false
|
||||||
|
|
||||||
|
const task = await store.dispatch('tasks/createNewTask', {
|
||||||
|
title: newTaskText.value,
|
||||||
|
bucketId: props.bucketId,
|
||||||
|
listId: props.listId,
|
||||||
|
})
|
||||||
|
newTaskText.value = ''
|
||||||
|
store.commit('kanban/addTaskToBucket', task)
|
||||||
|
emit('scrollTaskContainerToBottom')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.bucket-footer {
|
||||||
|
padding: .5rem;
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
border-bottom-left-radius: $radius;
|
||||||
|
border-bottom-right-radius: $radius;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,19 +2,17 @@ import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
const key = 'collapsedBuckets'
|
const key = 'collapsedBuckets'
|
||||||
|
|
||||||
const getAllState = () => {
|
function getAllState() {
|
||||||
const saved = localStorage.getItem(key)
|
const saved = localStorage.getItem(key)
|
||||||
if (saved === null) {
|
return saved === null
|
||||||
return {}
|
? {}
|
||||||
}
|
: JSON.parse(saved)
|
||||||
|
|
||||||
return JSON.parse(saved)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveCollapsedBucketState = (
|
export function saveCollapsedBucketState(
|
||||||
listId: IList['id'],
|
listId: IList['id'],
|
||||||
collapsedBuckets,
|
collapsedBuckets: any,
|
||||||
) => {
|
) {
|
||||||
const state = getAllState()
|
const state = getAllState()
|
||||||
state[listId] = collapsedBuckets
|
state[listId] = collapsedBuckets
|
||||||
for (const bucketId in state[listId]) {
|
for (const bucketId in state[listId]) {
|
||||||
|
@ -25,11 +23,7 @@ export const saveCollapsedBucketState = (
|
||||||
localStorage.setItem(key, JSON.stringify(state))
|
localStorage.setItem(key, JSON.stringify(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCollapsedBucketState = (listId : IList['id']) => {
|
export function getCollapsedBucketState(listId: IList['id']) {
|
||||||
const state = getAllState()
|
const state = getAllState()
|
||||||
if (typeof state[listId] !== 'undefined') {
|
return state[listId] ?? {}
|
||||||
return state[listId]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,8 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||||
this.updated = new Date(this.updated)
|
this.updated = new Date(this.updated)
|
||||||
|
|
||||||
this.listId = Number(this.listId)
|
this.listId = Number(this.listId)
|
||||||
|
|
||||||
|
this.kanbanPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
getTextIdentifier() {
|
getTextIdentifier() {
|
||||||
|
|
|
@ -1,95 +1,71 @@
|
||||||
<template>
|
<template>
|
||||||
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
|
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="filter-container" v-if="isSavedFilter">
|
<div class="filter-container" v-if="!isSavedFilter">
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<filter-popup
|
<filter-popup v-model="params" />
|
||||||
v-model="params"
|
|
||||||
@update:modelValue="loadBuckets"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="kanban-view">
|
<div
|
||||||
<div
|
:class="{ 'is-loading': loading && !oneTaskUpdating}"
|
||||||
:class="{ 'is-loading': loading && !oneTaskUpdating}"
|
class="kanban kanban-bucket-container loader-container"
|
||||||
class="kanban kanban-bucket-container loader-container"
|
>
|
||||||
|
<draggable
|
||||||
|
v-bind="DRAG_OPTIONS"
|
||||||
|
:modelValue="buckets"
|
||||||
|
@update:modelValue="updateBuckets"
|
||||||
|
@start="() => isDraggingBucket = true"
|
||||||
|
@end="updateBucketPosition"
|
||||||
|
group="buckets"
|
||||||
|
:disabled="!canWrite"
|
||||||
|
tag="transition-group"
|
||||||
|
:item-key="(id: number) => `bucket${id}`"
|
||||||
|
:component-data="bucketDraggableComponentData"
|
||||||
>
|
>
|
||||||
<draggable
|
<template #item="{element: bucket, index: bucketIndex }">
|
||||||
v-bind="DRAG_OPTIONS"
|
<Bucket
|
||||||
:modelValue="buckets"
|
class="bucket"
|
||||||
@update:modelValue="updateBuckets"
|
:bucket-index="bucketIndex"
|
||||||
@end="updateBucketPosition"
|
:is-collapsed="collapsedBuckets[bucket.id]"
|
||||||
@start="() => isDraggingBucket = true"
|
:can-write="canWrite"
|
||||||
group="buckets"
|
:bucket="bucket"
|
||||||
:disabled="!canWrite"
|
:isOnlyBucketLeft="buckets.length <= 1"
|
||||||
tag="transition-group"
|
:drag-options="DRAG_OPTIONS"
|
||||||
:item-key="(id: number) => `bucket${id}`"
|
:params="params"
|
||||||
:component-data="bucketDraggableComponentData"
|
:should-accept-drop="shouldAcceptDrop(bucket)"
|
||||||
>
|
:isDraggingTask="isDraggingTask"
|
||||||
<template #item="{element: bucket, index: bucketIndex }">
|
:taskUpdating="tasksUpdating"
|
||||||
<KanbanBucket
|
@dragstart="dragstart"
|
||||||
class="bucket"
|
@dragend="updateTaskPosition"
|
||||||
:bucket-index="bucketIndex"
|
@openDeleteBucketModal="openDeleteBucketModal"
|
||||||
:is-collapsed="collapsedBuckets[bucket.id]"
|
|
||||||
:can-write="canWrite"
|
|
||||||
:bucket="bucket"
|
|
||||||
:isOnlyBucketLeft="buckets.length <= 1"
|
|
||||||
:drag-options="DRAG_OPTIONS"
|
|
||||||
:params="params"
|
|
||||||
:should-accept-drop="shouldAcceptDrop(bucket)"
|
|
||||||
:isDraggingTask="isDraggingTask"
|
|
||||||
@dragstart="dragstart"
|
|
||||||
@updateTaskPosition="updateTaskPosition"
|
|
||||||
@openDeleteBucketModal="openDeleteBucketModal"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
|
|
||||||
<div class="bucket new-bucket" v-if="canWrite && !loading && buckets.length > 0">
|
|
||||||
<input
|
|
||||||
v-if="hasNewBucketInput"
|
|
||||||
:class="{'is-loading': loading}"
|
|
||||||
:disabled="loading || null"
|
|
||||||
@blur="hasNewBucketInput = false"
|
|
||||||
@keyup.enter="createNewBucket"
|
|
||||||
@keyup.esc="($event.target as HTMLInputElement).blur()"
|
|
||||||
class="input"
|
|
||||||
:placeholder="$t('list.kanban.addBucketPlaceholder')"
|
|
||||||
type="text"
|
|
||||||
v-focus.always
|
|
||||||
v-model="newBucketTitle"
|
|
||||||
/>
|
/>
|
||||||
<x-button
|
</template>
|
||||||
v-else
|
</draggable>
|
||||||
@click="hasNewBucketInput = true"
|
|
||||||
:shadow="false"
|
|
||||||
class="is-transparent is-fullwidth has-text-centered"
|
|
||||||
variant="secondary"
|
|
||||||
icon="plus"
|
|
||||||
>
|
|
||||||
{{ $t('list.kanban.addBucket') }}
|
|
||||||
</x-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="modal">
|
<BucketNew
|
||||||
<modal
|
v-if="canWrite && !loading && buckets.length > 0"
|
||||||
v-if="hasBucketDeleteModal"
|
class="bucket"
|
||||||
@close="hasBucketDeleteModal = false"
|
:listId="listId"
|
||||||
@submit="deleteBucket()"
|
/>
|
||||||
>
|
|
||||||
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
|
||||||
|
|
||||||
<template #text>
|
|
||||||
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
|
||||||
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<transition name="modal">
|
||||||
|
<modal
|
||||||
|
v-if="hasBucketDeleteModal"
|
||||||
|
@close="hasBucketDeleteModal = false"
|
||||||
|
@submit="deleteBucket()"
|
||||||
|
>
|
||||||
|
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
||||||
|
|
||||||
|
<template #text>
|
||||||
|
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
||||||
|
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
</ListWrapper>
|
</ListWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
@ -101,16 +77,19 @@ import cloneDeep from 'lodash.clonedeep'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {SortableEvent} from 'sortablejs'
|
import {SortableEvent} from 'sortablejs'
|
||||||
|
|
||||||
import { success } from '@/message'
|
import {success} from '@/message'
|
||||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||||
|
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
||||||
|
|
||||||
|
import ListModel from '@/models/list'
|
||||||
import BucketModel from '@/models/bucket'
|
import BucketModel from '@/models/bucket'
|
||||||
import TaskModel from '@/models/task'
|
import TaskModel from '@/models/task'
|
||||||
import Rights from '@/models/constants/rights.json'
|
import Rights from '@/models/constants/rights.json'
|
||||||
|
|
||||||
import ListWrapper from './ListWrapper.vue'
|
import ListWrapper from './ListWrapper.vue'
|
||||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||||
import KanbanBucket from '@/components/tasks/partials/KanbanBucket.vue'
|
import Bucket from '@/features/kanban/Bucket.vue'
|
||||||
|
import BucketNew from '@/features/kanban/BucketNew.vue'
|
||||||
|
|
||||||
const DRAG_OPTIONS = {
|
const DRAG_OPTIONS = {
|
||||||
// sortable options
|
// sortable options
|
||||||
|
@ -127,7 +106,16 @@ const props = defineProps<{
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
|
||||||
const collapsedBuckets = ref({})
|
/**
|
||||||
|
* Load collapsed Buckets
|
||||||
|
*/
|
||||||
|
|
||||||
|
const collapsedBuckets = ref<{ [listId: number]: any}>({})
|
||||||
|
watch(collapsedBuckets, (collapsedBuckets) => saveCollapsedBucketState(props.listId, collapsedBuckets))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Bucket Data
|
||||||
|
*/
|
||||||
|
|
||||||
const params = ref({
|
const params = ref({
|
||||||
filter_by: [],
|
filter_by: [],
|
||||||
|
@ -136,95 +124,53 @@ const params = ref({
|
||||||
filter_concat: 'and',
|
filter_concat: 'and',
|
||||||
})
|
})
|
||||||
|
|
||||||
interface LoadBucketsParams {
|
|
||||||
listId: number,
|
|
||||||
params: Object
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => ({
|
watch(() => ({
|
||||||
listId: props.listId,
|
listId: props.listId,
|
||||||
params: params.value,
|
params: params.value,
|
||||||
} as LoadBucketsParams), loadBuckets, {immediate: true})
|
}), ({listId, params}) => {
|
||||||
|
|
||||||
function loadBuckets({listId, params} : LoadBucketsParams) {
|
|
||||||
store.dispatch('kanban/loadBucketsForList', {listId, params})
|
store.dispatch('kanban/loadBucketsForList', {listId, params})
|
||||||
collapsedBuckets.value = getCollapsedBucketState(listId)
|
collapsedBuckets.value = getCollapsedBucketState(listId)
|
||||||
}
|
}, {immediate: true, deep: true})
|
||||||
|
|
||||||
const isSavedFilter = computed(() => list.value.isSavedFilter && !list.value.isSavedFilter())
|
const list = computed(() => store.state.currentList as ListModel)
|
||||||
|
|
||||||
const buckets = computed(() => store.state.kanban.buckets)
|
const buckets = computed({
|
||||||
|
get: () => store.state.kanban.buckets,
|
||||||
|
set(value: BucketModel[]) {
|
||||||
|
// (1) buckets get updated in store and tasks positions get invalidated
|
||||||
|
store.commit('kanban/setBuckets', value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const loading = computed(() => store.state.loading && store.state.loadingModule === 'kanban')
|
|
||||||
// const taskLoading = computed(() => store.state.loading && store.state.loadingModule === 'tasks') // unused ?
|
/**
|
||||||
|
* Template helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isSavedFilter = computed(() => list.value.isSavedFilter && list.value.isSavedFilter())
|
||||||
const canWrite = computed(() => store.state.currentList.maxRight > Rights.READ)
|
const canWrite = computed(() => store.state.currentList.maxRight > Rights.READ)
|
||||||
const list = computed(() => store.state.currentList)
|
const loading = computed(() => store.state.loading && store.state.loadingModule === 'kanban')
|
||||||
|
|
||||||
|
// FIXME: seems unused ?
|
||||||
|
// const taskLoading = computed(() => store.state.loading && store.state.loadingModule === 'tasks')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage list of updateing tasks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// FIXME: save globally if a specific task is updated.
|
||||||
// We're using this to show the loading animation only at the task when updating it
|
// We're using this to show the loading animation only at the task when updating it
|
||||||
const taskUpdating = ref<{ [id: TaskModel['id']]: boolean }>({})
|
const tasksUpdating = ref<{ [taskId: TaskModel['id']]: boolean }>({})
|
||||||
const oneTaskUpdating = ref(false)
|
|
||||||
|
|
||||||
const isDraggingTask = ref(false)
|
function setTaskUpdating(id: TaskModel['id'], isUpdating: boolean) {
|
||||||
|
tasksUpdating.value[id] = isUpdating
|
||||||
async function updateTaskPosition(e) {
|
|
||||||
isDraggingTask.value = false
|
|
||||||
|
|
||||||
// While we could just pass the bucket index in through the function call, this would not give us the
|
|
||||||
// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
|
|
||||||
// of the drop target works all the time.
|
|
||||||
const bucketIndex = parseInt(e.to.dataset.bucketIndex)
|
|
||||||
|
|
||||||
const newBucket = buckets.value[bucketIndex]
|
|
||||||
|
|
||||||
// HACK:
|
|
||||||
// this is a hacky workaround for a known problem of vue.draggable.next when using the footer slot
|
|
||||||
// the problem: https://github.com/SortableJS/vue.draggable.next/issues/108
|
|
||||||
// This hack doesn't remove the problem that the ghost item is still displayed below the footer
|
|
||||||
// It just makes releasing the item possible.
|
|
||||||
|
|
||||||
// The newIndex of the event doesn't count in the elements of the footer slot.
|
|
||||||
// This is why in case the length of the tasks is identical with the newIndex
|
|
||||||
// we have to remove 1 to get the correct index.
|
|
||||||
const newTaskIndex = newBucket.tasks.length === e.newIndex
|
|
||||||
? e.newIndex - 1
|
|
||||||
: e.newIndex
|
|
||||||
|
|
||||||
const taskBefore = newBucket.tasks[newTaskIndex - 1] ?? null
|
|
||||||
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
|
|
||||||
|
|
||||||
const task = {
|
|
||||||
// cloning the task to avoid vuex store mutations
|
|
||||||
...cloneDeep(newBucket.tasks[newTaskIndex]),
|
|
||||||
bucketId: newBucket.id,
|
|
||||||
kanbanPosition: calculateItemPosition(
|
|
||||||
taskBefore !== null ? taskBefore.kanbanPosition : null,
|
|
||||||
taskAfter !== null ? taskAfter.kanbanPosition : null,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await store.dispatch('tasks/update', task)
|
|
||||||
} finally {
|
|
||||||
taskUpdating.value[task.id] = false
|
|
||||||
oneTaskUpdating.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNewBucketInput = ref(false)
|
const oneTaskUpdating = computed(() => Object.values(tasksUpdating.value).some((isUpdating) => isUpdating))
|
||||||
|
|
||||||
const newBucketTitle = ref('')
|
/**
|
||||||
async function createNewBucket() {
|
* Delete Bucket
|
||||||
if (newBucketTitle.value === '') {
|
*/
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await store.dispatch('kanban/createBucket', new BucketModel({
|
|
||||||
title: newBucketTitle.value,
|
|
||||||
listId: props.listId,
|
|
||||||
}))
|
|
||||||
newBucketTitle.value = ''
|
|
||||||
hasNewBucketInput.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const bucketToDeleteId = ref<BucketModel['id']>(0)
|
const bucketToDeleteId = ref<BucketModel['id']>(0)
|
||||||
const hasBucketDeleteModal = ref(false)
|
const hasBucketDeleteModal = ref(false)
|
||||||
|
@ -253,6 +199,10 @@ async function deleteBucket() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move / Drag Bucket
|
||||||
|
*/
|
||||||
|
|
||||||
const isDraggingBucket = ref(false)
|
const isDraggingBucket = ref(false)
|
||||||
const bucketDraggableComponentData = computed(() => ({
|
const bucketDraggableComponentData = computed(() => ({
|
||||||
type: 'transition',
|
type: 'transition',
|
||||||
|
@ -264,18 +214,13 @@ const bucketDraggableComponentData = computed(() => ({
|
||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function updateBuckets(value: BucketModel[]) {
|
function updateBucketPosition({newDraggableIndex}: SortableEvent) {
|
||||||
// (1) buckets get updated in store and tasks positions get invalidated
|
|
||||||
store.commit('kanban/setBuckets', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBucketPosition(e) {
|
|
||||||
// (2) bucket positon is changed
|
// (2) bucket positon is changed
|
||||||
isDraggingBucket.value = false
|
isDraggingBucket.value = false
|
||||||
|
|
||||||
const bucket = buckets.value[e.newIndex]
|
const bucket = buckets.value[newDraggableIndex]
|
||||||
const bucketBefore = buckets.value[e.newIndex - 1] ?? null
|
const bucketBefore = buckets.value[newDraggableIndex - 1] ?? null
|
||||||
const bucketAfter = buckets.value[e.newIndex + 1] ?? null
|
const bucketAfter = buckets.value[newDraggableIndex + 1] ?? null
|
||||||
|
|
||||||
store.dispatch('kanban/updateBucket', {
|
store.dispatch('kanban/updateBucket', {
|
||||||
id: bucket.id,
|
id: bucket.id,
|
||||||
|
@ -286,11 +231,15 @@ function updateBucketPosition(e) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceBucketId = ref(0)
|
/**
|
||||||
|
* Move / Drag Task
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sourceBucketId = ref<number>()
|
||||||
|
|
||||||
function shouldAcceptDrop(bucket: BucketModel) {
|
function shouldAcceptDrop(bucket: BucketModel) {
|
||||||
return !isDraggingBucket.value && (
|
return !isDraggingBucket.value && (
|
||||||
// When dragging from a bucket who has its limit reached, dragging should still be possible
|
// It's always possible to drag a task inside the same a bucket
|
||||||
bucket.id === sourceBucketId.value ||
|
bucket.id === sourceBucketId.value ||
|
||||||
// If there is no limit set, dragging & dropping should always work
|
// If there is no limit set, dragging & dropping should always work
|
||||||
bucket.limit === 0 ||
|
bucket.limit === 0 ||
|
||||||
|
@ -299,10 +248,62 @@ function shouldAcceptDrop(bucket: BucketModel) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDraggingTask = ref(false)
|
||||||
|
|
||||||
function dragstart(bucketId: BucketModel['id']) {
|
function dragstart(bucketId: BucketModel['id']) {
|
||||||
isDraggingTask.value = true
|
isDraggingTask.value = true
|
||||||
sourceBucketId.value = bucketId
|
sourceBucketId.value = bucketId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function updateTaskPosition({newDraggableIndex, to}: SortableEvent) {
|
||||||
|
isDraggingTask.value = false
|
||||||
|
|
||||||
|
// FIXME: Cases
|
||||||
|
// e.to.dataset.bucketIndex === undefined
|
||||||
|
// e.newIndex === undefined
|
||||||
|
|
||||||
|
// While we could just pass the bucket index in through the function call, this would not give us the
|
||||||
|
// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
|
||||||
|
// of the drop target works all the time.
|
||||||
|
const bucketIndex = parseInt(to.dataset.bucketIndex)
|
||||||
|
|
||||||
|
const newBucket = buckets.value[bucketIndex]
|
||||||
|
|
||||||
|
// HACK:
|
||||||
|
// this is a hacky workaround for a known problem of vue.draggable.next when using the footer slot
|
||||||
|
// the problem: https://github.com/SortableJS/vue.draggable.next/issues/108
|
||||||
|
// This hack doesn't remove the problem that the ghost item is still displayed below the footer
|
||||||
|
// It just makes releasing the item possible.
|
||||||
|
|
||||||
|
// The newIndex of the event doesn't count in the elements of the footer slot.
|
||||||
|
// This is why in case the length of the tasks is identical with the newIndex
|
||||||
|
// we have to remove 1 to get the correct index.
|
||||||
|
// const newTaskIndex = newBucket.tasks.length === e.newIndex
|
||||||
|
// ? e.newIndex - 1
|
||||||
|
// : e.newIndex
|
||||||
|
|
||||||
|
const taskBefore = newBucket.tasks[newDraggableIndex - 1] ?? null
|
||||||
|
const taskAfter = newBucket.tasks[newDraggableIndex + 1] ?? null
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
// cloning the task to avoid vuex store mutations
|
||||||
|
...cloneDeep(newBucket.tasks[newDraggableIndex]),
|
||||||
|
bucketId: newBucket.id,
|
||||||
|
kanbanPosition: calculateItemPosition(
|
||||||
|
taskBefore !== null ? taskBefore.kanbanPosition : null,
|
||||||
|
taskAfter !== null ? taskAfter.kanbanPosition : null,
|
||||||
|
),
|
||||||
|
} as TaskModel
|
||||||
|
|
||||||
|
setTaskUpdating(task.id, false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('tasks/update', task)
|
||||||
|
} finally {
|
||||||
|
setTaskUpdating(task.id, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -312,28 +313,25 @@ function dragstart(bucketId: BucketModel['id']) {
|
||||||
.app-content.list\.kanban {
|
.app-content.list\.kanban {
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
|
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
|
||||||
$crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
|
// $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
|
||||||
$crazy-height-calculation-tasks: '#{$crazy-height-calculation} - 1rem - 2.5rem - 2rem - #{$button-height} - 1rem';
|
// $filter-container-height: '1rem - #{$switch-view-height}';
|
||||||
$filter-container-height: '1rem - #{$switch-view-height}';
|
|
||||||
|
|
||||||
.kanban {
|
.kanban {
|
||||||
--bucket-width: 300px;
|
--bucket-width: 300px;
|
||||||
--bucket-right-margin: 1rem;
|
--bucket-right-margin: 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
height: calc(#{$crazy-height-calculation});
|
// height: calc(#{$crazy-height-calculation});
|
||||||
margin: 0 -1.5rem;
|
margin: 0 -1.5rem;
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
// @media screen and (max-width: $tablet) {
|
||||||
height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
|
// height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
.kanban-bucket-container {
|
.kanban-bucket-container {
|
||||||
|
@ -341,7 +339,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket {
|
.bucket {
|
||||||
margin: 0 var(--bucket-right-margin) 0 0;
|
margin-right: var(--bucket-right-margin);
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
width: var(--bucket-width);
|
width: var(--bucket-width);
|
||||||
|
@ -367,22 +365,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-bucket {
|
|
||||||
// Because of reasons, this button ignores the margin we gave it to the right.
|
|
||||||
// To make it still look like it has some, we modify the container to have a padding of 1rem,
|
|
||||||
// which is the same as the margin it should have. Then we make the container itself bigger
|
|
||||||
// to hide the fact we just made the button smaller.
|
|
||||||
min-width: calc(var(--bucket-width) + 1rem);
|
|
||||||
background: transparent;
|
|
||||||
padding-right: 1rem;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background: var(--grey-100);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: This does not seem to work
|
|
||||||
.task-dragging {
|
.task-dragging {
|
||||||
transform: rotateZ(3deg);
|
transform: rotateZ(3deg);
|
||||||
transition: transform 0.18s ease;
|
transition: transform 0.18s ease;
|
||||||
|
|
Reference in New Issue
Block a user