1
0

fix(filters): rework filter popup button

This commit is contained in:
kolaente 2024-03-13 17:19:15 +01:00
parent 79577c14b7
commit 15215b30a0
No known key found for this signature in database
GPG Key ID: F40E70337AB24C9B
6 changed files with 232 additions and 290 deletions

View File

@ -1,11 +1,4 @@
<template> <template>
<x-button
v-if="hasFilters"
variant="secondary"
@click="clearFilters"
>
{{ $t('filters.clear') }}
</x-button>
<x-button <x-button
variant="secondary" variant="secondary"
icon="filter" icon="filter"

View File

@ -25,6 +25,13 @@
v-if="hasFooter" v-if="hasFooter"
#footer #footer
> >
<x-button
variant="secondary"
@click.prevent.stop="clearFiltersAndEmit"
class="mr-2"
>
{{ $t('filters.clear') }}
</x-button>
<x-button <x-button
variant="primary" variant="primary"
@click.prevent.stop="changeAndEmitButton" @click.prevent.stop="changeAndEmitButton"
@ -130,4 +137,9 @@ function changeAndEmitButton() {
change() change()
emit('showResultsButtonClicked') emit('showResultsButtonClicked')
} }
function clearFiltersAndEmit() {
params.value.filter = ''
changeAndEmitButton()
}
</script> </script>

View File

