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

View file

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

View file

@ -31,21 +31,17 @@
class="title"> class="title">
{{ currentList.title === '' ? 'Loading...' : currentList.title }} {{ currentList.title === '' ? 'Loading...' : currentList.title }}
</h1> </h1>
<router-link
:to="{ name: 'list.edit', params: { id: currentList.id } }" <list-settings-dropdown v-if="canWriteCurrentList" :list="currentList"/>
class="icon"
v-if="canWriteCurrentList">
<icon icon="cog" size="2x"/>
</router-link>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<update/> <update/>
<div class="user"> <div class="user">
<img :src="userAvatar" alt="" class="avatar"/> <img :src="userAvatar" alt="" class="avatar"/>
<div class="dropdown is-right is-active"> <dropdown class="is-right">
<div class="dropdown-trigger"> <template v-slot:trigger>
<x-button <x-button
@click.stop="userMenuActive = !userMenuActive"
type="secondary" type="secondary"
:shadow="false"> :shadow="false">
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span> <span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
@ -53,10 +49,8 @@
<icon icon="chevron-down"/> <icon icon="chevron-down"/>
</span> </span>
</x-button> </x-button>
</div> </template>
<transition name="fade">
<div class="dropdown-menu" v-if="userMenuActive">
<div class="dropdown-content">
<router-link :to="{name: 'user.settings'}" class="dropdown-item"> <router-link :to="{name: 'user.settings'}" class="dropdown-item">
Settings Settings
</router-link> </router-link>
@ -79,10 +73,7 @@
<a @click="logout()" class="dropdown-item"> <a @click="logout()" class="dropdown-item">
Logout Logout
</a> </a>
</div> </dropdown>
</div>
</transition>
</div>
</div> </div>
</div> </div>
</nav> </nav>
@ -93,21 +84,16 @@ import {mapState} from 'vuex'
import {CURRENT_LIST} from '@/store/mutation-types' import {CURRENT_LIST} from '@/store/mutation-types'
import Rights from '@/models/rights.json' import Rights from '@/models/rights.json'
import Update from '@/components/home/update' import Update from '@/components/home/update'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
import Dropdown from '@/components/misc/dropdown'
export default { export default {
name: 'topNavigation', name: 'topNavigation',
data() {
return {
userMenuActive: false,
}
},
components: { components: {
Dropdown,
ListSettingsDropdown,
Update, Update,
}, },
created() {
// This will hide the menu once clicked outside of it
this.$nextTick(() => document.addEventListener('click', () => this.userMenuActive = false))
},
computed: mapState({ computed: mapState({
userInfo: state => state.auth.info, userInfo: state => state.auth.info,
userAvatar: state => state.auth.avatarUrl, 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> </span>
</a> </a>
</header> </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}"> <div :class="{'content': hasContent}">
<slot></slot> <slot></slot>
</div> </div>
@ -46,6 +46,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
loading: {
type: Boolean,
default: false,
},
}, },
} }
</script> </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"> <transition name="modal">
<div class="modal-mask"> <div class="modal-mask">
<div class="modal-container" @click.self.prevent.stop="$emit('close')"> <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> <slot>
<div class="header"> <div class="header">
<slot name="header"></slot> <slot name="header"></slot>
@ -49,6 +49,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
wide: {
type: Boolean,
default: false,
},
}, },
} }
</script> </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> <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="sharables-list">
<div class="p-4"> <div class="p-4">
<p>Share with a link:</p> <p>Share with a link:</p>
@ -21,7 +22,7 @@
</div> </div>
</div> </div>
<table <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" v-if="linkShares.length > 0"
> >
<thead> <thead>
@ -112,7 +113,7 @@
</p> </p>
</modal> </modal>
</transition> </transition>
</card> </div>
</template> </template>
<script> <script>

View file

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

View file

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

View file

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

View file

