Move list edit/namespace to separate pages and in a menu (#397)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/397
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2021-01-30 16:17:04 +00:00
parent 649714e8a9
commit e0be77d88f
54 changed files with 1773 additions and 974 deletions

View file

@ -21,7 +21,10 @@ describe('Lists', () => {
it('Should create a new list', () => {
cy.visit('/')
cy.get('.namespace-title a[href="/namespaces/1/list"]')
cy.get('.namespace-title .dropdown-trigger')
.click()
cy.get('.namespace-title .dropdown .dropdown-item')
.contains('New list')
.click()
cy.url()
.should('contain', '/namespaces/1/list')
@ -58,9 +61,8 @@ describe('Lists', () => {
.should('contain', '/lists/1/list')
cy.get('.list-title h1')
.should('contain', 'First List')
cy.get('.list-title a.icon')
.should('have.attr', 'href')
.and('include', '/lists/1/edit')
cy.get('.list-title .dropdown')
.should('exist')
cy.get('p')
.contains('This list is currently empty.')
.should('exist')
@ -363,6 +365,7 @@ describe('Lists', () => {
cy.getAttached('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.should('be.visible')
.click()
cy.url()

View file

@ -64,25 +64,7 @@
{{ n.title }} ({{ n.lists.filter(l => !l.isArchived).length }})
</span>
</label>
<div class="actions">
<router-link
:key="n.id + 'list.create'"
:to="{ name: 'list.create', params: { id: n.id} }"
v-if="n.id > 0"
v-tooltip="'Add a new list in the ' + n.title + ' namespace'">
<span class="icon">
<icon icon="plus"/>
</span>
</router-link>
<router-link
:to="{name: 'namespace.edit', params: {id: n.id} }"
v-if="n.id > 0"
v-tooltip="'Settings'">
<span class="icon">
<icon icon="cog"/>
</span>
</router-link>
</div>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div>
<input
:id="n.id + 'checker'"
@ -118,6 +100,7 @@
<icon :icon="['far', 'star']" v-else/>
</span>
</router-link>
<list-settings-dropdown :list="l"/>
</li>
</template>
</ul>
@ -134,9 +117,15 @@
<script>
import {mapState} from 'vuex'
import {CURRENT_LIST, MENU_ACTIVE, LOADING, LOADING_MODULE} from '@/store/mutation-types'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
export default {
name: 'navigation',
components: {
ListSettingsDropdown,
NamespaceSettingsDropdown,
},
computed: mapState({
namespaces(state) {
return state.namespaces.namespaces.filter(n => !n.isArchived)

View file

@ -31,21 +31,17 @@
class="title">
{{ currentList.title === '' ? 'Loading...' : currentList.title }}
</h1>
<router-link
:to="{ name: 'list.edit', params: { id: currentList.id } }"
class="icon"
v-if="canWriteCurrentList">
<icon icon="cog" size="2x"/>
</router-link>
<list-settings-dropdown v-if="canWriteCurrentList" :list="currentList"/>
</div>
<div class="navbar-end">
<update/>
<div class="user">
<img :src="userAvatar" alt="" class="avatar"/>
<div class="dropdown is-right is-active">
<div class="dropdown-trigger">
<dropdown class="is-right">
<template v-slot:trigger>
<x-button
@click.stop="userMenuActive = !userMenuActive"
type="secondary"
:shadow="false">
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
@ -53,36 +49,31 @@
<icon icon="chevron-down"/>
</span>
</x-button>
</div>
<transition name="fade">
<div class="dropdown-menu" v-if="userMenuActive">
<div class="dropdown-content">
<router-link :to="{name: 'user.settings'}" class="dropdown-item">
Settings
</router-link>
<a
:href="imprintUrl"
class="dropdown-item"
target="_blank"
v-if="imprintUrl">
Imprint
</a>
<a
:href="privacyPolicyUrl"
class="dropdown-item"
target="_blank"
v-if="privacyPolicyUrl">
Privacy policy
</a>
<a @click="$store.commit('keyboardShortcutsActive', true)" class="dropdown-item">Keyboard
Shortcuts</a>
<a @click="logout()" class="dropdown-item">
Logout
</a>
</div>
</div>
</transition>
</div>
</template>
<router-link :to="{name: 'user.settings'}" class="dropdown-item">
Settings
</router-link>
<a
:href="imprintUrl"
class="dropdown-item"
target="_blank"
v-if="imprintUrl">
Imprint
</a>
<a
:href="privacyPolicyUrl"
class="dropdown-item"
target="_blank"
v-if="privacyPolicyUrl">
Privacy policy
</a>
<a @click="$store.commit('keyboardShortcutsActive', true)" class="dropdown-item">Keyboard
Shortcuts</a>
<a @click="logout()" class="dropdown-item">
Logout
</a>
</dropdown>
</div>
</div>
</nav>
@ -93,21 +84,16 @@ import {mapState} from 'vuex'
import {CURRENT_LIST} from '@/store/mutation-types'
import Rights from '@/models/rights.json'
import Update from '@/components/home/update'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
import Dropdown from '@/components/misc/dropdown'
export default {
name: 'topNavigation',
data() {
return {
userMenuActive: false,
}
},
components: {
Dropdown,
ListSettingsDropdown,
Update,
},
created() {
// This will hide the menu once clicked outside of it
this.$nextTick(() => document.addEventListener('click', () => this.userMenuActive = false))
},
computed: mapState({
userInfo: state => state.auth.info,
userAvatar: state => state.auth.avatarUrl,

View file

@ -0,0 +1,106 @@
<template>
<dropdown>
<template v-if="isSavedFilter">
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.edit`, params: { listId: list.id } }"
icon="pen"
>
Edit
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
icon="trash-alt"
>
Delete
</dropdown-item>
</template>
<template v-else-if="list.isArchived">
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.archive`, params: { listId: list.id } }"
icon="archive"
>
Un-Archive
</dropdown-item>
</template>
<template v-else>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.edit`, params: { listId: list.id } }"
icon="pen"
>
Edit
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.background`, params: { listId: list.id } }"
v-if="backgroundsEnabled"
icon="image"
>
Set background
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.share`, params: { listId: list.id } }"
icon="share-alt"
>
Share
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.duplicate`, params: { listId: list.id } }"
icon="paste"
>
Duplicate
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.archive`, params: { listId: list.id } }"
icon="archive"
>
Archive
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
icon="trash-alt"
class="has-text-danger"
>
Delete
</dropdown-item>
</template>
</dropdown>
</template>
<script>
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown'
import DropdownItem from '@/components/misc/dropdown-item'
export default {
name: 'list-settings-dropdown',
components: {
DropdownItem,
Dropdown,
},
props: {
list: {
required: true,
},
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders.length > 0
},
listRoutePrefix() {
let name = 'list'
if (this.$route.name.startsWith('list.')) {
name = this.$route.name
}
if (this.isSavedFilter) {
name = name.replace('list.', 'filter.')
}
return name
},
isSavedFilter() {
return getSavedFilterIdFromListId(this.list.id) > 0
},
},
}
</script>

View file

@ -10,7 +10,7 @@
</span>
</a>
</header>
<div class="card-content" :class="{'p-0': !padding}">
<div class="card-content loader-container" :class="{'p-0': !padding, 'is-loading': loading}">
<div :class="{'content': hasContent}">
<slot></slot>
</div>
@ -46,6 +46,10 @@ export default {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
},
}
</script>

View file

@ -0,0 +1,85 @@
<template>
<modal @close="$router.back()" :overflow="true" :wide="wide">
<card
:title="title"
:shadow="false"
:padding="false"
class="has-text-left has-overflow"
:has-close="true"
close-icon="times"
@close="$router.back()"
:loading="loading"
>
<div class="p-4">
<slot></slot>
</div>
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
<x-button
:shadow="false"
type="tertary"
@click.prevent.stop="$emit('tertary')"
v-if="tertary !== ''"
>
{{ tertary }}
</x-button>
<x-button
type="secondary"
@click.prevent.stop="$router.back()"
>
Cancel
</x-button>
<x-button
type="primary"
@click.prevent.stop="primary"
:icon="primaryIcon"
:disabled="primaryDisabled"
v-if="primaryLabel !== ''"
>
{{ primaryLabel }}
</x-button>
</footer>
</card>
</modal>
</template>
<script>
export default {
name: 'create-edit',
props: {
title: {
type: String,
default: '',
},
primaryLabel: {
type: String,
default: 'Create',
},
primaryIcon: {
type: String,
default: 'plus',
},
primaryDisabled: {
type: Boolean,
default: false,
},
tertary: {
type: String,
default: '',
},
wide: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
},
methods: {
primary() {
this.$emit('create')
this.$emit('primary')
},
},
}
</script>

View file

@ -1,55 +0,0 @@
<template>
<modal @close="$router.back()" :overflow="true">
<card
:title="title"
:shadow="false"
:padding="false"
class="has-text-left has-overflow"
:has-close="true"
close-icon="times"
@close="$router.back()"
>
<div class="p-4">
<slot></slot>
</div>
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
<x-button
:shadow="false"
type="secondary"
@click.prevent.stop="$router.back()"
>
Cancel
</x-button>
<x-button
:shadow="false"
type="primary"
@click.prevent.stop="$emit('create')"
icon="plus"
:disabled="createDisabled"
>
{{ createLabel }}
</x-button>
</footer>
</card>
</modal>
</template>
<script>
export default {
name: 'create',
props: {
title: {
type: String,
default: '',
},
createLabel: {
type: String,
default: 'Create',
},
createDisabled: {
type: Boolean,
default: false,
},
},
}
</script>

View file

@ -0,0 +1,28 @@
<template>
<router-link
:to="to"
class="dropdown-item">
<span class="icon" v-if="icon !== ''">
<icon :icon="icon"/>
</span>
<span>
<slot></slot>
</span>
</router-link>
</template>
<script>
export default {
name: 'dropdown-item',
props: {
to: {
required: true,
},
icon: {
type: String,
required: false,
default: '',
}
},
}
</script>

View file

@ -0,0 +1,50 @@
<template>
<div class="dropdown is-right is-active" ref="dropdown">
<div class="dropdown-trigger" @click="open = !open">
<slot name="trigger">
<icon :icon="triggerIcon" class="icon"/>
</slot>
</div>
<transition name="fade">
<div class="dropdown-menu" v-if="open">
<div class="dropdown-content">
<slot></slot>
</div>
</div>
</transition>
</div>
</template>
<script>
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
export default {
name: 'dropdown',
data() {
return {
open: false,
}
},
mounted() {
document.addEventListener('click', this.hide)
},
beforeDestroy() {
document.removeEventListener('click', this.hide)
},
props: {
triggerIcon: {
type: String,
default: 'ellipsis-h',
},
},
methods: {
hide(e) {
if (this.open) {
closeWhenClickedOutside(e, this.$refs.dropdown, () => {
this.open = false
})
}
},
},
}
</script>

View file

@ -0,0 +1,11 @@
<template>
<p class="has-text-centered has-text-grey is-italic p-4 mb-4">
<slot></slot>
</p>
</template>
<script>
export default {
name: 'nothing'
}
</script>

View file

@ -2,7 +2,7 @@
<transition name="modal">
<div class="modal-mask">
<div class="modal-container" @click.self.prevent.stop="$emit('close')">
<div class="modal-content" :class="{'has-overflow': overflow}">
<div class="modal-content" :class="{'has-overflow': overflow, 'is-wide': wide}">
<slot>
<div class="header">
<slot name="header"></slot>
@ -49,6 +49,10 @@ export default {
type: Boolean,
default: false,
},
wide: {
type: Boolean,
default: false,
},
},
}
</script>

View file

@ -0,0 +1,63 @@
<template>
<dropdown>
<template v-if="namespace.isArchived">
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
icon="archive"
>
Un-Archive
</dropdown-item>
</template>
<template v-else>
<dropdown-item
:to="{ name: 'namespace.settings.edit', params: { id: namespace.id } }"
icon="pen"
>
Edit
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.share', params: { id: namespace.id } }"
icon="share-alt"
>
Share
</dropdown-item>
<dropdown-item
:to="{ name: 'list.create', params: { id: namespace.id } }"
icon="plus"
>
New list
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
icon="archive"
>
Archive
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
icon="trash-alt"
class="has-text-danger"
>
Delete
</dropdown-item>
</template>
</dropdown>
</template>
<script>
import Dropdown from '@/components/misc/dropdown'
import DropdownItem from '@/components/misc/dropdown-item'
export default {
name: 'namespace-settings-dropdown',
components: {
DropdownItem,
Dropdown,
},
props: {
namespace: {
required: true,
},
},
}
</script>

View file

@ -1,5 +1,6 @@
<template>
<card title="Share links" class="is-fullwidth" :padding="false">
<div>
<p class="has-text-weight-bold">Share Links</p>
<div class="sharables-list">
<div class="p-4">
<p>Share with a link:</p>
@ -21,7 +22,7 @@
</div>
</div>
<table
class="table is-striped is-hoverable is-fullwidth link-share-list"
class="table has-actions is-striped is-hoverable is-fullwidth link-share-list"
v-if="linkShares.length > 0"
>
<thead>
@ -112,7 +113,7 @@
</p>
</modal>
</transition>
</card>
</div>
</template>
<script>

View file

@ -1,10 +1,11 @@
<template>
<card class="is-fullwidth has-overflow" :title="`Shared with these ${shareType}s`" :padding="false">
<div class="p-4" v-if="userIsAdmin">
<div>
<p class="has-text-weight-bold">Shared with these {{ shareType }}s</p>
<div v-if="userIsAdmin">
<div class="field has-addons">
<p
class="control is-expanded"
v-bind:class="{ 'is-loading': searchService.loading }"
:class="{ 'is-loading': searchService.loading }"
>
<multiselect
:loading="searchService.loading"
@ -20,7 +21,8 @@
</p>
</div>
</div>
<table class="table is-striped is-hoverable is-fullwidth">
<table class="table has-actions is-striped is-hoverable is-fullwidth mb-4" v-if="sharables.length > 0">
<tbody>
<tr :key="s.id" v-for="s in sharables">
<template v-if="shareType === 'user'">
@ -105,6 +107,10 @@
</tbody>
</table>
<nothing v-else>
Not shared with any {{ shareType }} yet.
</nothing>
<transition name="modal">
<modal
@close="showDeleteModal = false"
@ -121,7 +127,7 @@
</p>
</modal>
</transition>
</card>
</div>
</template>
<script>
@ -143,6 +149,7 @@ import TeamModel from '../../models/team'
import rights from '../../models/rights'
import Multiselect from '@/components/input/multiselect'
import Nothing from '@/components/misc/nothing'
export default {
name: 'userTeamShare',
@ -182,6 +189,7 @@ export default {
}
},
components: {
Nothing,
Multiselect,
},
computed: mapState({

View file

@ -12,6 +12,8 @@ export default {
pages: [],
currentPage: 0,
loadedList: null,
showTaskSearch: false,
searchTerm: '',
@ -53,6 +55,17 @@ export default {
return
}
const list = {listId: parseInt(this.$route.params.listId)}
const currentList = {
id: list.listId,
params: params,
search: search,
}
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList)) {
return
}
this.$set(this, 'tasks', [])
if (params === null) {
@ -62,7 +75,8 @@ export default {
if (search !== '') {
params.s = search
}
this.taskCollectionService.getAll({listId: this.$route.params.listId}, params, page)
this.taskCollectionService.getAll(list, params, page)
.then(r => {
this.$set(this, 'tasks', r)
this.$set(this, 'pages', [])
@ -95,6 +109,8 @@ export default {
isEllipsis: false,
})
}
this.loadedList = currentList
})
.catch(e => {
this.error(e, this)

View file

@ -1,4 +1,8 @@
export const saveListView = (listId, routeName) => {
if(routeName.includes('settings.')) {
return
}
const savedListView = localStorage.getItem('listView')
let savedListViewJson = false
if (savedListView !== null) {

View file

@ -57,6 +57,10 @@ import {
faChessKnight,
faCoffee,
faCocktail,
faEllipsisH,
faArchive,
faShareAlt,
faImage,
} from '@fortawesome/free-solid-svg-icons'
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
@ -144,6 +148,10 @@ library.add(faSun)
library.add(faChessKnight)
library.add(faCoffee)
library.add(faCocktail)
library.add(faEllipsisH)
library.add(faArchive)
library.add(faShareAlt)
library.add(faImage)
Vue.component('icon', FontAwesomeIcon)

View file

@ -29,6 +29,20 @@ import Kanban from '../views/list/views/Kanban'
import List from '../views/list/views/List'
import Gantt from '../views/list/views/Gantt'
import Table from '../views/list/views/Table'
// List Settings
import ListSettingEdit from '@/views/list/settings/edit'
import ListSettingBackground from '@/views/list/settings/background'
import ListSettingDuplicate from '@/views/list/settings/duplicate'
import ListSettingShare from '@/views/list/settings/share'
import ListSettingDelete from '@/views/list/settings/delete'
import ListSettingArchive from '@/views/list/settings/archive'
import FilterSettingEdit from '@/views/filters/settings/edit'
import FilterSettingDelete from '@/views/filters/settings/delete'
// Namespace Settings
import NamespaceSettingEdit from '@/views/namespaces/settings/edit'
import NamespaceSettingShare from '@/views/namespaces/settings/share'
import NamespaceSettingArchive from '@/views/namespaces/settings/archive'
import NamespaceSettingDelete from '@/views/namespaces/settings/delete'
// Saved Filters
import CreateSavedFilter from '@/views/filters/CreateSavedFilter'
@ -57,12 +71,6 @@ const NewListComponent = () => ({
error: ErrorComponent,
timeout: 60000,
})
const EditListComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/list/EditListView'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
})
// Namespace Handling
const NewNamespaceComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/namespaces/NewNamespace'),
@ -70,12 +78,6 @@ const NewNamespaceComponent = () => ({
error: ErrorComponent,
timeout: 60000,
})
const EditNamespaceComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/namespaces/EditNamespace'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
})
const EditTeamComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/teams/EditTeam'),
@ -163,11 +165,6 @@ export default new Router({
popup: NewNamespaceComponent,
},
},
{
path: '/namespaces/:id/edit',
name: 'namespace.edit',
component: EditNamespaceComponent,
},
{
path: '/namespaces/:id/list',
name: 'list.create',
@ -176,9 +173,32 @@ export default new Router({
}
},
{
path: '/lists/:id/edit',
name: 'list.edit',
component: EditListComponent,
path: '/namespaces/:id/settings/edit',
name: 'namespace.settings.edit',
components: {
popup: NamespaceSettingEdit,
},
},
{
path: '/namespaces/:id/settings/share',
name: 'namespace.settings.share',
components: {
popup: NamespaceSettingShare,
},
},
{
path: '/namespaces/:id/settings/archive',
name: 'namespace.settings.archive',
components: {
popup: NamespaceSettingArchive,
},
},
{
path: '/namespaces/:id/settings/delete',
name: 'namespace.settings.delete',
components: {
popup: NamespaceSettingDelete,
},
},
{
path: '/tasks/:id',
@ -190,6 +210,62 @@ export default new Router({
name: 'tasks.range',
component: ShowTasksInRangeComponent,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.settings.edit',
components: {
popup: ListSettingEdit,
},
},
{
path: '/lists/:listId/settings/background',
name: 'list.settings.background',
components: {
popup: ListSettingBackground,
},
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.settings.duplicate',
components: {
popup: ListSettingDuplicate,
},
},
{
path: '/lists/:listId/settings/share',
name: 'list.settings.share',
components: {
popup: ListSettingShare,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'list.settings.delete',
components: {
popup: ListSettingDelete,
},
},
{
path: '/lists/:listId/settings/archive',
name: 'list.settings.archive',
components: {
popup: ListSettingArchive,
},
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.settings.edit',
components: {
popup: FilterSettingEdit,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.settings.delete',
components: {
popup: FilterSettingDelete,
},
},
{
path: '/lists/:listId',
name: 'list.index',
@ -205,6 +281,46 @@ export default new Router({
name: 'task.list.detail',
component: TaskDetailViewModal,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.list.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.list.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.list.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.list.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.list.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.list.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.list.settings.edit',
component: FilterSettingEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.list.settings.delete',
component: FilterSettingDelete,
},
],
},
{
@ -217,12 +333,94 @@ export default new Router({
name: 'task.gantt.detail',
component: TaskDetailViewModal,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.gantt.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.gantt.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.gantt.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.gantt.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.gantt.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.gantt.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.gantt.settings.edit',
component: FilterSettingEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.gantt.settings.delete',
component: FilterSettingDelete,
},
],
},
{
path: '/lists/:listId/table',
name: 'list.table',
component: Table,
children: [
{
path: '/lists/:listId/settings/edit',
name: 'list.table.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.table.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.table.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.table.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.table.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.table.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.table.settings.edit',
component: FilterSettingEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.table.settings.delete',
component: FilterSettingDelete,
},
],
},
{
path: '/lists/:listId/kanban',
@ -234,6 +432,46 @@ export default new Router({
name: 'task.kanban.detail',
component: TaskDetailViewModal,
},
{
path: '/lists/:listId/settings/edit',
name: 'list.kanban.settings.edit',
component: ListSettingEdit,
},
{
path: '/lists/:listId/settings/background',
name: 'list.kanban.settings.background',
component: ListSettingBackground,
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.kanban.settings.duplicate',
component: ListSettingDuplicate,
},
{
path: '/lists/:listId/settings/share',
name: 'list.kanban.settings.share',
component: ListSettingShare,
},
{
path: '/lists/:listId/settings/delete',
name: 'list.kanban.settings.delete',
component: ListSettingDelete,
},
{
path: '/lists/:listId/settings/archive',
name: 'list.kanban.settings.archive',
component: ListSettingArchive,
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.kanban.settings.edit',
component: FilterSettingEdit,
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.kanban.settings.delete',
component: FilterSettingDelete,
},
],
},
],

View file

@ -106,7 +106,7 @@ export const store = new Vuex.Store({
// Server updates don't return the right. Therefore the right is reset after updating the list which is
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right
// when updating the list in global state.
if(typeof state.currentList.maxRight !== 'undefined') {
if (typeof state.currentList.maxRight !== 'undefined') {
currentList.maxRight = state.currentList.maxRight
}
state.currentList = currentList

View file

@ -14,6 +14,11 @@
border-bottom: 1px solid $grey-200;
border-radius: $radius $radius 0 0;
}
.modal-card-foot {
background: $grey-50;
border-top: 0;
}
}
.box, .card {

View file

@ -250,10 +250,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
}
}
.dropdown-trigger {
cursor: pointer;
}
.title.input {
height: auto;
padding: .4rem .5rem;
@ -261,6 +257,11 @@ $filter-container-height: '1rem - #{$switch-view-height}';
}
}
.dropdown-trigger {
cursor: pointer;
padding: .5rem;
}
.bucket-footer {
padding: .5rem;

View file

@ -77,7 +77,7 @@
}
.is-load-more-button {
margin: 0 auto;
margin: 1rem auto 0 !important;
display: block;
width: 200px;
}

View file

@ -8,24 +8,6 @@
.content {
padding: 0;
}
.table {
border-top: 1px solid $grey-100;
border-radius: 4px;
overflow: hidden;
td {
vertical-align: middle;
}
td.type, td.actions {
width: 250px;
}
td.actions {
text-align: right;
}
}
}
.task-add {
@ -51,11 +33,12 @@
margin: 0;
}
.icon {
.dropdown-trigger {
color: $grey-400;
margin-left: 1rem;
height: 1rem;
width: 1rem;
cursor: pointer;
}
}
@ -63,6 +46,10 @@
padding-bottom: 1rem;
}
$filter-container-top-default: -59px;
$filter-container-top-link-share-gantt: -138px;
$filter-container-top-link-share-list: -47px;
.filter-container {
text-align: right;
width: 100%;
@ -70,7 +57,7 @@
max-width: 180px;
position: absolute;
right: 1.5rem;
margin-top: -59px;
margin-top: $filter-container-top-default;
z-index: 4;
.items {
@ -146,20 +133,42 @@
}
}
.list-namespace-title {
color: $grey-500;
}
.link-share-container .gantt-chart-container .filter-container,
.gantt-chart-container .filter-container {
margin-top: calc(-138px - 2rem);
right: 0;
margin-top: calc(#{$filter-container-top-link-share-gantt} - 2rem);
}
.link-share-container .list-view .filter-container {
margin-top: -47px;
margin-top: $filter-container-top-link-share-list;
}
.link-share-container .filter-container {
right: 9rem;
margin-top: -59px;
margin-top: $filter-container-top-default;
}
.list-namespace-title {
color: $grey-500;
.is-archived {
$notification-height: 1.25rem + 1.25rem + 1.5rem + 1.5rem;
.filter-container {
margin-top: calc(#{$filter-container-top-default} - #{$notification-height});
}
.link-share-container .gantt-chart-container .filter-container,
.gantt-chart-container .filter-container {
margin-top: calc(#{$filter-container-top-link-share-gantt} - 2rem - #{$notification-height});
}
.link-share-container .list-view .filter-container {
margin-top: calc(#{$filter-container-top-link-share-list} - #{$notification-height});
}
.link-share-container .filter-container {
margin-top: calc(#{$filter-container-top-default} - #{$notification-height});
}
}

View file

@ -39,6 +39,12 @@
transform: translate(-50%, -50%);
text-align: center;
@media screen and (max-width: $tablet) {
margin: 0;
top: 25%;
transform: translate(-50%, -25%);
}
.header {
font-size: 2rem;
font-weight: 700;
@ -56,6 +62,11 @@
color: $white;
font-size: 2rem;
}
.is-wide {
max-width: $desktop;
width: calc(100% - 2rem);
}
}
}

View file

@ -64,3 +64,17 @@
.underline-none {
text-decoration: none !important;
}
.table.has-actions {
border-top: 1px solid $grey-100;
border-radius: 4px;
overflow: hidden;
td {
vertical-align: middle;
}
td.actions {
text-align: right;
}
}

View file

@ -36,6 +36,11 @@
.field.has-addons .button {
height: 2.5rem;
margin-left: 0 !important;
}
.field.has-addons .select select {
margin-right: 0;
}
.input,

View file

@ -179,13 +179,14 @@
}
}
.actions {
flex: 0 0 auto;
a:not(.dropdown-item) {
color: $vikunja-nav-color;
padding: 0 .25rem;
}
a {
color: $vikunja-nav-color;
padding: 0 .25rem;
}
.dropdown-trigger {
padding: .5rem;
cursor: pointer;
}
}
@ -223,15 +224,36 @@
&.can-be-hidden {
transition: all $transition;
height: 0;
overflow: hidden;
//overflow: hidden;
opacity: 0;
}
li {
height: 44px;
display: flex;
align-items: center;
.dropdown-trigger {
opacity: 0;
padding: .5rem;
cursor: pointer;
transition: $transition;
}
&:hover {
background: $white;
.dropdown-trigger {
opacity: 1;
}
}
}
span.list-menu-link, a {
a:hover {
background: transparent;
}
span.list-menu-link, li > a {
padding: 0.75rem .5rem 0.75rem $navbar-padding * 1.5;
transition: all 0.2s ease;
@ -261,10 +283,8 @@
}
&:hover {
background: $white;
border-left: $vikunja-nav-selected-width solid $primary;
}
}
}
@ -302,7 +322,7 @@
font-family: $vikunja-font;
}
span.list-menu-link, a {
span.list-menu-link, li > a {
padding-left: 2rem;
display: inline-block;
}
@ -342,12 +362,6 @@
box-shadow: none !important;
}
}
.dropdown-menu {
.dropdown-content {
box-shadow: $shadow-md;
}
}
}
.menu-hide-button, .menu-show-button {

View file

@ -72,11 +72,26 @@ button.table {
margin-bottom: 0 !important;
}
.dropdown-item.is-disabled {
cursor: not-allowed;
.dropdown-item {
display: flex;
align-items: center;
justify-content: left !important;
&:hover {
background-color: transparent;
.icon {
padding-right: .5rem;
color: $grey-300 !important;
}
&.has-text-danger .icon {
color: $danger !important;
}
&.is-disabled {
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
@ -120,3 +135,9 @@ button.table {
border-radius: 100%;
margin-right: 4px;
}
.dropdown-menu {
.dropdown-content {
box-shadow: $shadow-md;
}
}

View file

@ -1,3 +1,4 @@
$grey-50: #F9FAFB;
$grey-100: #f3f4f6;
$grey-200: #E5E7EB;
$grey-300: #D1D5DB;

View file

@ -1,159 +0,0 @@
<template>
<div :class="{ 'is-loading': filterService.loading}" class="loader-container edit-list is-max-width-desktop">
<card title="Edit Saved Filter">
<form @submit.prevent="save()">
<div class="field">
<label class="label" for="listtext">Filter Name</label>
<div class="control">
<input
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
@keyup.enter="save"
class="input"
id="listtext"
placeholder="The list title goes here..."
type="text"
v-focus
v-model="filter.title"/>
</div>
</div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="filter.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="filters">Filters</label>
<div class="control">
<filters
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
class="has-no-shadow has-no-border"
v-model="filters"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="save()"
:loading="filterService.loading"
class="is-fullwidth">
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:loading="filterService.loading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div>
</card>
<modal
@close="showDeleteModal = false"
@submit="() => deleteSavedFilter()"
v-if="showDeleteModal">
<span slot="header">Delete this saved filter</span>
<p slot="text">
Are you sure you want to delete this saved filter?
</p>
</modal>
</div>
</template>
<script>
import ErrorComponent from '../../components/misc/error'
import LoadingComponent from '../../components/misc/loading'
import SavedFilterModel from '@/models/savedFilter'
import SavedFilterService from '@/services/savedFilter'
import ListModel from '@/models/list'
import Filters from '@/components/list/partials/filters'
import {objectToSnakeCase} from '@/helpers/case'
export default {
name: 'EditFilter',
data() {
return {
filter: SavedFilterModel,
filterService: SavedFilterService,
filters: {
sort_by: ['done', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
filter_include_nulls: true,
},
showDeleteModal: false,
}
},
components: {
Filters,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
created() {
this.filterService = new SavedFilterService()
this.loadSavedFilter()
},
watch: {
// call again the method if the route changes
'$route': 'loadSavedFilter',
},
methods: {
loadSavedFilter() {
// We assume the listId in the route is the pseudolist
const list = new ListModel({id: this.$route.params.id})
this.filter = new SavedFilterModel({id: list.getSavedFilterId()})
this.filterService.get(this.filter)
.then(r => {
this.filter = r
this.filters = objectToSnakeCase(this.filter.filters)
})
.catch(e => this.error(e, this))
},
save() {
this.filter.filters = this.filters
this.filterService.update(this.filter)
.then(r => {
this.$store.dispatch('namespaces/loadNamespaces')
this.success({message: 'The filter was saved successfully.'}, this)
this.filter = r
this.filters = objectToSnakeCase(this.filter.filters)
})
.catch(e => this.error(e, this))
},
deleteSavedFilter() {
this.filterService.delete(this.filter)
.then(() => {
this.$store.dispatch('namespaces/loadNamespaces')
this.success({message: 'The filter was deleted successfully.'}, this)
this.$router.push({name: 'namespaces.index'})
})
.catch(e => this.error(e, this))
},
},
}
</script>

View file

@ -0,0 +1,44 @@
<template>
<modal
@close="$router.back()"
@submit="deleteSavedFilter()"
>
<span slot="header">Delete this saved filter</span>
<p slot="text">
Are you sure you want to delete this saved filter?
</p>
</modal>
</template>
<script>
import SavedFilterModel from '@/models/savedFilter'
import SavedFilterService from '@/services/savedFilter'
import ListModel from '@/models/list'
export default {
name: 'filter-settings-delete',
data() {
return {
filterService: SavedFilterService,
}
},
created() {
this.filterService = new SavedFilterService()
},
methods: {
deleteSavedFilter() {
// We assume the listId in the route is the pseudolist
const list = new ListModel({id: this.$route.params.listId})
const filter = new SavedFilterModel({id: list.getSavedFilterId()})
this.filterService.delete(filter)
.then(() => {
this.$store.dispatch('namespaces/loadNamespaces')
this.success({message: 'The filter was deleted successfully.'}, this)
this.$router.push({name: 'namespaces.index'})
})
.catch(e => this.error(e, this))
},
},
}
</script>

View file

@ -0,0 +1,128 @@
<template>
<create-edit
title="Edit This Saved Filter"
primary-icon=""
primary-label="Save"
@primary="save"
tertary="Delete"
@tertary="$router.push({ name: 'filter.list.settings.delete', params: { id: $route.params.listId } })"
>
<form @submit.prevent="save()">
<div class="field">
<label class="label" for="title">Filter Title</label>
<div class="control">
<input
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
@keyup.enter="save"
class="input"
id="title"
placeholder="The title goes here..."
type="text"
v-focus
v-model="filter.title"/>
</div>
</div>
<div class="field">
<label class="label" for="description">Description</label>
<div class="control">
<editor
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
:preview-is-default="false"
id="description"
placeholder="The description goes here..."
v-model="filter.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="filters">Filters</label>
<div class="control">
<filters
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
class="has-no-shadow has-no-border"
v-model="filters"
/>
</div>
</div>
</form>
</create-edit>
</template>
<script>
import ErrorComponent from '@/components/misc/error'
import LoadingComponent from '@/components/misc/loading'
import CreateEdit from '@/components/misc/create-edit'
import SavedFilterModel from '@/models/savedFilter'
import SavedFilterService from '@/services/savedFilter'
import ListModel from '@/models/list'
import Filters from '@/components/list/partials/filters'
import {objectToSnakeCase} from '@/helpers/case'
export default {
name: 'filter-settings-edit',
data() {
return {
filter: SavedFilterModel,
filterService: SavedFilterService,
filters: {
sort_by: ['done', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
filter_include_nulls: true,
},
showDeleteModal: false,
}
},
components: {
CreateEdit,
Filters,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '@/components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
created() {
this.filterService = new SavedFilterService()
this.loadSavedFilter()
},
watch: {
// call again the method if the route changes
'$route': 'loadSavedFilter',
},
methods: {
loadSavedFilter() {
// We assume the listId in the route is the pseudolist
const list = new ListModel({id: this.$route.params.listId})
this.filter = new SavedFilterModel({id: list.getSavedFilterId()})
this.filterService.get(this.filter)
.then(r => {
this.filter = r
this.filters = objectToSnakeCase(this.filter.filters)
})
.catch(e => this.error(e, this))
},
save() {
this.filter.filters = this.filters
this.filterService.update(this.filter)
.then(r => {
this.$store.dispatch('namespaces/loadNamespaces')
this.success({message: 'The filter was saved successfully.'}, this)
this.filter = r
this.filters = objectToSnakeCase(this.filter.filters)
})
.catch(e => this.error(e, this))
},
},
}
</script>

View file

@ -1,5 +1,5 @@
<template>
<create
<create-edit
title="Create a new label"
@create="newLabel()"
:create-disabled="label.title === ''"
@ -31,7 +31,7 @@
<color-picker v-model="label.hexColor" />
</div>
</div>
</create>
</create-edit>
</template>
<script>
@ -39,7 +39,7 @@ import labelModel from '../../models/label'
import labelService from '../../services/label'
import LabelModel from '../../models/label'
import LabelService from '../../services/label'
import Create from '@/components/misc/create'
import CreateEdit from '@/components/misc/create-edit'
import ColorPicker from '../../components/input/colorPicker'
export default {
@ -52,7 +52,7 @@ export default {
}
},
components: {
Create,
CreateEdit,
ColorPicker,
},
created() {

View file

@ -1,266 +0,0 @@
<template>
<div :class="{ 'is-loading': listService.loading}" class="loader-container edit-list is-max-width-desktop">
<div class="notification is-warning" v-if="list.isArchived">
This list is archived.
It is not possible to create new or edit tasks or it.
</div>
<card title="Edit List">
<form @submit.prevent="submit()">
<div class="field">
<label class="label" for="listtext">List Name</label>
<div class="control">
<input
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
@keyup.enter="submit"
class="input"
id="listtext"
placeholder="The list title goes here..."
type="text"
v-focus
v-model="list.title"/>
</div>
</div>
<div class="field">
<label
class="label"
for="listtext"
v-tooltip="'The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.'">
List Identifier
</label>
<div class="control">
<input
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
@keyup.enter="submit"
class="input"
id="listtext"
placeholder="The list identifier goes here..."
type="text"
v-focus
v-model="list.identifier"/>
</div>
</div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="list.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="list.isArchived"
v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
This list is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="list.hexColor"/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="submit()"
:loading="listService.loading"
class="is-fullwidth">
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:locading="listService.loading"
icon="trash-alt"
class="is-danger"
/>
</div>
</div>
</card>
<!-- Duplicate list -->
<card class="has-overflow" title="Duplicate this list">
<p>Select a namespace which should hold the duplicated list:</p>
<div class="field has-addons">
<div class="control is-expanded">
<namespace-search @selected="selectNamespace"/>
</div>
<div class="control">
<x-button
:loading="listDuplicateService.loading"
@click="duplicateList"
>
Duplicate
</x-button>
</div>
</div>
</card>
<background :list-id="$route.params.id"/>
<component
:id="list.id"
:is="manageUsersComponent"
:userIsAdmin="userIsAdmin"
shareType="user"
type="list"/>
<component
:id="list.id"
:is="manageTeamsComponent"
:userIsAdmin="userIsAdmin"
shareType="team"
type="list"/>
<link-sharing :list-id="$route.params.id" v-if="linkSharingEnabled"/>
<transition name="modal">
<modal
@close="showDeleteModal = false"
@submit="deleteList()"
v-if="showDeleteModal">
<span slot="header">Delete the list</span>
<p slot="text">Are you sure you want to delete this list and all of its contents?
<br/>This includes all tasks and <b>CANNOT BE UNDONE!</b></p>
</modal>
</transition>
</div>
</template>
<script>
import router from '../../router'
import manageSharing from '../../components/sharing/userTeam'
import LinkSharing from '../../components/sharing/linkSharing'
import ListModel from '../../models/list'
import ListService from '../../services/list'
import Fancycheckbox from '../../components/input/fancycheckbox'
import Background from '../../components/list/partials/background-settings'
import {CURRENT_LIST} from '@/store/mutation-types'
import ColorPicker from '../../components/input/colorPicker'
import NamespaceSearch from '../../components/namespace/namespace-search'
import ListDuplicateService from '../../services/listDuplicateService'
import ListDuplicateModel from '../../models/listDuplicateModel'
import LoadingComponent from '../../components/misc/loading'
import ErrorComponent from '../../components/misc/error'
export default {
name: 'EditList',
data() {
return {
list: ListModel,
listService: ListService,
showDeleteModal: false,
manageUsersComponent: '',
manageTeamsComponent: '',
listDuplicateService: ListDuplicateService,
selectedNamespace: null,
}
},
components: {
NamespaceSearch,
ColorPicker,
Background,
Fancycheckbox,
LinkSharing,
manageSharing,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
created() {
this.listService = new ListService()
this.listDuplicateService = new ListDuplicateService()
this.loadList()
},
watch: {
// call again the method if the route changes
'$route': 'loadList',
},
computed: {
linkSharingEnabled() {
return this.$store.state.config.linkSharingEnabled
},
userIsAdmin() {
return this.list.owner && this.list.owner.id === this.$store.state.auth.info.id
},
},
methods: {
loadList() {
let list = new ListModel({id: this.$route.params.id})
this.listService.get(list)
.then(r => {
this.$set(this, 'list', r)
this.$store.commit(CURRENT_LIST, r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing'
this.setTitle(`Edit ${this.list.title}`)
})
.catch(e => {
this.error(e, this)
})
},
submit() {
this.listService.update(this.list)
.then(r => {
this.$store.commit('namespaces/setListInNamespaceById', r)
this.success({message: 'The list was successfully updated.'}, this)
})
.catch(e => {
this.error(e, this)
})
},
deleteList() {
this.listService.delete(this.list)
.then(() => {
this.$store.commit('namespaces/removeListFromNamespaceById', this.list)
this.success({message: 'The list was successfully deleted.'}, this)
router.push({name: 'home'})
})
.catch(e => {
this.error(e, this)
})
},
selectNamespace(namespace) {
this.selectedNamespace = namespace
},
duplicateList() {
const listDuplicate = new ListDuplicateModel({
listId: this.list.id,
namespaceId: this.selectedNamespace.id,
})
this.listDuplicateService.create(listDuplicate)
.then(r => {
this.$store.commit('namespaces/addListToNamespace', r.list)
this.$store.commit('lists/addList', r.list)
this.success({message: 'The list was successfully duplicated.'}, this)
router.push({name: 'list.index', params: {listId: r.list.id}})
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -1,25 +0,0 @@
<template>
<div>
<edit-filter v-if="isSavedFilter"/>
<edit-list v-else/>
</div>
</template>
<script>
import EditList from '@/views/list/EditList'
import EditFilter from '@/views/filters/EditSavedFilter'
import {mapState} from 'vuex'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
export default {
name: 'EditListView',
components: {
EditFilter,
EditList,
},
computed: mapState({
isSavedFilter: state => getSavedFilterIdFromListId(state.currentList.id) > 0
})
}
</script>

View file

@ -1,5 +1,5 @@
<template>
<create title="Create a new list" @create="newList()" :create-disabled="list.title === ''">
<create-edit title="Create a new list" @create="newList()" :create-disabled="list.title === ''">
<div class="field">
<label class="label" for="listTitle">List Title</label>
<div
@ -28,13 +28,13 @@
<color-picker v-model="list.hexColor" />
</div>
</div>
</create>
</create-edit>
</template>
<script>
import ListService from '../../services/list'
import ListModel from '../../models/list'
import Create from '@/components/misc/create'
import CreateEdit from '@/components/misc/create-edit'
import ColorPicker from '../../components/input/colorPicker'
export default {
@ -47,7 +47,7 @@ export default {
}
},
components: {
Create,
CreateEdit,
ColorPicker,
},
created() {

View file

@ -1,36 +1,38 @@
<template>
<div
:class="{ 'is-loading': listService.loading}"
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
class="loader-container"
>
<div class="switch-view-container">
<div class="switch-view">
<router-link
:class="{'is-active': $route.name === 'list.list'}"
:class="{'is-active': $route.name.includes('list.list')}"
:to="{ name: 'list.list', params: { listId: listId } }">
List
</router-link>
<router-link
:class="{'is-active': $route.name === 'list.gantt'}"
:class="{'is-active': $route.name.includes('list.gantt')}"
:to="{ name: 'list.gantt', params: { listId: listId } }">
Gantt
</router-link>
<router-link
:class="{'is-active': $route.name === 'list.table'}"
:class="{'is-active': $route.name.includes('list.table')}"
:to="{ name: 'list.table', params: { listId: listId } }">
Table
</router-link>
<router-link
:class="{'is-active': $route.name === 'list.kanban'}"
:class="{'is-active': $route.name.includes('list.kanban')}"
:to="{ name: 'list.kanban', params: { listId: listId } }">
Kanban
</router-link>
</div>
</div>
<div class="notification is-warning" v-if="list.isArchived">
This list is archived.
It is not possible to create new or edit tasks or it.
</div>
<transition name="fade">
<div class="notification is-warning" v-if="currentList.isArchived">
This list is archived.
It is not possible to create new or edit tasks or it.
</div>
</transition>
<router-view/>
</div>
@ -75,6 +77,7 @@ export default {
return typeof this.$store.state.currentList === 'undefined' ? {
id: 0,
title: '',
isArchived: false,
} : this.$store.state.currentList
},
},
@ -86,6 +89,10 @@ export default {
return
},
loadList() {
if(this.$route.name.includes('.settings.')) {
return
}
this.setTitle(this.currentList.title)
// This invalidates the loaded list at the kanban board which lets it reload its content when

View file

@ -0,0 +1,52 @@
<template>
<modal
@close="$router.back()"
@submit="archiveList()"
>
<span slot="header">{{ list.isArchived ? 'Un-' : '' }}Archive this list</span>
<p slot="text" v-if="list.isArchived">
You will be able to create new tasks or edit it.
</p>
<p slot="text" v-else>
You won't be able to edit this list or create new tasks until you un-archive it.
</p>
</modal>
</template>
<script>
import ListService from '@/services/list'
export default {
name: 'list-setting-archive',
data() {
return {
listService: ListService,
list: null,
}
},
created() {
this.listService = new ListService()
this.list = this.$store.getters['lists/getListById'](this.$route.params.listId)
this.setTitle(`Archive "${this.list.title}"`)
},
methods: {
archiveList() {
this.list.isArchived = !this.list.isArchived
this.listService.update(this.list)
.then(r => {
this.$store.commit('currentList', r)
this.$store.commit('namespaces/setListInNamespaceById', r)
this.success({message: 'The list was successfully archived.'}, this)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.$router.back()
})
},
},
}
</script>

View file

@ -1,9 +1,11 @@
<template>
<card
:class="{ 'is-loading': backgroundService.loading}"
class="list-background-setting loader-container"
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
<create-edit
title="Set list background"
primary-label=""
:loading="backgroundService.loading"
class="list-background-setting"
:wide="true"
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
>
<div class="mb-4" v-if="uploadBackgroundEnabled">
<input
@ -51,24 +53,21 @@
type="secondary"
v-if="backgroundSearchResult.length > 0"
>
<template v-if="backgroundService.loading">
Loading...
</template>
<template v-else>
Load more photos
</template>
{{ backgroundService.loading ? 'Loading...' : 'Load more photos'}}
</x-button>
</template>
</card>
</create-edit>
</template>
<script>
import BackgroundUnsplashService from '../../../services/backgroundUnsplash'
import BackgroundUploadService from '../../../services/backgroundUpload'
import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit'
export default {
name: 'background-settings',
name: 'list-setting-background',
components: {CreateEdit},
data() {
return {
backgroundSearchTerm: '',
@ -81,12 +80,6 @@ export default {
backgroundUploadService: null,
}
},
props: {
listId: {
default: 0,
required: true,
},
},
computed: {
unsplashBackgroundEnabled() {
return this.$store.state.config.enabledBackgroundProviders.includes('unsplash')
@ -98,6 +91,7 @@ export default {
created() {
this.backgroundService = new BackgroundUnsplashService()
this.backgroundUploadService = new BackgroundUploadService()
this.setTitle('Set a list background')
// Show the default collection of backgrounds
this.newBackgroundSearch()
},
@ -142,7 +136,7 @@ export default {
return
}
this.backgroundService.update({id: backgroundId, listId: this.listId})
this.backgroundService.update({id: backgroundId, listId: this.$route.params.listId})
.then(l => {
this.$store.commit(CURRENT_LIST, l)
this.$store.commit('namespaces/setListInNamespaceById', l)
@ -157,7 +151,7 @@ export default {
return
}
this.backgroundUploadService.create(this.listId, this.$refs.backgroundUploadInput.files[0])
this.backgroundUploadService.create(this.$route.params.listId, this.$refs.backgroundUploadInput.files[0])
.then(l => {
this.$store.commit(CURRENT_LIST, l)
this.$store.commit('namespaces/setListInNamespaceById', l)

View file

@ -0,0 +1,43 @@
<template>
<modal
@close="$router.back()"
@submit="deleteList()"
>
<span slot="header">Delete this list</span>
<p slot="text">Are you sure you want to delete this list and all of its contents?
<br/>This includes all tasks and <b>CANNOT BE UNDONE!</b></p>
</modal>
</template>
<script>
import ListService from '@/services/list'
export default {
name: 'list-setting-delete',
data() {
return {
listService: ListService,
}
},
created() {
this.listService = new ListService()
const list = this.$store.getters['lists/getListById'](this.$route.params.listId)
this.setTitle(`Delete "${list.title}"`)
},
methods: {
deleteList() {
const list = this.$store.getters['lists/getListById'](this.$route.params.listId)
this.listService.delete(list)
.then(() => {
this.$store.commit('namespaces/removeListFromNamespaceById', list)
this.success({message: 'The list was successfully deleted.'}, this)
this.$router.push({name: 'home'})
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -0,0 +1,58 @@
<template>
<create-edit
title="Duplicate this list"
primary-icon="paste"
primary-label="Duplicate"
@primary="duplicateList"
:loading="listDuplicateService.loading"
>
<p>Select a namespace which should hold the duplicated list:</p>
<namespace-search @selected="selectNamespace"/>
</create-edit>
</template>
<script>
import ListDuplicateService from '@/services/listDuplicateService'
import NamespaceSearch from '@/components/namespace/namespace-search'
import ListDuplicateModel from '@/models/listDuplicateModel'
import CreateEdit from '@/components/misc/create-edit'
export default {
name: 'list-setting-duplicate',
data() {
return {
listDuplicateService: ListDuplicateService,
selectedNamespace: null,
}
},
components: {
CreateEdit,
NamespaceSearch,
},
created() {
this.listDuplicateService = new ListDuplicateService()
this.setTitle('Duplicate List')
},
methods: {
selectNamespace(namespace) {
this.selectedNamespace = namespace
},
duplicateList() {
const listDuplicate = new ListDuplicateModel({
listId: this.$route.params.listId,
namespaceId: this.selectedNamespace.id,
})
this.listDuplicateService.create(listDuplicate)
.then(r => {
this.$store.commit('namespaces/addListToNamespace', r.list)
this.$store.commit('lists/addList', r.list)
this.success({message: 'The list was successfully duplicated.'}, this)
this.$router.push({name: 'list.index', params: {listId: r.list.id}})
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -0,0 +1,127 @@
<template>
<create-edit
title="Edit This List"
primary-icon=""
primary-label="Save"
@primary="save"
tertary="Delete"
@tertary="$router.push({ name: 'list.list.settings.delete', params: { id: $route.params.listId } })"
>
<div class="field">
<label class="label" for="listtext">List Name</label>
<div class="control">
<input
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
@keyup.enter="save"
class="input"
id="listtext"
placeholder="The list title goes here..."
type="text"
v-focus
v-model="list.title"/>
</div>
</div>
<div class="field">
<label
class="label"
for="listtext"
v-tooltip="'The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.'">
List Identifier
</label>
<div class="control">
<input
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
@keyup.enter="save"
class="input"
id="listtext"
placeholder="The list identifier goes here..."
type="text"
v-focus
v-model="list.identifier"/>
</div>
</div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="list.description"
/>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="list.hexColor"/>
</div>
</div>
</create-edit>
</template>
<script>
import ListModel from '@/models/list'
import ListService from '@/services/list'
import ColorPicker from '@/components/input/colorPicker'
import LoadingComponent from '@/components/misc/loading'
import ErrorComponent from '@/components/misc/error'
import ListDuplicateService from '@/services/listDuplicateService'
import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit'
export default {
name: 'list-setting-edit',
data() {
return {
list: ListModel,
listService: ListService,
}
},
components: {
CreateEdit,
ColorPicker,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '@/components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
created() {
this.listService = new ListService()
this.listDuplicateService = new ListDuplicateService()
this.loadList()
},
methods: {
loadList() {
const list = new ListModel({id: this.$route.params.listId})
this.listService.get(list)
.then(r => {
this.$set(this, 'list', r)
this.$store.commit(CURRENT_LIST, r)
this.setTitle(`Edit "${this.list.title}"`)
})
.catch(e => {
this.error(e, this)
})
},
save() {
this.listService.update(this.list)
.then(r => {
this.$store.commit('namespaces/setListInNamespaceById', r)
this.success({message: 'The list was successfully updated.'}, this)
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -0,0 +1,78 @@
<template>
<create-edit
title="Share this list"
primary-label=""
>
<component
:id="list.id"
:is="manageUsersComponent"
:userIsAdmin="userIsAdmin"
shareType="user"
type="list"/>
<component
:id="list.id"
:is="manageTeamsComponent"
:userIsAdmin="userIsAdmin"
shareType="team"
type="list"/>
<link-sharing :list-id="$route.params.listId" v-if="linkSharingEnabled" class="mt-4"/>
</create-edit>
</template>
<script>
import ListService from '@/services/list'
import ListModel from '@/models/list'
import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit'
import LinkSharing from '@/components/sharing/linkSharing'
import userTeam from '@/components/sharing/userTeam'
export default {
name: 'list-setting-share',
data() {
return {
list: ListModel,
listService: ListService,
manageUsersComponent: '',
manageTeamsComponent: '',
}
},
components: {
CreateEdit,
LinkSharing,
userTeam,
},
computed: {
linkSharingEnabled() {
return this.$store.state.config.linkSharingEnabled
},
userIsAdmin() {
return this.list.owner && this.list.owner.id === this.$store.state.auth.info.id
},
},
created() {
this.listService = new ListService()
this.loadList()
},
methods: {
loadList() {
const list = new ListModel({id: this.$route.params.listId})
this.listService.get(list)
.then(r => {
this.$set(this, 'list', r)
this.$store.commit(CURRENT_LIST, r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'userTeam'
this.manageUsersComponent = 'userTeam'
this.setTitle(`Share "${this.list.title}"`)
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -32,57 +32,47 @@
v-if="bucket.limit > 0">
{{ bucket.tasks.length }}/{{ bucket.limit }}
</span>
<div
class="dropdown is-right is-active options"
<dropdown
class="is-right options"
v-if="canWrite"
trigger-icon="ellipsis-v"
>
<div @click.stop="toggleBucketDropdown(bucket.id)" class="dropdown-trigger">
<span class="icon">
<icon icon="ellipsis-v"/>
</span>
</div>
<transition name="fade">
<div class="dropdown-menu" role="menu" v-if="bucketOptionsDropDownActive[bucket.id]">
<div class="dropdown-content">
<a
@click.stop="showSetLimitInput = true"
class="dropdown-item"
>
<div class="field has-addons" v-if="showSetLimitInput">
<div class="control">
<input
@change="() => updateBucket(bucket)"
@keyup.enter="() => updateBucket(bucket)"
class="input"
type="number"
v-focus.always
v-model="bucket.limit"
/>
</div>
<div class="control">
<x-button
:icon="['far', 'save']"
:shadow="false"
/>
</div>
</div>
<template v-else>
Limit: {{ bucket.limit > 0 ? bucket.limit : 'Not set' }}
</template>
</a>
<a
:class="{'is-disabled': buckets.length <= 1}"
@click="() => deleteBucketModal(bucket.id)"
class="dropdown-item has-text-danger"
v-tooltip="buckets.length <= 1 ? 'You cannot remove the last bucket.' : ''"
>
<span class="icon is-small"><icon icon="trash-alt"/></span>
Delete
</a>
<a
@click.stop="showSetLimitInput = true"
class="dropdown-item"
>
<div class="field has-addons" v-if="showSetLimitInput">
<div class="control">
<input
@change="() => updateBucket(bucket)"
@keyup.enter="() => updateBucket(bucket)"
class="input"
type="number"
v-focus.always
v-model="bucket.limit"
/>
</div>
<div class="control">
<x-button
:icon="['far', 'save']"
:shadow="false"
/>
</div>
</div>
</transition>
</div>
<template v-else>
Limit: {{ bucket.limit > 0 ? bucket.limit : 'Not set' }}
</template>
</a>
<a
:class="{'is-disabled': buckets.length <= 1}"
@click="() => deleteBucketModal(bucket.id)"
class="dropdown-item has-text-danger"
v-tooltip="buckets.length <= 1 ? 'You cannot remove the last bucket.' : ''"
>
<span class="icon is-small"><icon icon="trash-alt"/></span>
Delete
</a>
</dropdown>
</div>
<div :ref="`tasks-container${bucket.id}`" class="tasks">
<!-- Make the component either a div or a draggable component based on the user rights -->
@ -272,12 +262,14 @@ import {applyDrag} from '@/helpers/applyDrag'
import {mapState} from 'vuex'
import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/rights.json'
import {LOADING, LOADING_MODULE} from '../../../store/mutation-types'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import FilterPopup from '@/components/list/partials/filter-popup'
import Dropdown from '@/components/misc/dropdown'
export default {
name: 'Kanban',
components: {
Dropdown,
FilterPopup,
Container,
Draggable,
@ -295,7 +287,6 @@ export default {
showOnTop: true,
},
sourceBucket: 0,
bucketOptionsDropDownActive: {},
showBucketDeleteModal: false,
bucketToDelete: 0,
@ -324,7 +315,6 @@ export default {
created() {
this.taskService = new TaskService()
this.loadBuckets()
this.$nextTick(() => document.addEventListener('click', this.closeBucketDropdowns))
// Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads.
@ -449,17 +439,6 @@ export default {
toggleShowNewTaskInput(bucket) {
this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[bucket])
},
toggleBucketDropdown(bucketId) {
const oldState = this.bucketOptionsDropDownActive[bucketId]
this.closeBucketDropdowns() // Close all eventually open dropdowns
this.$set(this.bucketOptionsDropDownActive, bucketId, !oldState)
},
closeBucketDropdowns() {
this.showSetLimitInput = false
for (const bucketId in this.bucketOptionsDropDownActive) {
this.bucketOptionsDropDownActive[bucketId] = false
}
},
addTaskToBucket(bucketId) {
if (this.newTaskText === '') {

View file

@ -84,12 +84,10 @@
</p>
</div>
<p
class="has-text-centered has-text-grey is-italic p-4 mb-4"
v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
This list is currently empty.
<a @click="$refs.newTaskInput.focus()">Create a new task.</a>
</p>
</nothing>
<div class="tasks-container">
<div :class="{'short': isTaskEdit}" class="tasks mt-0" v-if="tasks && tasks.length > 0">
@ -174,6 +172,7 @@ import Rights from '../../../models/rights.json'
import {mapState} from 'vuex'
import FilterPopup from '@/components/list/partials/filter-popup'
import {HAS_TASKS} from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing'
export default {
name: 'List',
@ -195,6 +194,7 @@ export default {
taskList,
],
components: {
Nothing,
FilterPopup,
SingleTaskInList,
EditTask,

View file

@ -42,7 +42,7 @@
</div>
<card :padding="false" :has-content="false">
<table class="table is-hoverable is-fullwidth mb-0">
<table class="table has-actions is-hoverable is-fullwidth mb-0">
<thead>
<tr>
<th v-if="activeColumns.id">

View file

@ -1,201 +0,0 @@
<template>
<div class="loader-container is-max-width-desktop" v-bind:class="{ 'is-loading': namespaceService.loading}">
<div class="notification is-warning" v-if="namespace.isArchived">
This namespace is archived.
It is not possible to create new lists or edit it.
</div>
<card title="Edit Namespace">
<form @submit.prevent="submit()">
<div class="field">
<label class="label" for="namespacetext">Namespace Name</label>
<div class="control">
<input
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
class="input"
id="namespacetext"
placeholder="The namespace text is here..."
type="text"
v-focus
v-model="namespace.title"/>
</div>
</div>
<div class="field">
<label class="label" for="namespacedescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
:preview-is-default="false"
id="namespacedescription"
placeholder="The namespaces description goes here..."
v-if="editorActive"
v-model="namespace.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="namespace.isArchived"
v-tooltip="'If a namespace is archived, you cannot create new lists or edit it.'">
This namespace is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="namespace.hexColor"/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="submit()"
:loading="namespaceService.loading"
class="is-fullwidth"
>
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:loading="namespaceService.loading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div>
</card>
<component
:id="namespace.id"
:is="manageUsersComponent"
:userIsAdmin="userIsAdmin"
shareType="user"
type="namespace"/>
<component
:id="namespace.id"
:is="manageTeamsComponent"
:userIsAdmin="userIsAdmin"
shareType="team"
type="namespace"/>
<transition name="modal">
<modal
@close="showDeleteModal = false"
v-if="showDeleteModal"
@submit="deleteNamespace()">
<span slot="header">Delete the namespace</span>
<p slot="text">Are you sure you want to delete this namespace and all of its contents?
<br/>This includes lists & tasks and <b>CANNOT BE UNDONE!</b></p>
</modal>
</transition>
</div>
</template>
<script>
import router from '../../router'
import manageSharing from '../../components/sharing/userTeam'
import NamespaceService from '../../services/namespace'
import NamespaceModel from '../../models/namespace'
import Fancycheckbox from '../../components/input/fancycheckbox'
import ColorPicker from '../../components/input/colorPicker'
import LoadingComponent from '../../components/misc/loading'
import ErrorComponent from '../../components/misc/error'
export default {
name: 'EditNamespace',
data() {
return {
namespaceService: NamespaceService,
manageUsersComponent: '',
manageTeamsComponent: '',
namespace: NamespaceModel,
showDeleteModal: false,
editorActive: false,
}
},
components: {
ColorPicker,
Fancycheckbox,
manageSharing,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
beforeMount() {
this.namespace.id = this.$route.params.id
},
created() {
this.namespaceService = new NamespaceService()
this.namespace = new NamespaceModel()
this.loadNamespace()
},
watch: {
// call again the method if the route changes
'$route': 'loadNamespace',
},
computed: {
userIsAdmin() {
return this.namespace.owner && this.namespace.owner.id === this.$store.state.auth.info.id
},
},
methods: {
loadNamespace() {
// 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
this.editorActive = false
this.$nextTick(() => this.editorActive = true)
let namespace = new NamespaceModel({id: this.$route.params.id})
this.namespaceService.get(namespace)
.then(r => {
this.$set(this, 'namespace', r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing'
this.setTitle(`Edit ${this.namespace.title}`)
})
.catch(e => {
this.error(e, this)
})
},
submit() {
this.namespaceService.update(this.namespace)
.then(r => {
// Update the namespace in the parent
this.$store.commit('namespaces/setNamespaceById', r)
this.success({message: 'The namespace was successfully updated.'}, this)
})
.catch(e => {
this.error(e, this)
})
},
deleteNamespace() {
this.namespaceService.delete(this.namespace)
.then(() => {
this.$store.commit('namespaces/removeNamespaceById', this.namespace.id)
this.success({message: 'The namespace was successfully deleted.'}, this)
router.push({name: 'home'})
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -1,5 +1,5 @@
<template>
<create
<create-edit
title="Create a new namespace"
@create="newNamespace()"
:create-disabled="namespace.title === ''"
@ -39,13 +39,13 @@
>
What's a namespace?
</p>
</create>
</create-edit>
</template>
<script>
import NamespaceModel from '../../models/namespace'
import NamespaceService from '../../services/namespace'
import Create from '@/components/misc/create'
import CreateEdit from '@/components/misc/create-edit'
import ColorPicker from '../../components/input/colorPicker'
export default {
@ -59,7 +59,7 @@ export default {
},
components: {
ColorPicker,
Create,
CreateEdit,
},
created() {
this.namespace = new NamespaceModel()

View file

@ -0,0 +1,52 @@
<template>
<modal
@close="$router.back()"
@submit="archiveNamespace()"
>
<span slot="header">{{ namespace.isArchived ? 'Un-' : '' }}Archive this namespace</span>
<p slot="text" v-if="namespace.isArchived">
You will be able to create new lists or edit it.
</p>
<p slot="text" v-else>
You won't be able to edit this namespace or create new list until you un-archive it.<br/>
This will also archive all lists in this namespace.
</p>
</modal>
</template>
<script>
import NamespaceService from '@/services/namespace'
export default {
name: 'namespace-setting-archive',
data() {
return {
namespaceService: NamespaceService,
namespace: null,
}
},
created() {
this.namespaceService = new NamespaceService()
this.namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.setTitle(`Archive "${this.namespace.title}"`)
},
methods: {
archiveNamespace() {
this.namespace.isArchived = !this.namespace.isArchived
this.namespaceService.update(this.namespace)
.then(r => {
this.$store.commit('namespaces/setNamespaceById', r)
this.success({message: 'The namespace was successfully archived.'}, this)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.$router.back()
})
},
},
}
</script>

View file

@ -0,0 +1,44 @@
<template>
<modal
@close="$router.back()"
@submit="deleteNamespace()"
>
<span slot="header">Delete this namespace</span>
<p slot="text">Are you sure you want to delete this namespace and all of its contents?
<br/>This includes all tasks and <b>CANNOT BE UNDONE!</b></p>
</modal>
</template>
<script>
import NamespaceService from '@/services/namespace'
export default {
name: 'namespace-setting-delete',
data() {
return {
namespaceService: NamespaceService,
}
},
created() {
this.namespaceService = new NamespaceService()
const namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.setTitle(`Delete "${namespace.title}"`)
},
methods: {
deleteNamespace() {
const namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.namespaceService.delete(namespace)
.then(() => {
this.$store.commit('namespaces/removeNamespaceFromNamespaceById', namespace)
this.success({message: 'The namespace was successfully deleted.'}, this)
this.$router.push({name: 'home'})
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -0,0 +1,137 @@
<template>
<create-edit
title="Edit This Namespace"
primary-icon=""
primary-label="Save"
@primary="save"
tertary="Delete"
@tertary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
>
<form @submit.prevent="save()">
<div class="field">
<label class="label" for="namespacetext">Namespace Name</label>
<div class="control">
<input
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
class="input"
id="namespacetext"
placeholder="The namespace text is here..."
type="text"
v-focus
v-model="namespace.title"/>
</div>
</div>
<div class="field">
<label class="label" for="namespacedescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
:preview-is-default="false"
id="namespacedescription"
placeholder="The namespaces description goes here..."
v-if="editorActive"
v-model="namespace.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="namespace.isArchived"
v-tooltip="'If a namespace is archived, you cannot create new lists or edit it.'">
This namespace is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="namespace.hexColor"/>
</div>
</div>
</form>
</create-edit>
</template>
<script>
import NamespaceService from '@/services/namespace'
import NamespaceModel from '@/models/namespace'
import Fancycheckbox from '@/components/input/fancycheckbox'
import ColorPicker from '@/components/input/colorPicker'
import LoadingComponent from '@/components/misc/loading'
import ErrorComponent from '@/components/misc/error'
import CreateEdit from '@/components/misc/create-edit'
export default {
name: 'namespace-setting-edit',
data() {
return {
namespaceService: NamespaceService,
namespace: NamespaceModel,
editorActive: false,
}
},
components: {
CreateEdit,
ColorPicker,
Fancycheckbox,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '@/components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
beforeMount() {
this.namespace.id = this.$route.params.id
},
created() {
this.namespaceService = new NamespaceService()
this.namespace = new NamespaceModel()
this.loadNamespace()
},
watch: {
// call again the method if the route changes
'$route': 'loadNamespace',
},
methods: {
loadNamespace() {
// 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
this.editorActive = false
this.$nextTick(() => this.editorActive = true)
const namespace = new NamespaceModel({id: this.$route.params.id})
this.namespaceService.get(namespace)
.then(r => {
this.$set(this, 'namespace', r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing'
this.setTitle(`Edit "${r.title}"`)
})
.catch(e => {
this.error(e, this)
})
},
save() {
this.namespaceService.update(this.namespace)
.then(r => {
// Update the namespace in the parent
this.$store.commit('namespaces/setNamespaceById', r)
this.success({message: 'The namespace was successfully updated.'}, this)
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -0,0 +1,77 @@
<template>
<create-edit
title="Share this Namespace"
primary-label=""
>
<component
:id="namespace.id"
:is="manageUsersComponent"
:userIsAdmin="userIsAdmin"
shareType="user"
type="namespace"/>
<component
:id="namespace.id"
:is="manageTeamsComponent"
:userIsAdmin="userIsAdmin"
shareType="team"
type="namespace"/>
</create-edit>
</template>
<script>
import manageSharing from '@/components/sharing/userTeam'
import CreateEdit from '@/components/misc/create-edit'
import NamespaceService from '@/services/namespace'
import NamespaceModel from '@/models/namespace'
export default {
name: 'namespace-setting-share',
data() {
return {
namespaceService: NamespaceService,
manageUsersComponent: '',
manageTeamsComponent: '',
namespace: NamespaceModel,
}
},
components: {
CreateEdit,
manageSharing,
},
beforeMount() {
this.namespace.id = this.$route.params.id
},
created() {
this.namespaceService = new NamespaceService()
this.namespace = new NamespaceModel()
this.loadNamespace()
},
watch: {
// call again the method if the route changes
'$route': 'loadNamespace',
},
computed: {
userIsAdmin() {
return this.namespace.owner && this.namespace.owner.id === this.$store.state.auth.info.id
},
},
methods: {
loadNamespace() {
const namespace = new NamespaceModel({id: this.$route.params.id})
this.namespaceService.get(namespace)
.then(r => {
this.$set(this, 'namespace', r)
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageSharing'
this.manageUsersComponent = 'manageSharing'
this.setTitle(`Share "${this.namespace.title}"`)
})
.catch(e => {
this.error(e, this)
})
},
},
}
</script>

View file

@ -88,7 +88,7 @@
</div>
</div>
</div>
<table class="table is-striped is-hoverable is-fullwidth">
<table class="table has-actions is-striped is-hoverable is-fullwidth">
<tbody>
<tr :key="m.id" v-for="m in team.members">
<td>{{ m.getDisplayName() }}</td>

View file

@ -1,5 +1,5 @@
<template>
<create
<create-edit
title="Create a new team"
@create="newTeam()"
:create-disabled="team.name === ''"
@ -25,14 +25,14 @@
<p class="help is-danger" v-if="showError && team.name === ''">
Please specify a name.
</p>
</create>
</create-edit>
</template>
<script>
import router from '../../router'
import TeamModel from '../../models/team'
import TeamService from '../../services/team'
import Create from '@/components/misc/create'
import CreateEdit from '@/components/misc/create-edit'
export default {
name: 'NewTeam',
@ -44,7 +44,7 @@ export default {
}
},
components: {
Create,
CreateEdit,
},
created() {
this.teamService = new TeamService()