feat: unify modal view

fix: List.vue
This commit is contained in:
Dominik Pschenitschni 2021-11-01 18:19:59 +01:00
parent 281c922de1
commit c70211ad32
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
13 changed files with 314 additions and 411 deletions

View File

@ -22,12 +22,9 @@
<router-view :route="routeWithModal"/>
<!-- TODO: is this still used? -->
<router-view name="popup" v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
</transition>
</router-view>
<transition name="modal">
<component v-if="currentModal" :is="currentModal" />
</transition>
<a
class="keyboard-shortcuts-button"
@ -42,7 +39,7 @@
</template>
<script lang="ts" setup>
import {watch, computed} from 'vue'
import {watch, computed, shallowRef, watchEffect} from 'vue'
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
@ -64,7 +61,16 @@ function useRouteWithModal() {
}
})
return { routeWithModal }
const currentModal = shallowRef(null)
watchEffect(() => {
currentModal.value = historyState.value.backgroundView
? route.matched[0]?.components.default
: null
})
return { routeWithModal, currentModal }
}
useRouteWithModal()

View File

@ -2,21 +2,22 @@
<dropdown>
<template v-if="isSavedFilter">
<dropdown-item
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
icon="trash-alt"
>
{{ $t('misc.delete') }}
</dropdown-item>
</template>
<template v-else-if="list.isArchived">
<dropdown-item
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.unarchive') }}
@ -24,32 +25,32 @@
</template>
<template v-else>
<dropdown-item
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.background`, params: { listId: list.id } }"
v-if="backgroundsEnabled"
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
icon="image"
>
{{ $t('menu.setBackground') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.share`, params: { listId: list.id } }"
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.duplicate`, params: { listId: list.id } }"
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
icon="paste"
>
{{ $t('menu.duplicate') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.archive') }}
@ -63,7 +64,7 @@
@change="sub => subscription = sub"
/>
<dropdown-item
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
icon="trash-alt"
class="has-text-danger"
>
@ -101,24 +102,7 @@ export default {
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
},
listRoutePrefix() {
let name = 'list'
if (this.$route.name !== null && this.$route.name.startsWith('list.')) {
// HACK: we should implement a better routing for the modals
const settingsRoutes = ['edit', 'delete', 'archive', 'background', 'share', 'duplicate']
const suffix = settingsRoutes.find((route) => this.$route.name.endsWith(`.settings.${route}`))
name = this.$route.name.replace(`.settings.${suffix}`,'')
}
if (this.isSavedFilter) {
name = name.replace('list.', 'filter.')
}
return `${name}.settings`
return this.$store.state.config.enabledBackgroundProviders?.length > 0
},
isSavedFilter() {
return getSavedFilterIdFromListId(this.list.id) > 0

View File

@ -16,7 +16,7 @@ export const getDefaultParams = () => ({
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function createTaskList(initTasks) {
export function useTaskList(initTasks) {
const taskCollectionService = ref(new TaskCollectionService())
const loading = computed(() => taskCollectionService.value.loading)
const totalPages = computed(() => taskCollectionService.value.totalPages)
@ -70,12 +70,14 @@ export function createTaskList(initTasks) {
tasks.value = await taskCollectionService.value.getAll(list, loadParams, page)
currentPage.value = page
loadedList.value = JSON.parse(JSON.stringify(currentList))
return tasks.value
}
async function loadTasksForPage(query) {
const { page, search } = query
initTasks(params)
await loadTasks(
return await loadTasks(
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
typeof page === 'undefined' ? 1 : Number(page),
search,

View File

@ -13,8 +13,8 @@ import DataExportDownload from '../views/user/DataExportDownload'
// Tasks
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange'
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
import TaskDetailView from '../views/tasks/TaskDetailView'
import ListNamespaces from '../views/namespaces/ListNamespaces'
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal'
// Team Handling
import ListTeamsComponent from '../views/teams/ListTeams'
// Label Handling
@ -29,6 +29,7 @@ import Kanban from '../views/list/views/Kanban'
import List from '../views/list/views/List'
import Gantt from '../views/list/views/Gantt'
import Table from '../views/list/views/Table'
// List Settings
import ListSettingEdit from '../views/list/settings/edit'
import ListSettingBackground from '../views/list/settings/background'
@ -200,109 +201,123 @@ const router = createRouter({
{
path: '/namespaces/new',
name: 'namespace.create',
components: {
popup: NewNamespaceComponent,
},
},
{
path: '/namespaces/:id/list',
name: 'list.create',
components: {
popup: NewListComponent,
component: NewNamespaceComponent,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/edit',
name: 'namespace.settings.edit',
components: {
popup: NamespaceSettingEdit,
component: NamespaceSettingEdit,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/share',
name: 'namespace.settings.share',
components: {
popup: NamespaceSettingShare,
component: NamespaceSettingShare,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/archive',
name: 'namespace.settings.archive',
components: {
popup: NamespaceSettingArchive,
component: NamespaceSettingArchive,
meta: {
showAsModal: true,
},
},
{
path: '/namespaces/:id/settings/delete',
name: 'namespace.settings.delete',
components: {
popup: NamespaceSettingDelete,
component: NamespaceSettingDelete,
meta: {
showAsModal: true,
},
},
{
path: '/tasks/:id',
name: 'task.detail',
component: TaskDetailView,
component: TaskDetailViewModal,
},
{
path: '/tasks/by/upcoming',
name: 'tasks.range',
component: ShowTasksInRangeComponent,
},
{
path: '/lists/:id/new',
name: 'list.create',
component: NewListComponent,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/edit',
name: 'list.settings.edit',
components: {
popup: ListSettingEdit,
component: ListSettingEdit,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/background',
name: 'list.settings.background',
components: {
popup: ListSettingBackground,
component: ListSettingBackground,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.settings.duplicate',
components: {
popup: ListSettingDuplicate,
component: ListSettingDuplicate,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/share',
name: 'list.settings.share',
components: {
popup: ListSettingShare,
component: ListSettingShare,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'list.settings.delete',
components: {
popup: ListSettingDelete,
component: ListSettingDelete,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/archive',
name: 'list.settings.archive',
components: {
popup: ListSettingArchive,
component: ListSettingArchive,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.settings.edit',
components: {
popup: FilterEdit,
component: FilterEdit,
meta: {
showAsModal: true,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.settings.delete',
components: {
popup: FilterDelete,
component: FilterDelete,
meta: {
showAsModal: true,
},
},
{
@ -340,8 +355,9 @@ const router = createRouter({
{
path: '/teams/new',
name: 'teams.create',
components: {
popup: NewTeamComponent,
component: NewTeamComponent,
meta: {
showAsModal: true,
},
},
{
@ -357,8 +373,9 @@ const router = createRouter({
{
path: '/labels/new',
name: 'labels.create',
components: {
popup: NewLabelComponent,
component: NewLabelComponent,
meta: {
showAsModal: true,
},
},
{
@ -374,8 +391,9 @@ const router = createRouter({
{
path: '/filters/new',
name: 'filters.create',
components: {
popup: FilterNew,
component: FilterNew,
meta: {
showAsModal: true,
},
},
{

View File

@ -1,12 +1,12 @@
import {HTTPFactory} from '@/http-common'
import {getCurrentLanguage, saveLanguage} from '@/i18n'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {LOADING} from '../mutation-types'
import UserModel from '@/models/user'
import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper'
import {i18n} from '@/i18n'
import {success} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider'
const AUTH_TYPES = {
'UNKNOWN': 0,
@ -201,7 +201,19 @@ export default {
ctx.commit('authenticated', authenticated)
if (!authenticated) {
ctx.commit('info', null)
ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true})
ctx.dispatch('redirectToProviderIfNothingElseIsEnabled')
}
},
redirectToProviderIfNothingElseIsEnabled({rootState}) {
const {auth} = rootState.config
if (
auth.local.enabled === false &&
auth.openidConnect.enabled &&
auth.openidConnect.providers?.length === 1 &&
window.location.pathname.startsWith('/login') // Kinda hacky, but prevents an endless loop.
) {
redirectToProvider(auth.openidConnect.providers[0], auth.openidConnect.redirectUrl)
}
},

View File

@ -1,7 +1,6 @@
import {CONFIG} from '../mutation-types'
import {HTTPFactory} from '@/http-common'
import {objectToCamelCase} from '@/helpers/case'
import {redirectToProvider} from '../../helpers/redirectToProvider'
import {parseURL} from 'ufo'
export default {
@ -75,16 +74,5 @@ export default {
ctx.commit(CONFIG, info)
return info
},
redirectToProviderIfNothingElseIsEnabled(ctx) {
if (ctx.state.auth.local.enabled === false &&
ctx.state.auth.openidConnect.enabled &&
ctx.state.auth.openidConnect.providers &&
ctx.state.auth.openidConnect.providers.length === 1 &&
window.location.pathname.startsWith('/login') // Kinda hacky, but prevents an endless loop.
) {
redirectToProvider(ctx.state.auth.openidConnect.providers[0])
}
},
},
}

View File

@ -51,10 +51,6 @@
</div>
</div>
<ShowTasks class="mt-4" :show-all="true" v-if="hasLists" :key="showTasksKey"/>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</div>
</template>
@ -71,9 +67,6 @@ import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
const showTaskDetail = useShowModal()
const welcome = useDateTimeSalutation()

View File

@ -9,153 +9,128 @@
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
:class="{'is-active': $route.name === 'list.list'}"
:to="{ name: 'list.list', params: { listId: listId } }">
:to="{ name: 'list.list', params: { listId } }">
{{ $t('list.list.title') }}
</router-link>
<router-link
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')"
:class="{'is-active': $route.name === 'list.gantt'}"
:to="{ name: 'list.gantt', params: { listId: listId } }">
:to="{ name: 'list.gantt', params: { listId } }">
{{ $t('list.gantt.title') }}
</router-link>
<router-link
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')"
:class="{'is-active': $route.name === 'list.table'}"
:to="{ name: 'list.table', params: { listId: listId } }">
:to="{ name: 'list.table', params: { listId } }">
{{ $t('list.table.title') }}
</router-link>
<router-link
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
:class="{'is-active': $route.name === 'list.kanban'}"
:to="{ name: 'list.kanban', params: { listId: listId } }">
:to="{ name: 'list.kanban', params: { listId } }">
{{ $t('list.kanban.title') }}
</router-link>
</div>
</div>
<transition name="fade">
<message variant="warning" v-if="currentList.isArchived" class="mb-4">
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</message>
</Message>
</transition>
<router-view/>
</div>
</template>
<script>
<script setup>
import {ref, shallowRef, computed, watchEffect} from 'vue'
import {useRouter, useRoute} from 'vue-router'
import Message from '@/components/misc/message'
import ListModel from '../../models/list'
import ListService from '../../services/list'
import {CURRENT_LIST} from '../../store/mutation-types'
import {getListView} from '../../helpers/saveListView'
import {saveListToHistory} from '../../modules/listHistory'
export default {
components: {Message},
data() {
return {
listService: new ListService(),
listLoaded: 0,
}
},
watch: {
// call again the method if the route changes
'$route.path': {
handler: 'loadList',
immediate: true,
},
},
computed: {
// Computed property to let "listId" always have a value
listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
},
background() {
return this.$store.state.background
},
currentList() {
return typeof this.$store.state.currentList === 'undefined' ? {
id: 0,
title: '',
isArchived: false,
} : this.$store.state.currentList
},
},
methods: {
replaceListView() {
const savedListView = getListView(this.$route.params.listId)
this.$router.replace({name: savedListView, params: {id: this.$route.params.listId}})
console.debug('Replaced list view with', savedListView)
},
import ListModel from '@/models/list'
import ListService from '@/services/list'
async loadList() {
if (this.$route.name.includes('.settings.')) {
return
}
import {store} from '@/store'
import {CURRENT_LIST} from '@/store/mutation-types'
const listData = {id: parseInt(this.$route.params.listId)}
import {getListView} from '@/helpers/saveListView'
import {getListTitle} from '@/helpers/getListTitle'
import {saveListToHistory} from '@/modules/listHistory'
import { useTitle } from '@/composables/useTitle'
saveListToHistory(listData)
const route = useRoute()
const router = useRouter()
this.setTitle(this.currentList.id ? this.getListTitle(this.currentList) : '')
const listService = shallowRef(new ListService())
const loadedListId = ref(0)
// This invalidates the loaded list at the kanban board which lets it reload its content when
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
// shown in all views while preventing reloads when closing a task popup.
// We don't do this for the table view because that does not change tasks.
if (
this.$route.name === 'list.list' ||
this.$route.name === 'list.gantt'
) {
this.$store.commit('kanban/setListId', 0)
}
// beforeRouteEnter(to) {
// Redirect the user to list view by default
if (route.name !== 'list.index') {
const savedListView = getListView(route.params.listId)
console.debug('Replaced list view with', savedListView)
router.replace({name: 'list.list', params: {id: route.params.listId}})
}
// },
// // When clicking again on a list in the menu, there would be no list view selected which means no list
// // at all. Users will then have to click on the list view menu again which is quite confusing.
// if (this.$route.name === 'list.index') {
// return this.replaceListView()
// }
const currentList = computed(() => {
return typeof store.state.currentList === 'undefined' ? {
id: 0,
title: '',
isArchived: false,
} : store.state.currentList
})
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
// the currently loaded list has the right set.
if (
(
this.$route.params.listId === this.listLoaded ||
typeof this.$route.params.listId === 'undefined' ||
this.$route.params.listId === this.currentList.id ||
parseInt(this.$route.params.listId) === this.currentList.id
)
&& typeof this.currentList !== 'undefined' && this.currentList.maxRight !== null
) {
return
}
// Computed property to let "listId" always have a value
const listId = computed(() => typeof route.params.listId === 'undefined' ? 0 : parseInt(route.params.listId))
// call again the method if the listId changes
watchEffect(() => loadList(listId.value))
// Redirect the user to list view by default
if (
this.$route.name !== 'list.list' &&
this.$route.name !== 'list.gantt' &&
this.$route.name !== 'list.table' &&
this.$route.name !== 'list.kanban'
) {
return this.replaceListView()
}
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
console.debug(`Loading list, $route.name = ${this.$route.name}, $route.params =`, this.$route.params, `, listLoaded = ${this.listLoaded}, currentList = `, this.currentList)
async function loadList(listId) {
const listData = {id: listId}
saveListToHistory(listData)
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
try {
const loadedList = await this.listService.get(list)
await this.$store.dispatch(CURRENT_LIST, loadedList)
this.setTitle(this.getListTitle(loadedList))
} finally {
this.listLoaded = this.$route.params.listId
}
},
},
// This invalidates the loaded list at the kanban board which lets it reload its content when
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
// shown in all views while preventing reloads when closing a task popup.
// We don't do this for the table view because that does not change tasks.
// FIXME: remove this
if (
route.name === 'list.list' ||
route.name === 'list.gantt'
) {
store.commit('kanban/setListId', 0)
}
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
// the currently loaded list has the right set.
if (
(
listId.value === loadedListId.value ||
typeof listId.value === 'undefined' ||
listId.value === currentList.value.id
)
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
) {
return
}
console.debug(`Loading list, $route.name = ${route.name}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value)
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
try {
const loadedList = await listService.value.get(list)
await store.dispatch(CURRENT_LIST, loadedList)
} finally {
loadedListId.value = listId
}
}
</script>