@ -57,6 +57,10 @@ import {
faChessKnight, faChessKnight,
faCoffee, faCoffee,
faCocktail, faCocktail,
faEllipsisH,
faArchive,
faShareAlt,
faImage,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons' import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
@ -144,6 +148,10 @@ library.add(faSun)
library.add(faChessKnight) library.add(faChessKnight)
library.add(faCoffee) library.add(faCoffee)
library.add(faCocktail) library.add(faCocktail)
library.add(faEllipsisH)
library.add(faArchive)
library.add(faShareAlt)
library.add(faImage)
Vue.component('icon', FontAwesomeIcon) 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 List from '../views/list/views/List'
import Gantt from '../views/list/views/Gantt' import Gantt from '../views/list/views/Gantt'
import Table from '../views/list/views/Table' 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 // Saved Filters
import CreateSavedFilter from '@/views/filters/CreateSavedFilter' import CreateSavedFilter from '@/views/filters/CreateSavedFilter'
@ -57,12 +71,6 @@ const NewListComponent = () => ({
error: ErrorComponent, error: ErrorComponent,
timeout: 60000, timeout: 60000,
}) })
const EditListComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/list/EditListView'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
})
// Namespace Handling // Namespace Handling
const NewNamespaceComponent = () => ({ const NewNamespaceComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/namespaces/NewNamespace'), component: import(/* webpackChunkName: "settings" */'../views/namespaces/NewNamespace'),
@ -70,12 +78,6 @@ const NewNamespaceComponent = () => ({
error: ErrorComponent, error: ErrorComponent,
timeout: 60000, timeout: 60000,
}) })
const EditNamespaceComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/namespaces/EditNamespace'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
})
const EditTeamComponent = () => ({ const EditTeamComponent = () => ({
component: import(/* webpackChunkName: "settings" */'../views/teams/EditTeam'), component: import(/* webpackChunkName: "settings" */'../views/teams/EditTeam'),
@ -163,11 +165,6 @@ export default new Router({
popup: NewNamespaceComponent, popup: NewNamespaceComponent,
}, },
}, },
{
path: '/namespaces/:id/edit',
name: 'namespace.edit',
component: EditNamespaceComponent,
},
{ {
path: '/namespaces/:id/list', path: '/namespaces/:id/list',
name: 'list.create', name: 'list.create',
@ -176,9 +173,32 @@ export default new Router({
} }
}, },
{ {
path: '/lists/:id/edit', path: '/namespaces/:id/settings/edit',
name: 'list.edit', name: 'namespace.settings.edit',
component: EditListComponent, 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', path: '/tasks/:id',
@ -190,6 +210,62 @@ export default new Router({
name: 'tasks.range', name: 'tasks.range',
component: ShowTasksInRangeComponent, 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', path: '/lists/:listId',
name: 'list.index', name: 'list.index',
@ -205,6 +281,46 @@ export default new Router({
name: 'task.list.detail', name: 'task.list.detail',
component: TaskDetailViewModal, 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', name: 'task.gantt.detail',
component: TaskDetailViewModal, 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', path: '/lists/:listId/table',
name: 'list.table', name: 'list.table',
component: 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', path: '/lists/:listId/kanban',
@ -234,6 +432,46 @@ export default new Router({
name: 'task.kanban.detail', name: 'task.kanban.detail',
component: TaskDetailViewModal, 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

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

View file

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

View file

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

View file

@ -8,24 +8,6 @@
.content { .content {
padding: 0; 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 { .task-add {
@ -51,11 +33,12 @@
margin: 0; margin: 0;
} }
.icon { .dropdown-trigger {
color: $grey-400; color: $grey-400;
margin-left: 1rem; margin-left: 1rem;
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;
cursor: pointer;
} }
} }
@ -63,6 +46,10 @@
padding-bottom: 1rem; padding-bottom: 1rem;
} }
$filter-container-top-default: -59px;
$filter-container-top-link-share-gantt: -138px;
$filter-container-top-link-share-list: -47px;
.filter-container { .filter-container {
text-align: right; text-align: right;
width: 100%; width: 100%;
@ -70,7 +57,7 @@
max-width: 180px; max-width: 180px;
position: absolute; position: absolute;
right: 1.5rem; right: 1.5rem;
margin-top: -59px; margin-top: $filter-container-top-default;
z-index: 4; z-index: 4;
.items { .items {
@ -146,20 +133,42 @@
} }
} }
.list-namespace-title {
color: $grey-500;
}
.link-share-container .gantt-chart-container .filter-container, .link-share-container .gantt-chart-container .filter-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 { .link-share-container .list-view .filter-container {
margin-top: -47px; margin-top: $filter-container-top-link-share-list;
} }
.link-share-container .filter-container { .link-share-container .filter-container {
right: 9rem; right: 9rem;
margin-top: -59px; margin-top: $filter-container-top-default;
} }
.list-namespace-title { .is-archived {
color: $grey-500; $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%); transform: translate(-50%, -50%);
text-align: center; text-align: center;
@media screen and (max-width: $tablet) {
margin: 0;
top: 25%;
transform: translate(-50%, -25%);
}
.header { .header {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
@ -56,6 +62,11 @@
color: $white; color: $white;
font-size: 2rem; font-size: 2rem;
} }
.is-wide {
max-width: $desktop;
width: calc(100% - 2rem);
}
} }
} }

View file

@ -64,3 +64,17 @@
.underline-none { .underline-none {
text-decoration: none !important; 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 { .field.has-addons .button {
height: 2.5rem; height: 2.5rem;
margin-left: 0 !important;
}
.field.has-addons .select select {
margin-right: 0;
} }
.input, .input,

View file

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

View file

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

View file

@ -1,3 +1,4 @@
$grey-50: #F9FAFB;
$grey-100: #f3f4f6; $grey-100: #f3f4f6;
$grey-200: #E5E7EB; $grey-200: #E5E7EB;
$grey-300: #D1D5DB; $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> <template>
<create <create-edit
title="Create a new label" title="Create a new label"
@create="newLabel()" @create="newLabel()"
:create-disabled="label.title === ''" :create-disabled="label.title === ''"
@ -31,7 +31,7 @@
<color-picker v-model="label.hexColor" /> <color-picker v-model="label.hexColor" />
</div> </div>
</div> </div>
</create> </create-edit>
</template> </template>
<script> <script>
@ -39,7 +39,7 @@ import labelModel from '../../models/label'
import labelService from '../../services/label' import labelService from '../../services/label'
import LabelModel from '../../models/label' import LabelModel from '../../models/label'
import LabelService from '../../services/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' import ColorPicker from '../../components/input/colorPicker'
export default { export default {
@ -52,7 +52,7 @@ export default {
} }
}, },
components: { components: {
Create, CreateEdit,
ColorPicker, ColorPicker,
}, },
created() { 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> <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"> <div class="field">
<label class="label" for="listTitle">List Title</label> <label class="label" for="listTitle">List Title</label>
<div <div
@ -28,13 +28,13 @@
<color-picker v-model="list.hexColor" /> <color-picker v-model="list.hexColor" />
</div> </div>
</div> </div>
</create> </create-edit>
</template> </template>
<script> <script>
import ListService from '../../services/list' import ListService from '../../services/list'
import ListModel from '../../models/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' import ColorPicker from '../../components/input/colorPicker'
export default { export default {
@ -47,7 +47,7 @@ export default {
} }
}, },
components: { components: {
Create, CreateEdit,
ColorPicker, ColorPicker,
}, },
created() { created() {

View file

@ -1,36 +1,38 @@
<template> <template>
<div <div
:class="{ 'is-loading': listService.loading}" :class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
class="loader-container" class="loader-container"
> >
<div class="switch-view-container"> <div class="switch-view-container">
<div class="switch-view"> <div class="switch-view">
<router-link <router-link
:class="{'is-active': $route.name === 'list.list'}" :class="{'is-active': $route.name.includes('list.list')}"
:to="{ name: 'list.list', params: { listId: listId } }"> :to="{ name: 'list.list', params: { listId: listId } }">
List List
</router-link> </router-link>
<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 } }"> :to="{ name: 'list.gantt', params: { listId: listId } }">
Gantt Gantt
</router-link> </router-link>
<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 } }"> :to="{ name: 'list.table', params: { listId: listId } }">
Table Table
</router-link> </router-link>
<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 } }"> :to="{ name: 'list.kanban', params: { listId: listId } }">
Kanban Kanban
</router-link> </router-link>
</div> </div>
</div> </div>
<div class="notification is-warning" v-if="list.isArchived"> <transition name="fade">
<div class="notification is-warning" v-if="currentList.isArchived">
This list is archived. This list is archived.
It is not possible to create new or edit tasks or it. It is not possible to create new or edit tasks or it.
</div> </div>
</transition>
<router-view/> <router-view/>
</div> </div>
@ -75,6 +77,7 @@ export default {
return typeof this.$store.state.currentList === 'undefined' ? { return typeof this.$store.state.currentList === 'undefined' ? {
id: 0, id: 0,
title: '', title: '',
isArchived: false,
} : this.$store.state.currentList } : this.$store.state.currentList
}, },
}, },
@ -86,6 +89,10 @@ export default {
return return
}, },
loadList() { loadList() {
if(this.$route.name.includes('.settings.')) {
return
}
this.setTitle(this.currentList.title) this.setTitle(this.currentList.title)
// This invalidates the loaded list at the kanban board which lets it reload its content when // 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> <template>
<card <create-edit
:class="{ 'is-loading': backgroundService.loading}"
class="list-background-setting loader-container"
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
title="Set list background" 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"> <div class="mb-4" v-if="uploadBackgroundEnabled">
<input <input
@ -51,24 +53,21 @@
type="secondary" type="secondary"
v-if="backgroundSearchResult.length > 0" v-if="backgroundSearchResult.length > 0"
> >
<template v-if="backgroundService.loading"> {{ backgroundService.loading ? 'Loading...' : 'Load more photos'}}
Loading...
</template>
<template v-else>
Load more photos
</template>
</x-button> </x-button>
</template> </template>
</card> </create-edit>
</template> </template>
<script> <script>
import BackgroundUnsplashService from '../../../services/backgroundUnsplash' import BackgroundUnsplashService from '../../../services/backgroundUnsplash'
import BackgroundUploadService from '../../../services/backgroundUpload' import BackgroundUploadService from '../../../services/backgroundUpload'
import {CURRENT_LIST} from '@/store/mutation-types' import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit'
export default { export default {
name: 'background-settings', name: 'list-setting-background',
components: {CreateEdit},
data() { data() {
return { return {
backgroundSearchTerm: '', backgroundSearchTerm: '',
@ -81,12 +80,6 @@ export default {
backgroundUploadService: null, backgroundUploadService: null,
} }
}, },
props: {
listId: {
default: 0,
required: true,
},
},
computed: { computed: {
unsplashBackgroundEnabled() { unsplashBackgroundEnabled() {
return this.$store.state.config.enabledBackgroundProviders.includes('unsplash') return this.$store.state.config.enabledBackgroundProviders.includes('unsplash')
@ -98,6 +91,7 @@ export default {
created() { created() {
this.backgroundService = new BackgroundUnsplashService() this.backgroundService = new BackgroundUnsplashService()
this.backgroundUploadService = new BackgroundUploadService() this.backgroundUploadService = new BackgroundUploadService()
this.setTitle('Set a list background')
// Show the default collection of backgrounds // Show the default collection of backgrounds
this.newBackgroundSearch() this.newBackgroundSearch()
}, },
@ -142,7 +136,7 @@ export default {
return return
} }
this.backgroundService.update({id: backgroundId, listId: this.listId}) this.backgroundService.update({id: backgroundId, listId: this.$route.params.listId})
.then(l => { .then(l => {
this.$store.commit(CURRENT_LIST, l) this.$store.commit(CURRENT_LIST, l)
this.$store.commit('namespaces/setListInNamespaceById', l) this.$store.commit('namespaces/setListInNamespaceById', l)
@ -157,7 +151,7 @@ export default {
return 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 => { .then(l => {
this.$store.commit(CURRENT_LIST, l) this.$store.commit(CURRENT_LIST, l)
this.$store.commit('namespaces/setListInNamespaceById', 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,18 +32,11 @@
v-if="bucket.limit > 0"> v-if="bucket.limit > 0">
{{ bucket.tasks.length }}/{{ bucket.limit }} {{ bucket.tasks.length }}/{{ bucket.limit }}
</span> </span>
<div <dropdown
class="dropdown is-right is-active options" class="is-right options"
v-if="canWrite" 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 <a
@click.stop="showSetLimitInput = true" @click.stop="showSetLimitInput = true"
class="dropdown-item" class="dropdown-item"
@ -79,10 +72,7 @@
<span class="icon is-small"><icon icon="trash-alt"/></span> <span class="icon is-small"><icon icon="trash-alt"/></span>
Delete Delete
</a> </a>
</div> </dropdown>
</div>
</transition>
</div>
</div> </div>
<div :ref="`tasks-container${bucket.id}`" class="tasks"> <div :ref="`tasks-container${bucket.id}`" class="tasks">
<!-- Make the component either a div or a draggable component based on the user rights --> <!-- 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 {mapState} from 'vuex'
import {saveListView} from '@/helpers/saveListView' import {saveListView} from '@/helpers/saveListView'
import Rights from '../../../models/rights.json' 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 FilterPopup from '@/components/list/partials/filter-popup'
import Dropdown from '@/components/misc/dropdown'
export default { export default {
name: 'Kanban', name: 'Kanban',
components: { components: {
Dropdown,
FilterPopup, FilterPopup,
Container, Container,
Draggable, Draggable,
@ -295,7 +287,6 @@ export default {
showOnTop: true, showOnTop: true,
}, },
sourceBucket: 0, sourceBucket: 0,
bucketOptionsDropDownActive: {},
showBucketDeleteModal: false, showBucketDeleteModal: false,
bucketToDelete: 0, bucketToDelete: 0,
@ -324,7 +315,6 @@ export default {
created() { created() {
this.taskService = new TaskService() this.taskService = new TaskService()
this.loadBuckets() this.loadBuckets()
this.$nextTick(() => document.addEventListener('click', this.closeBucketDropdowns))
// Save the current list view to local storage // Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads. // We use local storage and not vuex here to make it persistent across reloads.
@ -449,17 +439,6 @@ export default {
toggleShowNewTaskInput(bucket) { toggleShowNewTaskInput(bucket) {
this.$set(this.showNewTaskInput, bucket, !this.showNewTaskInput[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) { addTaskToBucket(bucketId) {
if (this.newTaskText === '') { if (this.newTaskText === '') {

View file

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

View file

@ -42,7 +42,7 @@
</div> </div>
<card :padding="false" :has-content="false"> <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> <thead>
<tr> <tr>
<th v-if="activeColumns.id"> <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> <template>
<create <create-edit
title="Create a new namespace" title="Create a new namespace"
@create="newNamespace()" @create="newNamespace()"
:create-disabled="namespace.title === ''" :create-disabled="namespace.title === ''"
@ -39,13 +39,13 @@
> >
What's a namespace? What's a namespace?
</p> </p>
</create> </create-edit>
</template> </template>
<script> <script>
import NamespaceModel from '../../models/namespace' import NamespaceModel from '../../models/namespace'
import NamespaceService from '../../services/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' import ColorPicker from '../../components/input/colorPicker'
export default { export default {
@ -59,7 +59,7 @@ export default {
}, },
components: { components: {
ColorPicker, ColorPicker,
Create, CreateEdit,
}, },
created() { created() {
this.namespace = new NamespaceModel() 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> </div>
</div> </div>
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table has-actions is-striped is-hoverable is-fullwidth">
<tbody> <tbody>
<tr :key="m.id" v-for="m in team.members"> <tr :key="m.id" v-for="m in team.members">
<td>{{ m.getDisplayName() }}</td> <td>{{ m.getDisplayName() }}</td>

View file

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