@ -18,13 +18,11 @@ $filter-container-top-link-share-list: -47px;
margin-top: $filter-container-top-default; margin-top: $filter-container-top-default;
z-index: 4; z-index: 4;
.items { display: flex;
display: flex; justify-content: flex-end;
justify-content: flex-end;
.button:not(:last-of-type) { .button:not(:last-of-type) {
margin-right: .5rem; margin-right: .5rem;
}
} }
.button { .button {
@ -35,37 +33,6 @@ $filter-container-top-link-share-list: -47px;
text-align: left; text-align: left;
} }
.fancycheckbox {
display: block;
}
.search {
display: flex;
align-items: center;
justify-content: space-between;
margin-right: .5rem;
.field {
transition: width $transition;
width: 100%;
&.hidden {
width: 0;
height: 0;
margin: 0;
overflow: hidden;
}
.button {
height: 100%;
}
}
}
.filters input {
font-size: .9rem;
}
@media screen and (max-width: $tablet) { @media screen and (max-width: $tablet) {
position: static; position: static;
margin: 0 0 1rem 0 !important; margin: 0 0 1rem 0 !important;

View File

@ -5,13 +5,11 @@
view-name="kanban" view-name="kanban"
> >
<template #header> <template #header>
<div <div class="filter-container">
v-if="!isSavedFilter(project)" <FilterPopup
class="filter-container" v-if="!isSavedFilter(project)"
> v-model="params"
<div class="items"> />
<FilterPopup v-model="params" />
</div>
</div> </div>
</template> </template>
@ -47,7 +45,7 @@
v-tooltip="$t('project.kanban.doneBucketHint')" v-tooltip="$t('project.kanban.doneBucketHint')"
class="icon is-small has-text-success mr-2" class="icon is-small has-text-success mr-2"
> >
<icon icon="check-double" /> <icon icon="check-double"/>
</span> </span>
<h2 <h2
class="title input" class="title input"
@ -197,7 +195,9 @@
variant="secondary" variant="secondary"
@click="toggleShowNewTaskInput(bucket.id)" @click="toggleShowNewTaskInput(bucket.id)"
> >
{{ bucket.tasks.length === 0 ? $t('project.kanban.addTask') : $t('project.kanban.addAnotherTask') }} {{
bucket.tasks.length === 0 ? $t('project.kanban.addTask') : $t('project.kanban.addAnotherTask')
}}
</x-button> </x-button>
</div> </div>
</template> </template>
@ -290,7 +290,11 @@ import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import {getCollapsedBucketState, saveCollapsedBucketState, type CollapsedBuckets} from '@/helpers/saveCollapsedBucketState' import {
type CollapsedBuckets,
getCollapsedBucketState,
saveCollapsedBucketState,
} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {isSavedFilter} from '@/services/savedFilter' import {isSavedFilter} from '@/services/savedFilter'
@ -322,7 +326,7 @@ const kanbanStore = useKanbanStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({}) const taskContainerRefs = ref<{ [id: IBucket['id']]: HTMLElement }>({})
const bucketLimitInputRef = ref<HTMLInputElement | null>(null) const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
const drag = ref(false) const drag = ref(false)
@ -334,18 +338,18 @@ const bucketToDelete = ref(0)
const bucketTitleEditable = ref(false) const bucketTitleEditable = ref(false)
const newTaskText = ref('') const newTaskText = ref('')
const showNewTaskInput = ref<{[id: IBucket['id']]: boolean}>({}) const showNewTaskInput = ref<{ [id: IBucket['id']]: boolean }>({})
const newBucketTitle = ref('') const newBucketTitle = ref('')
const showNewBucketInput = ref(false) const showNewBucketInput = ref(false)
const newTaskError = ref<{[id: IBucket['id']]: boolean}>({}) const newTaskError = ref<{ [id: IBucket['id']]: boolean }>({})
const newTaskInputFocused = ref(false) const newTaskInputFocused = ref(false)
const showSetLimitInput = ref(false) const showSetLimitInput = ref(false)
const collapsedBuckets = ref<CollapsedBuckets>({}) const collapsedBuckets = ref<CollapsedBuckets>({})
// 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: ITask['id']]: boolean}>({}) const taskUpdating = ref<{ [id: ITask['id']]: boolean }>({})
const oneTaskUpdating = ref(false) const oneTaskUpdating = ref(false)
const params = ref<TaskFilterParams>({ const params = ref<TaskFilterParams>({
@ -378,7 +382,7 @@ const bucketDraggableComponentData = computed(() => ({
], ],
})) }))
const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ) const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
const project = computed(() => projectId ? projectStore.projects[projectId]: null) const project = computed(() => projectId ? projectStore.projects[projectId] : null)
const buckets = computed(() => kanbanStore.buckets) const buckets = computed(() => kanbanStore.buckets)
const loading = computed(() => kanbanStore.isLoading) const loading = computed(() => kanbanStore.isLoading)
@ -497,7 +501,7 @@ async function updateTaskPosition(e) {
await taskStore.update(newTask) await taskStore.update(newTask)
// Make sure the first and second task don't both get position 0 assigned // Make sure the first and second task don't both get position 0 assigned
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) { if (newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
const newTaskAfter = klona(taskAfter) // cloning the task to avoid pinia store manipulation const newTaskAfter = klona(taskAfter) // cloning the task to avoid pinia store manipulation
newTaskAfter.bucketId = newBucket.id newTaskAfter.bucketId = newBucket.id
@ -602,7 +606,7 @@ function updateBuckets(value: IBucket[]) {
} }
// TODO: fix type // TODO: fix type
function updateBucketPosition(e: {newIndex: number}) { function updateBucketPosition(e: { newIndex: number }) {
// (2) bucket positon is changed // (2) bucket positon is changed
dragBucket.value = false dragBucket.value = false
@ -631,19 +635,19 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
success({message: t('project.kanban.bucketLimitSavedSuccess')}) success({message: t('project.kanban.bucketLimitSavedSuccess')})
} }
const setBucketLimitCancel = ref<number|null>(null) const setBucketLimitCancel = ref<number | null>(null)
async function setBucketLimit(bucketId: IBucket['id'], now: boolean = false) { async function setBucketLimit(bucketId: IBucket['id'], now: boolean = false) {
const limit = parseInt(bucketLimitInputRef.value?.value || '') const limit = parseInt(bucketLimitInputRef.value?.value || '')
if (setBucketLimitCancel.value !== null) { if (setBucketLimitCancel.value !== null) {
clearTimeout(setBucketLimitCancel.value) clearTimeout(setBucketLimitCancel.value)
} }
if (now) { if (now) {
return saveBucketLimit(bucketId, limit) return saveBucketLimit(bucketId, limit)
} }
setBucketLimitCancel.value = setTimeout(saveBucketLimit, 2500, bucketId, limit) setBucketLimitCancel.value = setTimeout(saveBucketLimit, 2500, bucketId, limit)
} }
@ -739,6 +743,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
* { * {
opacity: 0; opacity: 0;
} }
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
@ -780,6 +785,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
&:first-of-type { &:first-of-type {
padding-top: .5rem; padding-top: .5rem;
} }
&:last-of-type { &:last-of-type {
padding-bottom: .5rem; padding-bottom: .5rem;
} }

View File

@ -5,52 +5,12 @@
view-name="project" view-name="project"
> >
<template #header> <template #header>
<div <div class="filter-container">
v-if="!isSavedFilter(project)" <FilterPopup
class="filter-container" v-if="!isSavedFilter(project)"
> v-model="params"
<div class="items"> @update:modelValue="prepareFiltersAndLoadTasks()"
<div class="search"> />
<div
:class="{ hidden: !showTaskSearch }"
class="field has-addons"
>
<div class="control has-icons-left has-icons-right">
<input
v-model="searchTerm"
v-focus
class="input"
:placeholder="$t('misc.search')"
type="text"
@blur="hideSearchBar()"
@keyup.enter="searchTasks"
>
<span class="icon is-left">
<icon icon="search" />
</span>
</div>
<div class="control">
<x-button
:loading="loading"
:shadow="false"
@click="searchTasks"
>
{{ $t('misc.search') }}
</x-button>
</div>
</div>
<x-button
v-if="!showTaskSearch"
icon="search"
variant="secondary"
@click="showTaskSearch = !showTaskSearch"
/>
</div>
<FilterPopup
v-model="params"
@update:modelValue="prepareFiltersAndLoadTasks()"
/>
</div>
</div> </div>
</template> </template>
@ -113,14 +73,14 @@
> >
<template v-if="canWrite"> <template v-if="canWrite">
<span class="icon handle"> <span class="icon handle">
<icon icon="grip-lines" /> <icon icon="grip-lines"/>
</span> </span>
</template> </template>
</SingleTaskInProject> </SingleTaskInProject>
</template> </template>
</draggable> </draggable>
<Pagination <Pagination
:total-pages="totalPages" :total-pages="totalPages"
:current-page="currentPage" :current-page="currentPage"
/> />
@ -131,7 +91,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
export default { name: 'List' } export default {name: 'List'}
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
@ -183,7 +143,7 @@ const {
searchTerm, searchTerm,
params, params,
sortByParam, sortByParam,
} = useTaskList(() => projectId, {position: 'asc' }) } = useTaskList(() => projectId, {position: 'asc'})
const tasks = ref<ITask[]>([]) const tasks = ref<ITask[]>([])
watch( watch(
@ -203,7 +163,7 @@ watch(
// If the task is a subtask, make sure the parent task is available in the current view as well // If the task is a subtask, make sure the parent task is available in the current view as well
for (const pt of t.relatedTasks.parenttask) { for (const pt of t.relatedTasks.parenttask) {
if(typeof tasksById[pt.id] === 'undefined') { if (typeof tasksById[pt.id] === 'undefined') {
return true return true
} }
} }
@ -265,16 +225,16 @@ function hideSearchBar() {
} }
const addTaskRef = ref<typeof AddTask | null>(null) const addTaskRef = ref<typeof AddTask | null>(null)
function focusNewTaskInput() { function focusNewTaskInput() {
addTaskRef.value?.focusTaskInput() addTaskRef.value?.focusTaskInput()
} }
function updateTaskList(task: ITask) { function updateTaskList(task: ITask) {
if (isAlphabeticalSorting.value ) { if (isAlphabeticalSorting.value) {
// reload tasks with current filter and sorting // reload tasks with current filter and sorting
loadTasks() loadTasks()
} } else {
else {
allTasks.value = [ allTasks.value = [
task, task,
...allTasks.value, ...allTasks.value,
@ -310,8 +270,8 @@ async function saveTaskPosition(e) {
} }
function prepareFiltersAndLoadTasks() { function prepareFiltersAndLoadTasks() {
if(isAlphabeticalSorting.value) { if (isAlphabeticalSorting.value) {
sortByParam.value = {} sortByParam.value = {}
sortByParam.value[ALPHABETICAL_SORT] = 'asc' sortByParam.value[ALPHABETICAL_SORT] = 'asc'
} }
@ -328,7 +288,7 @@ function prepareFiltersAndLoadTasks() {
border-radius: $radius; border-radius: $radius;
background: var(--grey-100); background: var(--grey-100);
border: 2px dashed var(--grey-300); border: 2px dashed var(--grey-300);
* { * {
opacity: 0; opacity: 0;
} }
@ -339,8 +299,8 @@ function prepareFiltersAndLoadTasks() {
} }
.link-share-view .card { .link-share-view .card {
border: none; border: none;
box-shadow: none; box-shadow: none;
} }
.control.has-icons-left .icon, .control.has-icons-left .icon,

View File

@ -67,7 +67,7 @@
</card> </card>
</template> </template>
</Popup> </Popup>
<FilterPopup v-model="params" /> <FilterPopup v-model="params"/>
</div> </div>
</div> </div>
</template> </template>
@ -84,175 +84,175 @@
<div class="has-horizontal-overflow"> <div class="has-horizontal-overflow">
<table class="table has-actions is-hoverable is-fullwidth mb-0"> <table class="table has-actions is-hoverable is-fullwidth mb-0">
<thead> <thead>
<tr> <tr>
<th v-if="activeColumns.index"> <th v-if="activeColumns.index">
# #
<Sort <Sort
:order="sortBy.index" :order="sortBy.index"
@click="sort('index')" @click="sort('index')"
/> />
</th> </th>
<th v-if="activeColumns.done"> <th v-if="activeColumns.done">
{{ $t('task.attributes.done') }} {{ $t('task.attributes.done') }}
<Sort <Sort
:order="sortBy.done" :order="sortBy.done"
@click="sort('done')" @click="sort('done')"
/> />
</th> </th>
<th v-if="activeColumns.title"> <th v-if="activeColumns.title">
{{ $t('task.attributes.title') }} {{ $t('task.attributes.title') }}
<Sort <Sort
:order="sortBy.title" :order="sortBy.title"
@click="sort('title')" @click="sort('title')"
/> />
</th> </th>
<th v-if="activeColumns.priority"> <th v-if="activeColumns.priority">
{{ $t('task.attributes.priority') }} {{ $t('task.attributes.priority') }}
<Sort <Sort
:order="sortBy.priority" :order="sortBy.priority"
@click="sort('priority')" @click="sort('priority')"
/> />
</th> </th>
<th v-if="activeColumns.labels"> <th v-if="activeColumns.labels">
{{ $t('task.attributes.labels') }} {{ $t('task.attributes.labels') }}
</th> </th>
<th v-if="activeColumns.assignees"> <th v-if="activeColumns.assignees">
{{ $t('task.attributes.assignees') }} {{ $t('task.attributes.assignees') }}
</th> </th>
<th v-if="activeColumns.dueDate"> <th v-if="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }} {{ $t('task.attributes.dueDate') }}
<Sort <Sort
:order="sortBy.due_date" :order="sortBy.due_date"
@click="sort('due_date')" @click="sort('due_date')"
/> />
</th> </th>
<th v-if="activeColumns.startDate"> <th v-if="activeColumns.startDate">
{{ $t('task.attributes.startDate') }} {{ $t('task.attributes.startDate') }}
<Sort <Sort
:order="sortBy.start_date" :order="sortBy.start_date"
@click="sort('start_date')" @click="sort('start_date')"
/> />
</th> </th>
<th v-if="activeColumns.endDate"> <th v-if="activeColumns.endDate">
{{ $t('task.attributes.endDate') }} {{ $t('task.attributes.endDate') }}
<Sort <Sort
:order="sortBy.end_date" :order="sortBy.end_date"
@click="sort('end_date')" @click="sort('end_date')"
/> />
</th> </th>
<th v-if="activeColumns.percentDone"> <th v-if="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }} {{ $t('task.attributes.percentDone') }}
<Sort <Sort
:order="sortBy.percent_done" :order="sortBy.percent_done"
@click="sort('percent_done')" @click="sort('percent_done')"
/> />
</th> </th>
<th v-if="activeColumns.doneAt"> <th v-if="activeColumns.doneAt">
{{ $t('task.attributes.doneAt') }} {{ $t('task.attributes.doneAt') }}
<Sort <Sort
:order="sortBy.done_at" :order="sortBy.done_at"
@click="sort('done_at')" @click="sort('done_at')"
/> />
</th> </th>
<th v-if="activeColumns.created"> <th v-if="activeColumns.created">
{{ $t('task.attributes.created') }} {{ $t('task.attributes.created') }}
<Sort <Sort
:order="sortBy.created" :order="sortBy.created"
@click="sort('created')" @click="sort('created')"
/> />
</th> </th>
<th v-if="activeColumns.updated"> <th v-if="activeColumns.updated">
{{ $t('task.attributes.updated') }} {{ $t('task.attributes.updated') }}
<Sort <Sort
:order="sortBy.updated" :order="sortBy.updated"
@click="sort('updated')" @click="sort('updated')"
/> />
</th> </th>
<th v-if="activeColumns.createdBy"> <th v-if="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }} {{ $t('task.attributes.createdBy') }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="t in tasks" v-for="t in tasks"
:key="t.id" :key="t.id"
> >
<td v-if="activeColumns.index"> <td v-if="activeColumns.index">
<router-link :to="taskDetailRoutes[t.id]"> <router-link :to="taskDetailRoutes[t.id]">
<template v-if="t.identifier === ''"> <template v-if="t.identifier === ''">
#{{ t.index }} #{{ t.index }}
</template> </template>
<template v-else> <template v-else>
{{ t.identifier }} {{ t.identifier }}
</template> </template>
</router-link> </router-link>
</td> </td>
<td v-if="activeColumns.done"> <td v-if="activeColumns.done">
<Done <Done
:is-done="t.done" :is-done="t.done"
variant="small" variant="small"
/>
</td>
<td v-if="activeColumns.title">
<router-link :to="taskDetailRoutes[t.id]">
{{ t.title }}
</router-link>
</td>
<td v-if="activeColumns.priority">
<PriorityLabel
:priority="t.priority"
:done="t.done"
:show-all="true"
/>
</td>
<td v-if="activeColumns.labels">
<Labels :labels="t.labels" />
</td>
<td v-if="activeColumns.assignees">
<AssigneeList
v-if="t.assignees.length > 0"
:assignees="t.assignees"
:avatar-size="28"
class="ml-1"
:inline="true"
/>
</td>
<DateTableCell
v-if="activeColumns.dueDate"
:date="t.dueDate"
/> />
<DateTableCell </td>
v-if="activeColumns.startDate" <td v-if="activeColumns.title">
:date="t.startDate" <router-link :to="taskDetailRoutes[t.id]">
{{ t.title }}
</router-link>
</td>
<td v-if="activeColumns.priority">
<PriorityLabel
:priority="t.priority"
:done="t.done"
:show-all="true"
/> />
<DateTableCell </td>
v-if="activeColumns.endDate" <td v-if="activeColumns.labels">
:date="t.endDate" <Labels :labels="t.labels"/>
</td>
<td v-if="activeColumns.assignees">
<AssigneeList
v-if="t.assignees.length > 0"
:assignees="t.assignees"
:avatar-size="28"
class="ml-1"
:inline="true"
/> />
<td v-if="activeColumns.percentDone"> </td>
{{ t.percentDone * 100 }}% <DateTableCell
</td> v-if="activeColumns.dueDate"
<DateTableCell :date="t.dueDate"
v-if="activeColumns.doneAt" />
:date="t.doneAt" <DateTableCell
v-if="activeColumns.startDate"
:date="t.startDate"
/>
<DateTableCell
v-if="activeColumns.endDate"
:date="t.endDate"
/>
<td v-if="activeColumns.percentDone">
{{ t.percentDone * 100 }}%
</td>
<DateTableCell
v-if="activeColumns.doneAt"
:date="t.doneAt"
/>
<DateTableCell
v-if="activeColumns.created"
:date="t.created"
/>
<DateTableCell
v-if="activeColumns.updated"
:date="t.updated"
/>
<td v-if="activeColumns.createdBy">
<User
:avatar-size="27"
:show-username="false"
:user="t.createdBy"
/> />
<DateTableCell </td>
v-if="activeColumns.created" </tr>
:date="t.created"
/>
<DateTableCell
v-if="activeColumns.updated"
:date="t.updated"
/>
<td v-if="activeColumns.createdBy">
<User
:avatar-size="27"
:show-username="false"
:user="t.createdBy"
/>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -284,9 +284,8 @@ import FilterPopup from '@/components/project/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue' import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup.vue' import Popup from '@/components/misc/popup.vue'
import {useTaskList} from '@/composables/useTaskList'
import type {SortBy} from '@/composables/useTaskList' import type {SortBy} from '@/composables/useTaskList'
import {useTaskList} from '@/composables/useTaskList'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue' import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
@ -381,6 +380,11 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
.columns-filter { .columns-filter {
margin: 0; margin: 0;
:deep(.card-content .content) {
display: flex;
flex-direction: column;
}
&.is-open { &.is-open {
margin: 2rem 0 1rem; margin: 2rem 0 1rem;
} }