View File

@ -52,61 +52,43 @@
:show-taskswithout-dates="showTaskswithoutDates"
/>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</card>
</div>
</template>
<script>
import GanttChart from '../../../components/tasks/gantt-component'
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import flatPickr from 'vue-flatpickr-component'
import Fancycheckbox from '../../../components/input/fancycheckbox'
import { i18n } from '@/i18n'
import { store } from '@/store'
import GanttChart from '@/components/tasks/gantt-component'
import Fancycheckbox from '@/components/input/fancycheckbox'
import {saveListView} from '@/helpers/saveListView'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
const route = useRoute()
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
saveListView(route.params.listId, route.name)
export default {
name: 'Gantt',
components: {
Fancycheckbox,
flatPickr,
GanttChart,
TaskDetailViewModal,
const showTaskswithoutDates = ref(false)
const dayWidth = ref(35)
const dateFrom = ref(new Date((new Date()).setDate((new Date()).getDate() - 15)))
const dateTo = ref(new Date((new Date()).setDate((new Date()).getDate() + 30)))
const flatPickerConfig = computed(() => ({
altFormat: i18n.global.t('date.altFormatShort'),
altInput: true,
dateFormat: 'Y-m-d',
enableTime: false,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
},
setup() {
return {
showTaskDetail: useShowModal(),
}
},
created() {
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
saveListView(this.$route.params.listId, this.$route.name)
},
data() {
return {
showTaskswithoutDates: false,
dayWidth: 35,
dateFrom: new Date((new Date()).setDate((new Date()).getDate() - 15)),
dateTo: new Date((new Date()).setDate((new Date()).getDate() + 30)),
}
},
computed: {
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatShort'),
altInput: true,
dateFormat: 'Y-m-d',
enableTime: false,
locale: {
firstDayOfWeek: this.$store.state.auth.settings.weekStart,
},
}
},
},
}
}))
</script>
<style lang="scss">

