1
0

Merge branch 'main' into feature/login-improvements

This commit is contained in:
Dominik Pschenitschni
2022-02-05 18:04:33 +01:00
72 changed files with 2486 additions and 2407 deletions

View File

@ -23,7 +23,7 @@
<template v-if="defaultNamespaceId > 0">
<p class="mt-4">{{ $t('home.list.newText') }}</p>
<x-button
:to="{ name: 'list.create', params: { id: defaultNamespaceId } }"
:to="{ name: 'list.create', params: { namespaceId: defaultNamespaceId } }"
:shadow="false"
class="ml-2"
>

View File

@ -1,6 +1,6 @@
<template>
<div class="gantt-chart-container">
<card :padding="false" class="has-overflow">
<ListWrapper class="list-gantt" :list-id="props.listId" viewName="gantt">
<template #header>
<div class="gantt-options p-4">
<fancycheckbox class="is-block" v-model="showTaskswithoutDates">
{{ $t('list.gantt.showTasksWithoutDates') }}
@ -44,65 +44,64 @@
</div>
</div>
</div>
</template>
<template #default>
<div class="gantt-chart-container">
<card :padding="false" class="has-overflow">
<gantt-chart
:date-from="dateFrom"
:date-to="dateTo"
:day-width="dayWidth"
:list-id="Number($route.params.listId)"
:list-id="props.listId"
:show-taskswithout-dates="showTaskswithoutDates"
/>
<!-- This router view is used to show the task popup while keeping the gantt chart itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component" />
</transition>
</router-view>
</card>
</div>
</div>
</template>
</ListWrapper>
</template>
<script>
import GanttChart from '../../../components/tasks/gantt-component'
<script setup lang="ts">
import { ref, computed } from 'vue'
import flatPickr from 'vue-flatpickr-component'
import Fancycheckbox from '../../../components/input/fancycheckbox'
import {saveListView} from '@/helpers/saveListView'
export default {
name: 'Gantt',
components: {
Fancycheckbox,
flatPickr,
GanttChart,
import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
import ListWrapper from './ListWrapper.vue'
import GanttChart from '@/components/tasks/gantt-component.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
const props = defineProps({
listId: {
type: Number,
required: true,
},
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)
})
const DEFAULT_DAY_COUNT = 35
const showTaskswithoutDates = ref(false)
const dayWidth = ref(DEFAULT_DAY_COUNT)
const now = ref(new Date())
const dateFrom = ref(new Date((new Date()).setDate(now.value.getDate() - 15)))
const dateTo = ref(new Date((new Date()).setDate(now.value.getDate() + 30)))
const {t} = useI18n()
const store = useStore()
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatShort'),
altInput: true,
dateFormat: 'Y-m-d',
enableTime: false,
locale: {
firstDayOfWeek: store.state.auth.settings.weekStart,
},
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

@ -1,17 +1,22 @@
<template>
<div class="kanban-view">
<div class="filter-container" v-if="isSavedFilter">
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
<template #header>
<div class="filter-container" v-if="isSavedFilter">
<div class="items">
<filter-popup
v-model="params"
@update:modelValue="loadBuckets"
/>
</div>
</div>
<div
:class="{ 'is-loading': loading && !oneTaskUpdating}"
class="kanban kanban-bucket-container loader-container"
>
</div>
</template>
<template #default>
<div class="kanban-view">
<div
:class="{ 'is-loading': loading && !oneTaskUpdating}"
class="kanban kanban-bucket-container loader-container"
>
<draggable
v-bind="dragOptions"
:modelValue="buckets"
@ -204,18 +209,11 @@
</div>
</div>
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component"/>
</transition>
</router-view>
<transition name="modal">
<modal
v-if="showBucketDeleteModal"
@close="showBucketDeleteModal = false"
@submit="deleteBucket()"
v-if="showBucketDeleteModal"
>
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
@ -225,22 +223,24 @@
</template>
</modal>
</transition>
</div>
</div>
</template>
</ListWrapper>
</template>
<script>
import draggable from 'vuedraggable'
import cloneDeep from 'lodash.clonedeep'
import BucketModel from '../../../models/bucket'
import BucketModel from '../../models/bucket'
import {mapState} from 'vuex'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/constants/rights.json'
import Rights from '../../models/constants/rights.json'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import ListWrapper from './ListWrapper'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
import {calculateItemPosition} from '../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card'
const DRAG_OPTIONS = {
@ -257,11 +257,20 @@ const MIN_SCROLL_HEIGHT_PERCENT = 0.25
export default {
name: 'Kanban',
components: {
ListWrapper,
KanbanCard,
Dropdown,
FilterPopup,
draggable,
},
props: {
listId: {
type: Number,
required: true,
},
},
data() {
return {
taskContainerRefs: {},
@ -296,11 +305,7 @@ export default {
},
}
},
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)
},
watch: {
loadBucketParameter: {
handler: 'loadBuckets',
@ -313,7 +318,7 @@ export default {
},
loadBucketParameter() {
return {
listId: this.$route.params.listId,
listId: this.listId,
params: this.params,
}
},
@ -353,16 +358,11 @@ export default {
methods: {
loadBuckets() {
// Prevent trying to load buckets if the task popup view is active
if (this.$route.name !== 'list.kanban') {
return
}
const {listId, params} = this.loadBucketParameter
this.collapsedBuckets = getCollapsedBucketState(listId)
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $route.params =`, this.$route.params)
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $attrs = ${this.$attrs} $route.params =`, this.$route.params)
this.$store.dispatch('kanban/loadBucketsForList', {listId, params})
},
@ -437,7 +437,7 @@ export default {
const task = await this.$store.dispatch('tasks/createNewTask', {
title: this.newTaskText,
bucketId,
listId: this.$route.params.listId,
listId: this.listId,
})
this.newTaskText = ''
this.$store.commit('kanban/addTaskToBucket', task)
@ -459,7 +459,7 @@ export default {
const newBucket = new BucketModel({
title: this.newBucketTitle,
listId: parseInt(this.$route.params.listId),
listId: this.listId,
})
await this.$store.dispatch('kanban/createBucket', newBucket)
@ -479,7 +479,7 @@ export default {
async deleteBucket() {
const bucket = new BucketModel({
id: this.bucketToDelete,
listId: parseInt(this.$route.params.listId),
listId: this.listId,
})
try {
@ -567,7 +567,7 @@ export default {
collapseBucket(bucket) {
this.collapsedBuckets[bucket.id] = true
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)
saveCollapsedBucketState(this.listId, this.collapsedBuckets)
},
unCollapseBucket(bucket) {
if (!this.collapsedBuckets[bucket.id]) {
@ -575,7 +575,7 @@ export default {
}
this.collapsedBuckets[bucket.id] = false
saveCollapsedBucketState(this.$route.params.listId, this.collapsedBuckets)
saveCollapsedBucketState(this.listId, this.collapsedBuckets)
},
},
}
@ -746,4 +746,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
.move-card-leave-active {
display: none;
}
@include modal-transition();
</style>

View File

@ -1,8 +1,6 @@
<template>
<div
:class="{ 'is-loading': taskCollectionService.loading }"
class="loader-container is-max-width-desktop list-view"
>
<ListWrapper class="list-list" :list-id="listId" viewName="list">
<template #header>
<div
class="filter-container"
v-if="list.isSavedFilter && !list.isSavedFilter()"
@ -26,7 +24,7 @@
</div>
<div class="control">
<x-button
:loading="taskCollectionService.loading"
:loading="loading"
@click="searchTasks"
:shadow="false"
>
@ -47,7 +45,13 @@
/>
</div>
</div>
</template>
<template #default>
<div
:class="{ 'is-loading': loading }"
class="loader-container is-max-width-desktop list-view"
>
<card :padding="false" :has-content="false" class="has-overflow">
<template
v-if="!list.isArchived && canWrite && list.id > 0"
@ -59,7 +63,7 @@
/>
</template>
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
{{ $t('list.list.empty') }}
<a @click="focusNewTaskInput()">
{{ $t('list.list.newTaskCta') }}
@ -90,7 +94,6 @@
:disabled="!canWrite"
:the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
>
<template v-if="canWrite">
<span class="icon handle">
@ -118,40 +121,33 @@
/>
</div>
<Pagination
:total-pages="taskCollectionService.totalPages"
<Pagination
:total-pages="totalPages"
:current-page="currentPage"
/>
</card>
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
<router-view v-slot="{ Component }">
<transition name="modal">
<component :is="Component"/>
</transition>
</router-view>
</div>
</div>
</template>
</ListWrapper>
</template>
<script>
import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import { ref, toRef, defineComponent } from 'vue'
import EditTask from '../../../components/tasks/edit-task'
import AddTask from '../../../components/tasks/add-task'
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
import taskList from '../../../components/tasks/mixins/taskList'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/constants/rights.json'
import ListWrapper from './ListWrapper.vue'
import EditTask from '@/components/tasks/edit-task'
import AddTask from '@/components/tasks/add-task'
import SingleTaskInList from '@/components/tasks/partials/singleTaskInList'
import { useTaskList } from '@/composables/taskList'
import Rights from '../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup'
import { ALPHABETICAL_SORT } from '@/components/list/partials/filters'
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import draggable from 'vuedraggable'
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
import {calculateItemPosition} from '../../helpers/calculateItemPosition'
function sortTasks(tasks) {
if (tasks === null || tasks === []) {
@ -171,13 +167,18 @@ function sortTasks(tasks) {
})
}
export default {
export default defineComponent({
name: 'List',
props: {
listId: {
type: Number,
required: true,
},
},
data() {
return {
taskService: new TaskService(),
isTaskEdit: false,
taskEditTask: TaskModel,
ctaVisible: false,
showTaskSearch: false,
@ -188,11 +189,8 @@ export default {
},
}
},
mixins: [
taskList,
],
components: {
Popup,
ListWrapper,
Nothing,
FilterPopup,
SingleTaskInList,
@ -201,10 +199,24 @@ export default {
draggable,
Pagination,
},
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)
setup(props) {
const taskEditTask = ref(null)
const isTaskEdit = ref(false)
// This function initializes the tasks page and loads the first page of tasks
// function beforeLoad() {
// taskEditTask.value = null
// isTaskEdit.value = false
// }
const taskList = useTaskList(toRef(props, 'listId'))
return {
taskEditTask,
isTaskEdit,
...taskList,
}
},
computed: {
isAlphabeticalSorting() {
@ -244,17 +256,11 @@ export default {
// When clicking on the search button, @blur from the input is fired. If we
// would then directly hide the whole search bar directly, no click event
// from the button gets fired. To prevent this, we wait 200ms until we hide
// everything so the button has a chance of firering the search event.
// everything so the button has a chance of firing the search event.
setTimeout(() => {
this.showTaskSearch = false
}, 200)
},
// This function initializes the tasks page and loads the first page of tasks
initTasks(page, search = '') {
this.taskEditTask = null
this.isTaskEdit = false
this.loadTasks(page, search)
},
focusNewTaskInput() {
this.$refs.newTaskInput.$refs.newTaskInput.focus()
},
@ -312,7 +318,7 @@ export default {
this.tasks[e.newIndex] = updatedTask
},
},
}
})
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,311 @@
<template>
<ListWrapper class="list-table" :list-id="listId" viewName="table">
<template #header>
<div class="filter-container">
<div class="items">
<popup>
<template #trigger="{toggle}">
<x-button
@click.prevent.stop="toggle()"
icon="th"
variant="secondary"
>
{{ $t('list.table.columns') }}
</x-button>
</template>
<template #content="{isOpen}">
<card class="columns-filter" :class="{'is-open': isOpen}">
<fancycheckbox v-model="activeColumns.id">#</fancycheckbox>
<fancycheckbox v-model="activeColumns.done">
{{ $t('task.attributes.done') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.title">
{{ $t('task.attributes.title') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.created">
{{ $t('task.attributes.created') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }}
</fancycheckbox>
<fancycheckbox v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</fancycheckbox>
</card>
</template>
</popup>
<filter-popup v-model="params" />
</div>
</div>
</template>
<template #default>
<div :class="{'is-loading': loading}" class="loader-container">
<card :padding="false" :has-content="false">
<div class="has-horizontal-overflow">
<table class="table has-actions is-hoverable is-fullwidth mb-0">
<thead>
<tr>
<th v-if="activeColumns.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')"/>
</th>
<th v-if="activeColumns.title">
{{ $t('task.attributes.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')"/>
</th>
<th v-if="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</th>
<th v-if="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</th>
<th v-if="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
<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')"/>
</th>
<th v-if="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
<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')"/>
</th>
<th v-if="activeColumns.created">
{{ $t('task.attributes.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')"/>
</th>
<th v-if="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</th>
</tr>
</thead>
<tbody>
<tr :key="t.id" v-for="t in tasks">
<td v-if="activeColumns.id">
<router-link :to="taskDetailRoutes[t.id]">
<template v-if="t.identifier === ''">
#{{ t.index }}
</template>
<template v-else>
{{ t.identifier }}
</template>
</router-link>
</td>
<td v-if="activeColumns.done">
<Done :is-done="t.done" variant="small" />
</td>
<td v-if="activeColumns.title">
<router-link :to="taskDetailRoutes[t.id]">{{ t.title }}</router-link>
</td>
<td v-if="activeColumns.priority">
<priority-label :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">
<user
:avatar-size="27"
:is-inline="true"
:key="t.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
v-for="(a, i) in t.assignees"
/>
</td>
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
<date-table-cell :date="t.startDate" v-if="activeColumns.startDate"/>
<date-table-cell :date="t.endDate" v-if="activeColumns.endDate"/>
<td v-if="activeColumns.percentDone">{{ t.percentDone * 100 }}%</td>
<date-table-cell :date="t.created" v-if="activeColumns.created"/>
<date-table-cell :date="t.updated" v-if="activeColumns.updated"/>
<td v-if="activeColumns.createdBy">
<user
:avatar-size="27"
:show-username="false"
:user="t.createdBy"/>
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
:total-pages="totalPages"
:current-page="currentPage"
/>
</card>
</div>
</template>
</ListWrapper>
</template>
<script setup lang="ts">
import { toRef, computed, Ref } from 'vue'
import { useStorage } from '@vueuse/core'
import ListWrapper from './ListWrapper.vue'
import Done from '@/components/misc/Done.vue'
import User from '@/components/misc/user.vue'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials/labels.vue'
import DateTableCell from '@/components/tasks/partials/date-table-cell.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import Sort from '@/components/tasks/partials/sort.vue'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup.vue'
import { useTaskList } from '@/composables/taskList'
import TaskModel from '@/models/task'
const ACTIVE_COLUMNS_DEFAULT = {
id: true,
done: true,
title: true,
priority: false,
labels: true,
assignees: true,
dueDate: true,
startDate: false,
endDate: false,
percentDone: false,
created: false,
updated: false,
createdBy: false,
}
const props = defineProps({
listId: {
type: Number,
required: true,
},
})
type Order = 'asc' | 'desc' | 'none'
interface SortBy {
id : Order
done? : Order
title? : Order
priority? : Order
due_date? : Order
start_date? : Order
end_date? : Order
percent_done? : Order
created? : Order
updated? : Order
}
const SORT_BY_DEFAULT : SortBy = {
id: 'desc',
}
const activeColumns = useStorage('tableViewColumns', { ...ACTIVE_COLUMNS_DEFAULT })
const sortBy = useStorage<SortBy>('tableViewSortBy', { ...SORT_BY_DEFAULT })
const taskList = useTaskList(toRef(props, 'listId'))
const {
loading,
params,
totalPages,
currentPage,
} = taskList
const tasks : Ref<TaskModel[]> = taskList.tasks
Object.assign(params.value, {
filter_by: [],
filter_value: [],
filter_comparator: [],
})
// FIXME: by doing this we can have multiple sort orders
function sort(property : keyof SortBy) {
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]
}
}
// TODO: re-enable opening task detail in modal
// const router = useRouter()
const taskDetailRoutes = computed(() => Object.fromEntries(
tasks.value.map(({id}) => ([
id,
{
name: 'task.detail',
params: { id },
// state: { backdropView: router.currentRoute.value.fullPath },
},
])),
))
</script>
<style lang="scss" scoped>
.table {
background: transparent;
overflow-x: auto;
overflow-y: hidden;
th {
white-space: nowrap;
}
.user {
margin: 0;
}
}
.columns-filter {
margin: 0;
&.is-open {
margin: 2rem 0 1rem;
}
}
</style>

View File

@ -0,0 +1,186 @@
<template>
<div
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
class="loader-container"
>
<div class="switch-view-container">
<div class="switch-view">
<router-link
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
:class="{'is-active': viewName === 'list'}"
: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': viewName === 'gantt'}"
: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': viewName === 'table'}"
: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': viewName === 'kanban'}"
:to="{ name: 'list.kanban', params: { listId } }">
{{ $t('list.kanban.title') }}
</router-link>
</div>
<slot name="header" />
</div>
<transition name="fade">
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</Message>
</transition>
<slot />
</div>
</template>
<script setup lang="ts">
import {ref, shallowRef, computed, watchEffect} from 'vue'
import {useRoute} from 'vue-router'
import Message from '@/components/misc/message.vue'
import ListModel from '@/models/list'
import ListService from '@/services/list'
import {store} from '@/store'
import {CURRENT_LIST} from '@/store/mutation-types'
import {getListTitle} from '@/helpers/getListTitle'
import {saveListToHistory} from '@/modules/listHistory'
import { useTitle } from '@/composables/useTitle'
const props = defineProps({
listId: {
type: Number,
required: true,
},
viewName: {
type: String,
required: true,
},
})
const route = useRoute()
const listService = shallowRef(new ListService())
const loadedListId = ref(0)
const currentList = computed(() => {
return typeof store.state.currentList === 'undefined' ? {
id: 0,
title: '',
isArchived: false,
maxRight: null,
} : store.state.currentList
})
// call again the method if the listId changes
watchEffect(() => loadList(props.listId))
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
async function loadList(listIdToLoad: number) {
const listData = {id: listIdToLoad}
saveListToHistory(listData)
// 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 (
props.viewName === 'list.list' ||
props.viewName === '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 (
(
listIdToLoad === loadedListId.value ||
typeof listIdToLoad === 'undefined' ||
listIdToLoad === currentList.value.id
)
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
) {
return
}
console.debug(`Loading list, props.viewName = ${props.viewName}, $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 = props.listId
}
}
</script>
<style lang="scss" scoped>
.switch-view-container {
@media screen and (max-width: $tablet) {
display: flex;
justify-content: center;
}
}
.switch-view {
background: var(--white);
display: inline-flex;
border-radius: $radius;
font-size: .75rem;
box-shadow: var(--shadow-sm);
height: $switch-view-height;
margin-bottom: 1rem;
padding: .5rem;
a {
padding: .25rem .5rem;
display: block;
border-radius: $radius;
transition: all 100ms;
&:not(:last-child) {
margin-right: .5rem;
}
&.is-active,
&:hover {
color: var(--switch-view-color);
}
&.is-active {
background: var(--primary);
font-weight: bold;
box-shadow: var(--shadow-xs);
}
&:hover {
background: var(--primary);
}
}
}
.is-archived .notification.is-warning {
margin-bottom: 1rem;
}
</style>

View File

@ -61,7 +61,7 @@ export default {
}
this.showError = false
this.list.namespaceId = parseInt(this.$route.params.id)
this.list.namespaceId = parseInt(this.$route.params.namespaceId)
const list = await this.$store.dispatch('lists/createList', this.list)
this.$message.success({message: this.$t('list.create.createdSuccess') })
this.$router.push({

View File

@ -1,211 +0,0 @@
<template>
<div
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
class="loader-container"
>
<div class="switch-view-container">
<div class="switch-view">
<router-link
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
:class="{'is-active': $route.name.includes('list.list')}"
:to="{ name: 'list.list', params: { listId: listId } }">
{{ $t('list.list.title') }}
</router-link>
<router-link
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')"
:class="{'is-active': $route.name.includes('list.gantt')}"
:to="{ name: 'list.gantt', params: { listId: listId } }">
{{ $t('list.gantt.title') }}
</router-link>
<router-link
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')"
:class="{'is-active': $route.name.includes('list.table')}"
:to="{ name: 'list.table', params: { listId: listId } }">
{{ $t('list.table.title') }}
</router-link>
<router-link
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
:class="{'is-active': $route.name.includes('list.kanban')}"
:to="{ name: 'list.kanban', params: { listId: listId } }">
{{ $t('list.kanban.title') }}
</router-link>
</div>
</div>
<transition name="fade">
<message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</message>
</transition>
<router-view/>
</div>
</template>
<script>
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)
},
async loadList() {
if (this.$route.name.includes('.settings.')) {
return
}
const listData = {id: parseInt(this.$route.params.listId)}
saveListToHistory(listData)
this.setTitle(this.currentList.id ? this.getListTitle(this.currentList) : '')
// 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)
}
// 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()
}
// 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
}
// 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()
}
console.debug(`Loading list, $route.name = ${this.$route.name}, $route.params =`, this.$route.params, `, listLoaded = ${this.listLoaded}, currentList = `, this.currentList)
// 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
}
},
},
}
</script>
<style lang="scss" scoped>
.switch-view-container {
@media screen and (max-width: $tablet) {
display: flex;
justify-content: center;
}
}
.switch-view {
background: var(--white);
display: inline-flex;
border-radius: $radius;
font-size: .75rem;
box-shadow: var(--shadow-sm);
height: $switch-view-height;
margin-bottom: 1rem;
padding: .5rem;
a {
padding: .25rem .5rem;
display: block;
border-radius: $radius;
transition: all 100ms;
&:not(:last-child) {
margin-right: .5rem;
}
&.is-active,
&:hover {
color: var(--switch-view-color);
}
&.is-active {
background: var(--primary);
font-weight: bold;
box-shadow: var(--shadow-xs);
}
&:hover {
background: var(--primary);
}
}
}
.is-archived .notification.is-warning {
margin-bottom: 1rem;
}
</style>

View File

@ -3,24 +3,38 @@
:title="$t('list.share.header')"
primary-label=""
>
<component
:id="list.id"
:is="manageUsersComponent"
:userIsAdmin="userIsAdmin"
shareType="user"
type="list"/>
<component
:id="list.id"
:is="manageTeamsComponent"
:userIsAdmin="userIsAdmin"
shareType="team"
type="list"/>
<template v-if="list">
<userTeam
:id="list.id"
:userIsAdmin="userIsAdmin"
shareType="user"
type="list"
/>
<userTeam
:id="list.id"
:userIsAdmin="userIsAdmin"
shareType="team"
type="list"
/>
</template>
<link-sharing :list-id="$route.params.listId" v-if="linkSharingEnabled" class="mt-4"/>
<link-sharing :list-id="listId" v-if="linkSharingEnabled" class="mt-4"/>
</create-edit>
</template>
<script>
<script lang="ts">
export default {
name: 'list-setting-share',
}
</script>
<script lang="ts" setup>
import {ref, computed, watchEffect} from 'vue'
import {useStore} from 'vuex'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core'
import ListService from '@/services/list'
import ListModel from '@/models/list'
import {CURRENT_LIST} from '@/store/mutation-types'
@ -29,43 +43,30 @@ import CreateEdit from '@/components/misc/create-edit.vue'
import LinkSharing from '@/components/sharing/linkSharing.vue'
import userTeam from '@/components/sharing/userTeam.vue'
export default {
name: 'list-setting-share',
data() {
return {
list: ListModel,
listService: new ListService(),
manageUsersComponent: '',
manageTeamsComponent: '',
}
},
components: {
CreateEdit,
LinkSharing,
userTeam,
},
computed: {
linkSharingEnabled() {
return this.$store.state.config.linkSharingEnabled
},
userIsAdmin() {
return this.list.owner && this.list.owner.id === this.$store.state.auth.info.id
},
},
created() {
this.loadList()
},
methods: {
async loadList() {
const list = new ListModel({id: this.$route.params.listId})
const {t} = useI18n()
this.list = await this.listService.get(list)
await this.$store.dispatch(CURRENT_LIST, this.list)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'userTeam'
this.manageUsersComponent = 'userTeam'
this.setTitle(this.$t('list.share.title', {list: this.list.title}))
},
},
const list = ref()
const title = computed(() => list.value?.title
? t('list.share.title', {list: list.value.title})
: '',
)
useTitle(title)
const store = useStore()
const linkSharingEnabled = computed(() => store.state.config.linkSharingEnabled)
const userIsAdmin = computed(() => 'owner' in list.value && list.value.owner.id === store.state.auth.info.id)
async function loadList(listId: number) {
const listService = new ListService()
const newList = await listService.get(new ListModel({id: listId}))
await store.dispatch(CURRENT_LIST, newList)
list.value = newList
}
const route = useRoute()
const listId = computed(() => route.params.listId !== undefined
? parseInt(route.params.listId as string)
: undefined,
)
watchEffect(() => listId.value !== undefined && loadList(listId.value))
</script>

View File

@ -1,331 +0,0 @@
<template>
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
<div class="filter-container">
<div class="items">
<popup>
<template #trigger="{toggle}">
<x-button
@click.prevent.stop="toggle()"
icon="th"
variant="secondary"
>
{{ $t('list.table.columns') }}
</x-button>
</template>
<template #content="{isOpen}">
<card class="columns-filter" :class="{'is-open': isOpen}">
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
{{ $t('task.attributes.done') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
{{ $t('task.attributes.title') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
{{ $t('task.attributes.created') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }}
</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</fancycheckbox>
</card>
</template>
</popup>
<filter-popup
v-model="params"
@update:modelValue="loadTasks()"
/>
</div>
</div>
<card :padding="false" :has-content="false">
<div class="has-horizontal-overflow">
<table class="table has-actions is-hoverable is-fullwidth mb-0">
<thead>
<tr>
<th v-if="activeColumns.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')"/>
</th>
<th v-if="activeColumns.title">
{{ $t('task.attributes.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')"/>
</th>
<th v-if="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</th>
<th v-if="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</th>
<th v-if="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
<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')"/>
</th>
<th v-if="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
<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')"/>
</th>
<th v-if="activeColumns.created">
{{ $t('task.attributes.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')"/>
</th>
<th v-if="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</th>
</tr>
</thead>
<tbody>
<tr :key="t.id" v-for="t in tasks">
<td v-if="activeColumns.id">
<router-link :to="{name: 'task.detail', params: { id: t.id }}">
<template v-if="t.identifier === ''">
#{{ t.index }}
</template>
<template v-else>
{{ t.identifier }}
</template>
</router-link>
</td>
<td v-if="activeColumns.done">
<Done :is-done="t.done" variant="small" />
</td>
<td v-if="activeColumns.title">
<router-link :to="{name: 'task.detail', params: { id: t.id }}">{{ t.title }}</router-link>
</td>
<td v-if="activeColumns.priority">
<priority-label :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">
<user
:avatar-size="27"
:is-inline="true"
:key="t.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
v-for="(a, i) in t.assignees"
/>
</td>
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
<date-table-cell :date="t.startDate" v-if="activeColumns.startDate"/>
<date-table-cell :date="t.endDate" v-if="activeColumns.endDate"/>
<td v-if="activeColumns.percentDone">{{ t.percentDone * 100 }}%</td>
<date-table-cell :date="t.created" v-if="activeColumns.created"/>
<date-table-cell :date="t.updated" v-if="activeColumns.updated"/>
<td v-if="activeColumns.createdBy">
<user
:avatar-size="27"
:show-username="false"
:user="t.createdBy"/>
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
:total-pages="taskCollectionService.totalPages"
: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 taskList from '@/components/tasks/mixins/taskList'
import Done from '@/components/misc/Done.vue'
import User from '@/components/misc/user'
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
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'
export default {
name: 'Table',
components: {
Popup,
Done,
FilterPopup,
Sort,
Fancycheckbox,
DateTableCell,
Labels,
PriorityLabel,
User,
Pagination,
},
mixins: [
taskList,
],
data() {
return {
activeColumns: {
id: true,
done: true,
title: true,
priority: false,
labels: true,
assignees: true,
dueDate: true,
startDate: false,
endDate: false,
percentDone: false,
created: false,
updated: false,
createdBy: false,
},
sortBy: {
id: 'desc',
},
}
},
created() {
const savedShowColumns = localStorage.getItem('tableViewColumns')
if (savedShowColumns !== null) {
this.activeColumns = JSON.parse(savedShowColumns)
}
const savedSortBy = localStorage.getItem('tableViewSortBy')
if (savedSortBy !== null) {
this.sortBy = JSON.parse(savedSortBy)
}
this.params.filter_by = []
this.params.filter_value = []
this.params.filter_comparator = []
this.initTasks(1)
// 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)
},
methods: {
initTasks(page, search = '') {
// 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.
const sortKeys = Object.keys(this.sortBy)
let hasIdFilter = false
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
hasIdFilter = true
break
}
}
if (hasIdFilter) {
sortKeys.push('id')
}
const params = this.params
params.sort_by = []
params.order_by = []
sortKeys.map(s => {
params.sort_by.push(s)
params.order_by.push(this.sortBy[s])
})
this.loadTasks(page, search, params)
},
sort(property) {
const order = this.sortBy[property]
if (typeof order === 'undefined' || order === 'none') {
this.sortBy[property] = 'desc'
} else if (order === 'desc') {
this.sortBy[property] = 'asc'
} else {
delete this.sortBy[property]
}
this.initTasks(this.currentPage, this.searchTerm)
// Save the order to be able to retrieve them later
localStorage.setItem('tableViewSortBy', JSON.stringify(this.sortBy))
},
saveTaskColumns() {
localStorage.setItem('tableViewColumns', JSON.stringify(this.activeColumns))
},
},
}
</script>
<style lang="scss" scoped>
.table-view {
.table {
background: transparent;
overflow-x: auto;
overflow-y: hidden;
th {
white-space: nowrap;
}
.user {
margin: 0;
}
}
}
.columns-filter {
margin: 0;
&.is-open {
margin: 2rem 0 1rem;
}
}
</style>

View File

@ -24,7 +24,7 @@
<section :key="`n${n.id}`" class="namespace" v-for="n in namespaces">
<x-button
:to="{name: 'list.create', params: {id: n.id}}"
:to="{name: 'list.create', params: {namespaceId: n.id}}"
class="is-pulled-right"
variant="secondary"
v-if="n.id > 0 && n.lists.length > 0"
@ -51,7 +51,7 @@
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="n.lists.length === 0">
{{ $t('namespace.noLists') }}
<router-link :to="{name: 'list.create', params: {id: n.id}}">
<router-link :to="{name: 'list.create', params: {namespaceId: n.id}}">
{{ $t('namespace.createList') }}
</router-link>
</p>

View File

@ -4,9 +4,11 @@
@submit="archiveNamespace()"
>
<template #header><span>{{ title }}</span></template>
<template #text>
<p>{{ list.isArchived ? $t('namespace.archive.unarchiveText') : $t('namespace.archive.archiveText') }}</p>
<p>
{{ namespace.isArchived ? $t('namespace.archive.unarchiveText') : $t('namespace.archive.archiveText')}}
</p>
</template>
</modal>
</template>
@ -27,17 +29,18 @@ export default {
created() {
this.namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.title = this.namespace.isArchived ?
this.$t('namespace.archive.titleUnarchive', { namespace: this.namespace.title }) :
this.$t('namespace.archive.titleArchive', { namespace: this.namespace.title })
this.$t('namespace.archive.titleUnarchive', {namespace: this.namespace.title}) :
this.$t('namespace.archive.titleArchive', {namespace: this.namespace.title})
this.setTitle(this.title)
},
methods: {
async archiveNamespace() {
this.namespace.isArchived = !this.namespace.isArchived
try {
const namespace = await this.namespaceService.update(this.namespace)
const namespace = await this.namespaceService.update({
...this.namespace,
isArchived: !this.namespace.isArchived,
})
this.$store.commit('namespaces/setNamespaceById', namespace)
this.$message.success({message: this.$t('namespace.archive.success')})
} finally {

View File

@ -3,69 +3,67 @@
:title="title"
primary-label=""
>
<component
:id="namespace.id"
:is="manageUsersComponent"
:userIsAdmin="userIsAdmin"
shareType="user"
type="namespace"/>
<component
:id="namespace.id"
:is="manageTeamsComponent"
:userIsAdmin="userIsAdmin"
shareType="team"
type="namespace"/>
<template v-if="namespace">
<manageSharing
:id="namespace.id"
:userIsAdmin="userIsAdmin"
shareType="user"
type="namespace"
/>
<manageSharing
:id="namespace.id"
:userIsAdmin="userIsAdmin"
shareType="team"
type="namespace"
/>
</template>
</create-edit>
</template>
<script>
import manageSharing from '@/components/sharing/userTeam.vue'
import CreateEdit from '@/components/misc/create-edit.vue'
<script lang="ts">
export default {
name: 'namespace-setting-share',
}
</script>
<script lang="ts" setup>
import {ref, computed, watchEffect} from 'vue'
import {useStore} from 'vuex'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core'
import NamespaceService from '@/services/namespace'
import NamespaceModel from '@/models/namespace'
export default {
name: 'namespace-setting-share',
data() {
return {
namespaceService: new NamespaceService(),
namespace: new NamespaceModel(),
manageUsersComponent: '',
manageTeamsComponent: '',
title: '',
}
},
components: {
CreateEdit,
manageSharing,
},
beforeMount() {
this.namespace.id = this.$route.params.id
},
watch: {
// call again the method if the route changes
'$route': {
handler: 'loadNamespace',
deep: true,
immediate: true,
},
},
computed: {
userIsAdmin() {
return this.namespace.owner && this.namespace.owner.id === this.$store.state.auth.info.id
},
},
methods: {
async loadNamespace() {
const namespace = new NamespaceModel({id: this.$route.params.id})
this.namespace = await this.namespaceService.get(namespace)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing'
this.title = this.$t('namespace.share.title', { namespace: this.namespace.title })
this.setTitle(this.title)
},
},
import CreateEdit from '@/components/misc/create-edit.vue'
import manageSharing from '@/components/sharing/userTeam.vue'
const {t} = useI18n()
const namespace = ref()
const title = computed(() => namespace.value?.title
? t('namespace.share.title', { namespace: namespace.value.title })
: '',
)
useTitle(title)
const store = useStore()
const userIsAdmin = computed(() => 'owner' in namespace.value && namespace.value.owner.id === store.state.auth.info.id)
async function loadNamespace(namespaceId: number) {
if (!namespaceId) return
const namespaceService = new NamespaceService()
namespace.value = await namespaceService.get(new NamespaceModel({id: namespaceId}))
// TODO: set namespace in store
}
const route = useRoute()
const namespaceId = computed(() => route.params.namespaceId !== undefined
? parseInt(route.params.namespaceId as string)
: undefined,
)
watchEffect(() => namespaceId.value !== undefined && loadNamespace(namespaceId.value))
</script>

View File

@ -231,23 +231,25 @@ export default {
},
setDatesToNextWeek() {
this.cStartDate = new Date()
this.cEndDate = new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
const now = new Date()
this.cStartDate = now
this.cEndDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
this.showOverdue = false
this.setDate()
},
setDatesToNextMonth() {
this.cStartDate = new Date()
this.cEndDate = new Date((new Date()).setMonth((new Date()).getMonth() + 1))
const now = new Date()
this.cStartDate = now
this.cEndDate = new Date((new Date()).setMonth(now.getMonth() + 1))
this.showOverdue = false
this.setDate()
},
showTodaysTasks() {
const d = new Date()
this.cStartDate = new Date()
this.cEndDate = new Date(d.setDate(d.getDate() + 1))
const now = new Date()
this.cStartDate = now
this.cEndDate = new Date((new Date()).setDate(now.getDate() + 1))
this.showOverdue = true
this.setDate()
},

View File

@ -263,6 +263,7 @@
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
</x-button>
<task-subscription
v-if="task.subscription"
entity="task"
:entity-id="task.id"
:subscription="task.subscription"
@ -459,8 +460,10 @@ import {CURRENT_LIST} from '@/store/mutation-types'
import {uploadFile} from '@/helpers/attachments'
import ChecklistSummary from '../../components/tasks/partials/checklist-summary'
export default {
name: 'TaskDetailView',
compatConfig: { ATTR_FALSE_VALUE: false },
components: {
ChecklistSummary,
TaskSubscription,
@ -479,6 +482,14 @@ export default {
description,
heading,
},
props: {
taskId: {
type: Number,
required: true,
},
},
data() {
return {
taskService: new TaskService(),
@ -529,10 +540,6 @@ export default {
},
},
computed: {
taskId() {
const {id} = this.$route.params
return id === undefined ? id : Number(id)
},
currentList() {
return this.$store.state[CURRENT_LIST]
},
@ -948,4 +955,6 @@ $flash-background-duration: 750ms;
}
}
}
@include modal-transition();
</style>

View File

@ -1,71 +0,0 @@
<template>
<modal
@close="close()"
variant="scrolling"
class="task-detail-view-modal"
>
<a @click="close()" class="close">
<icon icon="times"/>
</a>
<task-detail-view/>
</modal>
</template>
<script>
import TaskDetailView from './TaskDetailView'
export default {
name: 'TaskDetailViewModal',
components: {
TaskDetailView,
},
data() {
return {
lastRoute: null,
}
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.lastRoute = from
})
},
beforeRouteLeave(to, from, next) {
if (from.name === 'task.kanban.detail' && to.name === 'task.detail') {
this.$router.replace({name: 'task.kanban.detail', params: to.params})
return
}
next()
},
methods: {
close() {
if (this.lastRoute === null) {
this.$router.back()
} else {
this.$router.push(this.lastRoute)
}
},
},
}
</script>
<style lang="scss" scoped>
.close {
position: fixed;
top: 5px;
right: 26px;
color: var(--white);
font-size: 2rem;
@media screen and (max-width: $desktop) {
color: var(--dark);
}
}
</style>
<style lang="scss">
// Close icon SVG uses currentColor, change the color to keep it visible
.dark .task-detail-view-modal .close {
color: var(--grey-900);
}
</style>

View File

@ -308,4 +308,6 @@ export default {
padding: 0;
}
}
@include modal-transition();
</style>