feat: rename list to project everywhere
fix: project table view fix: e2e tests fix: typo in readme fix: list view route fix: don't wait until background is loaded for list to show fix: rename component imports fix: lint fix: parse task text fix: use list card grid fix: use correct class names fix: i18n keys fix: load project fix: task overview fix: list view spacing fix: find project fix: setLoading when updating a project fix: loading saved filter fix: project store loading fix: color picker import fix: cypress tests feat: migrate old list settings chore: add const for project settings fix: wrong projecten rename from lists chore: rename unused variable fix: editor list fix: shortcut list class name fix: pagination list class name fix: notifications list class name fix: list view variable name chore: clarify comment fix: i18n keys fix: router imports fix: comment chore: remove debugging leftover fix: remove duplicate variables fix: change comment fix: list view variable name fix: list view css class name fix: list item property name fix: name update tasks function correctly fix: update comment fix: project create route fix: list view class names fix: list view component name fix: result list class name fix: animation class list name fix: change debug log fix: revert a few navigation changes fix: use @ for imports of all views fix: rename link share list class fix: remove unused css class fix: dynamically import project components again
This commit is contained in:
@ -1,67 +1,50 @@
|
||||
<template>
|
||||
<header
|
||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
||||
aria-label="main navigation"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
<router-link :to="{name: 'home'}" class="logo-link">
|
||||
<Logo width="164" height="48"/>
|
||||
<header :class="{ 'has-background': background, 'menu-active': menuActive }" aria-label="main navigation"
|
||||
class="navbar d-print-none">
|
||||
<router-link :to="{ name: 'home' }" class="logo-link">
|
||||
<Logo width="164" height="48" />
|
||||
</router-link>
|
||||
|
||||
<MenuButton class="menu-button"/>
|
||||
<MenuButton class="menu-button" />
|
||||
|
||||
<div
|
||||
v-if="currentList.id"
|
||||
class="list-title-wrapper"
|
||||
>
|
||||
<h1 class="list-title">{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}</h1>
|
||||
|
||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="list-title-button">
|
||||
<icon icon="circle-info"/>
|
||||
<div v-if="currentProject.id" class="project-title-wrapper">
|
||||
<h1 class="project-title">{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
|
||||
</h1>
|
||||
|
||||
<BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
|
||||
<icon icon="circle-info" />
|
||||
</BaseButton>
|
||||
|
||||
<list-settings-dropdown
|
||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
||||
class="list-title-dropdown"
|
||||
:list="currentList"
|
||||
>
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="list-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
<project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1"
|
||||
class="project-title-dropdown" :project="currentProject">
|
||||
<template #trigger="{ toggleOpen }">
|
||||
<BaseButton class="project-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
</project-settings-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<BaseButton
|
||||
@click="openQuickActions"
|
||||
class="trigger-button"
|
||||
v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
<BaseButton @click="openQuickActions" class="trigger-button" v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')">
|
||||
<icon icon="search" />
|
||||
</BaseButton>
|
||||
<Notifications />
|
||||
<dropdown>
|
||||
<template #trigger="{toggleOpen, open}">
|
||||
<BaseButton
|
||||
class="username-dropdown-trigger"
|
||||
@click="toggleOpen"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
||||
<template #trigger="{ toggleOpen, open }">
|
||||
<BaseButton class="username-dropdown-trigger" @click="toggleOpen" variant="secondary" :shadow="false">
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40" />
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span class="icon is-small" :style="{
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||
}">
|
||||
<icon icon="chevron-down"/>
|
||||
<icon icon="chevron-down" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<dropdown-item :to="{name: 'user.settings'}">
|
||||
<dropdown-item :to="{ name: 'user.settings' }">
|
||||
{{ $t('user.settings.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
||||
@ -73,7 +56,7 @@
|
||||
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item :to="{name: 'about'}">
|
||||
<dropdown-item :to="{ name: 'about' }">
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item @click="authStore.logout()">
|
||||
@ -85,11 +68,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
import { RIGHTS as Rights } from '@/constants/rights'
|
||||
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Notifications from '@/components/notifications/notifications.vue'
|
||||
@ -97,16 +80,16 @@ import Logo from '@/components/home/Logo.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import MenuButton from '@/components/home/MenuButton.vue'
|
||||
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import { getProjectTitle } from '@/helpers/getProjectTitle'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { useBaseStore } from '@/stores/base'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const background = computed(() => baseStore.background)
|
||||
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
|
||||
const canWriteCurrentProject = computed(() => baseStore.currentProject.maxRight > Rights.READ)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@ -166,7 +149,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||
|
||||
.logo-link {
|
||||
display: none;
|
||||
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
@ -185,12 +168,12 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-wrapper {
|
||||
.project-title-wrapper {
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// this makes the truncated text of the list title work
|
||||
// this makes the truncated text of the project title work
|
||||
// inside the flexbox parent
|
||||
min-width: 0;
|
||||
|
||||
@ -199,7 +182,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title {
|
||||
.project-title {
|
||||
font-size: 1rem;
|
||||
// We need the following for overflowing ellipsis to work
|
||||
text-overflow: ellipsis;
|
||||
@ -211,15 +194,15 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-dropdown {
|
||||
.project-title-dropdown {
|
||||
align-self: stretch;
|
||||
|
||||
.list-title-button {
|
||||
.project-title-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-button {
|
||||
.project-title-button {
|
||||
align-self: stretch;
|
||||
min-width: var(--navbar-button-min-width);
|
||||
display: flex;
|
||||
@ -235,7 +218,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
> * {
|
||||
>* {
|
||||
min-width: var(--navbar-button-min-width);
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@
|
||||
<quick-actions/>
|
||||
|
||||
<router-view :route="routeWithModal" v-slot="{ Component }">
|
||||
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
||||
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
|
||||
<component :is="Component"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
@ -87,7 +87,7 @@ function showKeyboardShortcuts() {
|
||||
const route = useRoute()
|
||||
|
||||
// FIXME: this is really error prone
|
||||
// Reset the current list highlight in menu if the current route is not list related.
|
||||
// Reset the current project highlight in menu if the current route is not project related.
|
||||
watch(() => route.name as string, (routeName) => {
|
||||
if (
|
||||
routeName &&
|
||||
@ -106,7 +106,7 @@ watch(() => route.name as string, (routeName) => {
|
||||
routeName.startsWith('user.settings')
|
||||
)
|
||||
) {
|
||||
baseStore.handleSetCurrentList({list: null})
|
||||
baseStore.handleSetCurrentProject({project: null})
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -9,9 +9,9 @@
|
||||
<Logo class="logo" v-if="logoVisible"/>
|
||||
<h1
|
||||
:class="{'m-0': !logoVisible}"
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
:style="{ 'opacity': currentProject.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
|
||||
{{ currentProject.title === '' ? $t('misc.loading') : currentProject.title }}
|
||||
</h1>
|
||||
<div class="box has-text-left view">
|
||||
<router-view/>
|
||||
@ -31,7 +31,7 @@ import Logo from '@/components/home/Logo.vue'
|
||||
import PoweredByLink from './PoweredByLink.vue'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const background = computed(() => baseStore.background)
|
||||
const logoVisible = computed(() => baseStore.logoVisible)
|
||||
</script>
|
||||
|
@ -52,37 +52,37 @@
|
||||
<template v-for="(n, nk) in namespaces" :key="n.id">
|
||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||
<BaseButton
|
||||
@click="toggleLists(n.id)"
|
||||
@click="toggleProjects(n.id)"
|
||||
class="menu-label"
|
||||
v-tooltip="namespaceTitles[nk]"
|
||||
>
|
||||
<ColorBubble
|
||||
v-if="n.hexColor !== ''"
|
||||
:color="n.hexColor"
|
||||
class="mr-1"
|
||||
v-if="n.hexColor !== ''"
|
||||
:color="n.hexColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
<span class="name">{{ namespaceTitles[nk] }}</span>
|
||||
<div
|
||||
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
|
||||
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
||||
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
|
||||
:class="{'active': typeof projectsVisible[n.id] !== 'undefined' ? projectsVisible[n.id] : true}"
|
||||
>
|
||||
<icon icon="chevron-down"/>
|
||||
</div>
|
||||
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
|
||||
({{ namespaceListsCount[nk] }})
|
||||
({{ namespaceProjectsCount[nk] }})
|
||||
</span>
|
||||
</BaseButton>
|
||||
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
|
||||
</div>
|
||||
<!--
|
||||
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
||||
triggered by the change needs to have access to the current namespace
|
||||
-->
|
||||
<draggable
|
||||
v-if="listsVisible[n.id] ?? true"
|
||||
<!--
|
||||
NOTE: a v-model / computed setter is not possible, since the updateActiveProjects function
|
||||
triggered by the change needs to have access to the current namespace
|
||||
-->
|
||||
<draggable
|
||||
v-if="projectsVisible[n.id] ?? true"
|
||||
v-bind="dragOptions"
|
||||
:modelValue="activeLists[nk]"
|
||||
@update:modelValue="(lists) => updateActiveLists(n, lists)"
|
||||
:modelValue="activeProjects[nk]"
|
||||
@update:modelValue="(projects) => updateActiveProjects(n, projects)"
|
||||
group="namespace-lists"
|
||||
@start="() => drag = true"
|
||||
@end="saveListPosition"
|
||||
@ -100,46 +100,46 @@
|
||||
{ 'dragging-disabled': n.id < 0 }
|
||||
]
|
||||
}"
|
||||
>
|
||||
<template #item="{element: l}">
|
||||
<li
|
||||
class="list-menu loader-container is-loading-small"
|
||||
:class="{'is-loading': listUpdating[l.id]}"
|
||||
>
|
||||
<template #item="{element: l}">
|
||||
<li
|
||||
class="list-menu loader-container is-loading-small"
|
||||
:class="{'is-loading': projectUpdating[l.id]}"
|
||||
>
|
||||
<BaseButton
|
||||
:to="{ name: 'project.index', params: { projectId: l.id} }"
|
||||
class="list-menu-link"
|
||||
:class="{'router-link-exact-active': currentProject.id === l.id}"
|
||||
>
|
||||
<BaseButton
|
||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||
class="list-menu-link"
|
||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
||||
>
|
||||
<span class="icon menu-item-icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<ColorBubble
|
||||
<span class="icon menu-item-icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<ColorBubble
|
||||
v-if="l.hexColor !== ''"
|
||||
:color="l.hexColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="l.id > 0"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
@click="listStore.toggleListFavorite(l)"
|
||||
>
|
||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
</BaseButton>
|
||||
<list-settings-dropdown class="menu-list-dropdown" :list="l" v-if="l.id > 0">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
<span class="list-setting-spacer" v-else></span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
/>
|
||||
<span class="list-menu-title">{{ getProjectTitle(l) }}</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="l.id > 0"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
@click="projectStore.toggleProjectFavorite(l)"
|
||||
>
|
||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
</BaseButton>
|
||||
<ProjectSettingsDropdown class="menu-list-dropdown" :project="l" v-if="l.id > 0">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</ProjectSettingsDropdown>
|
||||
<span class="list-setting-spacer" v-else></span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
</nav>
|
||||
<PoweredByLink/>
|
||||
@ -152,20 +152,20 @@ import draggable from 'zhyswan-vuedraggable'
|
||||
import type {SortableEvent} from 'sortablejs'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
|
||||
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
||||
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
|
||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const drag = ref(false)
|
||||
@ -176,7 +176,7 @@ const dragOptions = {
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
const loading = computed(() => namespaceStore.isLoading)
|
||||
|
||||
@ -184,9 +184,9 @@ const loading = computed(() => namespaceStore.isLoading)
|
||||
const namespaces = computed(() => {
|
||||
return namespaceStore.namespaces.filter(n => !n.isArchived)
|
||||
})
|
||||
const activeLists = computed(() => {
|
||||
return namespaces.value.map(({lists}) => {
|
||||
return lists?.filter(item => {
|
||||
const activeProjects = computed(() => {
|
||||
return namespaces.value.map(({projects}) => {
|
||||
return projects?.filter(item => {
|
||||
return typeof item !== 'undefined' && !item.isArchived
|
||||
})
|
||||
})
|
||||
@ -196,45 +196,45 @@ const namespaceTitles = computed(() => {
|
||||
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
|
||||
})
|
||||
|
||||
const namespaceListsCount = computed(() => {
|
||||
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
|
||||
const namespaceProjectsCount = computed(() => {
|
||||
return namespaces.value.map((_, index) => activeProjects.value[index]?.length ?? 0)
|
||||
})
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
function toggleLists(namespaceId: INamespace['id']) {
|
||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
||||
function toggleProjects(namespaceId: INamespace['id']) {
|
||||
projectsVisible.value[namespaceId] = !projectsVisible.value[namespaceId]
|
||||
}
|
||||
|
||||
const listsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||
const projectsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||
// FIXME: async action will be unfinished when component mounts
|
||||
onBeforeMount(async () => {
|
||||
const namespaces = await namespaceStore.loadNamespaces()
|
||||
namespaces.forEach(n => {
|
||||
if (typeof listsVisible.value[n.id] === 'undefined') {
|
||||
listsVisible.value[n.id] = true
|
||||
if (typeof projectsVisible.value[n.id] === 'undefined') {
|
||||
projectsVisible.value[n.id] = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
|
||||
function updateActiveProjects(namespace: INamespace, activeProjects: IProject[]) {
|
||||
// This is a bit hacky: since we do have to filter out the archived items from the list
|
||||
// for vue draggable updating it is not as simple as replacing it.
|
||||
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
|
||||
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
|
||||
// To work around this, we merge the active projects with the archived ones. Doing so breaks the order
|
||||
// because now all archived projects are sorted after the active ones. This is fine because they are sorted
|
||||
// later when showing them anyway, and it makes the merging happening here a lot easier.
|
||||
const lists = [
|
||||
...activeLists,
|
||||
...namespace.lists.filter(l => l.isArchived),
|
||||
const projects = [
|
||||
...activeProjects,
|
||||
...namespace.projects.filter(l => l.isArchived),
|
||||
]
|
||||
|
||||
namespaceStore.setNamespaceById({
|
||||
...namespace,
|
||||
lists,
|
||||
projects,
|
||||
})
|
||||
}
|
||||
|
||||
const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||
const projectUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||
|
||||
async function saveListPosition(e: SortableEvent) {
|
||||
if (!e.newIndex && e.newIndex !== 0) return
|
||||
@ -242,31 +242,31 @@ async function saveListPosition(e: SortableEvent) {
|
||||
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
|
||||
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
|
||||
|
||||
const listsActive = activeLists.value[newNamespaceIndex]
|
||||
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
|
||||
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
|
||||
const projectsActive = activeProjects.value[newNamespaceIndex]
|
||||
// If the project was dragged to the last position, Safari will report e.newIndex as the size of the projectsActive
|
||||
// array instead of using the position. Because the index is wrong in that case, dragging the project will fail.
|
||||
// To work around that we're explicitly checking that case here and decrease the index.
|
||||
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
|
||||
const newIndex = e.newIndex === projectsActive.length ? e.newIndex - 1 : e.newIndex
|
||||
|
||||
const list = listsActive[newIndex]
|
||||
const listBefore = listsActive[newIndex - 1] ?? null
|
||||
const listAfter = listsActive[newIndex + 1] ?? null
|
||||
listUpdating.value[list.id] = true
|
||||
const project = projectsActive[newIndex]
|
||||
const projectBefore = projectsActive[newIndex - 1] ?? null
|
||||
const projectAfter = projectsActive[newIndex + 1] ?? null
|
||||
projectUpdating.value[project.id] = true
|
||||
|
||||
const position = calculateItemPosition(
|
||||
listBefore !== null ? listBefore.position : null,
|
||||
listAfter !== null ? listAfter.position : null,
|
||||
projectBefore !== null ? projectBefore.position : null,
|
||||
projectAfter !== null ? projectAfter.position : null,
|
||||
)
|
||||
|
||||
try {
|
||||
// create a copy of the list in order to not violate pinia manipulation
|
||||
await listStore.updateList({
|
||||
...list,
|
||||
// create a copy of the project in order to not violate pinia manipulation
|
||||
await projectStore.updateProject({
|
||||
...project,
|
||||
position,
|
||||
namespaceId,
|
||||
})
|
||||
} finally {
|
||||
listUpdating.value[list.id] = false
|
||||
projectUpdating.value[project.id] = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<multiselect
|
||||
v-model="selectedLists"
|
||||
:search-results="foundLists"
|
||||
:loading="listService.loading"
|
||||
v-model="selectedProjects"
|
||||
:search-results="foundProjects"
|
||||
:loading="projectService.loading"
|
||||
:multiple="true"
|
||||
:placeholder="$t('list.search')"
|
||||
:placeholder="$t('project.search')"
|
||||
label="title"
|
||||
@search="findLists"
|
||||
@search="findProjects"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -15,49 +15,49 @@ import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import ProjectService from '@/services/project'
|
||||
import {includesById} from '@/helpers/utils'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<IList[]>,
|
||||
type: Array as PropType<IProject[]>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: IList[]): void
|
||||
(e: 'update:modelValue', value: IProject[]): void
|
||||
}>()
|
||||
|
||||
const lists = ref<IList[]>([])
|
||||
const projects = ref<IProject[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
lists.value = props.modelValue
|
||||
projects.value = props.modelValue
|
||||
})
|
||||
|
||||
const selectedLists = computed({
|
||||
const selectedProjects = computed({
|
||||
get() {
|
||||
return lists.value
|
||||
return projects.value
|
||||
},
|
||||
set: (value) => {
|
||||
lists.value = value
|
||||
projects.value = value
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
const listService = shallowReactive(new ListService())
|
||||
const foundLists = ref<IList[]>([])
|
||||
const projectService = shallowReactive(new ProjectService())
|
||||
const foundProjects = ref<IProject[]>([])
|
||||
|
||||
async function findLists(query: string) {
|
||||
async function findProjects(query: string) {
|
||||
if (query === '') {
|
||||
foundLists.value = []
|
||||
foundProjects.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const response = await listService.getAll({}, {s: query}) as IList[]
|
||||
const response = await projectService.getAll({}, {s: query}) as IProject[]
|
||||
|
||||
// Filter selected items from the results
|
||||
foundLists.value = response.filter(({id}) => !includesById(lists.value, id))
|
||||
foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
|
||||
}
|
||||
</script>
|
@ -286,11 +286,11 @@ function handleCheckboxClick(e: Event) {
|
||||
console.debug('no index found')
|
||||
return
|
||||
}
|
||||
const listPrefix = text.value.substring(index, index + 1)
|
||||
const projectPrefix = text.value.substring(index, index + 1)
|
||||
|
||||
console.debug({index, listPrefix, checked, text: text.value})
|
||||
console.debug({index, projectPrefix, checked, text: text.value})
|
||||
|
||||
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
|
||||
text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `)
|
||||
bubble()
|
||||
renderPreview()
|
||||
}
|
||||
|
@ -61,8 +61,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'list.kanban.title',
|
||||
available: (route) => route.name === 'list.kanban',
|
||||
title: 'project.kanban.title',
|
||||
available: (route) => route.name === 'project.kanban',
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.task.done',
|
||||
@ -71,26 +71,26 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.title',
|
||||
available: (route) => (route.name as string)?.startsWith('list.'),
|
||||
title: 'keyboardShortcuts.project.title',
|
||||
available: (route) => (route.name as string)?.startsWith('project.'),
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToListView',
|
||||
title: 'keyboardShortcuts.project.switchToProjectView',
|
||||
keys: ['g', 'l'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToGanttView',
|
||||
title: 'keyboardShortcuts.project.switchToGanttView',
|
||||
keys: ['g', 'g'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToTableView',
|
||||
title: 'keyboardShortcuts.project.switchToTableView',
|
||||
keys: ['g', 't'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToKanbanView',
|
||||
title: 'keyboardShortcuts.project.switchToKanbanView',
|
||||
keys: ['g', 'k'],
|
||||
combination: 'then',
|
||||
},
|
||||
|
@ -73,14 +73,14 @@ const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (disabled.value) {
|
||||
if (props.entity === 'list' && subscriptionEntity.value === 'namespace') {
|
||||
return t('task.subscription.subscribedListThroughParentNamespace')
|
||||
if (props.entity === 'project' && subscriptionEntity.value === 'namespace') {
|
||||
return t('task.subscription.subscribedProjectThroughParentNamespace')
|
||||
}
|
||||
if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
|
||||
return t('task.subscription.subscribedTaskThroughParentNamespace')
|
||||
}
|
||||
if (props.entity === 'task' && subscriptionEntity.value === 'list') {
|
||||
return t('task.subscription.subscribedTaskThroughParentList')
|
||||
if (props.entity === 'task' && subscriptionEntity.value === 'project') {
|
||||
return t('task.subscription.subscribedTaskThroughParentProject')
|
||||
}
|
||||
|
||||
return ''
|
||||
@ -91,10 +91,10 @@ const tooltipText = computed(() => {
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedNamespace') :
|
||||
t('task.subscription.notSubscribedNamespace')
|
||||
case 'list':
|
||||
case 'project':
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedList') :
|
||||
t('task.subscription.notSubscribedList')
|
||||
t('task.subscription.subscribedProject') :
|
||||
t('task.subscription.notSubscribedProject')
|
||||
case 'task':
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedTask') :
|
||||
@ -133,8 +133,8 @@ async function subscribe() {
|
||||
case 'namespace':
|
||||
message = t('task.subscription.subscribeSuccessNamespace')
|
||||
break
|
||||
case 'list':
|
||||
message = t('task.subscription.subscribeSuccessList')
|
||||
case 'project':
|
||||
message = t('task.subscription.subscribeSuccessProject')
|
||||
break
|
||||
case 'task':
|
||||
message = t('task.subscription.subscribeSuccessTask')
|
||||
@ -156,8 +156,8 @@ async function unsubscribe() {
|
||||
case 'namespace':
|
||||
message = t('task.subscription.unsubscribeSuccessNamespace')
|
||||
break
|
||||
case 'list':
|
||||
message = t('task.subscription.unsubscribeSuccessList')
|
||||
case 'project':
|
||||
message = t('task.subscription.unsubscribeSuccessProject')
|
||||
break
|
||||
case 'task':
|
||||
message = t('task.subscription.unsubscribeSuccessTask')
|
||||
|
@ -30,10 +30,10 @@
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }"
|
||||
:to="{ name: 'project.create', params: { namespaceId: namespace.id } }"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('menu.newList') }}
|
||||
{{ $t('menu.newProject') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
||||
|
@ -117,9 +117,9 @@ function to(n, index) {
|
||||
case names.TASK_DELETED:
|
||||
// Nothing
|
||||
break
|
||||
case names.LIST_CREATED:
|
||||
case names.PROJECT_CREATED:
|
||||
to.name = 'task.index'
|
||||
to.params.listId = n.notification.list.id
|
||||
to.params.projectId = n.notification.project.id
|
||||
break
|
||||
case names.TEAM_MEMBER_ADDED:
|
||||
to.name = 'teams.edit'
|
||||
|
@ -1,56 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
|
||||
:class="{ 'is-loading': projectService.loading, 'is-archived': currentProject.isArchived}"
|
||||
class="loader-container"
|
||||
>
|
||||
<div class="switch-view-container">
|
||||
<div class="switch-view">
|
||||
<BaseButton
|
||||
v-shortcut="'g l'"
|
||||
:title="$t('keyboardShortcuts.list.switchToListView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToProjectView')"
|
||||
class="switch-view-button"
|
||||
:class="{'is-active': viewName === 'list'}"
|
||||
:to="{ name: 'list.list', params: { listId } }"
|
||||
:class="{'is-active': viewName === 'project'}"
|
||||
:to="{ name: 'project.list', params: { projectId } }"
|
||||
>
|
||||
{{ $t('list.list.title') }}
|
||||
{{ $t('project.list.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-shortcut="'g g'"
|
||||
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToGanttView')"
|
||||
class="switch-view-button"
|
||||
:class="{'is-active': viewName === 'gantt'}"
|
||||
:to="{ name: 'list.gantt', params: { listId } }"
|
||||
:to="{ name: 'project.gantt', params: { projectId } }"
|
||||
>
|
||||
{{ $t('list.gantt.title') }}
|
||||
{{ $t('project.gantt.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-shortcut="'g t'"
|
||||
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToTableView')"
|
||||
class="switch-view-button"
|
||||
:class="{'is-active': viewName === 'table'}"
|
||||
:to="{ name: 'list.table', params: { listId } }"
|
||||
:to="{ name: 'project.table', params: { projectId } }"
|
||||
>
|
||||
{{ $t('list.table.title') }}
|
||||
{{ $t('project.table.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-shortcut="'g k'"
|
||||
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToKanbanView')"
|
||||
class="switch-view-button"
|
||||
:class="{'is-active': viewName === 'kanban'}"
|
||||
:to="{ name: 'list.kanban', params: { listId } }"
|
||||
:to="{ name: 'project.kanban', params: { projectId } }"
|
||||
>
|
||||
{{ $t('list.kanban.title') }}
|
||||
{{ $t('project.kanban.title') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<CustomTransition name="fade">
|
||||
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
|
||||
{{ $t('list.archived') }}
|
||||
<transition name="fade">
|
||||
<Message variant="warning" v-if="currentProject.isArchived" class="mb-4">
|
||||
{{ $t('project.archived') }}
|
||||
</Message>
|
||||
</CustomTransition>
|
||||
</transition>
|
||||
|
||||
<slot v-if="loadedListId"/>
|
||||
<slot v-if="loadedProjectId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -60,20 +60,19 @@ import {useRoute} from 'vue-router'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ListService from '@/services/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
import ProjectService from '@/services/project'
|
||||
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import {saveListToHistory} from '@/modules/listHistory'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import {saveProjectToHistory} from '@/modules/projectHistory'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
@ -86,64 +85,64 @@ const props = defineProps({
|
||||
const route = useRoute()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const listService = ref(new ListService())
|
||||
const loadedListId = ref(0)
|
||||
const projectStore = useProjectStore()
|
||||
const projectService = ref(new ProjectService())
|
||||
const loadedProjectId = ref(0)
|
||||
|
||||
const currentList = computed(() => {
|
||||
return typeof baseStore.currentList === 'undefined' ? {
|
||||
const currentProject = computed(() => {
|
||||
return typeof baseStore.currentProject === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
isArchived: false,
|
||||
maxRight: null,
|
||||
} : baseStore.currentList
|
||||
} : baseStore.currentProject
|
||||
})
|
||||
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
|
||||
useTitle(() => currentProject.value.id ? getProjectTitle(currentProject.value) : '')
|
||||
|
||||
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
||||
// This resulted in loading and setting the list multiple times, even when navigating away from it.
|
||||
// This caused wired bugs where the list background would be set on the home page but only right after setting a new
|
||||
// list background and then navigating to home. It also highlighted the list in the menu and didn't allow changing any
|
||||
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
||||
// This caused wired bugs where the project background would be set on the home page but only right after setting a new
|
||||
// project background and then navigating to home. It also highlighted the project in the menu and didn't allow changing any
|
||||
// of it, most likely due to the rights not being properly populated.
|
||||
watch(
|
||||
() => props.listId,
|
||||
// loadList
|
||||
async (listIdToLoad: number) => {
|
||||
const listData = {id: listIdToLoad}
|
||||
saveListToHistory(listData)
|
||||
() => props.projectId,
|
||||
// loadProject
|
||||
async (projectIdToLoad: number) => {
|
||||
const projectData = {id: projectIdToLoad}
|
||||
saveProjectToHistory(projectData)
|
||||
|
||||
// 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.
|
||||
// Don't load the project if we either already loaded it or aren't dealing with a project at all currently and
|
||||
// the currently loaded project has the right set.
|
||||
if (
|
||||
(
|
||||
listIdToLoad === loadedListId.value ||
|
||||
typeof listIdToLoad === 'undefined' ||
|
||||
listIdToLoad === currentList.value.id
|
||||
projectIdToLoad === loadedProjectId.value ||
|
||||
typeof projectIdToLoad === 'undefined' ||
|
||||
projectIdToLoad === currentProject.value.id
|
||||
)
|
||||
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
|
||||
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
|
||||
) {
|
||||
loadedListId.value = props.listId
|
||||
loadedProjectId.value = props.projectId
|
||||
return
|
||||
}
|
||||
|
||||
console.debug(`Loading list, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value)
|
||||
console.debug(`Loading project, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
|
||||
|
||||
// Set the current list to the one we're about to load so that the title is already shown at the top
|
||||
loadedListId.value = 0
|
||||
const listFromStore = listStore.getListById(listData.id)
|
||||
if (listFromStore !== null) {
|
||||
// Set the current project to the one we're about to load so that the title is already shown at the top
|
||||
loadedProjectId.value = 0
|
||||
const projectFromStore = projectStore.getProjectById(projectData.id)
|
||||
if (projectFromStore !== null) {
|
||||
baseStore.setBackground(null)
|
||||
baseStore.setBlurHash(null)
|
||||
baseStore.handleSetCurrentList({list: listFromStore})
|
||||
baseStore.handleSetCurrentProject({project: projectFromStore})
|
||||
}
|
||||
|
||||
// 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)
|
||||
// We create an extra project object instead of creating it in project.value because that would trigger a ui update which would result in bad ux.
|
||||
const project = new ProjectModel(projectData)
|
||||
try {
|
||||
const loadedList = await listService.value.get(list)
|
||||
baseStore.handleSetCurrentList({list: loadedList})
|
||||
const loadedProject = await projectService.value.get(project)
|
||||
baseStore.handleSetCurrentProject({project: loadedProject})
|
||||
} finally {
|
||||
loadedListId.value = props.listId
|
||||
loadedProjectId.value = props.projectId
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
|
@ -1,39 +1,39 @@
|
||||
<template>
|
||||
<div
|
||||
class="list-card"
|
||||
class="project-card"
|
||||
:class="{
|
||||
'has-light-text': background !== null,
|
||||
'has-background': blurHashUrl !== '' || background !== null
|
||||
}"
|
||||
:style="{
|
||||
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined,
|
||||
'border-left': project.hexColor ? `0.25rem solid ${project.hexColor}` : undefined,
|
||||
'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="list-background background-fade-in"
|
||||
class="project-background background-fade-in"
|
||||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
/>
|
||||
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
||||
<span v-if="project.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
||||
|
||||
<div class="list-title" aria-hidden="true">{{ list.title }}</div>
|
||||
<div class="project-title" aria-hidden="true">{{ project.title }}</div>
|
||||
<BaseButton
|
||||
class="list-button"
|
||||
:aria-label="list.title"
|
||||
:title="list.description"
|
||||
class="project-button"
|
||||
:aria-label="project.title"
|
||||
:title="project.description"
|
||||
:to="{
|
||||
name: 'list.index',
|
||||
params: { listId: list.id}
|
||||
name: 'project.index',
|
||||
params: { projectId: project.id}
|
||||
}"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="!list.isArchived"
|
||||
v-if="!project.isArchived"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': list.isFavorite}"
|
||||
@click.prevent.stop="listStore.toggleListFavorite(list)"
|
||||
:class="{'is-favorite': project.isFavorite}"
|
||||
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
||||
>
|
||||
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
|
||||
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -41,30 +41,30 @@
|
||||
<script lang="ts" setup>
|
||||
import {toRef, type PropType} from 'vue'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {useListBackground} from './useListBackground'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectBackground} from './useProjectBackground'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object as PropType<IList>,
|
||||
project: {
|
||||
type: Object as PropType<IProject>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
const {background, blurHashUrl} = useProjectBackground(toRef(props, 'project'))
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-card {
|
||||
--list-card-padding: 1rem;
|
||||
.project-card {
|
||||
--project-card-padding: 1rem;
|
||||
background: var(--white);
|
||||
padding: var(--list-card-padding);
|
||||
padding: var(--project-card-padding);
|
||||
border-radius: $radius;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow $transition;
|
||||
@ -91,14 +91,14 @@ const listStore = useListStore()
|
||||
}
|
||||
|
||||
.has-background,
|
||||
.list-background {
|
||||
.project-background {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.list-background,
|
||||
.list-button {
|
||||
.project-background,
|
||||
.project-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@ -111,7 +111,7 @@ const listStore = useListStore()
|
||||
float: left;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
.project-title {
|
||||
align-self: flex-end;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: 400;
|
||||
@ -120,7 +120,7 @@ const listStore = useListStore()
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
max-height: calc(100% - (var(--list-card-padding) + 1rem)); // padding & height of the "is archived" badge
|
||||
max-height: calc(100% - (var(--project-card-padding) + 1rem)); // padding & height of the "is archived" badge
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
@ -130,11 +130,11 @@ const listStore = useListStore()
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.has-light-text .list-title {
|
||||
.has-light-text .project-title {
|
||||
color: var(--grey-100);
|
||||
}
|
||||
|
||||
.has-background .list-title {
|
||||
.has-background .project-title {
|
||||
text-shadow:
|
||||
0 0 10px var(--black),
|
||||
1px 1px 5px var(--grey-700),
|
||||
@ -144,8 +144,8 @@ const listStore = useListStore()
|
||||
|
||||
.favorite {
|
||||
position: absolute;
|
||||
top: var(--list-card-padding);
|
||||
right: var(--list-card-padding);
|
||||
top: var(--project-card-padding);
|
||||
right: var(--project-card-padding);
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 1;
|
||||
|
||||
@ -165,7 +165,7 @@ const listStore = useListStore()
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.list-card:hover .favorite {
|
||||
.project-card:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<ul class="list-grid">
|
||||
<ul class="project-grid">
|
||||
<li
|
||||
v-for="(item, index) in filteredLists"
|
||||
:key="`list_${item.id}_${index}`"
|
||||
class="list-grid-item"
|
||||
v-for="(item, index) in filteredProjects"
|
||||
:key="`project_${item.id}_${index}`"
|
||||
class="project-grid-item"
|
||||
>
|
||||
<ListCard :list="item" />
|
||||
<ProjectCard :project="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, type PropType} from 'vue'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import ListCard from './ListCard.vue'
|
||||
import ProjectCard from './ProjectCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
lists: {
|
||||
type: Array as PropType<IList[]>,
|
||||
projects: {
|
||||
type: Array as PropType<IProject[]>,
|
||||
default: () => [],
|
||||
},
|
||||
showArchived: {
|
||||
@ -31,46 +31,46 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const filteredLists = computed(() => {
|
||||
const filteredProjects = computed(() => {
|
||||
return props.showArchived
|
||||
? props.lists
|
||||
: props.lists.filter(l => !l.isArchived)
|
||||
? props.projects
|
||||
: props.projects.filter(l => !l.isArchived)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$list-height: 150px;
|
||||
$list-spacing: 1rem;
|
||||
$project-height: 150px;
|
||||
$project-spacing: 1rem;
|
||||
|
||||
.list-grid {
|
||||
.project-grid {
|
||||
margin: 0; // reset li
|
||||
list-style-type: none;
|
||||
project-style-type: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--list-columns), 1fr);
|
||||
grid-auto-rows: $list-height;
|
||||
gap: $list-spacing;
|
||||
grid-template-columns: repeat(var(--project-columns), 1fr);
|
||||
grid-auto-rows: $project-height;
|
||||
gap: $project-spacing;
|
||||
|
||||
@media screen and (min-width: $mobile) {
|
||||
--list-rows: 4;
|
||||
--list-columns: 1;
|
||||
--project-rows: 4;
|
||||
--project-columns: 1;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $mobile) and (max-width: $tablet) {
|
||||
--list-columns: 2;
|
||||
--project-columns: 2;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: $widescreen) {
|
||||
--list-columns: 3;
|
||||
--list-rows: 3;
|
||||
--project-columns: 3;
|
||||
--project-rows: 3;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
--list-columns: 5;
|
||||
--list-rows: 2;
|
||||
--project-columns: 5;
|
||||
--project-rows: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.list-grid-item {
|
||||
.project-grid-item {
|
||||
display: grid;
|
||||
margin-top: 0; // remove padding coming form .content li + li
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue'
|
||||
|
||||
import Filters from '@/components/list/partials/filters.vue'
|
||||
import Filters from '@/components/project/partials/filters.vue'
|
||||
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
|
@ -20,7 +20,7 @@
|
||||
{{ $t('filters.attributes.showDoneTasks') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox
|
||||
v-if="!['list.kanban', 'list.table'].includes($route.name as string)"
|
||||
v-if="!['project.kanban', 'project.table'].includes($route.name as string)"
|
||||
v-model="sortAlphabetically"
|
||||
@update:model-value="change()"
|
||||
>
|
||||
@ -154,14 +154,14 @@
|
||||
</div>
|
||||
|
||||
<template
|
||||
v-if="['filters.create', 'list.edit', 'filter.settings.edit'].includes($route.name as string)">
|
||||
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('list.lists') }}</label>
|
||||
<label class="label">{{ $t('project.lists') }}</label>
|
||||
<div class="control">
|
||||
<SelectList
|
||||
v-model="entities.lists"
|
||||
@select="changeMultiselectFilter('lists', 'list_id')"
|
||||
@remove="changeMultiselectFilter('lists', 'list_id')"
|
||||
<SelectProject
|
||||
v-model="entities.projects"
|
||||
@select="changeMultiselectFilter('projects', 'project_id')"
|
||||
@remove="changeMultiselectFilter('projects', 'project_id')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -190,7 +190,7 @@ import {camelCase} from 'camel-case'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
|
||||
@ -200,7 +200,7 @@ import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue
|
||||
import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import SelectUser from '@/components/input/SelectUser.vue'
|
||||
import SelectList from '@/components/input/SelectList.vue'
|
||||
import SelectProject from '@/components/input/SelectProject.vue'
|
||||
import SelectNamespace from '@/components/input/SelectNamespace.vue'
|
||||
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
@ -208,13 +208,13 @@ import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import ListService from '@/services/list'
|
||||
import ProjectService from '@/services/project'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
|
||||
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
|
||||
const DEFAULT_PARAMS = {
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
@ -239,7 +239,7 @@ const DEFAULT_FILTERS = {
|
||||
reminders: '',
|
||||
assignees: '',
|
||||
labels: '',
|
||||
list_id: '',
|
||||
project_id: '',
|
||||
namespace: '',
|
||||
} as const
|
||||
|
||||
@ -264,23 +264,23 @@ const filters = ref({...DEFAULT_FILTERS})
|
||||
|
||||
const services = {
|
||||
users: shallowReactive(new UserService()),
|
||||
lists: shallowReactive(new ListService()),
|
||||
projects: shallowReactive(new ProjectService()),
|
||||
namespace: shallowReactive(new NamespaceService()),
|
||||
}
|
||||
|
||||
interface Entities {
|
||||
users: IUser[]
|
||||
labels: ILabel[]
|
||||
lists: IList[]
|
||||
projects: IProject[]
|
||||
namespace: INamespace[]
|
||||
}
|
||||
|
||||
type EntityType = 'users' | 'labels' | 'lists' | 'namespace'
|
||||
type EntityType = 'users' | 'labels' | 'projects' | 'namespace'
|
||||
|
||||
const entities: Entities = reactive({
|
||||
users: [],
|
||||
labels: [],
|
||||
lists: [],
|
||||
projects: [],
|
||||
namespace: [],
|
||||
})
|
||||
|
||||
@ -327,7 +327,7 @@ function prepareFilters() {
|
||||
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||
prepareDate('reminders')
|
||||
prepareRelatedObjectFilter('users', 'assignees')
|
||||
prepareRelatedObjectFilter('lists', 'list_id')
|
||||
prepareRelatedObjectFilter('projects', 'project_id')
|
||||
prepareRelatedObjectFilter('namespace')
|
||||
|
||||
prepareSingleValue('labels')
|
@ -1,30 +1,30 @@
|
||||
import {ref, watch, type Ref} from 'vue'
|
||||
import ListService from '@/services/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import ProjectService from '@/services/project'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
export function useListBackground(list: Ref<IList>) {
|
||||
export function useProjectBackground(project: Ref<IProject>) {
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
const blurHashUrl = ref('')
|
||||
|
||||
watch(
|
||||
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
|
||||
async ([listId, blurHash], oldValue) => {
|
||||
() => [project.value.id, project.value.backgroundBlurHash] as [IProject['id'], IProject['backgroundBlurHash']],
|
||||
async ([projectId, blurHash], oldValue) => {
|
||||
if (
|
||||
list.value === null ||
|
||||
!list.value.backgroundInformation ||
|
||||
project.value === null ||
|
||||
!project.value.backgroundInformation ||
|
||||
backgroundLoading.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [oldListId, oldBlurHash] = oldValue || []
|
||||
const [oldProjectId, oldBlurHash] = oldValue || []
|
||||
if (
|
||||
oldValue !== undefined &&
|
||||
listId === oldListId && blurHash === oldBlurHash
|
||||
oldValue !== undefined &&
|
||||
projectId === oldProjectId && blurHash === oldBlurHash
|
||||
) {
|
||||
// list hasn't changed
|
||||
// project hasn't changed
|
||||
return
|
||||
}
|
||||
|
||||
@ -35,8 +35,8 @@ export function useListBackground(list: Ref<IList>) {
|
||||
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
|
||||
})
|
||||
|
||||
const listService = new ListService()
|
||||
const backgroundPromise = listService.background(list.value).then((result) => {
|
||||
const projectService = new ProjectService()
|
||||
const backgroundPromise = projectService.background(project.value).then((result) => {
|
||||
background.value = result
|
||||
})
|
||||
await Promise.all([blurHashPromise, backgroundPromise])
|
||||
@ -44,7 +44,7 @@ export function useListBackground(list: Ref<IList>) {
|
||||
backgroundLoading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
return {
|
||||
@ -52,4 +52,4 @@ export function useListBackground(list: Ref<IList>) {
|
||||
blurHashUrl,
|
||||
backgroundLoading,
|
||||
}
|
||||
}
|
||||
}
|
@ -8,24 +8,24 @@
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="isSavedFilter(list)">
|
||||
<template v-if="isSavedFilter(project)">
|
||||
<dropdown-item
|
||||
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
|
||||
:to="{ name: 'filter.settings.edit', params: { projectId: project.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
|
||||
:to="{ name: 'filter.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="list.isArchived">
|
||||
<template v-else-if="project.isArchived">
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
@ -33,32 +33,32 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.edit', params: { projectId: project.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="backgroundsEnabled"
|
||||
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
|
||||
icon="image"
|
||||
>
|
||||
{{ $t('menu.setBackground') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.share', params: { projectId: project.id } }"
|
||||
icon="share-alt"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.duplicate', params: { projectId: project.id } }"
|
||||
icon="paste"
|
||||
>
|
||||
{{ $t('menu.duplicate') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
@ -66,14 +66,14 @@
|
||||
<Subscription
|
||||
class="has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="list"
|
||||
:entity-id="list.id"
|
||||
:model-value="list.subscription"
|
||||
entity="project"
|
||||
:entity-id="project.id"
|
||||
:model-value="project.subscription"
|
||||
@update:model-value="setSubscriptionInStore"
|
||||
type="dropdown"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
@ -90,26 +90,26 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Subscription from '@/components/misc/subscription.vue'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object as PropType<IList>,
|
||||
project: {
|
||||
type: Object as PropType<IProject>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const subscription = ref<ISubscription | null>(null)
|
||||
watchEffect(() => {
|
||||
subscription.value = props.list.subscription ?? null
|
||||
subscription.value = props.project.subscription ?? null
|
||||
})
|
||||
|
||||
const configStore = useConfigStore()
|
||||
@ -117,11 +117,11 @@ const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders
|
||||
|
||||
function setSubscriptionInStore(sub: ISubscription) {
|
||||
subscription.value = sub
|
||||
const updatedList = {
|
||||
...props.list,
|
||||
const updatedProject = {
|
||||
...props.project,
|
||||
subscription: sub,
|
||||
}
|
||||
listStore.setList(updatedList)
|
||||
namespaceStore.setListInNamespaceById(updatedList)
|
||||
projectStore.setProject(updatedProject)
|
||||
namespaceStore.setProjectInNamespaceById(updatedProject)
|
||||
}
|
||||
</script>
|
@ -63,18 +63,18 @@ import TeamService from '@/services/team'
|
||||
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import TeamModel from '@/models/team'
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
import {getHistory} from '@/modules/projectHistory'
|
||||
import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText'
|
||||
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
import {success} from '@/message'
|
||||
@ -82,13 +82,13 @@ import {success} from '@/message'
|
||||
import type {ITeam} from '@/modelTypes/ITeam'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const labelStore = useLabelStore()
|
||||
const taskStore = useTaskStore()
|
||||
@ -98,13 +98,13 @@ type DoAction<Type = any> = { type: ACTION_TYPE } & Type
|
||||
enum ACTION_TYPE {
|
||||
CMD = 'cmd',
|
||||
TASK = 'task',
|
||||
LIST = 'list',
|
||||
PROJECT = 'project',
|
||||
TEAM = 'team',
|
||||
}
|
||||
|
||||
enum COMMAND_TYPE {
|
||||
NEW_TASK = 'newTask',
|
||||
NEW_LIST = 'newList',
|
||||
NEW_PROJECT = 'newProject',
|
||||
NEW_NAMESPACE = 'newNamespace',
|
||||
NEW_TEAM = 'newTeam',
|
||||
}
|
||||
@ -112,7 +112,7 @@ enum COMMAND_TYPE {
|
||||
enum SEARCH_MODE {
|
||||
ALL = 'all',
|
||||
TASKS = 'tasks',
|
||||
LISTS = 'lists',
|
||||
PROJECTS = 'projects',
|
||||
TEAMS = 'teams',
|
||||
}
|
||||
|
||||
@ -137,26 +137,26 @@ function closeQuickActions() {
|
||||
baseStore.setQuickActionsActive(false)
|
||||
}
|
||||
|
||||
const foundLists = computed(() => {
|
||||
const { list } = parsedQuery.value
|
||||
const foundProjects = computed(() => {
|
||||
const { project } = parsedQuery.value
|
||||
if (
|
||||
searchMode.value === SEARCH_MODE.ALL ||
|
||||
searchMode.value === SEARCH_MODE.LISTS ||
|
||||
list === null
|
||||
searchMode.value === SEARCH_MODE.PROJECTS ||
|
||||
project === null
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ncache: { [id: ListModel['id']]: INamespace } = {}
|
||||
const ncache: { [id: ProjectModel['id']]: INamespace } = {}
|
||||
const history = getHistory()
|
||||
const allLists = [
|
||||
const allProjects = [
|
||||
...new Set([
|
||||
...history.map((l) => listStore.getListById(l.id)),
|
||||
...listStore.searchList(list),
|
||||
...history.map((l) => projectStore.getProjectById(l.id)),
|
||||
...projectStore.searchProject(project),
|
||||
]),
|
||||
]
|
||||
|
||||
return allLists.filter((l) => {
|
||||
return allProjects.filter((l) => {
|
||||
if (typeof l === 'undefined' || l === null) {
|
||||
return false
|
||||
}
|
||||
@ -191,9 +191,9 @@ const results = computed<Result[]>(() => {
|
||||
items: foundTasks.value,
|
||||
},
|
||||
{
|
||||
type: ACTION_TYPE.LIST,
|
||||
title: t('quickActions.lists'),
|
||||
items: foundLists.value,
|
||||
type: ACTION_TYPE.PROJECT,
|
||||
title: t('quickActions.projects'),
|
||||
items: foundProjects.value,
|
||||
},
|
||||
{
|
||||
type: ACTION_TYPE.TEAM,
|
||||
@ -206,7 +206,7 @@ const results = computed<Result[]>(() => {
|
||||
const loading = computed(() =>
|
||||
taskService.loading ||
|
||||
namespaceStore.isLoading ||
|
||||
listStore.isLoading ||
|
||||
projectStore.isLoading ||
|
||||
teamService.loading,
|
||||
)
|
||||
|
||||
@ -224,11 +224,11 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
|
||||
placeholder: t('quickActions.newTask'),
|
||||
action: newTask,
|
||||
},
|
||||
newList: {
|
||||
type: COMMAND_TYPE.NEW_LIST,
|
||||
title: t('quickActions.cmds.newList'),
|
||||
placeholder: t('quickActions.newList'),
|
||||
action: newList,
|
||||
newProject: {
|
||||
type: COMMAND_TYPE.NEW_PROJECT,
|
||||
title: t('quickActions.cmds.newProject'),
|
||||
placeholder: t('quickActions.newProject'),
|
||||
action: newProject,
|
||||
},
|
||||
newNamespace: {
|
||||
type: COMMAND_TYPE.NEW_NAMESPACE,
|
||||
@ -246,24 +246,24 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
|
||||
|
||||
const placeholder = computed(() => selectedCmd.value?.placeholder || t('quickActions.placeholder'))
|
||||
|
||||
const currentList = computed(() => Object.keys(baseStore.currentList).length === 0
|
||||
const currentProject = computed(() => Object.keys(baseStore.currentProject).length === 0
|
||||
? null
|
||||
: baseStore.currentList,
|
||||
: baseStore.currentProject,
|
||||
)
|
||||
|
||||
const hintText = computed(() => {
|
||||
let namespace
|
||||
if (selectedCmd.value !== null && currentList.value !== null) {
|
||||
if (selectedCmd.value !== null && currentProject.value !== null) {
|
||||
switch (selectedCmd.value.type) {
|
||||
case COMMAND_TYPE.NEW_TASK:
|
||||
return t('quickActions.createTask', {
|
||||
title: currentList.value.title,
|
||||
title: currentProject.value.title,
|
||||
})
|
||||
case COMMAND_TYPE.NEW_LIST:
|
||||
case COMMAND_TYPE.NEW_PROJECT:
|
||||
namespace = namespaceStore.getNamespaceById(
|
||||
currentList.value.namespaceId,
|
||||
currentProject.value.namespaceId,
|
||||
)
|
||||
return t('quickActions.createList', {
|
||||
return t('quickActions.createProject', {
|
||||
title: namespace?.title,
|
||||
})
|
||||
}
|
||||
@ -275,8 +275,8 @@ const hintText = computed(() => {
|
||||
|
||||
const availableCmds = computed(() => {
|
||||
const cmds = []
|
||||
if (currentList.value !== null) {
|
||||
cmds.push(commands.value.newTask, commands.value.newList)
|
||||
if (currentProject.value !== null) {
|
||||
cmds.push(commands.value.newTask, commands.value.newProject)
|
||||
}
|
||||
cmds.push(commands.value.newNamespace, commands.value.newTeam)
|
||||
return cmds
|
||||
@ -288,21 +288,21 @@ const searchMode = computed(() => {
|
||||
if (query.value === '') {
|
||||
return SEARCH_MODE.ALL
|
||||
}
|
||||
const { text, list, labels, assignees } = parsedQuery.value
|
||||
const { text, project, labels, assignees } = parsedQuery.value
|
||||
if (assignees.length === 0 && text !== '') {
|
||||
return SEARCH_MODE.TASKS
|
||||
}
|
||||
if (
|
||||
assignees.length === 0 &&
|
||||
list !== null &&
|
||||
project !== null &&
|
||||
text === '' &&
|
||||
labels.length === 0
|
||||
) {
|
||||
return SEARCH_MODE.LISTS
|
||||
return SEARCH_MODE.PROJECTS
|
||||
}
|
||||
if (
|
||||
assignees.length > 0 &&
|
||||
list === null &&
|
||||
project === null &&
|
||||
text === '' &&
|
||||
labels.length === 0
|
||||
) {
|
||||
@ -356,7 +356,7 @@ function searchTasks() {
|
||||
taskSearchTimeout.value = null
|
||||
}
|
||||
|
||||
const { text, list: listName, labels } = parsedQuery.value
|
||||
const { text, project: projectName, labels } = parsedQuery.value
|
||||
|
||||
const filters: Filter[] = []
|
||||
|
||||
@ -373,10 +373,10 @@ function searchTasks() {
|
||||
})
|
||||
}
|
||||
|
||||
if (listName !== null) {
|
||||
const list = listStore.findListByExactname(listName)
|
||||
if (list !== null) {
|
||||
addFilter('listId', list.id, 'equals')
|
||||
if (projectName !== null) {
|
||||
const project = projectStore.findProjectByExactname(projectName)
|
||||
if (project !== null) {
|
||||
addFilter('projectId', project.id, 'equals')
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,9 +396,9 @@ function searchTasks() {
|
||||
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
|
||||
foundTasks.value = r.map((t) => {
|
||||
t.type = ACTION_TYPE.TASK
|
||||
const list = listStore.getListById(t.listId)
|
||||
if (list !== null) {
|
||||
t.title = `${t.title} (${list.title})`
|
||||
const project = projectStore.getProjectById(t.projectId)
|
||||
if (project !== null) {
|
||||
t.title = `${t.title} (${project.title})`
|
||||
}
|
||||
return t
|
||||
})
|
||||
@ -444,11 +444,11 @@ const searchInput = ref<HTMLElement | null>(null)
|
||||
|
||||
async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||
switch (type) {
|
||||
case ACTION_TYPE.LIST:
|
||||
case ACTION_TYPE.PROJECT:
|
||||
closeQuickActions()
|
||||
await router.push({
|
||||
name: 'list.index',
|
||||
params: { listId: (item as DoAction<IList>).id },
|
||||
name: 'project.index',
|
||||
params: { projectId: (item as DoAction<IProject>).id },
|
||||
})
|
||||
break
|
||||
case ACTION_TYPE.TASK:
|
||||
@ -489,29 +489,29 @@ async function doCmd() {
|
||||
}
|
||||
|
||||
async function newTask() {
|
||||
if (currentList.value === null) {
|
||||
if (currentProject.value === null) {
|
||||
return
|
||||
}
|
||||
const task = await taskStore.createNewTask({
|
||||
title: query.value,
|
||||
listId: currentList.value.id,
|
||||
projectId: currentProject.value.id,
|
||||
})
|
||||
success({ message: t('task.createSuccess') })
|
||||
await router.push({ name: 'task.detail', params: { id: task.id } })
|
||||
}
|
||||
|
||||
async function newList() {
|
||||
if (currentList.value === null) {
|
||||
async function newProject() {
|
||||
if (currentProject.value === null) {
|
||||
return
|
||||
}
|
||||
const newList = await listStore.createList(new ListModel({
|
||||
const newProject = await projectStore.createProject(new ProjectModel({
|
||||
title: query.value,
|
||||
namespaceId: currentList.value.namespaceId,
|
||||
namespaceId: currentProject.value.namespaceId,
|
||||
}))
|
||||
success({ message: t('list.create.createdSuccess')})
|
||||
success({ message: t('project.create.createdSuccess')})
|
||||
await router.push({
|
||||
name: 'list.index',
|
||||
params: { listId: newList.id },
|
||||
name: 'project.index',
|
||||
params: { projectId: newProject.id },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,39 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $t('list.share.links.title') }}
|
||||
{{ $t('project.share.links.title') }}
|
||||
<span
|
||||
class="is-size-7 has-text-grey is-italic ml-3"
|
||||
v-tooltip="$t('list.share.links.explanation')">
|
||||
{{ $t('list.share.links.what') }}
|
||||
v-tooltip="$t('project.share.links.explanation')">
|
||||
{{ $t('project.share.links.what') }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="sharables-list">
|
||||
<div class="sharables-project">
|
||||
<x-button
|
||||
v-if="!(linkShares.length === 0 || showNewForm)"
|
||||
@click="showNewForm = true"
|
||||
icon="plus"
|
||||
class="mb-4">
|
||||
{{ $t('list.share.links.create') }}
|
||||
{{ $t('project.share.links.create') }}
|
||||
</x-button>
|
||||
|
||||
<div class="p-4" v-if="linkShares.length === 0 || showNewForm">
|
||||
<div class="field">
|
||||
<label class="label" for="linkShareRight">
|
||||
{{ $t('list.share.right.title') }}
|
||||
{{ $t('project.share.right.title') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select v-model="selectedRight" id="linkShareRight">
|
||||
<option :value="RIGHTS.READ">
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</option>
|
||||
<option :value="RIGHTS.READ_WRITE">
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</option>
|
||||
<option :value="RIGHTS.ADMIN">
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -41,21 +41,21 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="linkShareName">
|
||||
{{ $t('list.share.links.name') }}
|
||||
{{ $t('project.share.links.name') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="linkShareName"
|
||||
class="input"
|
||||
:placeholder="$t('list.share.links.namePlaceholder')"
|
||||
v-tooltip="$t('list.share.links.nameExplanation')"
|
||||
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||
v-tooltip="$t('project.share.links.nameExplanation')"
|
||||
v-model="name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="linkSharePassword">
|
||||
{{ $t('list.share.links.password') }}
|
||||
{{ $t('project.share.links.password') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@ -63,25 +63,25 @@
|
||||
type="password"
|
||||
class="input"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
v-tooltip="$t('list.share.links.passwordExplanation')"
|
||||
v-tooltip="$t('project.share.links.passwordExplanation')"
|
||||
v-model="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="add(listId)" icon="plus">
|
||||
{{ $t('list.share.share') }}
|
||||
<x-button @click="add(projectId)" icon="plus">
|
||||
{{ $t('project.share.share') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<table
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth link-share-list"
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth"
|
||||
v-if="linkShares.length > 0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ $t('list.share.links.view') }}</th>
|
||||
<th>{{ $t('list.share.attributes.delete') }}</th>
|
||||
<th>{{ $t('project.share.links.view') }}</th>
|
||||
<th>{{ $t('project.share.attributes.delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -92,7 +92,7 @@
|
||||
</p>
|
||||
|
||||
<p class="mb-2">
|
||||
<i18n-t keypath="list.share.links.sharedBy" scope="global">
|
||||
<i18n-t keypath="project.share.links.sharedBy" scope="global">
|
||||
<strong>{{ getDisplayName(s.sharedBy) }}</strong>
|
||||
</i18n-t>
|
||||
</p>
|
||||
@ -102,19 +102,19 @@
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</template>
|
||||
<template v-else-if="s.right === RIGHTS.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</template>
|
||||
</p>
|
||||
|
||||
@ -172,14 +172,14 @@
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="remove(listId)"
|
||||
@submit="remove(projectId)"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{ $t('list.share.links.remove') }}</span>
|
||||
<span>{{ $t('project.share.links.remove') }}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.links.removeText') }}</p>
|
||||
<p>{{ $t('project.share.links.removeText') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
@ -193,19 +193,19 @@ import {RIGHTS} from '@/constants/rights'
|
||||
import LinkShareModel from '@/models/linkShare'
|
||||
|
||||
import type {ILinkShare} from '@/modelTypes/ILinkShare'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import LinkShareService from '@/services/linkShare'
|
||||
|
||||
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
||||
import {success} from '@/message'
|
||||
import {getDisplayName} from '@/models/user'
|
||||
import type {ListView} from '@/types/ListView'
|
||||
import {LIST_VIEWS} from '@/types/ListView'
|
||||
import type {ProjectView} from '@/types/ProjectView'
|
||||
import {PROJECT_VIEWS} from '@/types/ProjectView'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
projectId: {
|
||||
default: 0,
|
||||
required: true,
|
||||
},
|
||||
@ -222,20 +222,20 @@ const showDeleteModal = ref(false)
|
||||
const linkIdToDelete = ref(0)
|
||||
const showNewForm = ref(false)
|
||||
|
||||
type SelectedViewMapper = Record<IList['id'], ListView>
|
||||
type SelectedViewMapper = Record<IProject['id'], ProjectView>
|
||||
|
||||
const selectedView = ref<SelectedViewMapper>({})
|
||||
|
||||
const availableViews = computed<Record<ListView, string>>(() => ({
|
||||
list: t('list.list.title'),
|
||||
gantt: t('list.gantt.title'),
|
||||
table: t('list.table.title'),
|
||||
kanban: t('list.kanban.title'),
|
||||
const availableViews = computed<Record<ProjectView, string>>(() => ({
|
||||
list: t('project.list.title'),
|
||||
gantt: t('project.gantt.title'),
|
||||
table: t('project.table.title'),
|
||||
kanban: t('project.kanban.title'),
|
||||
}))
|
||||
|
||||
const copy = useCopyToClipboard()
|
||||
watch(
|
||||
() => props.listId,
|
||||
() => props.projectId,
|
||||
load,
|
||||
{immediate: true},
|
||||
)
|
||||
@ -243,23 +243,23 @@ watch(
|
||||
const configStore = useConfigStore()
|
||||
const frontendUrl = computed(() => configStore.frontendUrl)
|
||||
|
||||
async function load(listId: IList['id']) {
|
||||
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
|
||||
if (listId === 0) {
|
||||
async function load(projectId: IProject['id']) {
|
||||
// If projectId == 0 the project on the calling component wasn't already loaded, so we just bail out here
|
||||
if (projectId === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const links = await linkShareService.getAll({listId})
|
||||
const links = await linkShareService.getAll({projectId})
|
||||
links.forEach((l: ILinkShare) => {
|
||||
selectedView.value[l.id] = 'list'
|
||||
selectedView.value[l.id] = 'project'
|
||||
})
|
||||
linkShares.value = links
|
||||
}
|
||||
|
||||
async function add(listId: IList['id']) {
|
||||
async function add(projectId: IProject['id']) {
|
||||
const newLinkShare = new LinkShareModel({
|
||||
right: selectedRight.value,
|
||||
listId,
|
||||
projectId,
|
||||
name: name.value,
|
||||
password: password.value,
|
||||
})
|
||||
@ -268,31 +268,31 @@ async function add(listId: IList['id']) {
|
||||
name.value = ''
|
||||
password.value = ''
|
||||
showNewForm.value = false
|
||||
success({message: t('list.share.links.createSuccess')})
|
||||
await load(listId)
|
||||
success({message: t('project.share.links.createSuccess')})
|
||||
await load(projectId)
|
||||
}
|
||||
|
||||
async function remove(listId: IList['id']) {
|
||||
async function remove(projectId: IProject['id']) {
|
||||
try {
|
||||
await linkShareService.delete(new LinkShareModel({
|
||||
id: linkIdToDelete.value,
|
||||
listId,
|
||||
projectId,
|
||||
}))
|
||||
success({message: t('list.share.links.deleteSuccess')})
|
||||
await load(listId)
|
||||
success({message: t('project.share.links.deleteSuccess')})
|
||||
await load(projectId)
|
||||
} finally {
|
||||
showDeleteModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getShareLink(hash: string, view: ListView = LIST_VIEWS.LIST) {
|
||||
function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.LIST) {
|
||||
return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// FIXME: I think this is not needed
|
||||
.sharables-list:not(.card-content) {
|
||||
.sharables-project:not(.card-content) {
|
||||
overflow-y: auto
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $t('list.share.userTeam.shared', {type: shareTypeNames}) }}
|
||||
{{ $t('project.share.userTeam.shared', {type: shareTypeNames}) }}
|
||||
</p>
|
||||
<div v-if="userIsAdmin">
|
||||
<div class="field has-addons">
|
||||
@ -19,7 +19,7 @@
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button @click="add()">{{ $t('list.share.share') }}</x-button>
|
||||
<x-button @click="add()">{{ $t('project.share.share') }}</x-button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,7 +31,7 @@
|
||||
<td>{{ getDisplayName(s) }}</td>
|
||||
<td>
|
||||
<template v-if="s.id === userInfo.id">
|
||||
<b class="is-success">{{ $t('list.share.userTeam.you') }}</b>
|
||||
<b class="is-success">{{ $t('project.share.userTeam.you') }}</b>
|
||||
</template>
|
||||
</td>
|
||||
</template>
|
||||
@ -52,19 +52,19 @@
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</template>
|
||||
<template v-else-if="s.right === RIGHTS.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</template>
|
||||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
@ -78,19 +78,19 @@
|
||||
:selected="s.right === RIGHTS.READ"
|
||||
:value="RIGHTS.READ"
|
||||
>
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</option>
|
||||
<option
|
||||
:selected="s.right === RIGHTS.READ_WRITE"
|
||||
:value="RIGHTS.READ_WRITE"
|
||||
>
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</option>
|
||||
<option
|
||||
:selected="s.right === RIGHTS.ADMIN"
|
||||
:value="RIGHTS.ADMIN"
|
||||
>
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -110,7 +110,7 @@
|
||||
</table>
|
||||
|
||||
<nothing v-else>
|
||||
{{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }}
|
||||
{{ $t('project.share.userTeam.notShared', {type: shareTypeNames}) }}
|
||||
</nothing>
|
||||
|
||||
<modal
|
||||
@ -120,11 +120,11 @@
|
||||
>
|
||||
<template #header>
|
||||
<span>{{
|
||||
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
|
||||
$t('project.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
|
||||
}}</span>
|
||||
</template>
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
|
||||
<p>{{ $t('project.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
@ -143,9 +143,9 @@ import UserNamespaceService from '@/services/userNamespace'
|
||||
import UserNamespaceModel from '@/models/userNamespace'
|
||||
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
|
||||
|
||||
import UserListService from '@/services/userList'
|
||||
import UserListModel from '@/models/userList'
|
||||
import type {IUserList} from '@/modelTypes/IUserList'
|
||||
import UserProjectService from '@/services/userProject'
|
||||
import UserProjectModel from '@/models/userProject'
|
||||
import type {IUserProject} from '@/modelTypes/IUserProject'
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import UserModel, { getDisplayName } from '@/models/user'
|
||||
@ -155,9 +155,9 @@ import TeamNamespaceService from '@/services/teamNamespace'
|
||||
import TeamNamespaceModel from '@/models/teamNamespace'
|
||||
import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
|
||||
|
||||
import TeamListService from '@/services/teamList'
|
||||
import TeamListModel from '@/models/teamList'
|
||||
import type { ITeamList } from '@/modelTypes/ITeamList'
|
||||
import TeamProjectService from '@/services/teamProject'
|
||||
import TeamProjectModel from '@/models/teamProject'
|
||||
import type { ITeamProject } from '@/modelTypes/ITeamProject'
|
||||
|
||||
import TeamService from '@/services/team'
|
||||
import TeamModel from '@/models/team'
|
||||
@ -172,7 +172,7 @@ import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<'list' | 'namespace'>,
|
||||
type: String as PropType<'project' | 'namespace'>,
|
||||
default: '',
|
||||
},
|
||||
shareType: {
|
||||
@ -191,9 +191,9 @@ const props = defineProps({
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
// This user service is either a userNamespaceService or a userListService, depending on the type we are using
|
||||
let stuffService: UserNamespaceService | UserListService | TeamListService | TeamNamespaceService
|
||||
let stuffModel: IUserNamespace | IUserList | ITeamList | ITeamNamespace
|
||||
// This user service is either a userNamespaceService or a userProjectService, depending on the type we are using
|
||||
let stuffService: UserNamespaceService | UserProjectService | TeamProjectService | TeamNamespaceService
|
||||
let stuffModel: IUserNamespace | IUserProject | ITeamProject | ITeamNamespace
|
||||
let searchService: UserService | TeamService
|
||||
let sharable: Ref<IUser | ITeam>
|
||||
|
||||
@ -201,7 +201,7 @@ const searchLabel = ref('')
|
||||
const selectedRight = ref({})
|
||||
|
||||
|
||||
// This holds either teams or users who this namepace or list is shared with
|
||||
// This holds either teams or users who this namepace or project is shared with
|
||||
const sharables = ref([])
|
||||
const showDeleteModal = ref(false)
|
||||
|
||||
@ -212,11 +212,11 @@ const userInfo = computed(() => authStore.info)
|
||||
function createShareTypeNameComputed(count: number) {
|
||||
return computed(() => {
|
||||
if (props.shareType === 'user') {
|
||||
return t('list.share.userTeam.typeUser', count)
|
||||
return t('project.share.userTeam.typeUser', count)
|
||||
}
|
||||
|
||||
if (props.shareType === 'team') {
|
||||
return t('list.share.userTeam.typeTeam', count)
|
||||
return t('project.share.userTeam.typeTeam', count)
|
||||
}
|
||||
|
||||
return ''
|
||||
@ -227,8 +227,8 @@ const shareTypeNames = createShareTypeNameComputed(2)
|
||||
const shareTypeName = createShareTypeNameComputed(1)
|
||||
|
||||
const sharableName = computed(() => {
|
||||
if (props.type === 'list') {
|
||||
return t('list.list.title')
|
||||
if (props.type === 'project') {
|
||||
return t('project.list.title')
|
||||
}
|
||||
|
||||
if (props.shareType === 'namespace') {
|
||||
@ -244,9 +244,9 @@ if (props.shareType === 'user') {
|
||||
sharable = ref(new UserModel())
|
||||
searchLabel.value = 'username'
|
||||
|
||||
if (props.type === 'list') {
|
||||
stuffService = shallowReactive(new UserListService())
|
||||
stuffModel = reactive(new UserListModel({listId: props.id}))
|
||||
if (props.type === 'project') {
|
||||
stuffService = shallowReactive(new UserProjectService())
|
||||
stuffModel = reactive(new UserProjectModel({projectId: props.id}))
|
||||
} else if (props.type === 'namespace') {
|
||||
stuffService = shallowReactive(new UserNamespaceService())
|
||||
stuffModel = reactive(new UserNamespaceModel({
|
||||
@ -261,9 +261,9 @@ if (props.shareType === 'user') {
|
||||
sharable = ref(new TeamModel())
|
||||
searchLabel.value = 'name'
|
||||
|
||||
if (props.type === 'list') {
|
||||
stuffService = shallowReactive(new TeamListService())
|
||||
stuffModel = reactive(new TeamListModel({listId: props.id}))
|
||||
if (props.type === 'project') {
|
||||
stuffService = shallowReactive(new TeamProjectService())
|
||||
stuffModel = reactive(new TeamProjectModel({projectId: props.id}))
|
||||
} else if (props.type === 'namespace') {
|
||||
stuffService = shallowReactive(new TeamNamespaceService())
|
||||
stuffModel = reactive(new TeamNamespaceModel({
|
||||
@ -303,7 +303,7 @@ async function deleteSharable() {
|
||||
}
|
||||
}
|
||||
success({
|
||||
message: t('list.share.userTeam.removeSuccess', {
|
||||
message: t('project.share.userTeam.removeSuccess', {
|
||||
type: shareTypeName.value,
|
||||
sharable: sharableName.value,
|
||||
}),
|
||||
@ -326,7 +326,7 @@ async function add(admin) {
|
||||
}
|
||||
|
||||
await stuffService.create(stuffModel)
|
||||
success({message: t('list.share.userTeam.addedSuccess', {type: shareTypeName.value})})
|
||||
success({message: t('project.share.userTeam.addedSuccess', {type: shareTypeName.value})})
|
||||
await load()
|
||||
}
|
||||
|
||||
@ -358,7 +358,7 @@ async function toggleType(sharable) {
|
||||
sharables.value[i].right = r.right
|
||||
}
|
||||
}
|
||||
success({message: t('list.share.userTeam.updatedSuccess', {type: shareTypeName.value})})
|
||||
success({message: t('project.share.userTeam.updatedSuccess', {type: shareTypeName.value})})
|
||||
}
|
||||
|
||||
const found = ref([])
|
||||
|
@ -50,7 +50,7 @@ import {parseKebabDate} from '@/helpers/time/parseKebabDate'
|
||||
|
||||
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {GanttFilters} from '@/views/list/helpers/useGanttFilters'
|
||||
import type {GanttFilters} from '@/views/project/helpers/useGanttFilters'
|
||||
|
||||
import {
|
||||
extendDayjs,
|
||||
|
@ -5,7 +5,7 @@
|
||||
<textarea
|
||||
class="add-task-textarea input"
|
||||
:class="{'textarea-empty': newTaskTitle === ''}"
|
||||
:placeholder="$t('list.list.addPlaceholder')"
|
||||
:placeholder="$t('project.list.addPlaceholder')"
|
||||
rows="1"
|
||||
v-focus
|
||||
v-model="newTaskTitle"
|
||||
@ -24,10 +24,10 @@
|
||||
@click="addTask()"
|
||||
icon="plus"
|
||||
:loading="loading"
|
||||
:aria-label="$t('list.list.add')"
|
||||
:aria-label="$t('project.list.add')"
|
||||
>
|
||||
<span class="button-text">
|
||||
{{ $t('list.list.add') }}
|
||||
{{ $t('project.list.add') }}
|
||||
</span>
|
||||
</x-button>
|
||||
</p>
|
||||
@ -107,7 +107,7 @@ const loading = computed(() => taskStore.isLoading)
|
||||
|
||||
async function addTask() {
|
||||
if (newTaskTitle.value === '') {
|
||||
errorMessage.value = t('list.create.addTitleRequired')
|
||||
errorMessage.value = t('project.create.addTitleRequired')
|
||||
return
|
||||
}
|
||||
errorMessage.value = ''
|
||||
@ -128,20 +128,20 @@ async function addTask() {
|
||||
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? [])
|
||||
await taskStore.ensureLabelsExist(allLabels.flat())
|
||||
|
||||
const newTasks = tasksToCreate.map(async ({title, list}) => {
|
||||
const newTasks = tasksToCreate.map(async ({title, project}) => {
|
||||
if (title === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// If the task has a list specified, make sure to use it
|
||||
let listId = null
|
||||
if (list !== null) {
|
||||
listId = await taskStore.findListId({list, listId: 0})
|
||||
// If the task has a project specified, make sure to use it
|
||||
let projectId = null
|
||||
if (project !== null) {
|
||||
projectId = await taskStore.findProjectId({project, projectId: 0})
|
||||
}
|
||||
|
||||
const task = await taskStore.createNewTask({
|
||||
title,
|
||||
listId: listId || authStore.settings.defaultListId,
|
||||
projectId: projectId || authStore.settings.defaultProjectId,
|
||||
position: props.defaultPosition,
|
||||
})
|
||||
createdTasks[title] = task
|
||||
@ -176,7 +176,7 @@ async function addTask() {
|
||||
}))
|
||||
|
||||
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask]
|
||||
// we're only emitting here so that the relation shows up in the task list
|
||||
// we're only emitting here so that the relation shows up in the project
|
||||
emit('taskAdded', createdTask)
|
||||
|
||||
return rel
|
||||
@ -184,8 +184,8 @@ async function addTask() {
|
||||
await Promise.all(relations)
|
||||
} catch (e: any) {
|
||||
newTaskTitle.value = taskTitleBackup
|
||||
if (e?.message === 'NO_LIST') {
|
||||
errorMessage.value = t('list.create.addListRequired')
|
||||
if (e?.message === 'NO_PROJECT') {
|
||||
errorMessage.value = t('project.create.addProjectRequired')
|
||||
return
|
||||
}
|
||||
throw e
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Multiselect
|
||||
:loading="listUserService.loading"
|
||||
:loading="projectUserService.loading"
|
||||
:placeholder="$t('task.assignee.placeholder')"
|
||||
:multiple="true"
|
||||
@search="findUser"
|
||||
@ -30,7 +30,7 @@ import Multiselect from '@/components/input/multiselect.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {includesById} from '@/helpers/utils'
|
||||
import ListUserService from '@/services/listUsers'
|
||||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {success} from '@/message'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
@ -42,7 +42,7 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
listId: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
@ -59,7 +59,7 @@ const emit = defineEmits(['update:modelValue'])
|
||||
const taskStore = useTaskStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const listUserService = shallowReactive(new ListUserService())
|
||||
const projectUserService = shallowReactive(new ProjectUserService())
|
||||
const foundUsers = ref<IUser[]>([])
|
||||
const assignees = ref<IUser[]>([])
|
||||
let isAdding = false
|
||||
@ -94,7 +94,7 @@ async function addAssignee(user: IUser) {
|
||||
async function removeAssignee(user: IUser) {
|
||||
await taskStore.removeAssignee({user: user, taskId: props.taskId})
|
||||
|
||||
// Remove the assignee from the list
|
||||
// Remove the assignee from the project
|
||||
for (const a in assignees.value) {
|
||||
if (assignees.value[a].id === user.id) {
|
||||
assignees.value.splice(a, 1)
|
||||
@ -109,7 +109,7 @@ async function findUser(query: string) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await listUserService.getAll({listId: props.listId}, {s: query}) as IUser[]
|
||||
const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[]
|
||||
|
||||
// Filter the results to not include users who are already assigned
|
||||
foundUsers.value = response
|
||||
|
@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<Multiselect
|
||||
class="control is-expanded"
|
||||
:placeholder="$t('list.search')"
|
||||
:search-results="foundLists"
|
||||
:placeholder="$t('project.search')"
|
||||
:search-results="foundProjects"
|
||||
label="title"
|
||||
:select-placeholder="$t('list.searchSelect')"
|
||||
:model-value="list"
|
||||
@update:model-value="Object.assign(list, $event)"
|
||||
:select-placeholder="$t('project.searchSelect')"
|
||||
:model-value="project"
|
||||
@update:model-value="Object.assign(project, $event)"
|
||||
@select="select"
|
||||
@search="findLists"
|
||||
@search="findProjects"
|
||||
>
|
||||
<template #searchResult="{option}">
|
||||
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
|
||||
{{ (option as IList).title }}
|
||||
<span class="project-namespace-title search-result">{{ namespace((option as IProject).namespaceId) }} ></span>
|
||||
{{ (option as IProject).title }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
@ -22,19 +22,19 @@ import {reactive, ref, watch} from 'vue'
|
||||
import type {PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<IList>,
|
||||
type: Object as PropType<IProject>,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
@ -42,45 +42,45 @@ const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const list: IList = reactive(new ListModel())
|
||||
const project: IProject = reactive(new ProjectModel())
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newList) => Object.assign(list, newList),
|
||||
(newProject) => Object.assign(project, newProject),
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const foundLists = ref<IList[]>([])
|
||||
function findLists(query: string) {
|
||||
const foundProjects = ref<IProject[]>([])
|
||||
function findProjects(query: string) {
|
||||
if (query === '') {
|
||||
select(null)
|
||||
}
|
||||
foundLists.value = listStore.searchList(query)
|
||||
foundProjects.value = projectStore.searchProject(query)
|
||||
}
|
||||
|
||||
function select(l: IList | null) {
|
||||
function select(l: IProject | null) {
|
||||
if (l === null) {
|
||||
return
|
||||
}
|
||||
Object.assign(list, l)
|
||||
emit('update:modelValue', list)
|
||||
Object.assign(project, l)
|
||||
emit('update:modelValue', project)
|
||||
}
|
||||
|
||||
function namespace(namespaceId: INamespace['id']) {
|
||||
const namespace = namespaceStore.getNamespaceById(namespaceId)
|
||||
return namespace !== null
|
||||
? namespace.title
|
||||
: t('list.shared')
|
||||
: t('project.shared')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-namespace-title {
|
||||
.project-namespace-title {
|
||||
color: var(--grey-500);
|
||||
}
|
||||
</style>
|
@ -37,14 +37,14 @@
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('list.list.title') }}</h3>
|
||||
<h3>{{ $t('project.list.title') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.list1', {prefix: prefixes.list}) }}
|
||||
{{ $t('task.quickAddMagic.list2') }}
|
||||
{{ $t('task.quickAddMagic.project1', {prefix: prefixes.project}) }}
|
||||
{{ $t('task.quickAddMagic.project2') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.list3') }}
|
||||
{{ $t('task.quickAddMagic.list4', {prefix: prefixes.list}) }}
|
||||
{{ $t('task.quickAddMagic.project3') }}
|
||||
{{ $t('task.quickAddMagic.project4', {prefix: prefixes.project}) }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3>
|
||||
|
@ -43,8 +43,8 @@
|
||||
:class="{'is-strikethrough': task.done}"
|
||||
>
|
||||
<span
|
||||
class="different-list"
|
||||
v-if="task.listId !== listId"
|
||||
class="different-project"
|
||||
v-if="task.projectId !== projectId"
|
||||
>
|
||||
<span
|
||||
v-if="task.differentNamespace !== null"
|
||||
@ -52,9 +52,9 @@
|
||||
{{ task.differentNamespace }} >
|
||||
</span>
|
||||
<span
|
||||
v-if="task.differentList !== null"
|
||||
v-tooltip="$t('task.relation.differentList')">
|
||||
{{ task.differentList }} >
|
||||
v-if="task.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
{{ task.differentProject }} >
|
||||
</span>
|
||||
</span>
|
||||
{{ task.title }}
|
||||
@ -98,8 +98,8 @@
|
||||
:class="{ 'is-strikethrough': t.done}"
|
||||
>
|
||||
<span
|
||||
class="different-list"
|
||||
v-if="t.listId !== listId"
|
||||
class="different-project"
|
||||
v-if="t.projectId !== projectId"
|
||||
>
|
||||
<span
|
||||
v-if="t.differentNamespace !== null"
|
||||
@ -107,9 +107,9 @@
|
||||
{{ t.differentNamespace }} >
|
||||
</span>
|
||||
<span
|
||||
v-if="t.differentList !== null"
|
||||
v-tooltip="$t('task.relation.differentList')">
|
||||
{{ t.differentList }} >
|
||||
v-if="t.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
{{ t.differentProject }} >
|
||||
</span>
|
||||
</span>
|
||||
{{ t.title }}
|
||||
@ -186,7 +186,7 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listId: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
@ -230,17 +230,17 @@ async function findTasks(newQuery: string) {
|
||||
foundTasks.value = await taskService.getAll({}, {s: newQuery})
|
||||
}
|
||||
|
||||
const getListAndNamespaceById = (listId: number) => namespaceStore.getListAndNamespaceById(listId, true)
|
||||
const getProjectAndNamespaceById = (projectId: number) => namespaceStore.getProjectAndNamespaceById(projectId, true)
|
||||
|
||||
const namespace = computed(() => getListAndNamespaceById(props.listId)?.namespace)
|
||||
const namespace = computed(() => getProjectAndNamespaceById(props.projectId)?.namespace)
|
||||
|
||||
function mapRelatedTasks(tasks: ITask[]) {
|
||||
return tasks.map(task => {
|
||||
// by doing this here once we can save a lot of duplicate calls in the template
|
||||
const {
|
||||
list,
|
||||
project,
|
||||
namespace: taskNamespace,
|
||||
} = getListAndNamespaceById(task.listId) || {list: null, namespace: null}
|
||||
} = getProjectAndNamespaceById(task.projectId) || {project: null, namespace: null}
|
||||
|
||||
return {
|
||||
...task,
|
||||
@ -248,10 +248,10 @@ function mapRelatedTasks(tasks: ITask[]) {
|
||||
(taskNamespace !== null &&
|
||||
taskNamespace.id !== namespace.value.id &&
|
||||
taskNamespace?.title) || null,
|
||||
differentList:
|
||||
(list !== null &&
|
||||
task.listId !== props.listId &&
|
||||
list?.title) || null,
|
||||
differentProject:
|
||||
(project !== null &&
|
||||
task.projectId !== props.projectId &&
|
||||
project?.title) || null,
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -343,7 +343,7 @@ async function removeTaskRelation() {
|
||||
}
|
||||
|
||||
async function createAndRelateTask(title: string) {
|
||||
const newTask = await taskService.create(new TaskModel({title, listId: props.listId}))
|
||||
const newTask = await taskService.create(new TaskModel({title, projectId: props.projectId}))
|
||||
newTaskRelation.task = newTask
|
||||
await addTaskRelation()
|
||||
}
|
||||
@ -351,7 +351,7 @@ async function createAndRelateTask(title: string) {
|
||||
async function toggleTaskDone(task: ITask) {
|
||||
await taskStore.update(task)
|
||||
|
||||
// Find the task in the list and update it so that it is correctly strike through
|
||||
// Find the task in the project and update it so that it is correctly strike through
|
||||
Object.entries(relatedTasks.value).some(([kind, tasks]) => {
|
||||
return (tasks as ITask[]).some((t, key) => {
|
||||
const found = t.id === task.id
|
||||
@ -379,7 +379,7 @@ async function toggleTaskDone(task: ITask) {
|
||||
}
|
||||
}
|
||||
|
||||
.different-list {
|
||||
.different-project {
|
||||
color: var(--grey-500);
|
||||
width: auto;
|
||||
}
|
||||
|
@ -11,23 +11,23 @@
|
||||
/>
|
||||
|
||||
<ColorBubble
|
||||
v-if="showListColor && listColor !== '' && currentList.id !== task.listId"
|
||||
:color="listColor"
|
||||
v-if="showProjectColor && projectColor !== '' && currentProject.id !== task.projectId"
|
||||
:color="projectColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
||||
<div
|
||||
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}"
|
||||
:class="{ 'done': task.done, 'show-project': showProject && project !== null}"
|
||||
class="tasktext"
|
||||
>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="showList && taskList !== null"
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
class="task-list"
|
||||
v-if="showProject && project !== null"
|
||||
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
|
||||
class="task-project"
|
||||
:class="{'mr-2': task.hexColor !== ''}"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})">
|
||||
{{ taskList.title }}
|
||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})">
|
||||
{{ project.title }}
|
||||
</router-link>
|
||||
|
||||
<ColorBubble
|
||||
@ -84,13 +84,13 @@
|
||||
<priority-label :priority="task.priority" :done="task.done"/>
|
||||
|
||||
<span>
|
||||
<span class="list-task-icon" v-if="task.attachments.length > 0">
|
||||
<span class="project-task-icon" v-if="task.attachments.length > 0">
|
||||
<icon icon="paperclip"/>
|
||||
</span>
|
||||
<span class="list-task-icon" v-if="task.description">
|
||||
<span class="project-task-icon" v-if="task.description">
|
||||
<icon icon="align-left"/>
|
||||
</span>
|
||||
<span class="list-task-icon" v-if="task.repeatAfter.amount > 0">
|
||||
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
|
||||
<icon icon="history"/>
|
||||
</span>
|
||||
</span>
|
||||
@ -107,12 +107,12 @@
|
||||
</progress>
|
||||
|
||||
<router-link
|
||||
v-if="!showList && currentList.id !== task.listId && taskList !== null"
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
class="task-list"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})"
|
||||
v-if="!showProject && currentProject.id !== task.projectId && project !== null"
|
||||
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
|
||||
class="task-project"
|
||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
||||
>
|
||||
{{ taskList.title }}
|
||||
{{ project.title }}
|
||||
</router-link>
|
||||
|
||||
<BaseButton
|
||||
@ -151,7 +151,7 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
@ -165,7 +165,7 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showList: {
|
||||
showProject: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@ -173,7 +173,7 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showListColor: {
|
||||
showProjectColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
@ -210,18 +210,18 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const taskList = computed(() => listStore.getListById(task.value.listId))
|
||||
const listColor = computed(() => taskList.value !== null ? taskList.value.hexColor : '')
|
||||
const project = computed(() => projectStore.getProjectById(task.value.projectId))
|
||||
const projectColor = computed(() => project.value !== null ? project.value.hexColor : '')
|
||||
|
||||
const currentList = computed(() => {
|
||||
return typeof baseStore.currentList === 'undefined' ? {
|
||||
const currentProject = computed(() => {
|
||||
return typeof baseStore.currentProject === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
} : baseStore.currentList
|
||||
} : baseStore.currentProject
|
||||
})
|
||||
|
||||
const taskDetailRoute = computed(() => ({
|
||||
@ -314,7 +314,7 @@ function hideDeferDueDatePopup(e) {
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
.task-project {
|
||||
width: auto;
|
||||
color: var(--grey-400);
|
||||
font-size: .9rem;
|
||||
@ -329,7 +329,7 @@ function hideDeferDueDatePopup(e) {
|
||||
width: 27px;
|
||||
}
|
||||
|
||||
.list-task-icon {
|
||||
.project-task-icon {
|
||||
margin-left: 6px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
@ -394,7 +394,7 @@ function hideDeferDueDatePopup(e) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.show-list .parent-tasks {
|
||||
.show-project .parent-tasks {
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
Reference in New Issue
Block a user