1
0

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:
kolaente
2022-11-13 22:04:57 +01:00
committed by Gitea
parent b9d3b5c756
commit befa6f27bb
133 changed files with 1873 additions and 1881 deletions

View File

@ -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);
}
}

View File

@ -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})
}
})

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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()
}

View File

@ -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',
},

View File

@ -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')

View File

@ -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 } }"

View File

@ -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'

View File

@ -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},

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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'

View File

@ -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')

View File

@ -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,
}
}
}

View File

@ -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>

View File

@ -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 },
})
}

View File

@ -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>&nbsp;
{{ $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>&nbsp;
{{ $t('list.share.right.readWrite') }}
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users"/>
</span>&nbsp;
{{ $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>

View File

@ -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([])

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}