feat: remove all namespace leftovers

This commit is contained in:
kolaente 2023-03-25 14:54:20 +01:00
parent a5e710bfe5
commit 1bd17d6e50
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
47 changed files with 44 additions and 1380 deletions

View File

@ -94,7 +94,6 @@ watch(() => route.name as string, (routeName) => {
( (
[ [
'home', 'home',
'namespace.edit',
'teams.index', 'teams.index',
'teams.edit', 'teams.edit',
'tasks.range', 'tasks.range',

View File

@ -159,12 +159,10 @@ import type {SortableEvent} from 'sortablejs'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue' import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue' import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getProjectTitle} from '@/helpers/getProjectTitle' import {getProjectTitle} from '@/helpers/getProjectTitle'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
@ -172,7 +170,6 @@ import ColorBubble from '@/components/misc/colorBubble.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
const drag = ref(false) const drag = ref(false)
const dragOptions = { const dragOptions = {
@ -199,7 +196,7 @@ const activeProjects = computed(() => {
}) })
const namespaceTitles = computed(() => { const namespaceTitles = computed(() => {
return namespaces.value.map((namespace) => getNamespaceTitle(namespace)) return []
}) })
const namespaceProjectsCount = computed(() => { const namespaceProjectsCount = computed(() => {

View File

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

View File

@ -44,8 +44,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
combination: 'then', combination: 'then',
}, },
{ {
title: 'keyboardShortcuts.navigation.namespaces', title: 'keyboardShortcuts.navigation.projects',
keys: ['g', 'n'], keys: ['g', 'p'],
combination: 'then', combination: 'then',
}, },
{ {

View File

@ -73,12 +73,6 @@ const {t} = useI18n({useScope: 'global'})
const tooltipText = computed(() => { const tooltipText = computed(() => {
if (disabled.value) { 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') { if (props.entity === 'task' && subscriptionEntity.value === 'project') {
return t('task.subscription.subscribedTaskThroughParentProject') return t('task.subscription.subscribedTaskThroughParentProject')
} }
@ -87,10 +81,6 @@ const tooltipText = computed(() => {
} }
switch (props.entity) { switch (props.entity) {
case 'namespace':
return props.modelValue !== null ?
t('task.subscription.subscribedNamespace') :
t('task.subscription.notSubscribedNamespace')
case 'project': case 'project':
return props.modelValue !== null ? return props.modelValue !== null ?
t('task.subscription.subscribedProject') : t('task.subscription.subscribedProject') :
@ -130,9 +120,6 @@ async function subscribe() {
let message = '' let message = ''
switch (props.entity) { switch (props.entity) {
case 'namespace':
message = t('task.subscription.subscribeSuccessNamespace')
break
case 'project': case 'project':
message = t('task.subscription.subscribeSuccessProject') message = t('task.subscription.subscribeSuccessProject')
break break
@ -153,9 +140,6 @@ async function unsubscribe() {
let message = '' let message = ''
switch (props.entity) { switch (props.entity) {
case 'namespace':
message = t('task.subscription.unsubscribeSuccessNamespace')
break
case 'project': case 'project':
message = t('task.subscription.unsubscribeSuccessProject') message = t('task.subscription.unsubscribeSuccessProject')
break break

View File

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

View File

@ -46,7 +46,7 @@
</div> </div>
<CustomTransition name="fade"> <CustomTransition name="fade">
<Message variant="warning" v-if="currentProject.isArchived" class="mb-4"> <Message variant="warning" v-if="currentProject.isArchived" class="mb-4">
{{ $t('project.archived') }} {{ $t('project.archivedText') }}
</Message> </Message>
</CustomTransition> </CustomTransition>

View File

@ -15,7 +15,7 @@
:class="{'is-visible': background}" :class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}" :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> <div class="project-title" aria-hidden="true">{{ project.title }}</div>
<BaseButton <BaseButton

View File

@ -165,16 +165,6 @@
/> />
</div> </div>
</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> </template>
</card> </card>
</template> </template>
@ -189,7 +179,6 @@ import {camelCase} from 'camel-case'
import type {ILabel} from '@/modelTypes/ILabel' import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import {useLabelStore} from '@/stores/labels' 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 Fancycheckbox from '@/components/input/fancycheckbox.vue'
import SelectUser from '@/components/input/SelectUser.vue' import SelectUser from '@/components/input/SelectUser.vue'
import SelectProject from '@/components/input/SelectProject.vue' import SelectProject from '@/components/input/SelectProject.vue'
import SelectNamespace from '@/components/input/SelectNamespace.vue'
import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate' import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
@ -209,7 +197,6 @@ import {objectToSnakeCase} from '@/helpers/case'
import UserService from '@/services/user' import UserService from '@/services/user'
import ProjectService from '@/services/project' 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 // FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
import {getDefaultParams} from '@/composables/useTaskList' import {getDefaultParams} from '@/composables/useTaskList'
@ -240,7 +227,6 @@ const DEFAULT_FILTERS = {
assignees: '', assignees: '',
labels: '', labels: '',
project_id: '', project_id: '',
namespace: '',
} as const } as const
const props = defineProps({ const props = defineProps({
@ -265,23 +251,20 @@ const filters = ref({...DEFAULT_FILTERS})
const services = { const services = {
users: shallowReactive(new UserService()), users: shallowReactive(new UserService()),
projects: shallowReactive(new ProjectService()), projects: shallowReactive(new ProjectService()),
namespace: shallowReactive(new NamespaceService()),
} }
interface Entities { interface Entities {
users: IUser[] users: IUser[]
labels: ILabel[] labels: ILabel[]
projects: IProject[] projects: IProject[]
namespace: INamespace[]
} }
type EntityType = 'users' | 'labels' | 'projects' | 'namespace' type EntityType = 'users' | 'labels' | 'projects'
const entities: Entities = reactive({ const entities: Entities = reactive({
users: [], users: [],
labels: [], labels: [],
projects: [], projects: [],
namespace: [],
}) })
onMounted(() => { onMounted(() => {
@ -328,7 +311,6 @@ function prepareFilters() {
prepareDate('reminders') prepareDate('reminders')
prepareRelatedObjectFilter('users', 'assignees') prepareRelatedObjectFilter('users', 'assignees')
prepareRelatedObjectFilter('projects', 'project_id') prepareRelatedObjectFilter('projects', 'project_id')
prepareRelatedObjectFilter('namespace')
prepareSingleValue('labels') prepareSingleValue('labels')

View File

@ -96,7 +96,6 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
import {isSavedFilter} from '@/services/savedFilter' import {isSavedFilter} from '@/services/savedFilter'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
project: { project: {
@ -106,7 +105,6 @@ const props = defineProps({
}) })
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) const subscription = ref<ISubscription | null>(null)
watchEffect(() => { watchEffect(() => {
subscription.value = props.project.subscription ?? null subscription.value = props.project.subscription ?? null
@ -122,6 +120,5 @@ function setSubscriptionInStore(sub: ISubscription) {
subscription: sub, subscription: sub,
} }
projectStore.setProject(updatedProject) projectStore.setProject(updatedProject)
namespaceStore.setProjectInNamespaceById(updatedProject)
} }
</script> </script>

View File

@ -61,7 +61,6 @@ import {useRouter} from 'vue-router'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TeamService from '@/services/team' import TeamService from '@/services/team'
import NamespaceModel from '@/models/namespace'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import ProjectModel from '@/models/project' 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 {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
@ -81,7 +79,6 @@ import {success} from '@/message'
import type {ITeam} from '@/modelTypes/ITeam' import type {ITeam} from '@/modelTypes/ITeam'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -89,7 +86,6 @@ const router = useRouter()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
@ -105,7 +101,6 @@ enum ACTION_TYPE {
enum COMMAND_TYPE { enum COMMAND_TYPE {
NEW_TASK = 'newTask', NEW_TASK = 'newTask',
NEW_PROJECT = 'newProject', NEW_PROJECT = 'newProject',
NEW_NAMESPACE = 'newNamespace',
NEW_TEAM = 'newTeam', NEW_TEAM = 'newTeam',
} }
@ -147,7 +142,6 @@ const foundProjects = computed(() => {
return [] return []
} }
const ncache: { [id: ProjectModel['id']]: INamespace } = {}
const history = getHistory() const history = getHistory()
const allProjects = [ const allProjects = [
...new Set([ ...new Set([
@ -156,15 +150,7 @@ const foundProjects = computed(() => {
]), ]),
] ]
return allProjects.filter((l) => { return allProjects.filter((l) => typeof l !== 'undefined' && l !== null)
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
})
}) })
// FIXME: use fuzzysearch // FIXME: use fuzzysearch
@ -205,7 +191,6 @@ const results = computed<Result[]>(() => {
const loading = computed(() => const loading = computed(() =>
taskService.loading || taskService.loading ||
namespaceStore.isLoading ||
projectStore.isLoading || projectStore.isLoading ||
teamService.loading, teamService.loading,
) )
@ -230,12 +215,6 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
placeholder: t('quickActions.newProject'), placeholder: t('quickActions.newProject'),
action: newProject, action: newProject,
}, },
newNamespace: {
type: COMMAND_TYPE.NEW_NAMESPACE,
title: t('quickActions.cmds.newNamespace'),
placeholder: t('quickActions.newNamespace'),
action: newNamespace,
},
newTeam: { newTeam: {
type: COMMAND_TYPE.NEW_TEAM, type: COMMAND_TYPE.NEW_TEAM,
title: t('quickActions.cmds.newTeam'), title: t('quickActions.cmds.newTeam'),
@ -252,7 +231,6 @@ const currentProject = computed(() => Object.keys(baseStore.currentProject).leng
) )
const hintText = computed(() => { const hintText = computed(() => {
let namespace
if (selectedCmd.value !== null && currentProject.value !== null) { if (selectedCmd.value !== null && currentProject.value !== null) {
switch (selectedCmd.value.type) { switch (selectedCmd.value.type) {
case COMMAND_TYPE.NEW_TASK: case COMMAND_TYPE.NEW_TASK:
@ -260,12 +238,7 @@ const hintText = computed(() => {
title: currentProject.value.title, title: currentProject.value.title,
}) })
case COMMAND_TYPE.NEW_PROJECT: case COMMAND_TYPE.NEW_PROJECT:
namespace = namespaceStore.getNamespaceById( return t('quickActions.createProject')
currentProject.value.namespaceId,
)
return t('quickActions.createProject', {
title: namespace?.title,
})
} }
} }
const prefixes = const prefixes =
@ -278,7 +251,7 @@ const availableCmds = computed(() => {
if (currentProject.value !== null) { if (currentProject.value !== null) {
cmds.push(commands.value.newTask, commands.value.newProject) cmds.push(commands.value.newTask, commands.value.newProject)
} }
cmds.push(commands.value.newNamespace, commands.value.newTeam) cmds.push(commands.value.newTeam)
return cmds return cmds
}) })
@ -506,7 +479,6 @@ async function newProject() {
} }
const newProject = await projectStore.createProject(new ProjectModel({ const newProject = await projectStore.createProject(new ProjectModel({
title: query.value, title: query.value,
namespaceId: currentProject.value.namespaceId,
})) }))
success({ message: t('project.create.createdSuccess')}) success({ message: t('project.create.createdSuccess')})
await router.push({ 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() { async function newTeam() {
const newTeam = new TeamModel({ name: query.value }) const newTeam = new TeamModel({ name: query.value })
const team = await teamService.create(newTeam) const team = await teamService.create(newTeam)

View File

@ -139,10 +139,6 @@ import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n' 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 UserProjectService from '@/services/userProject'
import UserProjectModel from '@/models/userProject' import UserProjectModel from '@/models/userProject'
import type {IUserProject} from '@/modelTypes/IUserProject' import type {IUserProject} from '@/modelTypes/IUserProject'
@ -151,10 +147,6 @@ import UserService from '@/services/user'
import UserModel, { getDisplayName } from '@/models/user' import UserModel, { getDisplayName } from '@/models/user'
import type {IUser} from '@/modelTypes/IUser' 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 TeamProjectService from '@/services/teamProject'
import TeamProjectModel from '@/models/teamProject' import TeamProjectModel from '@/models/teamProject'
import type { ITeamProject } from '@/modelTypes/ITeamProject' import type { ITeamProject } from '@/modelTypes/ITeamProject'
@ -170,13 +162,15 @@ import Nothing from '@/components/misc/nothing.vue'
import {success} from '@/message' import {success} from '@/message'
import {useAuthStore} from '@/stores/auth' 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?
const props = defineProps({ const props = defineProps({
type: { type: {
type: String as PropType<'project' | 'namespace'>, type: String as PropType<'project'>,
default: '', default: '',
}, },
shareType: { shareType: {
type: String as PropType<'user' | 'team' | 'namespace'>, type: String as PropType<'user' | 'team'>,
default: '', default: '',
}, },
id: { id: {
@ -191,9 +185,9 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
// This user service is either a userNamespaceService or a userProjectService, depending on the type we are using // This user service is a userProjectService, depending on the type we are using
let stuffService: UserNamespaceService | UserProjectService | TeamProjectService | TeamNamespaceService let stuffService: UserProjectService | TeamProjectService
let stuffModel: IUserNamespace | IUserProject | ITeamProject | ITeamNamespace let stuffModel: IUserProject | ITeamProject
let searchService: UserService | TeamService let searchService: UserService | TeamService
let sharable: Ref<IUser | ITeam> let sharable: Ref<IUser | ITeam>
@ -231,10 +225,6 @@ const sharableName = computed(() => {
return t('project.list.title') return t('project.list.title')
} }
if (props.shareType === 'namespace') {
return t('namespace.namespace')
}
return '' return ''
}) })
@ -247,11 +237,6 @@ if (props.shareType === 'user') {
if (props.type === 'project') { if (props.type === 'project') {
stuffService = shallowReactive(new UserProjectService()) stuffService = shallowReactive(new UserProjectService())
stuffModel = reactive(new UserProjectModel({projectId: props.id})) stuffModel = reactive(new UserProjectModel({projectId: props.id}))
} else if (props.type === 'namespace') {
stuffService = shallowReactive(new UserNamespaceService())
stuffModel = reactive(new UserNamespaceModel({
namespaceId: props.id,
}))
} else { } else {
throw new Error('Unknown type: ' + props.type) throw new Error('Unknown type: ' + props.type)
} }
@ -264,11 +249,6 @@ if (props.shareType === 'user') {
if (props.type === 'project') { if (props.type === 'project') {
stuffService = shallowReactive(new TeamProjectService()) stuffService = shallowReactive(new TeamProjectService())
stuffModel = reactive(new TeamProjectModel({projectId: props.id})) stuffModel = reactive(new TeamProjectModel({projectId: props.id}))
} else if (props.type === 'namespace') {
stuffService = shallowReactive(new TeamNamespaceService())
stuffModel = reactive(new TeamNamespaceModel({
namespaceId: props.id,
}))
} else { } else {
throw new Error('Unknown type: ' + props.type) throw new Error('Unknown type: ' + props.type)
} }

View File

@ -11,7 +11,6 @@
@search="findProjects" @search="findProjects"
> >
<template #searchResult="{option}"> <template #searchResult="{option}">
<span class="project-namespace-title search-result">{{ namespace((option as IProject).namespaceId) }} ></span>
{{ (option as IProject).title }} {{ (option as IProject).title }}
</template> </template>
</Multiselect> </Multiselect>
@ -23,10 +22,8 @@ import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import type {INamespace} from '@/modelTypes/INamespace'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import ProjectModel from '@/models/project' import ProjectModel from '@/models/project'
@ -54,7 +51,6 @@ watch(
) )
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const foundProjects = ref<IProject[]>([]) const foundProjects = ref<IProject[]>([])
function findProjects(query: string) { function findProjects(query: string) {
if (query === '') { if (query === '') {
@ -70,17 +66,4 @@ function select(l: IProject | null) {
Object.assign(project, l) Object.assign(project, l)
emit('update:modelValue', project) emit('update:modelValue', project)
} }
function namespace(namespaceId: INamespace['id']) {
const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null
? namespace.title
: t('project.shared')
}
</script> </script>
<style lang="scss" scoped>
.project-namespace-title {
color: var(--grey-500);
}
</style>

View File

@ -46,11 +46,6 @@
class="different-project" class="different-project"
v-if="task.projectId !== projectId" v-if="task.projectId !== projectId"
> >
<span
v-if="task.differentNamespace !== null"
v-tooltip="$t('task.relation.differentNamespace')">
{{ task.differentNamespace }} >
</span>
<span <span
v-if="task.differentProject !== null" v-if="task.differentProject !== null"
v-tooltip="$t('task.relation.differentProject')"> v-tooltip="$t('task.relation.differentProject')">
@ -101,11 +96,6 @@
class="different-project" class="different-project"
v-if="t.projectId !== projectId" v-if="t.projectId !== projectId"
> >
<span
v-if="t.differentNamespace !== null"
v-tooltip="$t('task.relation.differentNamespace')">
{{ t.differentNamespace }} >
</span>
<span <span
v-if="t.differentProject !== null" v-if="t.differentProject !== null"
v-tooltip="$t('task.relation.differentProject')"> 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 Multiselect from '@/components/input/multiselect.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {useNamespaceStore} from '@/stores/namespaces'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useProjectStore} from '@/stores/projects'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -196,7 +185,7 @@ const props = defineProps({
}) })
const taskStore = useTaskStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore() const projectStore = useProjectStore()
const route = useRoute() const route = useRoute()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -230,24 +219,13 @@ async function findTasks(newQuery: string) {
foundTasks.value = await taskService.getAll({}, {s: newQuery}) 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[]) { function mapRelatedTasks(tasks: ITask[]) {
return tasks.map(task => { return tasks.map(task => {
// by doing this here once we can save a lot of duplicate calls in the template // by doing this here once we can save a lot of duplicate calls in the template
const { const project = projectStore.getProjectById(task.ProjectId)
project,
namespace: taskNamespace,
} = getProjectAndNamespaceById(task.projectId) || {project: null, namespace: null}
return { return {
...task, ...task,
differentNamespace:
(taskNamespace !== null &&
taskNamespace.id !== namespace.value.id &&
taskNamespace?.title) || null,
differentProject: differentProject:
(project !== null && (project !== null &&
task.projectId !== props.projectId && task.projectId !== props.projectId &&

View File

@ -149,7 +149,6 @@ import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatD
import {success} from '@/message' import {success} from '@/message'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
@ -209,7 +208,6 @@ onBeforeUnmount(() => {
const baseStore = useBaseStore() const baseStore = useBaseStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore()
const project = computed(() => projectStore.getProjectById(task.value.projectId)) const project = computed(() => projectStore.getProjectById(task.value.projectId))
const projectColor = computed(() => project.value !== null ? project.value.hexColor : '') const projectColor = computed(() => project.value !== null ? project.value.hexColor : '')
@ -260,7 +258,6 @@ async function toggleFavorite() {
task.value.isFavorite = !task.value.isFavorite task.value.isFavorite = !task.value.isFavorite
task.value = await taskService.update(task.value) task.value = await taskService.update(task.value)
emit('task-updated', task.value) emit('task-updated', task.value)
namespaceStore.loadNamespacesIfFavoritesDontExist()
} }
const deferDueDate = ref<typeof DeferTask | null>(null) const deferDueDate = ref<typeof DeferTask | null>(null)

View File

@ -1,19 +0,0 @@
import {ref, computed} from 'vue'
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,
}
}

View File

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

View File

@ -143,7 +143,7 @@
}, },
"deletion": { "deletion": {
"title": "Delete your Vikunja Account", "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.", "text2": "To proceed, please enter your password. You will receive an email with further instructions.",
"confirm": "Delete my account", "confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.", "requestSuccess": "The request was successful. You'll receive an email with further instructions.",
@ -165,7 +165,8 @@
} }
}, },
"project": { "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", "showArchived": "Show Archived",
"title": "Project Title", "title": "Project Title",
"color": "Color", "color": "Color",
@ -211,7 +212,6 @@
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated project:",
"success": "The project was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "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": { "filters": {
"title": "Filters", "title": "Filters",
"clear": "Clear Filters", "clear": "Clear Filters",
@ -403,7 +343,7 @@
}, },
"create": { "create": {
"title": "New Saved Filter", "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", "action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -677,19 +617,13 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "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.", "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.", "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.", "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.", "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.", "notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "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", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "You are now subscribed to this task", "subscribeSuccessTask": "You are now subscribed to this task",
@ -766,7 +700,6 @@
"searchPlaceholder": "Type search for a new task to add as related…", "searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task", "createPlaceholder": "Add this as new related task",
"differentProject": "This task belongs to a different project.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "This task belongs to a different namespace.",
"noneYet": "No task relations yet.", "noneYet": "No task relations yet.",
"delete": "Delete Task Relation", "delete": "Delete Task Relation",
"deleteText1": "Are you sure you want to delete this task relation?", "deleteText1": "Are you sure you want to delete this task relation?",
@ -851,19 +784,19 @@
"delete": { "delete": {
"header": "Delete the team", "header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?", "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." "success": "The team was successfully deleted."
}, },
"deleteUser": { "deleteUser": {
"header": "Remove a user from the team", "header": "Remove a user from the team",
"text1": "Are you sure you want to remove this 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." "success": "The user was successfully deleted from the team."
}, },
"leave": { "leave": {
"title": "Leave team", "title": "Leave team",
"text1": "Are you sure you want to leave this 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." "success": "You have successfully left the team."
} }
}, },
@ -913,9 +846,9 @@
"title": "Navigation", "title": "Navigation",
"overview": "Navigate to overview", "overview": "Navigate to overview",
"upcoming": "Navigate to upcoming tasks", "upcoming": "Navigate to upcoming tasks",
"namespaces": "Navigate to namespaces & projects",
"labels": "Navigate to labels", "labels": "Navigate to labels",
"teams": "Navigate to teams" "teams": "Navigate to teams",
"projects": "Navigate to projects"
} }
}, },
"update": { "update": {
@ -949,7 +882,7 @@
"notification": { "notification": {
"title": "Notifications", "title": "Notifications",
"none": "You don't have any notifications. Have a nice day!", "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": { "quickActions": {
"commands": "Commands", "commands": "Commands",
@ -960,14 +893,12 @@
"teams": "Teams", "teams": "Teams",
"newProject": "Enter the title of the new project…", "newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…", "newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…", "newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current project ({title})", "createTask": "Create a task in the current project ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Create a project",
"cmds": { "cmds": {
"newTask": "New task", "newTask": "New task",
"newProject": "New project", "newProject": "New project",
"newNamespace": "New namespace",
"newTeam": "New team" "newTeam": "New team"
} }
}, },
@ -1023,16 +954,9 @@
"4017": "Invalid task filter comparator.", "4017": "Invalid task filter comparator.",
"4018": "Invalid task filter concatenator.", "4018": "Invalid task filter concatenator.",
"4019": "Invalid task filter value.", "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.", "6001": "The team name cannot be empty.",
"6002": "The team does not exist.", "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.", "6005": "The user is already a member of that team.",
"6006": "Cannot delete the last team member.", "6006": "Cannot delete the last team member.",
"6007": "The team does not have access to the project to perform that action.", "6007": "The team does not have access to the project to perform that action.",

View File

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

View File

@ -2,7 +2,6 @@ import type {IAbstract} from './IAbstract'
import type {ITask} from './ITask' import type {ITask} from './ITask'
import type {IUser} from './IUser' import type {IUser} from './IUser'
import type {ISubscription} from './ISubscription' import type {ISubscription} from './ISubscription'
import type {INamespace} from './INamespace'
export interface IProject extends IAbstract { export interface IProject extends IAbstract {
@ -11,7 +10,6 @@ export interface IProject extends IAbstract {
description: string description: string
owner: IUser owner: IUser
tasks: ITask[] tasks: ITask[]
namespaceId: INamespace['id']
isArchived: boolean isArchived: boolean
hexColor: string hexColor: string
identifier: string identifier: string

View File

@ -1,9 +1,7 @@
import type {IAbstract} from './IAbstract' import type {IAbstract} from './IAbstract'
import type {IProject} from './IProject' import type {IProject} from './IProject'
import type {INamespace} from './INamespace'
export interface IProjectDuplicate extends IAbstract { export interface IProjectDuplicate extends IAbstract {
projectId: number projectId: number
namespaceId: INamespace['id']
project: IProject project: IProject
} }

View File

@ -1,6 +0,0 @@
import type {ITeamShareBase} from './ITeamShareBase'
import type {INamespace} from './INamespace'
export interface ITeamNamespace extends ITeamShareBase {
namespaceId: INamespace['id']
}

View File

@ -1,6 +0,0 @@
import type {IUserShareBase} from './IUserShareBase'
import type {INamespace} from './INamespace'
export interface IUserNamespace extends IUserShareBase {
namespaceId: INamespace['id']
}

View File

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

View File

@ -6,7 +6,6 @@ import SubscriptionModel from '@/models/subscription'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
export default class ProjectModel extends AbstractModel<IProject> implements IProject { export default class ProjectModel extends AbstractModel<IProject> implements IProject {
@ -15,7 +14,6 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
description = '' description = ''
owner: IUser = UserModel owner: IUser = UserModel
tasks: ITask[] = [] tasks: ITask[] = []
namespaceId: INamespace['id'] = 0
isArchived = false isArchived = false
hexColor = '' hexColor = ''
identifier = '' identifier = ''

View File

@ -2,12 +2,10 @@ import AbstractModel from './abstractModel'
import ProjectModel from './project' import ProjectModel from './project'
import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate' import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplicate> implements IProjectDuplicate { export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplicate> implements IProjectDuplicate {
projectId = 0 projectId = 0
namespaceId: INamespace['id'] = 0
project: IProject = ProjectModel project: IProject = ProjectModel
constructor(data : Partial<IProjectDuplicate>) { constructor(data : Partial<IProjectDuplicate>) {

View File

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

View File

@ -6,7 +6,7 @@ import type {ITeam} from '@/modelTypes/ITeam'
/** /**
* This class is a base class for common team sharing model. * 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 { export default class TeamShareBaseModel extends AbstractModel<ITeamShareBase> implements ITeamShareBase {
teamId: ITeam['id'] = 0 teamId: ITeam['id'] = 0

View File

@ -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 {
namespaceId: INamespace['id'] = 0
constructor(data: Partial<IUserNamespace>) {
super(data)
this.assignData(data)
}
}

View File

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

View File

@ -7,7 +7,7 @@ import {colorFromHex} from '@/helpers/color/colorFromHex'
export default class ProjectService extends AbstractService<IProject> { export default class ProjectService extends AbstractService<IProject> {
constructor() { constructor() {
super({ super({
create: '/namespaces/{namespaceId}/projects', create: '/projects',
get: '/projects/{id}', get: '/projects/{id}',
getAll: '/projects', getAll: '/projects',
update: '/projects/{id}', update: '/projects/{id}',

View File

@ -12,7 +12,6 @@ import AbstractService from '@/services/abstractService'
import SavedFilterModel from '@/models/savedFilter' import SavedFilterModel from '@/models/savedFilter'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useNamespaceStore} from '@/stores/namespaces'
import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case' import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case'
import {success} from '@/message' import {success} from '@/message'
@ -81,7 +80,6 @@ export default class SavedFilterService extends AbstractService<ISavedFilter> {
export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) { export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
const router = useRouter() const router = useRouter()
const {t} = useI18n({useScope:'global'}) const {t} = useI18n({useScope:'global'})
const namespaceStore = useNamespaceStore()
const filterService = shallowReactive(new SavedFilterService()) const filterService = shallowReactive(new SavedFilterService())
@ -110,13 +108,11 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
async function createFilter() { async function createFilter() {
filter.value = await filterService.create(filter.value) filter.value = await filterService.create(filter.value)
await namespaceStore.loadNamespaces()
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}}) router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
} }
async function saveFilter() { async function saveFilter() {
const response = await filterService.update(filter.value) const response = await filterService.update(filter.value)
await namespaceStore.loadNamespaces()
success({message: t('filters.edit.success')}) success({message: t('filters.edit.success')})
response.filters = objectToSnakeCase(response.filters) response.filters = objectToSnakeCase(response.filters)
filter.value = response filter.value = response
@ -129,7 +125,6 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
async function deleteFilter() { async function deleteFilter() {
await filterService.delete(filter.value) await filterService.delete(filter.value)
await namespaceStore.loadNamespaces()
success({message: t('filters.delete.success')}) success({message: t('filters.delete.success')})
router.push({name: 'projects.index'}) router.push({name: 'projects.index'})
} }

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@ import ProjectService from '@/services/project'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
import {removeProjectFromHistory} from '@/modules/projectHistory' import {removeProjectFromHistory} from '@/modules/projectHistory'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import {useNamespaceStore} from './namespaces'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
@ -26,7 +25,6 @@ export interface ProjectState {
export const useProjectStore = defineStore('project', () => { export const useProjectStore = defineStore('project', () => {
const baseStore = useBaseStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore()
const isLoading = ref(false) const isLoading = ref(false)
@ -100,8 +98,6 @@ export const useProjectStore = defineStore('project', () => {
try { try {
const createdProject = await projectService.create(project) const createdProject = await projectService.create(project)
createdProject.namespaceId = project.namespaceId
namespaceStore.addProjectToNamespace(createdProject)
setProject(createdProject) setProject(createdProject)
return createdProject return createdProject
} finally { } finally {
@ -116,21 +112,13 @@ export const useProjectStore = defineStore('project', () => {
try { try {
await projectService.update(project) await projectService.update(project)
setProject(project) setProject(project)
namespaceStore.setProjectInNamespaceById(project)
// the returned project from projectService.update is the same! // 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 // in order to not create a manipulation in pinia store we have to create a new copy
const newProject = { const newProject = {
...project, ...project,
namespaceId: FavoriteProjectsNamespace,
} }
namespaceStore.removeProjectFromNamespaceById(newProject)
if (project.isFavorite) {
namespaceStore.addProjectToNamespace(newProject)
}
namespaceStore.loadNamespacesIfFavoritesDontExist()
namespaceStore.removeFavoritesNamespaceIfEmpty()
return newProject return newProject
} catch (e) { } catch (e) {
// Reset the project state to the initial one to avoid confusion for the user // Reset the project state to the initial one to avoid confusion for the user
@ -151,7 +139,6 @@ export const useProjectStore = defineStore('project', () => {
try { try {
const response = await projectService.delete(project) const response = await projectService.delete(project)
removeProjectById(project) removeProjectById(project)
namespaceStore.removeProjectFromNamespaceById(project)
removeProjectFromHistory({id: project.id}) removeProjectFromHistory({id: project.id})
return response return response
} finally { } finally {

View File

@ -17,22 +17,11 @@
@taskAdded="updateTaskKey" @taskAdded="updateTaskKey"
class="is-max-width-desktop" class="is-max-width-desktop"
/> />
<template v-if="!hasTasks && !loading"> <template v-if="!hasTasks && !loading && migratorsEnabled">
<template v-if="defaultNamespaceId > 0"> <p class="mt-4">
<p class="mt-4">{{ $t('home.project.newText') }}</p>
<x-button
: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">
{{ $t('home.project.importText') }} {{ $t('home.project.importText') }}
</p> </p>
<x-button <x-button
v-if="migratorsEnabled"
:to="{ name: 'migrate.start' }" :to="{ name: 'migrate.start' }"
:shadow="false"> :shadow="false">
{{ $t('home.project.import') }} {{ $t('home.project.import') }}
@ -66,7 +55,6 @@ import {useDaytimeSalutation} from '@/composables/useDaytimeSalutation'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useNamespaceStore} from '@/stores/namespaces'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
@ -76,7 +64,6 @@ const salutation = useDaytimeSalutation()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const configStore = useConfigStore() const configStore = useConfigStore()
const namespaceStore = useNamespaceStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
@ -93,8 +80,7 @@ const projectHistory = computed(() => {
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0) const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
const hasTasks = computed(() => baseStore.hasTasks) const hasTasks = computed(() => baseStore.hasTasks)
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0) const hasProjects = computed(() => projectStore.projects.length > 0)
const hasProjects = computed(() => namespaceStore.namespaces?.[0]?.projects.length > 0)
const loading = computed(() => taskStore.isLoading) const loading = computed(() => taskStore.isLoading)
const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt)) const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt))

View File

@ -89,8 +89,8 @@ import {formatDateLong} from '@/helpers/time/formatDate'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {MIGRATORS} from './migrators' import {MIGRATORS} from './migrators'
import {useNamespaceStore} from '@/stores/namespaces'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useProjectStore} from '@/stores/projects'
const PROGRESS_DOTS_COUNT = 8 const PROGRESS_DOTS_COUNT = 8
@ -163,8 +163,8 @@ async function migrate() {
? await migrationFileService.migrate(migrationConfig as File) ? await migrationFileService.migrate(migrationConfig as File)
: await migrationService.migrate(migrationConfig as MigrationConfig) : await migrationService.migrate(migrationConfig as MigrationConfig)
message.value = result.message message.value = result.message
const namespaceStore = useNamespaceStore() const projectStore = useProjectStore()
return namespaceStore.loadNamespaces() return projectStore.loadProjects()
} finally { } finally {
isMigrating.value = false isMigrating.value = false
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,6 @@ async function createNewProject() {
} }
showError.value = false showError.value = false
project.namespaceId = Number(route.params.namespaceId as string)
const newProject = await projectStore.createProject(project) const newProject = await projectStore.createProject(project)
await router.push({ await router.push({
name: 'project.index', name: 'project.index',

View File

@ -108,7 +108,6 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import BackgroundUnsplashService from '@/services/backgroundUnsplash' import BackgroundUnsplashService from '@/services/backgroundUnsplash'
@ -146,7 +145,6 @@ const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNC
const backgroundUploadService = ref(new BackgroundUploadService()) const backgroundUploadService = ref(new BackgroundUploadService())
const projectService = ref(new ProjectService()) const projectService = ref(new ProjectService())
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const configStore = useConfigStore() const configStore = useConfigStore()
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash')) const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
@ -195,7 +193,6 @@ async function setBackground(backgroundId: string) {
projectId: route.params.projectId, projectId: route.params.projectId,
}) })
await baseStore.handleSetCurrentProject({project, forceUpdate: true}) await baseStore.handleSetCurrentProject({project, forceUpdate: true})
namespaceStore.setProjectInNamespaceById(project)
projectStore.setProject(project) projectStore.setProject(project)
success({message: t('project.background.success')}) success({message: t('project.background.success')})
} }
@ -211,7 +208,6 @@ async function uploadBackground() {
backgroundUploadInput.value?.files[0], backgroundUploadInput.value?.files[0],
) )
await baseStore.handleSetCurrentProject({project, forceUpdate: true}) await baseStore.handleSetCurrentProject({project, forceUpdate: true})
namespaceStore.setProjectInNamespaceById(project)
projectStore.setProject(project) projectStore.setProject(project)
success({message: t('project.background.success')}) success({message: t('project.background.success')})
} }
@ -219,7 +215,6 @@ async function uploadBackground() {
async function removeBackground() { async function removeBackground() {
const project = await projectService.value.removeBackground(currentProject.value) const project = await projectService.value.removeBackground(currentProject.value)
await baseStore.handleSetCurrentProject({project, forceUpdate: true}) await baseStore.handleSetCurrentProject({project, forceUpdate: true})
namespaceStore.setProjectInNamespaceById(project)
projectStore.setProject(project) projectStore.setProject(project)
success({message: t('project.background.removeSuccess')}) success({message: t('project.background.removeSuccess')})
router.back() router.back()

View File

@ -7,15 +7,6 @@
:loading="projectDuplicateService.loading" :loading="projectDuplicateService.loading"
> >
<p>{{ $t('project.duplicate.text') }}</p> <p>{{ $t('project.duplicate.text') }}</p>
<Multiselect
:placeholder="$t('namespace.search')"
@search="findNamespaces"
:search-results="namespaces"
@select="selectNamespace"
label="title"
:search-delay="10"
/>
</create-edit> </create-edit>
</template> </template>
@ -29,32 +20,17 @@ import CreateEdit from '@/components/misc/create-edit.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import ProjectDuplicateModel from '@/models/projectDuplicateModel' import ProjectDuplicateModel from '@/models/projectDuplicateModel'
import type {INamespace} from '@/modelTypes/INamespace'
import {success} from '@/message' import {success} from '@/message'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useNamespaceSearch} from '@/composables/useNamespaceSearch'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(() => t('project.duplicate.title')) useTitle(() => t('project.duplicate.title'))
const {
namespaces,
findNamespaces,
} = useNamespaceSearch()
const selectedNamespace = ref<INamespace>()
function selectNamespace(namespace: INamespace) {
selectedNamespace.value = namespace
}
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const projectDuplicateService = shallowReactive(new ProjectDuplicateService()) const projectDuplicateService = shallowReactive(new ProjectDuplicateService())
@ -62,12 +38,10 @@ async function duplicateProject() {
const projectDuplicate = new ProjectDuplicateModel({ const projectDuplicate = new ProjectDuplicateModel({
// FIXME: should be parameter // FIXME: should be parameter
projectId: route.params.projectId, projectId: route.params.projectId,
namespaceId: selectedNamespace.value?.id,
}) })
const duplicate = await projectDuplicateService.create(projectDuplicate) const duplicate = await projectDuplicateService.create(projectDuplicate)
namespaceStore.addProjectToNamespace(duplicate.project)
projectStore.setProject(duplicate.project) projectStore.setProject(duplicate.project)
success({message: t('project.duplicate.success')}) success({message: t('project.duplicate.success')})
router.push({name: 'project.index', params: {projectId: duplicate.project.id}}) router.push({name: 'project.index', params: {projectId: duplicate.project.id}})

View File

@ -13,8 +13,7 @@
:can-write="canWrite" :can-write="canWrite"
ref="heading" ref="heading"
/> />
<h6 class="subtitle" v-if="parent && parent.namespace && parent.project"> <h6 class="subtitle" v-if="parent && parent.project">
{{ getNamespaceTitle(parent.namespace) }} &rsaquo;
<router-link :to="{ name: 'project.index', params: { projectId: parent.project.id } }"> <router-link :to="{ name: 'project.index', params: { projectId: parent.project.id } }">
{{ getProjectTitle(parent.project) }} {{ getProjectTitle(parent.project) }}
</router-link> </router-link>
@ -486,12 +485,10 @@ import TaskSubscription from '@/components/misc/subscription.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue' import CustomTransition from '@/components/misc/CustomTransition.vue'
import {uploadFile} from '@/helpers/attachments' import {uploadFile} from '@/helpers/attachments'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getProjectTitle} from '@/helpers/getProjectTitle' import {getProjectTitle} from '@/helpers/getProjectTitle'
import {scrollIntoView} from '@/helpers/scrollIntoView' import {scrollIntoView} from '@/helpers/scrollIntoView'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useNamespaceStore} from '@/stores/namespaces'
import {useAttachmentStore} from '@/stores/attachments' import {useAttachmentStore} from '@/stores/attachments'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban' import {useKanbanStore} from '@/stores/kanban'
@ -500,6 +497,7 @@ import {useTitle} from '@/composables/useTitle'
import {success} from '@/message' import {success} from '@/message'
import type {Action as MessageAction} from '@/message' import type {Action as MessageAction} from '@/message'
import {useProjectStore} from '@/stores/projects'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -517,7 +515,7 @@ const router = useRouter()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const baseStore = useBaseStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore() const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore() const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const kanbanStore = useKanbanStore() const kanbanStore = useKanbanStore()
@ -541,16 +539,11 @@ const taskId = toRef(props, 'taskId')
const parent = computed(() => { const parent = computed(() => {
if (!task.projectId) { if (!task.projectId) {
return { return {
namespace: null,
project: null, project: null,
} }
} }
if (!namespaceStore.getProjectAndNamespaceById) { return projectStore.getProjectById(task.projectId)
return null
}
return namespaceStore.getProjectAndNamespaceById(task.projectId)
}) })
watch( watch(
@ -775,7 +768,6 @@ async function toggleFavorite() {
task.isFavorite = !task.isFavorite task.isFavorite = !task.isFavorite
const newTask = await taskService.update(task) const newTask = await taskService.update(task)
Object.assign(task, newTask) Object.assign(task, newTask)
await namespaceStore.loadNamespacesIfFavoritesDontExist()
} }
async function setPriority(priority: Priority) { async function setPriority(priority: Priority) {