Compare commits
10 Commits
main
...
feat-list-
| Author | SHA1 | Date |
|---|---|---|
|
|
3765f9877c | |
|
|
f4883ef7c4 | |
|
|
46676289e0 | |
|
|
b392d130eb | |
|
|
f09201ff3b | |
|
|
13b10255c6 | |
|
|
a32f759228 | |
|
|
939eba7b44 | |
|
|
a0cc76c4c8 | |
|
|
b18b859b71 |
|
|
@ -304,7 +304,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBucketStore} from '@/stores/buckets'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
|
|
@ -356,7 +356,7 @@ const MIN_SCROLL_HEIGHT_PERCENT = 0.25
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const bucketStore = useBucketStore()
|
||||
const taskStore = useTaskStore()
|
||||
const projectStore = useProjectStore()
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -480,8 +480,8 @@ function onHandleTouchMove(e: TouchEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
const buckets = computed(() => kanbanStore.buckets)
|
||||
const loading = computed(() => kanbanStore.isLoading)
|
||||
const buckets = computed(() => bucketStore.buckets)
|
||||
const loading = computed(() => bucketStore.isLoading)
|
||||
const projectIdWithFallback = computed<number>(() => project.value?.id || projectId.value)
|
||||
|
||||
const taskLoading = computed(() => taskStore.isLoading || taskPositionService.value.loading)
|
||||
|
|
@ -497,7 +497,7 @@ watch(
|
|||
return
|
||||
}
|
||||
collapsedBuckets.value = getCollapsedBucketState(projectId)
|
||||
kanbanStore.loadBucketsForProject(projectId, viewId, params)
|
||||
bucketStore.loadBucketsForProject(projectId, viewId, params)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
|
|
@ -520,7 +520,7 @@ function handleTaskContainerScroll(id: IBucket['id'], el: HTMLElement) {
|
|||
return
|
||||
}
|
||||
|
||||
kanbanStore.loadNextTasksForBucket(
|
||||
bucketStore.loadNextTasksForBucket(
|
||||
projectId.value,
|
||||
props.viewId,
|
||||
params.value,
|
||||
|
|
@ -529,13 +529,13 @@ function handleTaskContainerScroll(id: IBucket['id'], el: HTMLElement) {
|
|||
}
|
||||
|
||||
function updateTasks(bucketId: IBucket['id'], tasks: IBucket['tasks']) {
|
||||
const bucket = kanbanStore.getBucketById(bucketId)
|
||||
const bucket = bucketStore.getBucketById(bucketId)
|
||||
|
||||
if (bucket === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
kanbanStore.setBucketById({
|
||||
bucketStore.setBucketById({
|
||||
...bucket,
|
||||
tasks,
|
||||
})
|
||||
|
|
@ -546,7 +546,7 @@ async function updateTaskPosition(e) {
|
|||
|
||||
// Check if dropped on a sidebar project
|
||||
const {moved} = await handleTaskDropToProject(e, (task) => {
|
||||
kanbanStore.removeTaskInBucket(task)
|
||||
bucketStore.removeTaskInBucket(task)
|
||||
})
|
||||
|
||||
if (moved) {
|
||||
|
|
@ -596,11 +596,11 @@ async function updateTaskPosition(e) {
|
|||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
||||
newBucket.id !== oldBucket.id
|
||||
) {
|
||||
kanbanStore.setBucketById({
|
||||
bucketStore.setBucketById({
|
||||
...oldBucket,
|
||||
count: oldBucket.count - 1,
|
||||
})
|
||||
kanbanStore.setBucketById({
|
||||
bucketStore.setBucketById({
|
||||
...newBucket,
|
||||
count: newBucket.count + 1,
|
||||
})
|
||||
|
|
@ -626,13 +626,13 @@ async function updateTaskPosition(e) {
|
|||
Object.assign(newTask, updatedTaskBucket.task)
|
||||
newTask.bucketId = updatedTaskBucket.bucketId
|
||||
if (updatedTaskBucket.bucketId !== newTask.bucketId) {
|
||||
kanbanStore.moveTaskToBucket(newTask, updatedTaskBucket.bucketId)
|
||||
bucketStore.moveTaskToBucket(newTask, updatedTaskBucket.bucketId)
|
||||
}
|
||||
if (updatedTaskBucket.bucket) {
|
||||
kanbanStore.setBucketById(updatedTaskBucket.bucket, false)
|
||||
bucketStore.setBucketById(updatedTaskBucket.bucket, false)
|
||||
}
|
||||
}
|
||||
kanbanStore.setTaskInBucket(newTask)
|
||||
bucketStore.setTaskInBucket(newTask)
|
||||
|
||||
// Make sure the first and second task don't both get position 0 assigned
|
||||
if (newTaskIndex === 0 && taskAfter !== null && taskAfter.position === 0) {
|
||||
|
|
@ -675,10 +675,10 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
|
|||
projectId: projectIdWithFallback.value,
|
||||
})
|
||||
newTaskText.value = ''
|
||||
kanbanStore.addTaskToBucket(task)
|
||||
bucketStore.addTaskToBucket(task)
|
||||
scrollTaskContainerToTop(bucketId)
|
||||
|
||||
const bucket = kanbanStore.getBucketById(bucketId)
|
||||
const bucket = bucketStore.getBucketById(bucketId)
|
||||
if (bucket && bucket.limit && bucket.count >= bucket.limit) {
|
||||
toggleShowNewTaskInput(bucketId)
|
||||
}
|
||||
|
|
@ -697,7 +697,7 @@ async function createNewBucket() {
|
|||
return
|
||||
}
|
||||
|
||||
await kanbanStore.createBucket(new BucketModel({
|
||||
await bucketStore.createBucket(new BucketModel({
|
||||
title: newBucketTitle.value,
|
||||
projectId: projectIdWithFallback.value,
|
||||
projectViewId: props.viewId,
|
||||
|
|
@ -716,7 +716,7 @@ function deleteBucketModal(bucketId: IBucket['id']) {
|
|||
|
||||
async function deleteBucket() {
|
||||
try {
|
||||
await kanbanStore.deleteBucket({
|
||||
await bucketStore.deleteBucket({
|
||||
bucket: new BucketModel({
|
||||
id: bucketToDelete.value,
|
||||
projectId: projectIdWithFallback.value,
|
||||
|
|
@ -740,13 +740,13 @@ async function focusBucketTitle(e: Event) {
|
|||
|
||||
async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
||||
|
||||
const bucket = kanbanStore.getBucketById(bucketId)
|
||||
const bucket = bucketStore.getBucketById(bucketId)
|
||||
if (bucket?.title === bucketTitle) {
|
||||
bucketTitleEditable.value = false
|
||||
return
|
||||
}
|
||||
|
||||
await kanbanStore.updateBucket({
|
||||
await bucketStore.updateBucket({
|
||||
id: bucketId,
|
||||
title: bucketTitle,
|
||||
projectId: projectId.value,
|
||||
|
|
@ -757,7 +757,7 @@ async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
|||
|
||||
function updateBuckets(value: IBucket[]) {
|
||||
// (1) buckets get updated in store and tasks positions get invalidated
|
||||
kanbanStore.setBuckets(value)
|
||||
bucketStore.setBuckets(value)
|
||||
}
|
||||
|
||||
function handleRecurringTaskCompletion() {
|
||||
|
|
@ -772,7 +772,7 @@ function handleRecurringTaskCompletion() {
|
|||
|
||||
if (filterContainsDateFields) {
|
||||
// Reload the kanban board to refresh tasks that now match/don't match the filter
|
||||
kanbanStore.loadBucketsForProject(projectId.value, props.viewId, params.value)
|
||||
bucketStore.loadBucketsForProject(projectId.value, props.viewId, params.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -785,7 +785,7 @@ function updateBucketPosition(e: { newIndex: number }) {
|
|||
const bucketBefore = buckets.value[e.newIndex - 1] ?? null
|
||||
const bucketAfter = buckets.value[e.newIndex + 1] ?? null
|
||||
|
||||
kanbanStore.updateBucket({
|
||||
bucketStore.updateBucket({
|
||||
id: bucket.id,
|
||||
projectId: projectId.value,
|
||||
position: calculateItemPosition(
|
||||
|
|
@ -800,8 +800,8 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
|
|||
return
|
||||
}
|
||||
|
||||
await kanbanStore.updateBucket({
|
||||
...kanbanStore.getBucketById(bucketId),
|
||||
await bucketStore.updateBucket({
|
||||
...bucketStore.getBucketById(bucketId),
|
||||
projectId: projectId.value,
|
||||
limit,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<template #default>
|
||||
<div
|
||||
:class="{ 'is-loading': loading }"
|
||||
:class="{ 'is-loading': hasBuckets ? bucketStore.isLoading : loading }"
|
||||
class="loader-container is-max-width-desktop list-view"
|
||||
>
|
||||
<Card
|
||||
|
|
@ -30,6 +30,188 @@
|
|||
:has-content="false"
|
||||
class="has-overflow"
|
||||
>
|
||||
<!-- Sectioned mode (list view with manual bucket configuration) -->
|
||||
<template v-if="hasBuckets">
|
||||
<AddTask
|
||||
v-if="!project?.isArchived && canWrite"
|
||||
ref="addTaskRef"
|
||||
class="list-view__add-task d-print-none"
|
||||
:default-position="firstNewPosition"
|
||||
@taskAdded="updateTaskList"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="bucket in buckets"
|
||||
:key="bucket.id"
|
||||
class="bucket-section"
|
||||
>
|
||||
<div
|
||||
class="bucket-section__header"
|
||||
@click="toggleBucketCollapse(bucket.id)"
|
||||
>
|
||||
<span
|
||||
class="icon bucket-section__collapse-icon"
|
||||
:class="{'is-collapsed': collapsedBuckets[bucket.id]}"
|
||||
>
|
||||
<Icon icon="chevron-down" />
|
||||
</span>
|
||||
<h2
|
||||
class="bucket-section__title"
|
||||
:contenteditable="(canWrite && !collapsedBuckets[bucket.id]) ? true : undefined"
|
||||
:spellcheck="false"
|
||||
@keydown.enter.prevent.stop="!$event.isComposing && ($event.target as HTMLElement).blur()"
|
||||
@keydown.esc.prevent.stop="!$event.isComposing && ($event.target as HTMLElement).blur()"
|
||||
@blur="saveBucketTitle(bucket.id, ($event.target as HTMLElement).textContent as string)"
|
||||
@click.stop
|
||||
>
|
||||
{{ bucket.title }}
|
||||
</h2>
|
||||
<span
|
||||
v-if="bucket.limit > 0 || bucket.count > 0"
|
||||
:class="{'is-max': bucket.limit > 0 && bucket.count >= bucket.limit}"
|
||||
class="bucket-section__count"
|
||||
>
|
||||
{{ bucket.limit > 0 ? `${bucket.count}/${bucket.limit}` : bucket.count }}
|
||||
</span>
|
||||
<Dropdown
|
||||
v-if="canWrite && !collapsedBuckets[bucket.id]"
|
||||
class="is-right bucket-section__options"
|
||||
trigger-icon="ellipsis-v"
|
||||
@click.stop
|
||||
>
|
||||
<DropdownItem
|
||||
v-tooltip="$t('project.kanban.defaultBucketHint')"
|
||||
:icon-class="{'has-text-primary': bucket.id === currentView?.defaultBucketId}"
|
||||
icon="th"
|
||||
@click.stop="toggleDefaultBucket(bucket)"
|
||||
>
|
||||
{{ $t('project.kanban.defaultBucket') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
icon="angles-up"
|
||||
@click.stop="toggleBucketCollapse(bucket.id)"
|
||||
>
|
||||
{{ $t('project.list.collapseSection') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="buckets.length <= 1 ? $t('project.kanban.deleteLast') : ''"
|
||||
class="has-text-danger"
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
icon-class="has-text-danger"
|
||||
icon="trash-alt"
|
||||
@click.stop="() => deleteBucketModal(bucket.id)"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-if="!collapsedBuckets[bucket.id]"
|
||||
:model-value="bucket.tasks"
|
||||
group="tasks"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: {
|
||||
tasks: true,
|
||||
'dragging-disabled': !canDragTasks || !isPositionSorting,
|
||||
},
|
||||
type: 'transition-group',
|
||||
}"
|
||||
:animation="100"
|
||||
:handle="dragHandle"
|
||||
:delay-on-touch-only="!isTouchDevice"
|
||||
:delay="isTouchDevice ? 0 : 1000"
|
||||
ghost-class="task-ghost"
|
||||
@start="handleDragStart"
|
||||
@end="(e) => saveTaskPositionInBucket(e, bucket)"
|
||||
>
|
||||
<template #item="{element: t}">
|
||||
<SingleTaskInProject
|
||||
:show-list-color="false"
|
||||
:can-mark-as-done="canWrite || isPseudoProject"
|
||||
:the-task="t"
|
||||
:all-tasks="getAllTasksFromBuckets()"
|
||||
@taskUpdated="updateTaskInBuckets"
|
||||
>
|
||||
<span
|
||||
v-if="canDragTasks && isPositionSorting"
|
||||
class="icon handle"
|
||||
>
|
||||
<Icon icon="grip-lines" />
|
||||
</span>
|
||||
</SingleTaskInProject>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- Load more -->
|
||||
<div
|
||||
v-if="!collapsedBuckets[bucket.id] && bucket.tasks.length < bucket.count"
|
||||
class="bucket-section__load-more"
|
||||
>
|
||||
<ButtonLink @click="loadMoreForBucket(bucket.id)">
|
||||
{{ $t('project.list.loadMore', {remaining: bucket.count - bucket.tasks.length}) }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add section button -->
|
||||
<div
|
||||
v-if="canWrite"
|
||||
class="bucket-section__add"
|
||||
>
|
||||
<div
|
||||
v-if="showNewSectionInput"
|
||||
class="field has-addons bucket-section__add-input"
|
||||
>
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
v-model="newSectionTitle"
|
||||
v-focus.always
|
||||
class="input"
|
||||
:placeholder="$t('project.list.addSectionPlaceholder')"
|
||||
type="text"
|
||||
@keyup.enter="createNewSection"
|
||||
@keyup.esc="showNewSectionInput = false"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<XButton
|
||||
:shadow="false"
|
||||
@click="createNewSection"
|
||||
>
|
||||
{{ $t('project.list.add') }}
|
||||
</XButton>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonLink
|
||||
v-else
|
||||
@click="showNewSectionInput = true"
|
||||
>
|
||||
<Icon icon="plus" />
|
||||
{{ $t('project.list.addSection') }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
|
||||
<!-- Delete section confirmation modal -->
|
||||
<Modal
|
||||
v-if="showSectionDeleteModal"
|
||||
@close="showSectionDeleteModal = false"
|
||||
@submit="deleteSection"
|
||||
>
|
||||
<template #header>
|
||||
{{ $t('project.list.deleteSection') }}
|
||||
</template>
|
||||
<template #text>
|
||||
<p>{{ $t('project.list.deleteSectionText1') }}</p>
|
||||
<p>{{ $t('project.list.deleteSectionText2') }}</p>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<!-- Flat mode (existing) -->
|
||||
<template v-else>
|
||||
<AddTask
|
||||
v-if="!project?.isArchived && canWrite"
|
||||
ref="addTaskRef"
|
||||
|
|
@ -93,6 +275,7 @@
|
|||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -102,6 +285,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, nextTick, onMounted, onBeforeUnmount, watch, toRef} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
|
|
@ -112,22 +296,39 @@ import FilterPopup from '@/components/project/partials/FilterPopup.vue'
|
|||
import Nothing from '@/components/misc/Nothing.vue'
|
||||
import Pagination from '@/components/misc/Pagination.vue'
|
||||
import SortPopup from '@/components/project/partials/SortPopup.vue'
|
||||
import Dropdown from '@/components/misc/Dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/DropdownItem.vue'
|
||||
import Modal from '@/components/misc/Modal.vue'
|
||||
|
||||
import {useTaskList} from '@/composables/useTaskList'
|
||||
import {useTaskDragToProject} from '@/composables/useTaskDragToProject'
|
||||
import {shouldShowTaskInListView} from '@/composables/useTaskListFiltering'
|
||||
import {PERMISSIONS as Permissions} from '@/constants/permissions'
|
||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {
|
||||
type CollapsedBuckets,
|
||||
getCollapsedBucketState,
|
||||
saveCollapsedBucketState,
|
||||
} from '@/helpers/saveCollapsedBucketState'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IBucket} from '@/modelTypes/IBucket'
|
||||
import {isSavedFilter, useSavedFilter} from '@/services/savedFilter'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useBucketStore} from '@/stores/buckets'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import TaskPositionService from '@/services/taskPosition'
|
||||
import TaskPositionModel from '@/models/taskPosition'
|
||||
import TaskBucketService from '@/services/taskBucket'
|
||||
import TaskBucketModel from '@/models/taskBucket'
|
||||
import BucketModel from '@/models/bucket'
|
||||
import ProjectViewService from '@/services/projectViews'
|
||||
import ProjectViewModel from '@/models/projectView'
|
||||
|
||||
const props = defineProps<{
|
||||
isLoadingProject: boolean,
|
||||
|
|
@ -139,6 +340,8 @@ const projectId = toRef(props, 'projectId')
|
|||
|
||||
defineOptions({name: 'List'})
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const ctaVisible = ref(false)
|
||||
|
||||
const drag = ref(false)
|
||||
|
|
@ -177,6 +380,14 @@ watch(
|
|||
const isPositionSorting = computed(() => 'position' in sortByParam.value)
|
||||
|
||||
const firstNewPosition = computed(() => {
|
||||
if (hasBuckets.value) {
|
||||
const defaultBucket = buckets.value.find(b => b.id === currentView.value?.defaultBucketId) || buckets.value[0]
|
||||
if (defaultBucket?.tasks?.length > 0) {
|
||||
return calculateItemPosition(null, defaultBucket.tasks[0].position)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (tasks.value.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -186,6 +397,8 @@ const firstNewPosition = computed(() => {
|
|||
|
||||
const baseStore = useBaseStore()
|
||||
const taskStore = useTaskStore()
|
||||
const bucketStore = useBucketStore()
|
||||
const projectStore = useProjectStore()
|
||||
const {handleTaskDropToProject} = useTaskDragToProject()
|
||||
const project = computed(() => baseStore.currentProject)
|
||||
|
||||
|
|
@ -214,7 +427,184 @@ function focusNewTaskInput() {
|
|||
addTaskRef.value?.focusTaskInput()
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Bucket/Section mode
|
||||
// ==========================================
|
||||
|
||||
const currentView = computed(() => {
|
||||
return project.value?.views?.find(v => v.id === props.viewId) as IProjectView || null
|
||||
})
|
||||
|
||||
const hasBuckets = computed(() => {
|
||||
return currentView.value?.bucketConfigurationMode !== 'none'
|
||||
&& currentView.value?.bucketConfigurationMode !== undefined
|
||||
})
|
||||
|
||||
const buckets = computed(() => bucketStore.buckets)
|
||||
|
||||
const collapsedBuckets = ref<CollapsedBuckets>({})
|
||||
|
||||
watch(
|
||||
[() => props.projectId, () => props.viewId, hasBuckets],
|
||||
async ([pId, vId, bucketed]) => {
|
||||
if (!bucketed) return
|
||||
await bucketStore.loadBucketsForProject(pId, vId, params.value)
|
||||
collapsedBuckets.value = getCollapsedBucketState(pId)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function toggleBucketCollapse(bucketId: IBucket['id']) {
|
||||
collapsedBuckets.value = {
|
||||
...collapsedBuckets.value,
|
||||
[bucketId]: !collapsedBuckets.value[bucketId],
|
||||
}
|
||||
saveCollapsedBucketState(props.projectId, collapsedBuckets.value)
|
||||
}
|
||||
|
||||
function getAllTasksFromBuckets(): ITask[] {
|
||||
return buckets.value.flatMap(b => b.tasks)
|
||||
}
|
||||
|
||||
function updateTaskInBuckets(updatedTask: ITask) {
|
||||
bucketStore.setTaskInBucket(updatedTask)
|
||||
}
|
||||
|
||||
async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
||||
const bucket = bucketStore.getBucketById(bucketId)
|
||||
if (bucket?.title === bucketTitle) {
|
||||
return
|
||||
}
|
||||
|
||||
await bucketStore.updateBucket({
|
||||
id: bucketId,
|
||||
title: bucketTitle,
|
||||
projectId: projectId.value,
|
||||
})
|
||||
success({message: t('project.list.sectionTitleSavedSuccess')})
|
||||
}
|
||||
|
||||
const sectionToDelete = ref<IBucket['id']>(0)
|
||||
const showSectionDeleteModal = ref(false)
|
||||
|
||||
function deleteBucketModal(bucketId: IBucket['id']) {
|
||||
if (buckets.value.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
sectionToDelete.value = bucketId
|
||||
showSectionDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteSection() {
|
||||
try {
|
||||
await bucketStore.deleteBucket({
|
||||
bucket: new BucketModel({
|
||||
id: sectionToDelete.value,
|
||||
projectId: projectId.value,
|
||||
projectViewId: props.viewId,
|
||||
}),
|
||||
params: params.value,
|
||||
})
|
||||
success({message: t('project.list.deleteSectionSuccess')})
|
||||
} finally {
|
||||
showSectionDeleteModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const newSectionTitle = ref('')
|
||||
const showNewSectionInput = ref(false)
|
||||
|
||||
async function createNewSection() {
|
||||
if (newSectionTitle.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
await bucketStore.createBucket(new BucketModel({
|
||||
title: newSectionTitle.value,
|
||||
projectId: projectId.value,
|
||||
projectViewId: props.viewId,
|
||||
}))
|
||||
newSectionTitle.value = ''
|
||||
showNewSectionInput.value = false
|
||||
}
|
||||
|
||||
async function toggleDefaultBucket(bucket: IBucket) {
|
||||
const defaultBucketId = currentView.value?.defaultBucketId === bucket.id
|
||||
? 0
|
||||
: bucket.id
|
||||
|
||||
const projectViewService = new ProjectViewService()
|
||||
const updatedView = await projectViewService.update(new ProjectViewModel({
|
||||
...currentView.value,
|
||||
defaultBucketId,
|
||||
}))
|
||||
|
||||
const views = project.value.views.map(v => v.id === currentView.value?.id ? updatedView : v)
|
||||
const updatedProject = {
|
||||
...project.value,
|
||||
views,
|
||||
}
|
||||
|
||||
projectStore.setProject(updatedProject)
|
||||
|
||||
success({message: t('project.kanban.defaultBucketSavedSuccess')})
|
||||
}
|
||||
|
||||
async function loadMoreForBucket(bucketId: IBucket['id']) {
|
||||
await bucketStore.loadNextTasksForBucket(
|
||||
props.projectId,
|
||||
props.viewId,
|
||||
params.value,
|
||||
bucketId,
|
||||
)
|
||||
}
|
||||
|
||||
async function saveTaskPositionInBucket(
|
||||
e: {originalEvent?: MouseEvent, to: HTMLElement, from: HTMLElement, newIndex: number, oldIndex: number},
|
||||
targetBucket: IBucket,
|
||||
) {
|
||||
drag.value = false
|
||||
|
||||
const task = targetBucket.tasks[e.newIndex]
|
||||
if (!task) return
|
||||
|
||||
const taskBefore = targetBucket.tasks[e.newIndex - 1] ?? null
|
||||
const taskAfter = targetBucket.tasks[e.newIndex + 1] ?? null
|
||||
|
||||
const position = calculateItemPosition(
|
||||
taskBefore?.position ?? null,
|
||||
taskAfter?.position ?? null,
|
||||
)
|
||||
|
||||
await taskPositionService.value.update(new TaskPositionModel({
|
||||
position,
|
||||
projectViewId: props.viewId,
|
||||
taskId: task.id,
|
||||
}))
|
||||
|
||||
// If bucket changed, update bucket assignment
|
||||
if (e.to !== e.from) {
|
||||
const taskBucketService = new TaskBucketService()
|
||||
await taskBucketService.update(new TaskBucketModel({
|
||||
taskId: task.id,
|
||||
bucketId: targetBucket.id,
|
||||
projectViewId: props.viewId,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Flat mode (existing logic)
|
||||
// ==========================================
|
||||
|
||||
function updateTaskList(task: ITask) {
|
||||
if (hasBuckets.value) {
|
||||
bucketStore.addTaskToBucket(task)
|
||||
baseStore.setHasTasks(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPositionSorting.value) {
|
||||
// reload tasks with current filter and sorting
|
||||
loadTasks()
|
||||
|
|
@ -246,7 +636,8 @@ function updateTasks(updatedTask: ITask) {
|
|||
function handleDragStart(e: { item: HTMLElement }) {
|
||||
drag.value = true
|
||||
const taskId = parseInt(e.item.dataset.taskId ?? '', 10)
|
||||
const task = tasks.value.find(t => t.id === taskId)
|
||||
const allAvailableTasks = hasBuckets.value ? getAllTasksFromBuckets() : tasks.value
|
||||
const task = allAvailableTasks.find(t => t.id === taskId)
|
||||
|
||||
if (task) {
|
||||
taskStore.setDraggedTask(task)
|
||||
|
|
@ -421,4 +812,84 @@ onBeforeUnmount(() => {
|
|||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bucket section styles
|
||||
.bucket-section {
|
||||
&:not(:first-child) {
|
||||
border-block-start: 1px solid var(--grey-200);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .75rem 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--grey-50);
|
||||
border-block-end: 1px solid var(--grey-100);
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__collapse-icon {
|
||||
transition: transform 150ms ease;
|
||||
color: var(--grey-500);
|
||||
|
||||
&.is-collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__title {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: .125rem .25rem;
|
||||
border-radius: $radius;
|
||||
min-inline-size: 0;
|
||||
|
||||
&[contenteditable='true'] {
|
||||
cursor: text;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__count {
|
||||
font-size: .85rem;
|
||||
color: var(--grey-500);
|
||||
font-weight: 500;
|
||||
|
||||
&.is-max {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__options {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.bucket-section__load-more {
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
.bucket-section__add {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bucket-section__add-input {
|
||||
max-inline-size: 400px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ function handleBubbleSave() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="view.viewKind === 'kanban'"
|
||||
v-if="view.viewKind === 'kanban' || view.viewKind === 'list'"
|
||||
class="field"
|
||||
>
|
||||
<label
|
||||
|
|
@ -212,7 +212,7 @@ function handleBubbleSave() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="view.viewKind === 'kanban' && view.bucketConfigurationMode === 'filter'"
|
||||
v-if="(view.viewKind === 'kanban' || view.viewKind === 'list') && view.bucketConfigurationMode === 'filter'"
|
||||
class="field"
|
||||
>
|
||||
<label class="label">
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import type {IBucket} from '@/modelTypes/IBucket'
|
|||
import {PROJECT_VIEW_KINDS} from '@/modelTypes/IProjectView'
|
||||
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBucketStore} from '@/stores/buckets'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
|
@ -69,30 +69,30 @@ const emit = defineEmits<{
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const bucketStore = useBucketStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
const project = computed(() => projectStore.projects[props.task.projectId])
|
||||
|
||||
// If the project has exactly one manual kanban view, always use it.
|
||||
// If the project has exactly one manual bucket view (kanban or list), always use it.
|
||||
// If there are multiple, only show the selector when the active view is one of them.
|
||||
const kanbanView = computed(() => {
|
||||
if (!project.value?.views) {
|
||||
return null
|
||||
}
|
||||
|
||||
const manualKanbanViews = project.value.views.filter(
|
||||
v => v.viewKind === PROJECT_VIEW_KINDS.KANBAN
|
||||
const manualBucketViews = project.value.views.filter(
|
||||
v => (v.viewKind === PROJECT_VIEW_KINDS.KANBAN || v.viewKind === PROJECT_VIEW_KINDS.LIST)
|
||||
&& v.bucketConfigurationMode === 'manual',
|
||||
)
|
||||
|
||||
if (manualKanbanViews.length === 1) {
|
||||
return manualKanbanViews[0]
|
||||
if (manualBucketViews.length === 1) {
|
||||
return manualBucketViews[0]
|
||||
}
|
||||
|
||||
if (manualKanbanViews.length > 1) {
|
||||
if (manualBucketViews.length > 1) {
|
||||
const activeViewId = baseStore.currentProjectViewId
|
||||
return manualKanbanViews.find(v => v.id === activeViewId) || null
|
||||
return manualBucketViews.find(v => v.id === activeViewId) || null
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
@ -156,7 +156,7 @@ async function changeBucket(bucket: IBucket) {
|
|||
updatedBuckets.push({...bucket})
|
||||
}
|
||||
|
||||
kanbanStore.moveTaskToBucket(props.task, bucket.id)
|
||||
bucketStore.moveTaskToBucket(props.task, bucket.id)
|
||||
|
||||
// Only pick up done state from the response since moving to/from the
|
||||
// done bucket can toggle it. Spreading the full response task would
|
||||
|
|
|
|||
|
|
@ -441,7 +441,17 @@
|
|||
"empty": "This project is currently empty.",
|
||||
"newTaskCta": "Create a task.",
|
||||
"editTask": "Edit Task",
|
||||
"sort": "Sort"
|
||||
"sort": "Sort",
|
||||
"loadMore": "Show {remaining} more tasks",
|
||||
"addSection": "Add section",
|
||||
"addSectionPlaceholder": "Enter the new section title…",
|
||||
"deleteSection": "Delete this section",
|
||||
"deleteSectionText1": "Are you sure you want to delete this section?",
|
||||
"deleteSectionText2": "This will not delete any tasks but move them into the default section.",
|
||||
"deleteSectionSuccess": "The section has been deleted successfully.",
|
||||
"sectionTitleSavedSuccess": "The section title has been saved successfully.",
|
||||
"sectionLimitSavedSuccess": "The section limit has been saved successfully.",
|
||||
"collapseSection": "Collapse this section"
|
||||
},
|
||||
"gantt": {
|
||||
"title": "Gantt",
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ function getTaskIndicesById(buckets: IBucket[], taskId: ITask['id']) {
|
|||
}
|
||||
|
||||
/**
|
||||
* This store is intended to hold the currently active kanban view.
|
||||
* This store is intended to hold the buckets for the currently active bucketed view (kanban or list).
|
||||
* It should hold only the current buckets.
|
||||
*/
|
||||
export const useKanbanStore = defineStore('kanban', () => {
|
||||
export const useBucketStore = defineStore('buckets', () => {
|
||||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
|
|
@ -399,5 +399,5 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
|
||||
// support hot reloading
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot))
|
||||
import.meta.hot.accept(acceptHMRUpdate(useBucketStore, import.meta.hot))
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelative
|
|||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBucketStore} from '@/stores/buckets'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
|
@ -127,7 +127,7 @@ async function findAssignees(parsedTaskAssignees: string[], projectId: number):
|
|||
|
||||
export const useTaskStore = defineStore('task', () => {
|
||||
const baseStore = useBaseStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const bucketStore = useBucketStore()
|
||||
const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -184,7 +184,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
const taskService = new TaskService()
|
||||
try {
|
||||
const updatedTask = await taskService.update(task)
|
||||
kanbanStore.ensureTaskIsInCorrectBucket(updatedTask)
|
||||
bucketStore.ensureTaskIsInCorrectBucket(updatedTask)
|
||||
lastUpdatedTask.value = updatedTask
|
||||
return updatedTask
|
||||
} finally {
|
||||
|
|
@ -195,7 +195,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
async function deleteTask(task: ITask) {
|
||||
const taskService = new TaskService()
|
||||
const response = await taskService.delete(task)
|
||||
kanbanStore.removeTaskInBucket(task)
|
||||
bucketStore.removeTaskInBucket(task)
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
taskId: ITask['id']
|
||||
attachment: IAttachment
|
||||
}) {
|
||||
const t = kanbanStore.getTaskById(taskId)
|
||||
const t = bucketStore.getTaskById(taskId)
|
||||
if (t.task !== null) {
|
||||
const attachments = [
|
||||
...t.task.attachments,
|
||||
|
|
@ -222,7 +222,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
attachments,
|
||||
},
|
||||
}
|
||||
kanbanStore.setTaskInBucketByIndex(newTask)
|
||||
bucketStore.setTaskInBucketByIndex(newTask)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
userId: user.id,
|
||||
taskId: taskId,
|
||||
}))
|
||||
const t = kanbanStore.getTaskById(taskId)
|
||||
const t = bucketStore.getTaskById(taskId)
|
||||
if (t.task === null) {
|
||||
// Don't try further adding a label if the task is not in kanban
|
||||
// Usually this means the kanban board hasn't been accessed until now.
|
||||
|
|
@ -250,7 +250,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
return r
|
||||
}
|
||||
|
||||
kanbanStore.setTaskInBucketByIndex({
|
||||
bucketStore.setTaskInBucketByIndex({
|
||||
...t,
|
||||
task: {
|
||||
...t.task,
|
||||
|
|
@ -279,7 +279,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
userId: user.id,
|
||||
taskId: taskId,
|
||||
}))
|
||||
const t = kanbanStore.getTaskById(taskId)
|
||||
const t = bucketStore.getTaskById(taskId)
|
||||
if (t.task === null) {
|
||||
// Don't try further adding a label if the task is not in kanban
|
||||
// Usually this means the kanban board hasn't been accessed until now.
|
||||
|
|
@ -290,7 +290,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
|
||||
const assignees = t.task.assignees.filter(({ id }) => id !== user.id)
|
||||
|
||||
kanbanStore.setTaskInBucketByIndex({
|
||||
bucketStore.setTaskInBucketByIndex({
|
||||
...t,
|
||||
task: {
|
||||
...t.task,
|
||||
|
|
@ -313,7 +313,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
taskId,
|
||||
labelId: label.id,
|
||||
}))
|
||||
const t = kanbanStore.getTaskById(taskId)
|
||||
const t = bucketStore.getTaskById(taskId)
|
||||
if (t.task === null) {
|
||||
// Don't try further adding a label if the task is not in kanban
|
||||
// Usually this means the kanban board hasn't been accessed until now.
|
||||
|
|
@ -322,7 +322,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
return r
|
||||
}
|
||||
|
||||
kanbanStore.setTaskInBucketByIndex({
|
||||
bucketStore.setTaskInBucketByIndex({
|
||||
...t,
|
||||
task: {
|
||||
...t.task,
|
||||
|
|
@ -345,7 +345,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
taskId, labelId:
|
||||
label.id,
|
||||
}))
|
||||
const t = kanbanStore.getTaskById(taskId)
|
||||
const t = bucketStore.getTaskById(taskId)
|
||||
if (t.task === null) {
|
||||
// Don't try further adding a label if the task is not in kanban
|
||||
// Usually this means the kanban board hasn't been accessed until now.
|
||||
|
|
@ -357,7 +357,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
// Remove the label from the project
|
||||
const labels = t.task.labels.filter(({ id }) => id !== label.id)
|
||||
|
||||
kanbanStore.setTaskInBucketByIndex({
|
||||
bucketStore.setTaskInBucketByIndex({
|
||||
...t,
|
||||
task: {
|
||||
...t.task,
|
||||
|
|
@ -558,9 +558,9 @@ export const useTaskStore = defineStore('task', () => {
|
|||
const taskService = new TaskService()
|
||||
await taskService.markTaskAsRead(taskId)
|
||||
|
||||
const t = kanbanStore.getTaskById(taskId)
|
||||
const t = bucketStore.getTaskById(taskId)
|
||||
if (t.task !== null) {
|
||||
kanbanStore.setTaskInBucket({
|
||||
bucketStore.setTaskInBucket({
|
||||
...t.task,
|
||||
isUnread: false,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ async function createView() {
|
|||
}
|
||||
|
||||
try {
|
||||
newView.value.bucketConfigurationMode = newView.value.viewKind === 'kanban'
|
||||
newView.value.bucketConfigurationMode = (newView.value.viewKind === 'kanban' || newView.value.viewKind === 'list')
|
||||
? newView.value.bucketConfigurationMode
|
||||
: 'none'
|
||||
newView.value.projectId = props.projectId
|
||||
|
|
@ -96,7 +96,7 @@ async function deleteView(viewId: number) {
|
|||
}
|
||||
|
||||
async function saveView(view: IProjectView) {
|
||||
if (view?.viewKind !== 'kanban') {
|
||||
if (view?.viewKind !== 'kanban' && view?.viewKind !== 'list') {
|
||||
view.bucketConfigurationMode = 'none'
|
||||
}
|
||||
const result = await projectViewService.update(view)
|
||||
|
|
|
|||
|
|
@ -677,7 +677,7 @@ import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelative
|
|||
import {playPopSound} from '@/helpers/playPop'
|
||||
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBucketStore} from '@/stores/buckets'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
|
@ -703,7 +703,7 @@ const {t} = useI18n({useScope: 'global'})
|
|||
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const bucketStore = useBucketStore()
|
||||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
|
|
@ -821,7 +821,7 @@ async function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
|
|||
|
||||
function onAttachmentsUpdated(attachments: IAttachment[]) {
|
||||
task.value.attachments = attachments
|
||||
kanbanStore.setTaskInBucket({
|
||||
bucketStore.setTaskInBucket({
|
||||
...task.value,
|
||||
attachments,
|
||||
})
|
||||
|
|
@ -1121,7 +1121,7 @@ async function changeProject(project: IProject | null) {
|
|||
if (project === null) {
|
||||
return
|
||||
}
|
||||
kanbanStore.removeTaskInBucket(task.value)
|
||||
bucketStore.removeTaskInBucket(task.value)
|
||||
await saveTask({
|
||||
...task.value,
|
||||
projectId: project.id,
|
||||
|
|
|
|||
|
|
@ -250,3 +250,25 @@
|
|||
position: 10
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
# Buckets for list view with manual bucket configuration (project_view 161)
|
||||
- id: 41
|
||||
title: Backlog
|
||||
project_view_id: 161
|
||||
created_by_id: 1
|
||||
position: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 42
|
||||
title: In Progress
|
||||
project_view_id: 161
|
||||
created_by_id: 1
|
||||
position: 2
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 43
|
||||
title: Review
|
||||
project_view_id: 161
|
||||
created_by_id: 1
|
||||
position: 3
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
|
|
|
|||
|
|
@ -1007,3 +1007,13 @@
|
|||
updated: '2024-03-18 15:14:13'
|
||||
created: '2018-03-18 15:14:13'
|
||||
bucket_configuration_mode: 1
|
||||
# List view with manual bucket configuration (for testing list view sections)
|
||||
- id: 161
|
||||
title: Sectioned List
|
||||
project_id: 1
|
||||
view_kind: 0
|
||||
bucket_configuration_mode: 1
|
||||
default_bucket_id: 41
|
||||
position: 5
|
||||
updated: '2024-03-18 15:14:13'
|
||||
created: '2018-03-18 15:14:13'
|
||||
|
|
|
|||
|
|
@ -115,6 +115,16 @@
|
|||
#- task_id: 39
|
||||
# project_view_id: 100
|
||||
# bucket_id: null
|
||||
# Task-bucket mappings for list view with manual buckets (project_view 161)
|
||||
- task_id: 1
|
||||
project_view_id: 161
|
||||
bucket_id: 41
|
||||
- task_id: 2
|
||||
project_view_id: 161
|
||||
bucket_id: 42
|
||||
- task_id: 3
|
||||
project_view_id: 161
|
||||
bucket_id: 43
|
||||
- task_id: 40
|
||||
project_view_id: 144
|
||||
bucket_id: 38
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ func createProjectView(s *xorm.Session, p *ProjectView, a web.Auth, createBacklo
|
|||
return
|
||||
}
|
||||
|
||||
if p.ViewKind == ProjectViewKindKanban && createBacklogBucket && p.BucketConfigurationMode == BucketConfigurationModeManual {
|
||||
if createBacklogBucket && p.BucketConfigurationMode == BucketConfigurationModeManual {
|
||||
// Create default buckets for kanban view
|
||||
backlog := &Bucket{
|
||||
ProjectViewID: p.ID,
|
||||
|
|
@ -344,9 +344,11 @@ func createProjectView(s *xorm.Session, p *ProjectView, a web.Auth, createBacklo
|
|||
return
|
||||
}
|
||||
|
||||
// Set Backlog as default bucket and Done as done bucket
|
||||
// Set Backlog as default bucket and Done as done bucket (only kanban views use done buckets)
|
||||
p.DefaultBucketID = backlog.ID
|
||||
if p.ViewKind == ProjectViewKindKanban {
|
||||
p.DoneBucketID = done.ID
|
||||
}
|
||||
_, err = s.ID(p.ID).Cols("default_bucket_id", "done_bucket_id").Update(p)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1034,7 +1034,6 @@ func setTaskInBucketInViews(s *xorm.Session, t *Task, a web.Auth, setBucket bool
|
|||
|
||||
for _, view := range views {
|
||||
if setBucket && !moveToDone &&
|
||||
view.ViewKind == ProjectViewKindKanban &&
|
||||
view.BucketConfigurationMode == BucketConfigurationModeManual {
|
||||
|
||||
bucketID := view.DoneBucketID
|
||||
|
|
|
|||
|
|
@ -158,6 +158,31 @@ func TestTask_Create(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
})
|
||||
t.Run("task assigned to list view with manual buckets", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
Title: "Test list view bucket assignment",
|
||||
ProjectID: 1,
|
||||
}
|
||||
err := task.Create(s, usr)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.Commit())
|
||||
|
||||
// Task should be assigned to default bucket of the list view with manual buckets (view 161, bucket 41)
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": task.ID,
|
||||
"bucket_id": 41,
|
||||
"project_view_id": 161,
|
||||
}, false)
|
||||
// Task should also still be assigned to the kanban view's default bucket
|
||||
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||
"task_id": task.ID,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("default bucket different", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
|
|
|||
Loading…
Reference in New Issue