View File

@ -205,9 +205,8 @@
</div>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
<modal
v-else-if="showBucketDeleteModal"
v-if="showBucketDeleteModal"
@close="showBucketDeleteModal = false"
@submit="deleteBucket()"
>
@ -236,7 +235,6 @@ import Dropdown from '@/components/misc/dropdown.vue'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
const DRAG_OPTIONS = {
// sortable options
@ -256,7 +254,6 @@ export default {
Dropdown,
FilterPopup,
draggable,
TaskDetailViewModal,
},
data() {
return {
@ -293,12 +290,6 @@ export default {
}
},
setup() {
return {
showTaskDetail: useShowModal(),
}
},
created() {
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.

View File

@ -122,10 +122,6 @@
:current-page="currentPage"
/>
</card>
<transition name="modal">
<task-detail-view-modal v-if="showTaskDetail" />
</transition>
</div>
</template>
@ -133,13 +129,10 @@
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import EditTask from '../../../components/tasks/edit-task'
import AddTask from '../../../components/tasks/add-task'
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
import { createTaskList } from '@/composables/taskList'
import { useTaskList } from '@/composables/taskList'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
@ -147,7 +140,6 @@ import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue'
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import TaskDetailViewModal, { useShowModal } from '@/views/tasks/TaskDetailViewModal.vue'
import draggable from 'vuedraggable'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
@ -174,7 +166,6 @@ export default {
name: 'List',
data() {
return {
taskService: new TaskService(),
ctaVisible: false,
showTaskSearch: false,
@ -193,11 +184,10 @@ export default {
AddTask,
draggable,
Pagination,
TaskDetailViewModal,
},
setup() {
const taskEditTask = ref(TaskModel)
const taskEditTask = ref(null)
const isTaskEdit = ref(false)
// This function initializes the tasks page and loads the first page of tasks
@ -206,17 +196,18 @@ export default {
isTaskEdit.value = false
}
const taskList = createTaskList(beforeLoad)
const taskList = useTaskList(beforeLoad)
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
const route = useRoute()
saveListView(route.params.listId, route.name)
taskList.initTaskList()
return {
taskEditTask,
isTaskEdit,
showTaskDetail: useShowModal(),
...taskList,
}
},

View File

@ -68,19 +68,19 @@
<tr>
<th v-if="activeColumns.id">
#
<sort :order="sortBy.id" @click="sort('id')"/>
<Sort :order="sortBy.id" @click="sort('id')"/>
</th>
<th v-if="activeColumns.done">
{{ $t('task.attributes.done') }}
<sort :order="sortBy.done" @click="sort('done')"/>
<Sort :order="sortBy.done" @click="sort('done')"/>
</th>
<th v-if="activeColumns.title">
{{ $t('task.attributes.title') }}
<sort :order="sortBy.title" @click="sort('title')"/>
<Sort :order="sortBy.title" @click="sort('title')"/>
</th>
<th v-if="activeColumns.priority">
{{ $t('task.attributes.priority') }}
<sort :order="sortBy.priority" @click="sort('priority')"/>
<Sort :order="sortBy.priority" @click="sort('priority')"/>
</th>
<th v-if="activeColumns.labels">
{{ $t('task.attributes.labels') }}
@ -90,27 +90,27 @@
</th>
<th v-if="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
<sort :order="sortBy.due_date" @click="sort('due_date')"/>
<Sort :order="sortBy.due_date" @click="sort('due_date')"/>
</th>
<th v-if="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
<sort :order="sortBy.start_date" @click="sort('start_date')"/>
<Sort :order="sortBy.start_date" @click="sort('start_date')"/>
</th>
<th v-if="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
<sort :order="sortBy.end_date" @click="sort('end_date')"/>
<Sort :order="sortBy.end_date" @click="sort('end_date')"/>
</th>
<th v-if="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
<sort :order="sortBy.percent_done" @click="sort('percent_done')"/>
<Sort :order="sortBy.percent_done" @click="sort('percent_done')"/>
</th>
<th v-if="activeColumns.created">
{{ $t('task.attributes.created') }}
<sort :order="sortBy.created" @click="sort('created')"/>
<Sort :order="sortBy.created" @click="sort('created')"/>
</th>
<th v-if="activeColumns.updated">
{{ $t('task.attributes.updated') }}
<sort :order="sortBy.updated" @click="sort('updated')"/>
<Sort :order="sortBy.updated" @click="sort('updated')"/>
</th>
<th v-if="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
@ -173,22 +173,13 @@
:current-page="currentPage"
/>
</card>
<!-- This router view is used to show the task popup while keeping the table view itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<script>
import { defineComponent, ref, reactive, computed, toRaw } from 'vue'
<script setup>
import { ref, reactive, computed, toRaw } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { createTaskList } from '@/composables/taskList'
import Done from '@/components/misc/Done.vue'
import User from '@/components/misc/user'
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
@ -196,11 +187,13 @@ import Labels from '@/components/tasks/partials/labels'
import DateTableCell from '@/components/tasks/partials/date-table-cell'
import Fancycheckbox from '@/components/input/fancycheckbox'
import Sort from '@/components/tasks/partials/sort'
import {saveListView} from '@/helpers/saveListView'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup'
import { useTaskList } from '@/composables/taskList'
import {saveListView} from '@/helpers/saveListView'
const ACTIVE_COLUMNS_DEFAULT = {
id: true,
done: true,
@ -233,102 +226,86 @@ function useSavedView(activeColumns, sortBy) {
}
}
export default defineComponent({
name: 'Table',
components: {
Popup,
Done,
FilterPopup,
Sort,
Fancycheckbox,
DateTableCell,
Labels,
PriorityLabel,
User,
Pagination,
},
setup() {
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
const sortBy = ref({ ...SORT_BY_DEFAULT })
const activeColumns = reactive({ ...ACTIVE_COLUMNS_DEFAULT })
const sortBy = ref({ ...SORT_BY_DEFAULT })
useSavedView(activeColumns, sortBy)
useSavedView(activeColumns, sortBy)
function beforeLoad(params) {
// This makes sure an id sort order is always sorted last.
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
// precedence over everything else, making any other sort columns pretty useless.
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
params.value.sort_by = sortKeys
params.value.order_by = sortKeys.map(s => sortBy.value[s])
function beforeLoad(params) {
// This makes sure an id sort order is always sorted last.
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
// precedence over everything else, making any other sort columns pretty useless.
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
params.value.sort_by = sortKeys
params.value.order_by = sortKeys.map(s => sortBy.value[s])
}
const taskList = createTaskList(beforeLoad)
const {
tasks,
loading,
showTaskFilter,
params,
loadTasks,
totalPages,
currentPage,
searchTerm,
initTaskList,
} = useTaskList(beforeLoad)
Object.assign(taskList.params.value, {
filter_by: [],
filter_value: [],
filter_comparator: [],
})
const router = useRouter()
const taskDetailRoutes = computed(() => Object.fromEntries(
taskList.tasks.value.map(({id}) => ([
id,
{
name: 'task.detail',
params: { id },
state: { backgroundView: router.currentRoute.value.fullPath },
},
])),
))
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
const route = useRoute()
saveListView(route.params.listId, route.name)
function sort(property) {
const order = sortBy.value[property]
if (typeof order === 'undefined' || order === 'none') {
sortBy.value[property] = 'desc'
} else if (order === 'desc') {
sortBy.value[property] = 'asc'
} else {
delete sortBy.value[property]
}
beforeLoad(taskList.currentPage.value, taskList.searchTerm.value)
// Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(sortBy.value))
}
function saveTaskColumns() {
localStorage.setItem('tableViewColumns', JSON.stringify(toRaw(activeColumns)))
}
taskList.initTaskList()
return {
...taskList,
sortBy,
activeColumns,
sort,
saveTaskColumns,
taskDetailRoutes,
}
},
Object.assign(params.value, {
filter_by: [],
filter_value: [],
filter_comparator: [],
})
const router = useRouter()
const taskDetailRoutes = computed(() => Object.fromEntries(
tasks.value.map(({id}) => ([
id,
{
name: 'task.detail',
params: { id },
state: { backgroundView: router.currentRoute.value.fullPath },
},
])),
))
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
const route = useRoute()
saveListView(route.params.listId, route.name)
function sort(property) {
const order = sortBy.value[property]
if (typeof order === 'undefined' || order === 'none') {
sortBy.value[property] = 'desc'
} else if (order === 'desc') {
sortBy.value[property] = 'asc'
} else {
delete sortBy.value[property]
}
beforeLoad(currentPage.value, searchTerm.value)
// Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(sortBy.value))
}
function saveTaskColumns() {
localStorage.setItem('tableViewColumns', JSON.stringify(toRaw(activeColumns)))
}
initTaskList()
</script>
<style lang="scss" scoped>

View File

@ -4,35 +4,19 @@
variant="scrolling"
class="task-detail-view-modal"
>
<a @click="close()" class="close">
<icon icon="times"/>
</a>
<task-detail-view/>
<a @click="close()" class="close">
<icon icon="times"/>
</a>
<task-detail-view/>
</modal>
</template>
<script>
<script setup>
import TaskDetailView from './TaskDetailView'
import {computed} from 'vue'
import {useRoute} from 'vue-router'
import router from '@/router'
export function useShowModal() {
const route = useRoute()
const historyState = computed(() => route.fullPath && window.history.state)
const show = computed(() => historyState.value.backgroundView)
return show
}
export default {
name: 'TaskDetailViewModal',
components: {
TaskDetailView,
},
methods: {
close() {
this.$router.back()
},
},
function close() {
router.back()
}
</script>