feature/projects-all-the-way-down #3323
|
@ -94,7 +94,6 @@ watch(() => route.name as string, (routeName) => {
|
|||
(
|
||||
[
|
||||
'home',
|
||||
'namespace.edit',
|
||||
'teams.index',
|
||||
'teams.edit',
|
||||
'tasks.range',
|
||||
|
|
|
@ -159,12 +159,10 @@ import type {SortableEvent} from 'sortablejs'
|
|||
|
||||
import BaseButton from '@/components/base/BaseButton.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 {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
@ -172,7 +170,6 @@ import ColorBubble from '@/components/misc/colorBubble.vue'
|
|||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const drag = ref(false)
|
||||
const dragOptions = {
|
||||
|
@ -199,7 +196,7 @@ const activeProjects = computed(() => {
|
|||
})
|
||||
|
||||
const namespaceTitles = computed(() => {
|
||||
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
|
||||
return []
|
||||
})
|
||||
|
||||
const namespaceProjectsCount = computed(() => {
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
<template>
|
||||
<multiselect
|
||||
v-model="selectedNamespaces"
|
||||
:search-results="foundNamespaces"
|
||||
:loading="namespaceService.loading"
|
||||
:multiple="true"
|
||||
:placeholder="$t('namespace.search')"
|
||||
label="namespace"
|
||||
@search="findNamespaces"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import {includesById} from '@/helpers/utils'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<INamespace[]>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: INamespace[]): void
|
||||
}>()
|
||||
|
||||
const namespaces = ref<INamespace[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
namespaces.value = props.modelValue
|
||||
})
|
||||
|
||||
const selectedNamespaces = computed({
|
||||
get() {
|
||||
return namespaces.value
|
||||
},
|
||||
set: (value) => {
|
||||
namespaces.value = value
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
const namespaceService = shallowReactive(new NamespaceService())
|
||||
const foundNamespaces = ref<INamespace[]>([])
|
||||
|
||||
async function findNamespaces(query: string) {
|
||||
if (query === '') {
|
||||
foundNamespaces.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const response = await namespaceService.getAll({}, {s: query}) as INamespace[]
|
||||
|
||||
// Filter selected items from the results
|
||||
foundNamespaces.value = response.filter(({id}) => !includesById(namespaces.value, id))
|
||||
}
|
||||
</script>
|
|
@ -44,8 +44,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
|||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.navigation.namespaces',
|
||||
keys: ['g', 'n'],
|
||||
title: 'keyboardShortcuts.navigation.projects',
|
||||
keys: ['g', 'p'],
|
||||
dpschen marked this conversation as resolved
Outdated
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -73,12 +73,6 @@ const {t} = useI18n({useScope: 'global'})
|
|||
|
||||
const tooltipText = computed(() => {
|
||||
if (disabled.value) {
|
||||
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 === 'project') {
|
||||
return t('task.subscription.subscribedTaskThroughParentProject')
|
||||
}
|
||||
|
@ -87,10 +81,6 @@ const tooltipText = computed(() => {
|
|||
}
|
||||
|
||||
switch (props.entity) {
|
||||
case 'namespace':
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedNamespace') :
|
||||
t('task.subscription.notSubscribedNamespace')
|
||||
case 'project':
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedProject') :
|
||||
|
@ -130,9 +120,6 @@ async function subscribe() {
|
|||
|
||||
let message = ''
|
||||
switch (props.entity) {
|
||||
case 'namespace':
|
||||
message = t('task.subscription.subscribeSuccessNamespace')
|
||||
break
|
||||
case 'project':
|
||||
message = t('task.subscription.subscribeSuccessProject')
|
||||
break
|
||||
|
@ -153,9 +140,6 @@ async function unsubscribe() {
|
|||
|
||||
let message = ''
|
||||
switch (props.entity) {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Add type to props definition: Should be Add type to props definition: Should be `ISubscription['entity']`
konrad
commented
Done. Done.
|
||||
case 'namespace':
|
||||
message = t('task.subscription.unsubscribeSuccessNamespace')
|
||||
break
|
||||
case 'project':
|
||||
message = t('task.subscription.unsubscribeSuccessProject')
|
||||
break
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
<template>
|
||||
<dropdown>
|
||||
<template #trigger="triggerProps">
|
||||
<slot name="trigger" v-bind="triggerProps">
|
||||
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="namespace.isArchived">
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.edit', params: { id: namespace.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id } }"
|
||||
icon="share-alt"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'project.create', params: { namespaceId: namespace.id } }"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('menu.newProject') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
</dropdown-item>
|
||||
<Subscription
|
||||
class="has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="namespace"
|
||||
:entity-id="namespace.id"
|
||||
:model-value="subscription"
|
||||
@update:model-value="setSubscriptionInStore"
|
||||
type="dropdown"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
{{ $t('menu.delete') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
</dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, type PropType} from 'vue'
|
||||
|
||||
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 {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const props = defineProps({
|
||||
namespace: {
|
||||
type: Object as PropType<INamespace>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const subscription = ref<ISubscription | null>(null)
|
||||
onMounted(() => {
|
||||
subscription.value = props.namespace.subscription
|
||||
})
|
||||
|
||||
function setSubscriptionInStore(sub: ISubscription) {
|
||||
subscription.value = sub
|
||||
namespaceStore.setNamespaceById({
|
||||
...props.namespace,
|
||||
subscription: sub,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dropdown-trigger {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
<CustomTransition name="fade">
|
||||
<Message variant="warning" v-if="currentProject.isArchived" class="mb-4">
|
||||
{{ $t('project.archived') }}
|
||||
{{ $t('project.archivedText') }}
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
How about How about `archivedMessage` or `archivedWarning`
konrad
commented
I like I like `archivedMessage`, renamed it.
|
||||
</Message>
|
||||
</CustomTransition>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
/>
|
||||
<span v-if="project.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
||||
<span v-if="project.isArchived" class="is-archived" >{{ $t('project.archived') }}</span>
|
||||
|
||||
<div class="project-title" aria-hidden="true">{{ project.title }}</div>
|
||||
<BaseButton
|
||||
|
|
|
@ -165,16 +165,6 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('namespace.namespaces') }}</label>
|
||||
<div class="control">
|
||||
<SelectNamespace
|
||||
v-model="entities.namespace"
|
||||
@select="changeMultiselectFilter('namespace', 'namespace')"
|
||||
@remove="changeMultiselectFilter('namespace', 'namespace')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</card>
|
||||
</template>
|
||||
|
@ -189,7 +179,6 @@ 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 {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
|
@ -201,7 +190,6 @@ import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
|||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import SelectUser from '@/components/input/SelectUser.vue'
|
||||
import SelectProject from '@/components/input/SelectProject.vue'
|
||||
import SelectNamespace from '@/components/input/SelectNamespace.vue'
|
||||
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
|
||||
|
@ -209,7 +197,6 @@ import {objectToSnakeCase} from '@/helpers/case'
|
|||
|
||||
import UserService from '@/services/user'
|
||||
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'
|
||||
|
@ -240,7 +227,6 @@ const DEFAULT_FILTERS = {
|
|||
assignees: '',
|
||||
labels: '',
|
||||
project_id: '',
|
||||
namespace: '',
|
||||
} as const
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -265,23 +251,20 @@ const filters = ref({...DEFAULT_FILTERS})
|
|||
const services = {
|
||||
users: shallowReactive(new UserService()),
|
||||
projects: shallowReactive(new ProjectService()),
|
||||
namespace: shallowReactive(new NamespaceService()),
|
||||
}
|
||||
|
||||
interface Entities {
|
||||
users: IUser[]
|
||||
labels: ILabel[]
|
||||
projects: IProject[]
|
||||
namespace: INamespace[]
|
||||
}
|
||||
|
||||
type EntityType = 'users' | 'labels' | 'projects' | 'namespace'
|
||||
type EntityType = 'users' | 'labels' | 'projects'
|
||||
|
||||
const entities: Entities = reactive({
|
||||
users: [],
|
||||
labels: [],
|
||||
projects: [],
|
||||
namespace: [],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -328,7 +311,6 @@ function prepareFilters() {
|
|||
prepareDate('reminders')
|
||||
prepareRelatedObjectFilter('users', 'assignees')
|
||||
prepareRelatedObjectFilter('projects', 'project_id')
|
||||
prepareRelatedObjectFilter('namespace')
|
||||
|
||||
prepareSingleValue('labels')
|
||||
|
||||
|
|
|
@ -96,7 +96,6 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
|
|||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
|
@ -106,7 +105,6 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const subscription = ref<ISubscription | null>(null)
|
||||
watchEffect(() => {
|
||||
subscription.value = props.project.subscription ?? null
|
||||
|
@ -122,6 +120,5 @@ function setSubscriptionInStore(sub: ISubscription) {
|
|||
subscription: sub,
|
||||
}
|
||||
projectStore.setProject(updatedProject)
|
||||
namespaceStore.setProjectInNamespaceById(updatedProject)
|
||||
}
|
||||
</script>
|
|
@ -61,7 +61,6 @@ import {useRouter} from 'vue-router'
|
|||
import TaskService from '@/services/task'
|
||||
import TeamService from '@/services/team'
|
||||
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import TeamModel from '@/models/team'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
|
@ -70,7 +69,6 @@ import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
|||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
|
@ -81,7 +79,6 @@ import {success} from '@/message'
|
|||
|
||||
import type {ITeam} from '@/modelTypes/ITeam'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
@ -89,7 +86,6 @@ const router = useRouter()
|
|||
|
||||
const baseStore = useBaseStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const labelStore = useLabelStore()
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
|
@ -105,7 +101,6 @@ enum ACTION_TYPE {
|
|||
enum COMMAND_TYPE {
|
||||
NEW_TASK = 'newTask',
|
||||
NEW_PROJECT = 'newProject',
|
||||
NEW_NAMESPACE = 'newNamespace',
|
||||
NEW_TEAM = 'newTeam',
|
||||
}
|
||||
|
||||
|
@ -147,7 +142,6 @@ const foundProjects = computed(() => {
|
|||
return []
|
||||
}
|
||||
|
||||
const ncache: { [id: ProjectModel['id']]: INamespace } = {}
|
||||
const history = getHistory()
|
||||
const allProjects = [
|
||||
...new Set([
|
||||
|
@ -156,15 +150,7 @@ const foundProjects = computed(() => {
|
|||
]),
|
||||
]
|
||||
|
||||
return allProjects.filter((l) => {
|
||||
if (typeof l === 'undefined' || l === null) {
|
||||
return false
|
||||
}
|
||||
if (typeof ncache[l.namespaceId] === 'undefined') {
|
||||
ncache[l.namespaceId] = namespaceStore.getNamespaceById(l.namespaceId)
|
||||
}
|
||||
return !ncache[l.namespaceId].isArchived
|
||||
})
|
||||
return allProjects.filter((l) => typeof l !== 'undefined' && l !== null)
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
If projects are archived they won't show up here automatically? If projects are archived they won't show up here automatically?
konrad
commented
yes. yes.
dpschen
commented
We didn't include archived namespaces if I understand the code correctly. We didn't include archived namespaces if I understand the code correctly.
konrad
commented
That's correct, so now we don't show archived projects. That's correct, so now we don't show archived projects.
dpschen
commented
Okay. I still don't understand where the archived filterting happens now (didn't check in detail) but if you are aware all good :) Okay. I still don't understand where the archived filterting happens now (didn't check in detail) but if you are aware all good :)
|
||||
})
|
||||
|
||||
// FIXME: use fuzzysearch
|
||||
|
@ -205,7 +191,6 @@ const results = computed<Result[]>(() => {
|
|||
|
||||
const loading = computed(() =>
|
||||
taskService.loading ||
|
||||
namespaceStore.isLoading ||
|
||||
projectStore.isLoading ||
|
||||
teamService.loading,
|
||||
)
|
||||
|
@ -230,12 +215,6 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
|
|||
placeholder: t('quickActions.newProject'),
|
||||
action: newProject,
|
||||
},
|
||||
newNamespace: {
|
||||
type: COMMAND_TYPE.NEW_NAMESPACE,
|
||||
title: t('quickActions.cmds.newNamespace'),
|
||||
placeholder: t('quickActions.newNamespace'),
|
||||
action: newNamespace,
|
||||
},
|
||||
newTeam: {
|
||||
type: COMMAND_TYPE.NEW_TEAM,
|
||||
title: t('quickActions.cmds.newTeam'),
|
||||
|
@ -252,7 +231,6 @@ const currentProject = computed(() => Object.keys(baseStore.currentProject).leng
|
|||
)
|
||||
|
||||
const hintText = computed(() => {
|
||||
let namespace
|
||||
if (selectedCmd.value !== null && currentProject.value !== null) {
|
||||
switch (selectedCmd.value.type) {
|
||||
case COMMAND_TYPE.NEW_TASK:
|
||||
|
@ -260,12 +238,7 @@ const hintText = computed(() => {
|
|||
title: currentProject.value.title,
|
||||
})
|
||||
case COMMAND_TYPE.NEW_PROJECT:
|
||||
namespace = namespaceStore.getNamespaceById(
|
||||
currentProject.value.namespaceId,
|
||||
)
|
||||
return t('quickActions.createProject', {
|
||||
title: namespace?.title,
|
||||
})
|
||||
return t('quickActions.createProject')
|
||||
}
|
||||
}
|
||||
const prefixes =
|
||||
|
@ -278,7 +251,7 @@ const availableCmds = computed(() => {
|
|||
if (currentProject.value !== null) {
|
||||
cmds.push(commands.value.newTask, commands.value.newProject)
|
||||
}
|
||||
cmds.push(commands.value.newNamespace, commands.value.newTeam)
|
||||
cmds.push(commands.value.newTeam)
|
||||
return cmds
|
||||
})
|
||||
|
||||
|
@ -506,7 +479,6 @@ async function newProject() {
|
|||
}
|
||||
const newProject = await projectStore.createProject(new ProjectModel({
|
||||
title: query.value,
|
||||
namespaceId: currentProject.value.namespaceId,
|
||||
}))
|
||||
success({ message: t('project.create.createdSuccess')})
|
||||
await router.push({
|
||||
|
@ -515,12 +487,6 @@ async function newProject() {
|
|||
})
|
||||
}
|
||||
|
||||
async function newNamespace() {
|
||||
const newNamespace = new NamespaceModel({ title: query.value })
|
||||
await namespaceStore.createNamespace(newNamespace)
|
||||
success({ message: t('namespace.create.success') })
|
||||
}
|
||||
|
||||
async function newTeam() {
|
||||
const newTeam = new TeamModel({ name: query.value })
|
||||
const team = await teamService.create(newTeam)
|
||||
|
|
|
@ -139,10 +139,6 @@ import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
|
|||
import type {PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import UserNamespaceService from '@/services/userNamespace'
|
||||
import UserNamespaceModel from '@/models/userNamespace'
|
||||
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
|
||||
|
||||
import UserProjectService from '@/services/userProject'
|
||||
import UserProjectModel from '@/models/userProject'
|
||||
import type {IUserProject} from '@/modelTypes/IUserProject'
|
||||
|
@ -151,10 +147,6 @@ import UserService from '@/services/user'
|
|||
import UserModel, { getDisplayName } from '@/models/user'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
|
||||
import TeamNamespaceService from '@/services/teamNamespace'
|
||||
import TeamNamespaceModel from '@/models/teamNamespace'
|
||||
import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
|
||||
|
||||
import TeamProjectService from '@/services/teamProject'
|
||||
import TeamProjectModel from '@/models/teamProject'
|
||||
import type { ITeamProject } from '@/modelTypes/ITeamProject'
|
||||
|
@ -170,13 +162,15 @@ import Nothing from '@/components/misc/nothing.vue'
|
|||
import {success} from '@/message'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
Not sure if we should do that here in this PR - it's narrow enough to do it later. Not sure if we should do that here in this PR - it's narrow enough to do it later.
dpschen
commented
I created a new issue #3326 I created a new issue https://kolaente.dev/vikunja/frontend/issues/3326
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<'project' | 'namespace'>,
|
||||
type: String as PropType<'project'>,
|
||||
default: '',
|
||||
},
|
||||
shareType: {
|
||||
type: String as PropType<'user' | 'team' | 'namespace'>,
|
||||
type: String as PropType<'user' | 'team'>,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
|
@ -191,9 +185,9 @@ const props = defineProps({
|
|||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
// 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
|
||||
// This user service is a userProjectService, depending on the type we are using
|
||||
let stuffService: UserProjectService | TeamProjectService
|
||||
let stuffModel: IUserProject | ITeamProject
|
||||
let searchService: UserService | TeamService
|
||||
let sharable: Ref<IUser | ITeam>
|
||||
|
||||
|
@ -231,10 +225,6 @@ const sharableName = computed(() => {
|
|||
return t('project.list.title')
|
||||
}
|
||||
|
||||
if (props.shareType === 'namespace') {
|
||||
return t('namespace.namespace')
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
|
@ -247,11 +237,6 @@ if (props.shareType === 'user') {
|
|||
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({
|
||||
namespaceId: props.id,
|
||||
}))
|
||||
} else {
|
||||
throw new Error('Unknown type: ' + props.type)
|
||||
}
|
||||
|
@ -264,11 +249,6 @@ if (props.shareType === 'user') {
|
|||
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({
|
||||
namespaceId: props.id,
|
||||
}))
|
||||
} else {
|
||||
throw new Error('Unknown type: ' + props.type)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
@search="findProjects"
|
||||
>
|
||||
<template #searchResult="{option}">
|
||||
<span class="project-namespace-title search-result">{{ namespace((option as IProject).namespaceId) }} ></span>
|
||||
{{ (option as IProject).title }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
|
@ -23,10 +22,8 @@ import type {PropType} from 'vue'
|
|||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
|
@ -54,7 +51,6 @@ watch(
|
|||
)
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const foundProjects = ref<IProject[]>([])
|
||||
function findProjects(query: string) {
|
||||
if (query === '') {
|
||||
|
@ -70,17 +66,4 @@ function select(l: IProject | null) {
|
|||
Object.assign(project, l)
|
||||
emit('update:modelValue', project)
|
||||
}
|
||||
|
||||
function namespace(namespaceId: INamespace['id']) {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
This seems like a relevant info. I think we shouldn't remove this feature. This seems like a relevant info. I think we shouldn't remove this feature.
konrad
commented
What do you mean? What do you mean?
dpschen
commented
We should show the parent project here now instead. We should show the parent project here now instead.
konrad
commented
Now doing that. Now doing that.
dpschen
commented
I didn't have a chance yet to check the actual UI, but I realised that the parent might be 'wrong' sometimes. Or at least in the future if there are third level nested projects it might not always be the same as what one would have expected. Because historically the namespace would be shown. The latter is now the 'root ancestor'. I didn't have a chance yet to check the actual UI, but I realised that the parent might be 'wrong' sometimes. Or at least in the future if there are third level nested projects it might not always be the same as what one would have expected. Because historically the namespace would be shown. The latter is now the 'root ancestor'.
konrad
commented
The way I implemented it, it will climb to the top of the tree and show each project. So if the hierarchy is something like first > second > third > task (task is a task in "third"), it will literally show "first > second > third" on the task detail view, while allowing to click on each project. The way I implemented it, it will climb to the top of the tree and show each project. So if the hierarchy is something like first > second > third > task (task is a task in "third"), it will literally show "first > second > third" on the task detail view, while allowing to click on each project.
dpschen
commented
That makes sense. Hopefully there is enough space though. That makes sense. Hopefully there is enough space though.
|
||||
const namespace = namespaceStore.getNamespaceById(namespaceId)
|
||||
return namespace !== null
|
||||
? namespace.title
|
||||
: t('project.shared')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.project-namespace-title {
|
||||
color: var(--grey-500);
|
||||
}
|
||||
</style>
|
|
@ -46,11 +46,6 @@
|
|||
class="different-project"
|
||||
v-if="task.projectId !== projectId"
|
||||
>
|
||||
<span
|
||||
v-if="task.differentNamespace !== null"
|
||||
v-tooltip="$t('task.relation.differentNamespace')">
|
||||
{{ task.differentNamespace }} >
|
||||
</span>
|
||||
<span
|
||||
v-if="task.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
|
@ -101,11 +96,6 @@
|
|||
class="different-project"
|
||||
v-if="t.projectId !== projectId"
|
||||
>
|
||||
<span
|
||||
v-if="t.differentNamespace !== null"
|
||||
v-tooltip="$t('task.relation.differentNamespace')">
|
||||
{{ t.differentNamespace }} >
|
||||
</span>
|
||||
<span
|
||||
v-if="t.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
|
@ -168,10 +158,9 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import {error, success} from '@/message'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
|
@ -196,7 +185,7 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const projectStore = useProjectStore()
|
||||
const route = useRoute()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
@ -230,24 +219,13 @@ async function findTasks(newQuery: string) {
|
|||
foundTasks.value = await taskService.getAll({}, {s: newQuery})
|
||||
}
|
||||
|
||||
const getProjectAndNamespaceById = (projectId: number) => namespaceStore.getProjectAndNamespaceById(projectId, true)
|
||||
|
||||
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 {
|
||||
project,
|
||||
namespace: taskNamespace,
|
||||
} = getProjectAndNamespaceById(task.projectId) || {project: null, namespace: null}
|
||||
const project = projectStore.getProjectById(task.ProjectId)
|
||||
|
||||
return {
|
||||
...task,
|
||||
differentNamespace:
|
||||
(taskNamespace !== null &&
|
||||
taskNamespace.id !== namespace.value.id &&
|
||||
taskNamespace?.title) || null,
|
||||
differentProject:
|
||||
(project !== null &&
|
||||
task.projectId !== props.projectId &&
|
||||
|
|
|
@ -149,7 +149,6 @@ import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatD
|
|||
import {success} from '@/message'
|
||||
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
|
@ -209,7 +208,6 @@ onBeforeUnmount(() => {
|
|||
const baseStore = useBaseStore()
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const project = computed(() => projectStore.getProjectById(task.value.projectId))
|
||||
const projectColor = computed(() => project.value !== null ? project.value.hexColor : '')
|
||||
|
@ -260,7 +258,6 @@ async function toggleFavorite() {
|
|||
task.value.isFavorite = !task.value.isFavorite
|
||||
task.value = await taskService.update(task.value)
|
||||
emit('task-updated', task.value)
|
||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
}
|
||||
|
||||
const deferDueDate = ref<typeof DeferTask | null>(null)
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import {ref, computed} from 'vue'
|
||||
dpschen
commented
This whole functionality is really nice for projects as well. Why not use it there => This whole functionality is really nice for projects as well. Why not use it there => `useProjectSearch`
konrad
commented
The project store has a The project store has a `projectSearch` method which does something similar. Or do you have something specific in mind?
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
export function useNamespaceSearch() {
|
||||
const query = ref('')
|
||||
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const namespaces = computed(() => namespaceStore.searchNamespace(query.value))
|
||||
|
||||
function findNamespaces(newQuery: string) {
|
||||
query.value = newQuery
|
||||
}
|
||||
|
||||
return {
|
||||
namespaces,
|
||||
findNamespaces,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
export const getNamespaceTitle = (n: INamespace) => {
|
||||
if (n.id === -1) {
|
||||
return i18n.global.t('namespace.pseudo.sharedProjects.title')
|
||||
}
|
||||
if (n.id === -2) {
|
||||
return i18n.global.t('namespace.pseudo.favorites.title')
|
||||
}
|
||||
if (n.id === -3) {
|
||||
return i18n.global.t('namespace.pseudo.savedFilters.title')
|
||||
}
|
||||
return n.title
|
||||
}
|
|
@ -143,7 +143,7 @@
|
|||
},
|
||||
"deletion": {
|
||||
"title": "Delete your Vikunja Account",
|
||||
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.",
|
||||
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
|
||||
"text2": "To proceed, please enter your password. You will receive an email with further instructions.",
|
||||
"confirm": "Delete my account",
|
||||
"requestSuccess": "The request was successful. You'll receive an email with further instructions.",
|
||||
|
@ -165,7 +165,8 @@
|
|||
}
|
||||
},
|
||||
"project": {
|
||||
"archived": "This project is archived. It is not possible to create new or edit tasks for it.",
|
||||
"archivedText": "This project is archived. It is not possible to create new or edit tasks for it.",
|
||||
"archived": "Archived",
|
||||
"showArchived": "Show Archived",
|
||||
"title": "Project Title",
|
||||
"color": "Color",
|
||||
|
@ -211,7 +212,6 @@
|
|||
"duplicate": {
|
||||
"title": "Duplicate this project",
|
||||
"label": "Duplicate",
|
||||
"text": "Select a namespace which should hold the duplicated project:",
|
||||
"success": "The project was successfully duplicated."
|
||||
},
|
||||
"edit": {
|
||||
|
@ -322,66 +322,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"title": "Namespaces & Projects",
|
||||
"namespace": "Namespace",
|
||||
"noneAvailable": "You don't have any namespaces right now.",
|
||||
"unarchive": "Un-Archive",
|
||||
"archived": "Archived",
|
||||
"noProjects": "This namespace does not contain any projects.",
|
||||
"createProject": "Create a new project in this namespace.",
|
||||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
"success": "The namespace was successfully created."
|
||||
},
|
||||
"archive": {
|
||||
"titleArchive": "Archive \"{namespace}\"",
|
||||
"titleUnarchive": "Un-Archive \"{namespace}\"",
|
||||
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
|
||||
"unarchiveText": "You will be able to create new projects or edit it.",
|
||||
"success": "The namespace was successfully archived.",
|
||||
"unarchiveSuccess": "The namespace was successfully un-archived.",
|
||||
"description": "If a namespace is archived, you cannot create new projects or edit it."
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete \"{namespace}\"",
|
||||
"text1": "Are you sure you want to delete this namespace and all of its contents?",
|
||||
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
|
||||
"success": "The namespace was successfully deleted."
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit \"{namespace}\"",
|
||||
"success": "The namespace was successfully updated."
|
||||
},
|
||||
"share": {
|
||||
"title": "Share \"{namespace}\""
|
||||
},
|
||||
"attributes": {
|
||||
"title": "Namespace Title",
|
||||
"titlePlaceholder": "The namespace title goes here…",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "The namespaces description goes here…",
|
||||
"color": "Color",
|
||||
"archived": "Is Archived",
|
||||
"isArchived": "This namespace is archived"
|
||||
},
|
||||
"pseudo": {
|
||||
"sharedProjects": {
|
||||
"title": "Shared Projects"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites"
|
||||
},
|
||||
"savedFilters": {
|
||||
"title": "Filters"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filters",
|
||||
"clear": "Clear Filters",
|
||||
|
@ -403,7 +343,7 @@
|
|||
},
|
||||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
|
@ -677,19 +617,13 @@
|
|||
"updated": "Updated"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
|
||||
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
|
||||
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
|
||||
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
|
||||
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
|
||||
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
|
||||
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
|
||||
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
|
||||
"subscribeSuccessProject": "You are now subscribed to this project",
|
||||
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
|
||||
"subscribeSuccessTask": "You are now subscribed to this task",
|
||||
|
@ -766,7 +700,6 @@
|
|||
"searchPlaceholder": "Type search for a new task to add as related…",
|
||||
"createPlaceholder": "Add this as new related task",
|
||||
"differentProject": "This task belongs to a different project.",
|
||||
"differentNamespace": "This task belongs to a different namespace.",
|
||||
"noneYet": "No task relations yet.",
|
||||
"delete": "Delete Task Relation",
|
||||
"deleteText1": "Are you sure you want to delete this task relation?",
|
||||
|
@ -851,19 +784,19 @@
|
|||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!",
|
||||
"text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
|
||||
"success": "The team was successfully deleted."
|
||||
},
|
||||
"deleteUser": {
|
||||
"header": "Remove a user from the team",
|
||||
"text1": "Are you sure you want to remove this user from the team?",
|
||||
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!",
|
||||
"text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
|
||||
"success": "The user was successfully deleted from the team."
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
@ -913,9 +846,9 @@
|
|||
"title": "Navigation",
|
||||
"overview": "Navigate to overview",
|
||||
"upcoming": "Navigate to upcoming tasks",
|
||||
"namespaces": "Navigate to namespaces & projects",
|
||||
"labels": "Navigate to labels",
|
||||
"teams": "Navigate to teams"
|
||||
"teams": "Navigate to teams",
|
||||
"projects": "Navigate to projects"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
@ -949,7 +882,7 @@
|
|||
"notification": {
|
||||
"title": "Notifications",
|
||||
"none": "You don't have any notifications. Have a nice day!",
|
||||
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen."
|
||||
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
|
||||
},
|
||||
"quickActions": {
|
||||
"commands": "Commands",
|
||||
|
@ -960,14 +893,12 @@
|
|||
"teams": "Teams",
|
||||
"newProject": "Enter the title of the new project…",
|
||||
"newTask": "Enter the title of the new task…",
|
||||
"newNamespace": "Enter the title of the new namespace…",
|
||||
"newTeam": "Enter the name of the new team…",
|
||||
"createTask": "Create a task in the current project ({title})",
|
||||
"createProject": "Create a project in the current namespace ({title})",
|
||||
"createProject": "Create a project",
|
||||
"cmds": {
|
||||
"newTask": "New task",
|
||||
"newProject": "New project",
|
||||
"newNamespace": "New namespace",
|
||||
"newTeam": "New team"
|
||||
}
|
||||
},
|
||||
|
@ -1023,16 +954,9 @@
|
|||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
"5006": "The namespace name cannot be empty.",
|
||||
"5009": "You need to have namespace read access to perform that action.",
|
||||
"5010": "This team does not have access to that namespace.",
|
||||
"5011": "This user has already access to that namespace.",
|
||||
"5012": "The namespace is archived and can therefore only be accessed read only.",
|
||||
"6001": "The team name cannot be empty.",
|
||||
"6002": "The team does not exist.",
|
||||
"6004": "The team already has access to that namespace or project.",
|
||||
"6004": "The team already has access to that project.",
|
||||
"6005": "The user is already a member of that team.",
|
||||
"6006": "Cannot delete the last team member.",
|
||||
"6007": "The team does not have access to the project to perform that action.",
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import type {IAbstract} from './IAbstract'
|
||||
import type {IProject} from './IProject'
|
||||
import type {IUser} from './IUser'
|
||||
import type {ISubscription} from './ISubscription'
|
||||
|
||||
export interface INamespace extends IAbstract {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
owner: IUser
|
||||
projects: IProject[]
|
||||
isArchived: boolean
|
||||
hexColor: string
|
||||
subscription: ISubscription
|
||||
|
||||
created: Date
|
||||
updated: Date
|
||||
}
|
|
@ -2,7 +2,6 @@ import type {IAbstract} from './IAbstract'
|
|||
import type {ITask} from './ITask'
|
||||
import type {IUser} from './IUser'
|
||||
import type {ISubscription} from './ISubscription'
|
||||
import type {INamespace} from './INamespace'
|
||||
|
||||
|
||||
export interface IProject extends IAbstract {
|
||||
|
@ -11,7 +10,6 @@ export interface IProject extends IAbstract {
|
|||
description: string
|
||||
owner: IUser
|
||||
tasks: ITask[]
|
||||
namespaceId: INamespace['id']
|
||||
isArchived: boolean
|
||||
hexColor: string
|
||||
identifier: string
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import type {IAbstract} from './IAbstract'
|
||||
import type {IProject} from './IProject'
|
||||
import type {INamespace} from './INamespace'
|
||||
|
||||
export interface IProjectDuplicate extends IAbstract {
|
||||
projectId: number
|
||||
namespaceId: INamespace['id']
|
||||
project: IProject
|
||||
}
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
Done. Done.
|
|
@ -1,6 +0,0 @@
|
|||
import type {ITeamShareBase} from './ITeamShareBase'
|
||||
import type {INamespace} from './INamespace'
|
||||
|
||||
export interface ITeamNamespace extends ITeamShareBase {
|
||||
namespaceId: INamespace['id']
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import type {IUserShareBase} from './IUserShareBase'
|
||||
import type {INamespace} from './INamespace'
|
||||
|
||||
export interface IUserNamespace extends IUserShareBase {
|
||||
namespaceId: INamespace['id']
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import ProjectModel from './project'
|
||||
import UserModel from './user'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
export default class NamespaceModel extends AbstractModel<INamespace> implements INamespace {
|
||||
id = 0
|
||||
title = ''
|
||||
description = ''
|
||||
owner: IUser = UserModel
|
||||
projects: IProject[] = []
|
||||
isArchived = false
|
||||
hexColor = ''
|
||||
subscription: ISubscription = null
|
||||
|
||||
created: Date = null
|
||||
updated: Date = null
|
||||
|
||||
constructor(data: Partial<INamespace> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
|
||||
if (this.hexColor !== '' && this.hexColor.substring(0, 1) !== '#') {
|
||||
this.hexColor = '#' + this.hexColor
|
||||
}
|
||||
|
||||
this.projects = this.projects.map(l => {
|
||||
return new ProjectModel(l)
|
||||
})
|
||||
|
||||
this.owner = new UserModel(this.owner)
|
||||
|
||||
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||
this.subscription = new SubscriptionModel(this.subscription)
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import SubscriptionModel from '@/models/subscription'
|
|||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
export default class ProjectModel extends AbstractModel<IProject> implements IProject {
|
||||
|
@ -15,7 +14,6 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
|
|||
description = ''
|
||||
owner: IUser = UserModel
|
||||
tasks: ITask[] = []
|
||||
namespaceId: INamespace['id'] = 0
|
||||
isArchived = false
|
||||
hexColor = ''
|
||||
identifier = ''
|
||||
|
|
|
@ -2,12 +2,10 @@ import AbstractModel from './abstractModel'
|
|||
import ProjectModel from './project'
|
||||
|
||||
import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplicate> implements IProjectDuplicate {
|
||||
projectId = 0
|
||||
namespaceId: INamespace['id'] = 0
|
||||
project: IProject = ProjectModel
|
||||
|
||||
constructor(data : Partial<IProjectDuplicate>) {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import TeamShareBaseModel from './teamShareBase'
|
||||
|
||||
import type {ITeamNamespace} from '@/modelTypes/ITeamNamespace'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
export default class TeamNamespaceModel extends TeamShareBaseModel implements ITeamNamespace {
|
||||
namespaceId: INamespace['id'] = 0
|
||||
|
||||
constructor(data: Partial<ITeamNamespace>) {
|
||||
super(data)
|
||||
this.assignData(data)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import type {ITeam} from '@/modelTypes/ITeam'
|
|||
|
||||
/**
|
||||
* This class is a base class for common team sharing model.
|
||||
* It is extended in a way so it can be used for namespaces as well for projects.
|
||||
* It is extended in a way, so it can be used for projects.
|
||||
*/
|
||||
export default class TeamShareBaseModel extends AbstractModel<ITeamShareBase> implements ITeamShareBase {
|
||||
teamId: ITeam['id'] = 0
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import UserShareBaseModel from './userShareBase'
|
||||
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
|
||||
|
||||
// This class extends the user share model with a 'rights' parameter which is used in sharing
|
||||
export default class UserNamespaceModel extends UserShareBaseModel implements IUserNamespace {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
`UserShareBaseModel` is now only used for `userProjects`. Unify?
konrad
commented
Yeah I think we should, but let's combine that with #3326 Yeah I think we should, but let's combine that with https://kolaente.dev/vikunja/frontend/issues/3326
|
||||
namespaceId: INamespace['id'] = 0
|
||||
|
||||
constructor(data: Partial<IUserNamespace>) {
|
||||
super(data)
|
||||
this.assignData(data)
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import AbstractService from './abstractService'
|
||||
import NamespaceModel from '../models/namespace'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
|
||||
export default class NamespaceService extends AbstractService<INamespace> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces',
|
||||
get: '/namespaces/{id}',
|
||||
getAll: '/namespaces',
|
||||
update: '/namespaces/{id}',
|
||||
delete: '/namespaces/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new NamespaceModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(namespace) {
|
||||
namespace.hexColor = colorFromHex(namespace.hexColor)
|
||||
return namespace
|
||||
}
|
||||
|
||||
beforeCreate(namespace) {
|
||||
namespace.hexColor = colorFromHex(namespace.hexColor)
|
||||
return namespace
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import {colorFromHex} from '@/helpers/color/colorFromHex'
|
|||
export default class ProjectService extends AbstractService<IProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceId}/projects',
|
||||
create: '/projects',
|
||||
get: '/projects/{id}',
|
||||
getAll: '/projects',
|
||||
update: '/projects/{id}',
|
||||
|
|
|
@ -12,7 +12,6 @@ import AbstractService from '@/services/abstractService'
|
|||
import SavedFilterModel from '@/models/savedFilter'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case'
|
||||
import {success} from '@/message'
|
||||
|
@ -81,7 +80,6 @@ export default class SavedFilterService extends AbstractService<ISavedFilter> {
|
|||
export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope:'global'})
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const filterService = shallowReactive(new SavedFilterService())
|
||||
|
||||
|
@ -110,13 +108,11 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
|
||||
async function createFilter() {
|
||||
filter.value = await filterService.create(filter.value)
|
||||
await namespaceStore.loadNamespaces()
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
Shouldn't we load the projects here now instead? Shouldn't we load the projects here now instead?
konrad
commented
Good point, added. Good point, added.
|
||||
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
|
||||
}
|
||||
|
||||
async function saveFilter() {
|
||||
const response = await filterService.update(filter.value)
|
||||
await namespaceStore.loadNamespaces()
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
Added. Added.
|
||||
success({message: t('filters.edit.success')})
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
|
@ -129,7 +125,6 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
|
||||
async function deleteFilter() {
|
||||
await filterService.delete(filter.value)
|
||||
await namespaceStore.loadNamespaces()
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
Added Added
|
||||
success({message: t('filters.delete.success')})
|
||||
router.push({name: 'projects.index'})
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import AbstractService from './abstractService'
|
||||
import TeamNamespaceModel from '@/models/teamNamespace'
|
||||
import type {ITeamNamespace} from '@/modelTypes/ITeamNamespace'
|
||||
import TeamModel from '@/models/team'
|
||||
|
||||
export default class TeamNamespaceService extends AbstractService<ITeamNamespace> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceId}/teams',
|
||||
getAll: '/namespaces/{namespaceId}/teams',
|
||||
update: '/namespaces/{namespaceId}/teams/{teamId}',
|
||||
delete: '/namespaces/{namespaceId}/teams/{teamId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamNamespaceModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new TeamModel(data)
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import AbstractService from './abstractService'
|
||||
import UserNamespaceModel from '@/models/userNamespace'
|
||||
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
export default class UserNamespaceService extends AbstractService<IUserNamespace> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceId}/users',
|
||||
getAll: '/namespaces/{namespaceId}/users',
|
||||
update: '/namespaces/{namespaceId}/users/{userId}',
|
||||
delete: '/namespaces/{namespaceId}/users/{userId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserNamespaceModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
import {computed, readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import NamespaceService from '../services/namespace'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
||||
|
||||
export const useNamespaceStore = defineStore('namespace', () => {
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
// FIXME: should be object with id as key
|
||||
const namespaces = ref<INamespace[]>([])
|
||||
|
||||
const getProjectAndNamespaceById = computed(() => (projectId: IProject['id'], ignorePseudoNamespaces = false) => {
|
||||
for (const n in namespaces.value) {
|
||||
|
||||
if (ignorePseudoNamespaces && namespaces.value[n].id < 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const l in namespaces.value[n].projects) {
|
||||
if (namespaces.value[n].projects[l].id === projectId) {
|
||||
return {
|
||||
project: namespaces.value[n].projects[l],
|
||||
namespace: namespaces.value[n],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const getNamespaceById = computed(() => (namespaceId: INamespace['id']) => {
|
||||
return namespaces.value.find(({id}) => id == namespaceId) || null
|
||||
})
|
||||
|
||||
const searchNamespace = computed(() => {
|
||||
return (query: string) => (
|
||||
search(query)
|
||||
?.filter(value => value > 0)
|
||||
.map(getNamespaceById.value)
|
||||
.filter(n => n !== null)
|
||||
|| []
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
function setIsLoading(newIsLoading: boolean) {
|
||||
isLoading.value = newIsLoading
|
||||
}
|
||||
|
||||
function setNamespaces(newNamespaces: INamespace[]) {
|
||||
namespaces.value = newNamespaces
|
||||
newNamespaces.forEach(n => {
|
||||
add(n)
|
||||
|
||||
// Check for each project in that namespace if it has a subscription and set it if not
|
||||
n.projects.forEach(l => {
|
||||
if (l.subscription === null || l.subscription.entity !== 'project') {
|
||||
l.subscription = n.subscription
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setNamespaceById(namespace: INamespace) {
|
||||
const namespaceIndex = namespaces.value.findIndex(n => n.id === namespace.id)
|
||||
|
||||
if (namespaceIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!namespace.projects || namespace.projects.length === 0) {
|
||||
namespace.projects = namespaces.value[namespaceIndex].projects
|
||||
}
|
||||
|
||||
// Check for each project in that namespace if it has a subscription and set it if not
|
||||
namespace.projects.forEach(l => {
|
||||
if (l.subscription === null || l.subscription.entity !== 'project') {
|
||||
l.subscription = namespace.subscription
|
||||
}
|
||||
})
|
||||
|
||||
namespaces.value[namespaceIndex] = namespace
|
||||
update(namespace)
|
||||
}
|
||||
|
||||
function setProjectInNamespaceById(project: IProject) {
|
||||
for (const n in namespaces.value) {
|
||||
// We don't have the namespace id on the project which means we need to loop over all projects until we find it.
|
||||
// FIXME: Not ideal at all - we should fix that at the api level.
|
||||
if (namespaces.value[n].id === project.namespaceId) {
|
||||
for (const l in namespaces.value[n].projects) {
|
||||
if (namespaces.value[n].projects[l].id === project.id) {
|
||||
const namespace = namespaces.value[n]
|
||||
namespace.projects[l] = project
|
||||
namespaces.value[n] = namespace
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addNamespace(namespace: INamespace) {
|
||||
namespaces.value.push(namespace)
|
||||
add(namespace)
|
||||
}
|
||||
|
||||
function removeNamespaceById(namespaceId: INamespace['id']) {
|
||||
for (const n in namespaces.value) {
|
||||
if (namespaces.value[n].id === namespaceId) {
|
||||
remove(namespaces.value[n])
|
||||
namespaces.value.splice(n, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addProjectToNamespace(project: IProject) {
|
||||
for (const n in namespaces.value) {
|
||||
if (namespaces.value[n].id === project.namespaceId) {
|
||||
namespaces.value[n].projects.push(project)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeProjectFromNamespaceById(project: IProject) {
|
||||
for (const n in namespaces.value) {
|
||||
// We don't have the namespace id on the project which means we need to loop over all projects until we find it.
|
||||
// FIXME: Not ideal at all - we should fix that at the api level.
|
||||
if (namespaces.value[n].id === project.namespaceId) {
|
||||
for (const l in namespaces.value[n].projects) {
|
||||
if (namespaces.value[n].projects[l].id === project.id) {
|
||||
namespaces.value[n].projects.splice(l, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNamespaces() {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const namespaceService = new NamespaceService()
|
||||
try {
|
||||
// We always load all namespaces and filter them on the frontend
|
||||
const namespaces = await namespaceService.getAll({}, {is_archived: true}) as INamespace[]
|
||||
setNamespaces(namespaces)
|
||||
|
||||
// Put all projects in the project state
|
||||
const projects = namespaces.flatMap(({projects}) => projects)
|
||||
|
||||
projectStore.setProjects(projects)
|
||||
|
||||
return namespaces
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
function loadNamespacesIfFavoritesDontExist() {
|
||||
// The first or second namespace should be the one holding all favorites
|
||||
if (namespaces.value[0].id === -2 || namespaces.value[1]?.id === -2) {
|
||||
return
|
||||
}
|
||||
return loadNamespaces()
|
||||
}
|
||||
|
||||
function removeFavoritesNamespaceIfEmpty() {
|
||||
if (namespaces.value[0].id === -2 && namespaces.value[0].projects.length === 0) {
|
||||
namespaces.value.splice(0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNamespace(namespace: INamespace) {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const namespaceService = new NamespaceService()
|
||||
|
||||
try {
|
||||
const response = await namespaceService.delete(namespace)
|
||||
removeNamespaceById(namespace.id)
|
||||
return response
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
async function createNamespace(namespace: INamespace) {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const namespaceService = new NamespaceService()
|
||||
|
||||
try {
|
||||
const createdNamespace = await namespaceService.create(namespace)
|
||||
addNamespace(createdNamespace)
|
||||
return createdNamespace
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: readonly(isLoading),
|
||||
namespaces: readonly(namespaces),
|
||||
|
||||
getProjectAndNamespaceById,
|
||||
getNamespaceById,
|
||||
searchNamespace,
|
||||
|
||||
setNamespaces,
|
||||
setNamespaceById,
|
||||
setProjectInNamespaceById,
|
||||
addNamespace,
|
||||
removeNamespaceById,
|
||||
addProjectToNamespace,
|
||||
removeProjectFromNamespaceById,
|
||||
loadNamespaces,
|
||||
loadNamespacesIfFavoritesDontExist,
|
||||
removeFavoritesNamespaceIfEmpty,
|
||||
deleteNamespace,
|
||||
createNamespace,
|
||||
}
|
||||
})
|
||||
|
||||
// support hot reloading
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useNamespaceStore, import.meta.hot))
|
||||
}
|
|
@ -6,7 +6,6 @@ import ProjectService from '@/services/project'
|
|||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {removeProjectFromHistory} from '@/modules/projectHistory'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import {useNamespaceStore} from './namespaces'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
|
@ -26,7 +25,6 @@ export interface ProjectState {
|
|||
|
||||
export const useProjectStore = defineStore('project', () => {
|
||||
const baseStore = useBaseStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
|
@ -100,8 +98,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||
|
||||
try {
|
||||
const createdProject = await projectService.create(project)
|
||||
createdProject.namespaceId = project.namespaceId
|
||||
dpschen
commented
If I set an id for If I set an id for `parentID` in the passed project: will this update the parents childProjects?
konrad
commented
It did not. While checking this, I discovered how we're not using child ids anywhere any more (including in the api) so I've removed all traces of them. It did not. While checking this, I discovered how we're not using child ids anywhere any more (including in the api) so I've removed all traces of them.
|
||||
namespaceStore.addProjectToNamespace(createdProject)
|
||||
setProject(createdProject)
|
||||
return createdProject
|
||||
} finally {
|
||||
|
@ -116,21 +112,13 @@ export const useProjectStore = defineStore('project', () => {
|
|||
try {
|
||||
await projectService.update(project)
|
||||
setProject(project)
|
||||
namespaceStore.setProjectInNamespaceById(project)
|
||||
|
||||
// the returned project from projectService.update is the same!
|
||||
// in order to not create a manipulation in pinia store we have to create a new copy
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
- Use returned value from update function. Then klona shouldn't be necessary, right?
- If I set an id for `parentID` in the passed project: will this update the parents childProjects?
konrad
commented
Done. Done.
|
||||
const newProject = {
|
||||
...project,
|
||||
namespaceId: FavoriteProjectsNamespace,
|
||||
}
|
||||
|
||||
namespaceStore.removeProjectFromNamespaceById(newProject)
|
||||
if (project.isFavorite) {
|
||||
namespaceStore.addProjectToNamespace(newProject)
|
||||
}
|
||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
namespaceStore.removeFavoritesNamespaceIfEmpty()
|
||||
return newProject
|
||||
} catch (e) {
|
||||
// Reset the project state to the initial one to avoid confusion for the user
|
||||
|
@ -151,7 +139,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||
try {
|
||||
const response = await projectService.delete(project)
|
||||
removeProjectById(project)
|
||||
namespaceStore.removeProjectFromNamespaceById(project)
|
||||
removeProjectFromHistory({id: project.id})
|
||||
return response
|
||||
} finally {
|
||||
|
|
|
@ -17,22 +17,11 @@
|
|||
@taskAdded="updateTaskKey"
|
||||
class="is-max-width-desktop"
|
||||
/>
|
||||
<template v-if="!hasTasks && !loading">
|
||||
<template v-if="defaultNamespaceId > 0">
|
||||
<p class="mt-4">{{ $t('home.project.newText') }}</p>
|
||||
<x-button
|
||||
dpschen
commented
Why did we remove this button? Why did we remove this button?
konrad
commented
Because it is related to having a namespace and no projects. With the new changes, there will always be at least one project for the user, which means this would never be shown anyway. Because it is related to having a namespace and no projects. With the new changes, there will always be at least one project for the user, which means this would never be shown anyway.
|
||||
:to="{ name: 'project.create', params: { namespaceId: defaultNamespaceId } }"
|
||||
:shadow="false"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ $t('home.project.new') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<p class="mt-4" v-if="migratorsEnabled">
|
||||
<template v-if="!hasTasks && !loading && migratorsEnabled">
|
||||
<p class="mt-4">
|
||||
{{ $t('home.project.importText') }}
|
||||
</p>
|
||||
<x-button
|
||||
v-if="migratorsEnabled"
|
||||
:to="{ name: 'migrate.start' }"
|
||||
:shadow="false">
|
||||
{{ $t('home.project.import') }}
|
||||
|
@ -66,7 +55,6 @@ import {useDaytimeSalutation} from '@/composables/useDaytimeSalutation'
|
|||
import {useBaseStore} from '@/stores/base'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
@ -76,7 +64,6 @@ const salutation = useDaytimeSalutation()
|
|||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
const configStore = useConfigStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
|
@ -93,8 +80,7 @@ const projectHistory = computed(() => {
|
|||
|
||||
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
|
||||
const hasTasks = computed(() => baseStore.hasTasks)
|
||||
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)
|
||||
const hasProjects = computed(() => namespaceStore.namespaces?.[0]?.projects.length > 0)
|
||||
const hasProjects = computed(() => projectStore.projects.length > 0)
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
This should be a computed of the store, like This should be a computed of the store, like `hasTasks`.
Shouldn't we check the length here (`projectStore.projects?.length`)?
konrad
commented
`projects` is an object, so checking the length of it won't work.
konrad
commented
Moved it to the store. Moved it to the store.
dpschen
commented
Then Then `Object.keys?.length`
konrad
commented
Wouldn't that cause a call to > Then `Object.keys?.length`
Wouldn't that cause a call to `Object.keys`? I think my solution is faster.
konrad
commented
Checked it: https://jsbench.me/hslfseq93y/1 Using Checked it: https://jsbench.me/hslfseq93y/1
Using `Object.keys` really is slower, and by a lot.
dpschen
commented
True. Using a Map with size would probably be the perfect solution here. But that is something for another time. True. Using a Map with size would probably be the perfect solution here. But that is something for another time.
dpschen
commented
Don't wrap computed in computed. use Don't wrap computed in computed. use `projectStore.hasProjects` directly
konrad
commented
Done Done
|
||||
const loading = computed(() => taskStore.isLoading)
|
||||
const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt))
|
||||
|
||||
|
|
|
@ -89,8 +89,8 @@ import {formatDateLong} from '@/helpers/time/formatDate'
|
|||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
|
||||
import {MIGRATORS} from './migrators'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const PROGRESS_DOTS_COUNT = 8
|
||||
|
||||
|
@ -163,8 +163,8 @@ async function migrate() {
|
|||
? await migrationFileService.migrate(migrationConfig as File)
|
||||
: await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||
message.value = result.message
|
||||
const namespaceStore = useNamespaceStore()
|
||||
return namespaceStore.loadNamespaces()
|
||||
const projectStore = useProjectStore()
|
||||
return projectStore.loadProjects()
|
||||
} finally {
|
||||
isMigrating.value = false
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
<template>
|
||||
<create-edit
|
||||
:title="$t('namespace.create.title')"
|
||||
@create="newNamespace()"
|
||||
:primary-disabled="namespace.title === ''"
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label" for="namespaceTitle">{{ $t('namespace.attributes.title') }}</label>
|
||||
<div
|
||||
class="control is-expanded"
|
||||
:class="{ 'is-loading': namespaceService.loading }"
|
||||
>
|
||||
<!-- The user should be able to close the modal by pressing escape - that already works with the default modal.
|
||||
But with the input modal here since it autofocuses the input that input field catches the focus instead.
|
||||
Hence we place the listener on the input field directly. -->
|
||||
<input
|
||||
@keyup.enter="newNamespace()"
|
||||
@keyup.esc="$router.back()"
|
||||
class="input"
|
||||
:placeholder="$t('namespace.attributes.titlePlaceholder')"
|
||||
type="text"
|
||||
:class="{ disabled: namespaceService.loading }"
|
||||
v-focus
|
||||
v-model="namespace.title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="showError && namespace.title === ''">
|
||||
{{ $t('namespace.create.titleRequired') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('namespace.attributes.color') }}</label>
|
||||
<div class="control">
|
||||
<color-picker v-model="namespace.hexColor"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<message class="mt-4">
|
||||
<h4 class="title">{{ $t('namespace.create.tooltip') }}</h4>
|
||||
|
||||
{{ $t('namespace.create.explanation') }}
|
||||
</message>
|
||||
</create-edit>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {success} from '@/message'
|
||||
|
||||
const showError = ref(false)
|
||||
const namespace = ref<INamespace>(new NamespaceModel())
|
||||
const namespaceService = shallowReactive(new NamespaceService())
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
|
||||
useTitle(() => t('namespace.create.title'))
|
||||
|
||||
async function newNamespace() {
|
||||
if (namespace.value.title === '') {
|
||||
showError.value = true
|
||||
return
|
||||
}
|
||||
showError.value = false
|
||||
|
||||
const newNamespace = await namespaceService.create(namespace.value)
|
||||
useNamespaceStore().addNamespace(newNamespace)
|
||||
success({message: t('namespace.create.success')})
|
||||
router.back()
|
||||
}
|
||||
</script>
|
|
@ -1,89 +0,0 @@
|
|||
<template>
|
||||
<modal
|
||||
@close="$router.back()"
|
||||
@submit="archiveNamespace()"
|
||||
>
|
||||
<template #header><span>{{ title }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>
|
||||
{{
|
||||
namespace.isArchived
|
||||
? $t('namespace.archive.unarchiveText')
|
||||
: $t('namespace.archive.archiveText')
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'namespace-setting-archive' }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {watch, ref, computed, shallowReactive, type PropType} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
const props = defineProps({
|
||||
namespaceId: {
|
||||
type: Number as PropType<INamespace['id']>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const namespaceService = shallowReactive(new NamespaceService())
|
||||
const namespace = ref<INamespace>(new NamespaceModel())
|
||||
|
||||
watch(
|
||||
() => props.namespaceId,
|
||||
async () => {
|
||||
namespace.value = namespaceStore.getNamespaceById(props.namespaceId) || new NamespaceModel()
|
||||
|
||||
// FIXME: ressouce should be loaded in store
|
||||
namespace.value = await namespaceService.get({id: props.namespaceId})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const title = computed(() => {
|
||||
if (!namespace.value) {
|
||||
return
|
||||
}
|
||||
return namespace.value.isArchived
|
||||
? t('namespace.archive.titleUnarchive', {namespace: namespace.value.title})
|
||||
: t('namespace.archive.titleArchive', {namespace: namespace.value.title})
|
||||
})
|
||||
useTitle(title)
|
||||
|
||||
async function archiveNamespace() {
|
||||
try {
|
||||
const isArchived = !namespace.value.isArchived
|
||||
const archivedNamespace = await namespaceService.update({
|
||||
...namespace.value,
|
||||
isArchived,
|
||||
})
|
||||
namespaceStore.setNamespaceById(archivedNamespace)
|
||||
success({
|
||||
message: isArchived
|
||||
? t('namespace.archive.success')
|
||||
: t('namespace.archive.unarchiveSuccess'),
|
||||
})
|
||||
} finally {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<modal
|
||||
@close="$router.back()"
|
||||
@submit="deleteNamespace()"
|
||||
>
|
||||
<template #header><span>{{ title }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('namespace.delete.text1') }}<br/>
|
||||
{{ $t('namespace.delete.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'namespace-setting-delete' }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, shallowReactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {success} from '@/message'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import type { INamespace } from '@/modelTypes/INamespace'
|
||||
|
||||
const props = defineProps({
|
||||
namespaceId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const namespaceService = shallowReactive(new NamespaceService())
|
||||
const namespace = ref<INamespace>(new NamespaceModel())
|
||||
|
||||
watch(
|
||||
() => props.namespaceId,
|
||||
async () => {
|
||||
namespace.value = namespaceStore.getNamespaceById(props.namespaceId) || new NamespaceModel()
|
||||
|
||||
// FIXME: ressouce should be loaded in store
|
||||
namespace.value = await namespaceService.get({id: props.namespaceId})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const title = computed(() => {
|
||||
if (!namespace.value) {
|
||||
return
|
||||
}
|
||||
return t('namespace.delete.title', {namespace: namespace.value.title})
|
||||
})
|
||||
useTitle(title)
|
||||
|
||||
async function deleteNamespace() {
|
||||
await namespaceStore.deleteNamespace(namespace.value)
|
||||
success({message: t('namespace.delete.success')})
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
</script>
|
|
@ -1,120 +0,0 @@
|
|||
<template>
|
||||
<create-edit
|
||||
:title="title"
|
||||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
|
||||
>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="field">
|
||||
<label class="label" for="namespacetext">{{ $t('namespace.attributes.title') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
:class="{ 'disabled': namespaceService.loading}"
|
||||
:disabled="namespaceService.loading || undefined"
|
||||
class="input"
|
||||
id="namespacetext"
|
||||
:placeholder="$t('namespace.attributes.titlePlaceholder')"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="namespace.title"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="namespacedescription">{{ $t('namespace.attributes.description') }}</label>
|
||||
<div class="control">
|
||||
<AsyncEditor
|
||||
:class="{ 'disabled': namespaceService.loading}"
|
||||
:preview-is-default="false"
|
||||
id="namespacedescription"
|
||||
:placeholder="$t('namespace.attributes.descriptionPlaceholder')"
|
||||
v-if="editorActive"
|
||||
v-model="namespace.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="isArchivedCheck">{{ $t('namespace.attributes.archived') }}</label>
|
||||
<div class="control">
|
||||
<fancycheckbox
|
||||
v-model="namespace.isArchived"
|
||||
v-tooltip="$t('namespace.archive.description')">
|
||||
{{ $t('namespace.attributes.isArchived') }}
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('namespace.attributes.color') }}</label>
|
||||
<div class="control">
|
||||
<color-picker v-model="namespace.hexColor"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</create-edit>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {nextTick, ref, watch} from 'vue'
|
||||
import {success} from '@/message'
|
||||
import router from '@/router'
|
||||
|
||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const namespaceService = ref(new NamespaceService())
|
||||
const namespace = ref<INamespace>(new NamespaceModel())
|
||||
const editorActive = ref(false)
|
||||
const title = ref('')
|
||||
useTitle(() => title.value)
|
||||
|
||||
const props = defineProps({
|
||||
namespaceId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.namespaceId,
|
||||
loadNamespace,
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
async function loadNamespace() {
|
||||
// HACK: This makes the editor trigger its mounted function again which makes it forget every input
|
||||
// it currently has in its textarea. This is a counter-hack to a hack inside of vue-easymde
|
||||
// which made it impossible to detect change from the outside. Therefore the component would
|
||||
// not update if new content from the outside was made available.
|
||||
// See https://github.com/NikulinIlya/vue-easymde/issues/3
|
||||
editorActive.value = false
|
||||
nextTick(() => editorActive.value = true)
|
||||
|
||||
namespace.value = await namespaceService.value.get({id: props.namespaceId})
|
||||
title.value = t('namespace.edit.title', {namespace: namespace.value.title})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const updatedNamespace = await namespaceService.value.update(namespace.value)
|
||||
// Update the namespace in the parent
|
||||
namespaceStore.setNamespaceById(updatedNamespace)
|
||||
success({message: t('namespace.edit.success')})
|
||||
router.back()
|
||||
}
|
||||
</script>
|
|
@ -1,67 +0,0 @@
|
|||
<template>
|
||||
<create-edit
|
||||
:title="title"
|
||||
:has-primary-action="false"
|
||||
>
|
||||
<template v-if="namespace">
|
||||
<manageSharing
|
||||
:id="namespace.id"
|
||||
:userIsAdmin="userIsAdmin"
|
||||
shareType="user"
|
||||
type="namespace"
|
||||
/>
|
||||
<manageSharing
|
||||
:id="namespace.id"
|
||||
:userIsAdmin="userIsAdmin"
|
||||
shareType="team"
|
||||
type="namespace"
|
||||
/>
|
||||
</template>
|
||||
</create-edit>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'namespace-setting-share' }
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed, watchEffect} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import manageSharing from '@/components/sharing/userTeam.vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const namespace = ref<INamespace>()
|
||||
|
||||
const title = computed(() => namespace.value?.title
|
||||
? t('namespace.share.title', { namespace: namespace.value.title })
|
||||
: '',
|
||||
)
|
||||
useTitle(title)
|
||||
|
||||
const userIsAdmin = computed(() => namespace?.value?.maxRight === RIGHTS.ADMIN)
|
||||
|
||||
async function loadNamespace(namespaceId: number) {
|
||||
if (!namespaceId) return
|
||||
const namespaceService = new NamespaceService()
|
||||
namespace.value = await namespaceService.get(new NamespaceModel({id: namespaceId}))
|
||||
|
||||
// TODO: set namespace in store
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const namespaceId = computed(() => route.params.namespaceId !== undefined
|
||||
? parseInt(route.params.namespaceId as string)
|
||||
: undefined,
|
||||
)
|
||||
watchEffect(() => namespaceId.value !== undefined && loadNamespace(namespaceId.value))
|
||||
</script>
|
|
@ -63,7 +63,6 @@ async function createNewProject() {
|
|||
}
|
||||
showError.value = false
|
||||
|
||||
project.namespaceId = Number(route.params.namespaceId as string)
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
If we create a project inside another one doesn't it still make sense to pass the parents id down? That was the idea of that parameter. If we create a project inside another one doesn't it still make sense to pass the parents id down? That was the idea of that parameter.
konrad
commented
See #3323 (comment) See https://kolaente.dev/vikunja/frontend/pulls/3323#issuecomment-47666
konrad
commented
It's now possible to set a parent project when creating or editing a project as well. It's now possible to set a parent project when creating or editing a project as well.
|
||||
const newProject = await projectStore.createProject(project)
|
||||
dpschen
commented
Use a computed getter / setter for parent project via Use a computed getter / setter for parent project via `project.id`.
konrad
commented
But this is only used to store the selection of the new parent project? That will never change from anywhere other than the project search component. But this is only used to store the selection of the new parent project? That will never change from anywhere other than the project search component.
dpschen
commented
See this comment: #3323 (comment) See this comment: https://kolaente.dev/vikunja/frontend/pulls/3323#issuecomment-49061
dpschen
commented
Main point is: source of truth should be url. And url will set parentId inside the Main point is: source of truth should be url. And url will set parentId inside the `project` reactive. The `parentProject` should be a computed that is filled based on that id. If the id changes (everything else shouldn't change) that id will be updated inside the `project` reactive. I guess it would also be nice to replace / update the url as well but that was not my main point here. Edit: see also this https://kolaente.dev/vikunja/frontend/pulls/3323#issuecomment-48860
|
||||
await router.push({
|
||||
dpschen
commented
Add immediate watcher from route. Watch for parentProjectId. Add immediate watcher from route. Watch for parentProjectId.
konrad
commented
But the route will never have a project id? (or parent project id for that matter) The route is always But the route will never have a project id? (or parent project id for that matter)
The route is always `/projects/new` and it's not possible to create a project from the context menu of an existing project.
dpschen
commented
We do remove functionality here if we disallow this. Because with namespaces it was possible to say in which namespace I wanted to create a new list via a url parameter. Since root projects are the pendant of the old namespaces we should keep the functionality to add new projects inside others directly. > But the route will never have a project id? (or parent project id for that matter)
>
> The route is always `/projects/new` and it's not possible to create a project from the context menu of an existing project.
We __do__ remove functionality here if we disallow this.
Because with namespaces it was possible to say in which namespace I wanted to create a new list via a url parameter. Since root projects are the pendant of the old namespaces we should keep the functionality to add new projects inside others directly.
konrad
commented
I've now added this. It may not be the most optimal version but I think it's good enough. I've now added this. It may not be the most optimal version but I think it's good enough.
|
||||
name: 'project.index',
|
||||
|
|
|
@ -108,7 +108,6 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
|
|||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
|
||||
|
@ -146,7 +145,6 @@ const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNC
|
|||
const backgroundUploadService = ref(new BackgroundUploadService())
|
||||
const projectService = ref(new ProjectService())
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
|
||||
|
@ -195,7 +193,6 @@ async function setBackground(backgroundId: string) {
|
|||
projectId: route.params.projectId,
|
||||
})
|
||||
await baseStore.handleSetCurrentProject({project, forceUpdate: true})
|
||||
namespaceStore.setProjectInNamespaceById(project)
|
||||
projectStore.setProject(project)
|
||||
success({message: t('project.background.success')})
|
||||
}
|
||||
|
@ -211,7 +208,6 @@ async function uploadBackground() {
|
|||
backgroundUploadInput.value?.files[0],
|
||||
)
|
||||
await baseStore.handleSetCurrentProject({project, forceUpdate: true})
|
||||
namespaceStore.setProjectInNamespaceById(project)
|
||||
projectStore.setProject(project)
|
||||
success({message: t('project.background.success')})
|
||||
}
|
||||
|
@ -219,7 +215,6 @@ async function uploadBackground() {
|
|||
async function removeBackground() {
|
||||
const project = await projectService.value.removeBackground(currentProject.value)
|
||||
await baseStore.handleSetCurrentProject({project, forceUpdate: true})
|
||||
namespaceStore.setProjectInNamespaceById(project)
|
||||
projectStore.setProject(project)
|
||||
success({message: t('project.background.removeSuccess')})
|
||||
router.back()
|
||||
|
|
|
@ -7,15 +7,6 @@
|
|||
:loading="projectDuplicateService.loading"
|
||||
>
|
||||
<p>{{ $t('project.duplicate.text') }}</p>
|
||||
|
||||
<Multiselect
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
This should be replaced by a project selector in order to select the new parent project. This should be replaced by a project selector in order to select the new parent project.
konrad
commented
See #3323 (comment) See https://kolaente.dev/vikunja/frontend/pulls/3323#issuecomment-47666
dpschen
commented
I don't think that in the case of duplication dragging inside the sidebar does replace that. Dragging on mobile also isn't working very well currently and has generally accessability issues. I don't think that in the case of duplication dragging inside the sidebar does replace that. Dragging on mobile also isn't working very well currently and has generally accessability issues.
konrad
commented
I've now added a project select when duplicating I've now added a project select when duplicating
|
||||
:placeholder="$t('namespace.search')"
|
||||
@search="findNamespaces"
|
||||
:search-results="namespaces"
|
||||
@select="selectNamespace"
|
||||
label="title"
|
||||
:search-delay="10"
|
||||
/>
|
||||
</create-edit>
|
||||
</template>
|
||||
|
||||
|
@ -29,32 +20,17 @@ import CreateEdit from '@/components/misc/create-edit.vue'
|
|||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
import ProjectDuplicateModel from '@/models/projectDuplicateModel'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useNamespaceSearch} from '@/composables/useNamespaceSearch'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => t('project.duplicate.title'))
|
||||
|
||||
const {
|
||||
namespaces,
|
||||
findNamespaces,
|
||||
} = useNamespaceSearch()
|
||||
|
||||
const selectedNamespace = ref<INamespace>()
|
||||
|
||||
function selectNamespace(namespace: INamespace) {
|
||||
selectedNamespace.value = namespace
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const projectDuplicateService = shallowReactive(new ProjectDuplicateService())
|
||||
|
||||
|
@ -62,12 +38,10 @@ async function duplicateProject() {
|
|||
const projectDuplicate = new ProjectDuplicateModel({
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Use Use `useProject` to get current project here and add `duplicateProject` method.
konrad
commented
Done. Done.
|
||||
// FIXME: should be parameter
|
||||
projectId: route.params.projectId,
|
||||
namespaceId: selectedNamespace.value?.id,
|
||||
})
|
||||
|
||||
const duplicate = await projectDuplicateService.create(projectDuplicate)
|
||||
|
||||
namespaceStore.addProjectToNamespace(duplicate.project)
|
||||
projectStore.setProject(duplicate.project)
|
||||
success({message: t('project.duplicate.success')})
|
||||
router.push({name: 'project.index', params: {projectId: duplicate.project.id}})
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
:can-write="canWrite"
|
||||
ref="heading"
|
||||
/>
|
||||
<h6 class="subtitle" v-if="parent && parent.namespace && parent.project">
|
||||
{{ getNamespaceTitle(parent.namespace) }} ›
|
||||
dpschen
commented
Shouldn't we still show the parent here? Shouldn't we still show the parent here?
konrad
commented
I've changed it now so that is shows this: (each project is clickable individually) For a hierarchy like this: I've changed it now so that is shows this:
![Screenshot_20230328_174428.png](/attachments/35babb55-b48a-49bf-a595-3fcc28da12dc)
(each project is clickable individually)
For a hierarchy like this:
![Screenshot_20230328_174646.png](/attachments/070d677e-d199-45ee-af6b-c0a6b3196681)
dpschen
commented
Can't see these images either Can't see these images either
konrad
commented
This starts to feel like a gitea bug... This starts to feel like a gitea bug...
|
||||
<h6 class="subtitle" v-if="parent && parent.project">
|
||||
<router-link :to="{ name: 'project.index', params: { projectId: parent.project.id } }">
|
||||
dpschen
commented
This won't update dynamically. Should we change the This won't update dynamically. Should we change the `getParentProjects` to a computed `parentProjects` that automatically updates instead?
konrad
commented
I tried to change it but it fails with I tried to change it but it fails with `getAncestors is not a function`. Any idea?
konrad
commented
Actually this does update dynamically when the project changes. I've had a task open, moved the project to another parent project and it updated instantly. Actually this does update dynamically when the project changes. I've had a task open, moved the project to another parent project and it updated instantly.
|
||||
{{ getProjectTitle(parent.project) }}
|
||||
</router-link>
|
||||
|
@ -486,12 +485,10 @@ import TaskSubscription from '@/components/misc/subscription.vue'
|
|||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import {uploadFile} from '@/helpers/attachments'
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import {scrollIntoView} from '@/helpers/scrollIntoView'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useAttachmentStore} from '@/stores/attachments'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
|
@ -500,6 +497,7 @@ import {useTitle} from '@/composables/useTitle'
|
|||
|
||||
import {success} from '@/message'
|
||||
import type {Action as MessageAction} from '@/message'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
|
@ -517,7 +515,7 @@ const router = useRouter()
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const projectStore = useProjectStore()
|
||||
const attachmentStore = useAttachmentStore()
|
||||
const taskStore = useTaskStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
|
@ -541,16 +539,11 @@ const taskId = toRef(props, 'taskId')
|
|||
const parent = computed(() => {
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
`getProjectById` returns `undefined` if there is no project with that id. So why not use that directly?
konrad
commented
Done Done
|
||||
if (!task.projectId) {
|
||||
return {
|
||||
namespace: null,
|
||||
project: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!namespaceStore.getProjectAndNamespaceById) {
|
||||
return null
|
||||
}
|
||||
|
||||
return namespaceStore.getProjectAndNamespaceById(task.projectId)
|
||||
|
||||
return projectStore.getProjectById(task.projectId)
|
||||
})
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Very bad pattern. A side effect of a computed can lead to various weird issues. Instead there should be a watcher! Why not use
__Very__ bad pattern.
A side effect of a computed can lead to various weird issues. Instead there should be a watcher! Why not use `getProjectById` directly?
```ts
const project = computed(() => projectStore.getProjectById(task.projectId)})
watchEffect(() => {
baseStore.handleSetCurrentProject({
project: project.value
})
})
```
konrad
commented
I don't even know why I moved that into the computed instead of leaving the watcher as it was before. Changed it back now. I don't even know why I moved that into the computed instead of leaving the watcher as it was before. Changed it back now.
|
||||
|
||||
watch(
|
||||
|
@ -775,7 +768,6 @@ async function toggleFavorite() {
|
|||
task.isFavorite = !task.isFavorite
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
It seems like this should be replaced with what you wrote above:
It seems like this should be replaced with what you wrote above:
```ts
await projectStore.loadProjects() // reloading the projects list so that the Favorites project shows up or is hidden when there are (or are not) favorite tasks
```
dpschen
commented
Maybe it would be better to create a Maybe it would be better to create a `toggleFavorite` method of the task store instead, that includes that call so we don't forget it in other occasions when we change the favorite of a task.
konrad
commented
I thought of that as well, but completely forgot to check the task detail view. I've now created that method and replaced its usages. > Maybe it would be better to create a toggleFavorite method of the task store instead, that includes that call so we don't forget it in other occasions when we change the favorite of a task.
I thought of that as well, but completely forgot to check the task detail view. I've now created that method and replaced its usages.
dpschen
commented
You forgot to await the new store method > I've now created that method and replaced its usages.
You forgot to await the new store method
konrad
commented
whoops, fixed now. whoops, fixed now.
|
||||
const newTask = await taskService.update(task)
|
||||
Object.assign(task, newTask)
|
||||
await namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
}
|
||||
|
||||
async function setPriority(priority: Priority) {
|
||||
|
|
Should we keep the old shortcut and deprecate it as long as it's not used by something else?
I think it's fine to "free" it up until its used for something else without explicit deprecation.