Move buttons to separate component (#380)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/380
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2021-01-17 17:57:57 +00:00
parent f3e0b79b26
commit 2aceca54ca
61 changed files with 2315 additions and 1825 deletions

View file

@ -29,7 +29,7 @@ describe('Lists', () => {
.contains('Create a new list') .contains('Create a new list')
cy.get('input.input') cy.get('input.input')
.type('New List') .type('New List')
cy.get('button') cy.get('.button')
.contains('Add') .contains('Add')
.click() .click()

View file

@ -30,7 +30,7 @@ describe('Namepaces', () => {
.should('contain', 'Create a new namespace') .should('contain', 'Create a new namespace')
cy.get('input.input') cy.get('input.input')
.type('New Namespace') .type('New Namespace')
cy.get('button') cy.get('.button')
.contains('Add') .contains('Add')
.click() .click()
cy.url() cy.url()

View file

@ -17,7 +17,7 @@ describe('Team', () => {
.contains('Create a new team') .contains('Create a new team')
cy.get('input.input') cy.get('input.input')
.type('New Team') .type('New Team')
cy.get('button') cy.get('.button')
.contains('Add') .contains('Add')
.click() .click()
@ -113,7 +113,7 @@ describe('Team', () => {
.click() .click()
cy.get('.card') cy.get('.card')
.contains('Team Members') .contains('Team Members')
.get('.card-content button.button') .get('.card-content .button')
.contains('Add To Team') .contains('Add To Team')
.click() .click()

View file

@ -28,7 +28,7 @@ describe('Task', () => {
cy.visit('/lists/1/list') cy.visit('/lists/1/list')
cy.get('input.input[placeholder="Add a new task..."') cy.get('input.input[placeholder="Add a new task..."')
.type('New Task') .type('New Task')
cy.get('button.button') cy.get('.button')
.contains('Add') .contains('Add')
.click() .click()
cy.get('.tasks .task .tasktext') cy.get('.tasks .task .tasktext')
@ -44,7 +44,7 @@ describe('Task', () => {
.should('not.exist') .should('not.exist')
cy.get('input.input[placeholder="Add a new task..."') cy.get('input.input[placeholder="Add a new task..."')
.type('New Task') .type('New Task')
cy.get('button.button') cy.get('.button')
.contains('Add') .contains('Add')
.click() .click()
@ -175,7 +175,7 @@ describe('Task', () => {
.should('exist') .should('exist')
}) })
it('Can add a new comment', () => { it.only('Can add a new comment', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
}) })
@ -183,7 +183,7 @@ describe('Task', () => {
cy.get('.task-view .comments .media.comment .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll') cy.get('.task-view .comments .media.comment .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
.type('{selectall}New Comment') .type('{selectall}New Comment')
cy.get('.task-view .comments .media.comment .button.is-primary') cy.get('.task-view .comments .media.comment .button:not([disabled])')
.contains('Comment') .contains('Comment')
.click() .click()

View file

@ -4,7 +4,7 @@ const testAndAssertFailed = fixture => {
cy.visit('/login') cy.visit('/login')
cy.get('input[id=username]').type(fixture.username) cy.get('input[id=username]').type(fixture.username)
cy.get('input[id=password]').type(fixture.password) cy.get('input[id=password]').type(fixture.password)
cy.get('button').contains('Login').click() cy.get('.button').contains('Login').click()
cy.wait(5000) // It can take waaaayy too long to log the user in cy.wait(5000) // It can take waaaayy too long to log the user in
cy.url().should('include', '/') cy.url().should('include', '/')
@ -32,7 +32,7 @@ context('Login', () => {
cy.visit('/login') cy.visit('/login')
cy.get('input[id=username]').type(fixture.username) cy.get('input[id=username]').type(fixture.username)
cy.get('input[id=password]').type(fixture.password) cy.get('input[id=password]').type(fixture.password)
cy.get('button').contains('Login').click() cy.get('.button').contains('Login').click()
cy.url().should('include', '/') cy.url().should('include', '/')
cy.get('h2').should('contain', `Hi ${fixture.username}!`) cy.get('h2').should('contain', `Hi ${fixture.username}!`)
}) })

View file

@ -26,7 +26,7 @@ context('Registration', () => {
cy.get('#email').type(fixture.email) cy.get('#email').type(fixture.email)
cy.get('#password1').type(fixture.password) cy.get('#password1').type(fixture.password)
cy.get('#password2').type(fixture.password) cy.get('#password2').type(fixture.password)
cy.get('button#register-submit').click() cy.get('#register-submit').click()
cy.url().should('include', '/') cy.url().should('include', '/')
cy.get('h2').should('contain', `Hi ${fixture.username}!`) cy.get('h2').should('contain', `Hi ${fixture.username}!`)
}) })
@ -43,7 +43,7 @@ context('Registration', () => {
cy.get('#email').type(fixture.email) cy.get('#email').type(fixture.email)
cy.get('#password1').type(fixture.password) cy.get('#password1').type(fixture.password)
cy.get('#password2').type(fixture.password) cy.get('#password2').type(fixture.password)
cy.get('button#register-submit').click() cy.get('#register-submit').click()
cy.get('div.notification.is-danger').contains('A user with this username already exists.') cy.get('div.notification.is-danger').contains('A user with this username already exists.')
}) })
}) })

View file

@ -32,7 +32,7 @@ describe('User Settings', () => {
cy.get('input#newName') cy.get('input#newName')
.type('Lorem Ipsum') .type('Lorem Ipsum')
cy.get('.card.update-name button.button.is-primary') cy.get('.card.general-settings .button.is-primary')
.contains('Save') .contains('Save')
.click() .click()

View file

@ -14,12 +14,12 @@
</h1> </h1>
<div class="box has-text-left view"> <div class="box has-text-left view">
<div class="logout"> <div class="logout">
<a @click="logout()" class="button"> <x-button @click="logout()" type="secondary">
<span>Logout</span> <span>Logout</span>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="sign-out-alt"/> <icon icon="sign-out-alt"/>
</span> </span>
</a> </x-button>
</div> </div>
<router-view/> <router-view/>
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank"> <a class="menu-bottom-link" href="https://vikunja.io" target="_blank">
@ -49,7 +49,3 @@ export default {
}, },
} }
</script> </script>
<style scoped>
</style>

View file

@ -44,12 +44,15 @@
<img :src="userAvatar" alt="" class="avatar"/> <img :src="userAvatar" alt="" class="avatar"/>
<div class="dropdown is-right is-active"> <div class="dropdown is-right is-active">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button @click.stop="userMenuActive = !userMenuActive" class="button has-no-shadow"> <x-button
@click.stop="userMenuActive = !userMenuActive"
type="secondary"
:shadow="false">
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span> <span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="chevron-down"/> <icon icon="chevron-down"/>
</span> </span>
</button> </x-button>
</div> </div>
<transition name="fade"> <transition name="fade">
<div class="dropdown-menu" v-if="userMenuActive"> <div class="dropdown-menu" v-if="userMenuActive">

View file

@ -1,7 +1,9 @@
<template> <template>
<div class="update-notification" v-if="updateAvailable"> <div class="update-notification" v-if="updateAvailable">
<p>There is an update for Vikunja available!</p> <p>There is an update for Vikunja available!</p>
<a @click="refreshApp()" class="button is-primary has-no-shadow">Update Now</a> <x-button @click="refreshApp()" :shadow="false">
Update Now
</x-button>
</div> </div>
</template> </template>

View file

@ -0,0 +1,74 @@
<template>
<a
class="button"
:class="{
'is-loading': loading,
'has-no-shadow': !shadow,
'is-primary': type === 'primary',
'is-outlined': type === 'secondary',
'is-text is-inverted has-no-shadow underline-none':
type === 'tertary',
}"
:disabled="disabled"
@click="click"
:href="href !== '' ? href : false"
>
<icon :icon="icon" v-if="showIconOnly"/>
<span class="icon is-small" v-else-if="icon !== ''">
<icon :icon="icon"/>
</span>
<slot></slot>
</a>
</template>
<script>
export default {
name: 'x-button',
props: {
type: {
type: String,
default: 'primary',
},
href: {
type: String,
default: '',
},
to: {
default: false,
},
icon: {
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
},
computed: {
showIconOnly() {
return this.icon !== '' && typeof this.$slots.default === 'undefined'
}
},
methods: {
click(e) {
if (this.disabled) {
return
}
if (this.to !== false) {
this.$router.push(this.to)
}
this.$emit('click', e)
},
},
}
</script>

View file

@ -16,9 +16,9 @@
model="hex" model="hex"
picker="square" picker="square"
v-model="color"/> v-model="color"/>
<a @click="reset" class="button has-no-shadow is-small ml-2"> <x-button @click="reset" class="is-small ml-2" :shadow="false" type="secondary">
Reset Color Reset Color
</a> </x-button>
</div> </div>
</template> </template>

View file

@ -97,12 +97,13 @@
v-model="flatPickrDate" v-model="flatPickrDate"
/> />
<a <x-button
class="button is-primary has-no-shadow is-fullwidth" class="is-fullwidth"
:shadow="false"
@click="close" @click="close"
> >
Confirm Confirm
</a> </x-button>
</div> </div>
</transition> </transition>
</div> </div>

View file

@ -1,12 +1,22 @@
<template> <template>
<div :class="{'is-pulled-up': isEditEnabled}" class="editor"> <div :class="{'is-pulled-up': isEditEnabled}" class="editor">
<div class="is-pulled-right mb-4" v-if="hasPreview && isEditEnabled && !hasEditBottom"> <div class="is-pulled-right mb-4" v-if="hasPreview && isEditEnabled && !hasEditBottom">
<a v-if="!isEditActive" @click="toggleEdit" class="button has-no-shadow"> <x-button
v-if="!isEditActive"
@click="toggleEdit"
:shadow="false"
type="secondary"
>
<icon icon="pen"/> <icon icon="pen"/>
</a> </x-button>
<a v-else @click="toggleEdit" class="button has-no-shadow"> <x-button
v-else
@click="toggleEdit"
:shadow="false"
type="secondary"
>
Done Done
</a> </x-button>
</div> </div>
<div class="clear"></div> <div class="clear"></div>

View file

@ -35,7 +35,7 @@
<div class="search-results" v-if="searchResultsVisible"> <div class="search-results" v-if="searchResultsVisible">
<button <button
v-if="creatableAvailable" v-if="creatableAvailable"
class="button is-ghost is-fullwidth" class="is-fullwidth"
ref="result--1" ref="result--1"
@keydown.up.prevent="() => preSelect(-2)" @keydown.up.prevent="() => preSelect(-2)"
@keydown.down.prevent="() => preSelect(0)" @keydown.down.prevent="() => preSelect(0)"
@ -55,7 +55,7 @@
</button> </button>
<button <button
class="button is-ghost is-fullwidth" class="is-fullwidth"
v-for="(data, key) in filteredSearchResults" v-for="(data, key) in filteredSearchResults"
:key="key" :key="key"
:ref="`result-${key}`" :ref="`result-${key}`"

View file

@ -1,15 +1,11 @@
<template> <template>
<div <card
:class="{ 'is-loading': backgroundService.loading}" :class="{ 'is-loading': backgroundService.loading}"
class="card list-background-setting loader-container" class="list-background-setting loader-container"
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"> v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
<header class="card-header"> title="Set list background"
<p class="card-header-title"> >
Set list background <div class="mb-4" v-if="uploadBackgroundEnabled">
</p>
</header>
<div class="card-content">
<div class="content" v-if="uploadBackgroundEnabled">
<input <input
@change="uploadBackground" @change="uploadBackground"
accept="image/*" accept="image/*"
@ -17,15 +13,15 @@
ref="backgroundUploadInput" ref="backgroundUploadInput"
type="file" type="file"
/> />
<a <x-button
:class="{'is-loading': backgroundUploadService.loading}" :loading="backgroundUploadService.loading"
@click="$refs.backgroundUploadInput.click()" @click="$refs.backgroundUploadInput.click()"
class="button is-primary" type="primary"
> >
Choose a background from your pc Choose a background from your pc
</a> </x-button>
</div> </div>
<div class="content" v-if="unsplashBackgroundEnabled"> <template v-if="unsplashBackgroundEnabled">
<input <input
:class="{'is-loading': backgroundService.loading}" :class="{'is-loading': backgroundService.loading}"
@keyup="() => newBackgroundSearch()" @keyup="() => newBackgroundSearch()"
@ -47,10 +43,12 @@
</a> </a>
</a> </a>
</div> </div>
<a <x-button
:disabled="backgroundService.loading" :disabled="backgroundService.loading"
@click="() => searchBackgrounds(currentPage + 1)" @click="() => searchBackgrounds(currentPage + 1)"
class="button is-centered is-load-more-button has-no-shadow mt-4" class="is-load-more-button mt-4"
:shadow="false"
type="secondary"
v-if="backgroundSearchResult.length > 0" v-if="backgroundSearchResult.length > 0"
> >
<template v-if="backgroundService.loading"> <template v-if="backgroundService.loading">
@ -59,10 +57,9 @@
<template v-else> <template v-else>
Load more photos Load more photos
</template> </template>
</a> </x-button>
</div> </template>
</div> </card>
</div>
</template> </template>
<script> <script>

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="card filters has-overflow"> <card class="filters has-overflow">
<div class="card-content">
<fancycheckbox v-model="params.filter_include_nulls"> <fancycheckbox v-model="params.filter_include_nulls">
Include Tasks which don't have a value set Include Tasks which don't have a value set
</fancycheckbox> </fancycheckbox>
@ -175,8 +174,7 @@
</div> </div>
</div> </div>
</template> </template>
</div> </card>
</div>
</template> </template>
<script> <script>

View file

@ -4,13 +4,13 @@
<p>Vikunja will import all lists, tasks, notes, reminders and files you have access to.</p> <p>Vikunja will import all lists, tasks, notes, reminders and files you have access to.</p>
<template v-if="isMigrating === false && message === '' && lastMigrationDate === null"> <template v-if="isMigrating === false && message === '' && lastMigrationDate === null">
<p>To authorize Vikunja to access your {{ name }} Account, click the button below.</p> <p>To authorize Vikunja to access your {{ name }} Account, click the button below.</p>
<a <x-button
:class="{'is-loading': migrationService.loading}" :loading="migrationService.loading"
:disabled="migrationService.loading" :disabled="migrationService.loading"
:href="authUrl" :href="authUrl"
class="button is-primary"> >
Get Started Get Started
</a> </x-button>
</template> </template>
<div <div
class="migration-in-progress-container" class="migration-in-progress-container"
@ -38,8 +38,8 @@
Are you sure? Are you sure?
</p> </p>
<div class="buttons"> <div class="buttons">
<button @click="migrate" class="button is-primary">I am sure, please start migrating now!</button> <x-button @click="migrate">I am sure, please start migrating now!</x-button>
<router-link :to="{name: 'home'}" class="button is-text has-text-danger is-inverted has-no-shadow underline-none">Cancel</router-link> <x-button :to="{name: 'home'}" type="tertary" class="has-text-danger">Cancel</x-button>
</div> </div>
</div> </div>
<div v-else> <div v-else>
@ -48,7 +48,7 @@
{{ message }} {{ message }}
</div> </div>
</div> </div>
<router-link :to="{name: 'home'}" class="button is-primary">Refresh</router-link> <x-button :to="{name: 'home'}">Refresh</x-button>
</div> </div>
</div> </div>
</template> </template>

View file

@ -5,7 +5,8 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input <input
class="input" id="api-url" class="input"
id="api-url"
placeholder="eg. https://localhost:3456" placeholder="eg. https://localhost:3456"
required required
type="url" type="url"
@ -15,21 +16,29 @@
/> />
</div> </div>
<div class="control"> <div class="control">
<a class="button is-primary" @click="setApiUrl" :disabled="apiUrl === ''"> <x-button @click="setApiUrl" :disabled="apiUrl === ''">
Change Change
</a> </x-button>
</div> </div>
</div> </div>
</div> </div>
<div class="api-url-info" v-else> <div class="api-url-info" v-else>
Sign in to your Vikunja account on <span v-tooltip="apiUrl">{{ apiDomain() }}</span><br/> Sign in to your Vikunja account on
<a @click="() => configureApi = true">change</a> <span v-tooltip="apiUrl"> {{ apiDomain() }} </span>
<br />
<a @click="() => (configureApi = true)">change</a>
</div> </div>
<div class="notification is-success mt-2" v-if="successMsg !== '' && errorMsg === ''"> <div
class="notification is-success mt-2"
v-if="successMsg !== '' && errorMsg === ''"
>
{{ successMsg }} {{ successMsg }}
</div> </div>
<div class="notification is-danger mt-2" v-if="errorMsg !== '' && successMsg === ''"> <div
class="notification is-danger mt-2"
v-if="errorMsg !== '' && successMsg === ''"
>
{{ errorMsg }} {{ errorMsg }}
</div> </div>
</div> </div>
@ -57,7 +66,9 @@ export default {
if (window.API_URL.startsWith('/api/v1')) { if (window.API_URL.startsWith('/api/v1')) {
return window.location.host return window.location.host
} }
const urlParts = window.API_URL.replace('http://', '').replace('https://', '').split(/[/?#]/) const urlParts = window.API_URL.replace('http://', '')
.replace('https://', '')
.split(/[/?#]/)
return urlParts[0] return urlParts[0]
}, },
setApiUrl() { setApiUrl() {
@ -68,7 +79,10 @@ export default {
let urlToCheck = this.apiUrl let urlToCheck = this.apiUrl
// Check if the url has an http prefix // Check if the url has an http prefix
if (!urlToCheck.startsWith('http://') && !urlToCheck.startsWith('https://')) { if (
!urlToCheck.startsWith('http://') &&
!urlToCheck.startsWith('https://')
) {
urlToCheck = `http://${urlToCheck}` urlToCheck = `http://${urlToCheck}`
} }
@ -79,17 +93,21 @@ export default {
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
// Check if the api is reachable at the provided url // Check if the api is reachable at the provided url
this.$store.dispatch('config/update') this.$store
.catch(e => { .dispatch('config/update')
.catch((e) => {
// Check if it is reachable at /api/v1 and http // Check if it is reachable at /api/v1 and http
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) { if (
!urlToCheck.pathname.endsWith('/api/v1') &&
!urlToCheck.pathname.endsWith('/api/v1/')
) {
urlToCheck.pathname = `${urlToCheck.pathname}api/v1` urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) return Promise.reject(e)
}) })
.catch(e => { .catch((e) => {
// Check if it has a port and if not check if it is reachable at https // Check if it has a port and if not check if it is reachable at https
if (urlToCheck.protocol === 'http:') { if (urlToCheck.protocol === 'http:') {
urlToCheck.protocol = 'https:' urlToCheck.protocol = 'https:'
@ -98,17 +116,20 @@ export default {
} }
return Promise.reject(e) return Promise.reject(e)
}) })
.catch(e => { .catch((e) => {
// Check if it is reachable at /api/v1 and https // Check if it is reachable at /api/v1 and https
urlToCheck.pathname = origUrlToCheck.pathname urlToCheck.pathname = origUrlToCheck.pathname
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) { if (
!urlToCheck.pathname.endsWith('/api/v1') &&
!urlToCheck.pathname.endsWith('/api/v1/')
) {
urlToCheck.pathname = `${urlToCheck.pathname}api/v1` urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) return Promise.reject(e)
}) })
.catch(e => { .catch((e) => {
// Check if it is reachable at port 3456 and https // Check if it is reachable at port 3456 and https
if (urlToCheck.port !== 3456) { if (urlToCheck.port !== 3456) {
urlToCheck.protocol = 'https:' urlToCheck.protocol = 'https:'
@ -118,17 +139,20 @@ export default {
} }
return Promise.reject(e) return Promise.reject(e)
}) })
.catch(e => { .catch((e) => {
// Check if it is reachable at :3456 and /api/v1 and https // Check if it is reachable at :3456 and /api/v1 and https
urlToCheck.pathname = origUrlToCheck.pathname urlToCheck.pathname = origUrlToCheck.pathname
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) { if (
!urlToCheck.pathname.endsWith('/api/v1') &&
!urlToCheck.pathname.endsWith('/api/v1/')
) {
urlToCheck.pathname = `${urlToCheck.pathname}api/v1` urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
} }
return Promise.reject(e) return Promise.reject(e)
}) })
.catch(e => { .catch((e) => {
// Check if it is reachable at port 3456 and http // Check if it is reachable at port 3456 and http
if (urlToCheck.port !== 3456) { if (urlToCheck.port !== 3456) {
urlToCheck.protocol = 'http:' urlToCheck.protocol = 'http:'
@ -138,10 +162,13 @@ export default {
} }
return Promise.reject(e) return Promise.reject(e)
}) })
.catch(e => { .catch((e) => {
// Check if it is reachable at :3456 and /api/v1 and http // Check if it is reachable at :3456 and /api/v1 and http
urlToCheck.pathname = origUrlToCheck.pathname urlToCheck.pathname = origUrlToCheck.pathname
if (!urlToCheck.pathname.endsWith('/api/v1') && !urlToCheck.pathname.endsWith('/api/v1/')) { if (
!urlToCheck.pathname.endsWith('/api/v1') &&
!urlToCheck.pathname.endsWith('/api/v1/')
) {
urlToCheck.pathname = `${urlToCheck.pathname}api/v1` urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update') return this.$store.dispatch('config/update')
@ -154,7 +181,7 @@ export default {
this.errorMsg = `Could not find or use Vikunja installation at "${this.apiDomain()}".` this.errorMsg = `Could not find or use Vikunja installation at "${this.apiDomain()}".`
window.API_URL = oldUrl window.API_URL = oldUrl
}) })
.then(r => { .then((r) => {
if (typeof r !== 'undefined') { if (typeof r !== 'undefined') {
// Set it + save it to local storage to save us the hoops // Set it + save it to local storage to save us the hoops
this.errorMsg = '' this.errorMsg = ''

View file

@ -0,0 +1,39 @@
<template>
<div class="card">
<header class="card-header" v-if="title !== ''">
<p class="card-header-title">
{{ title }}
</p>
<a @click="$emit('close')" class="card-header-icon" v-if="hasClose">
<span class="icon">
<icon icon="angle-right"/>
</span>
</a>
</header>
<div class="card-content" :class="{'p-0': !padding}">
<div class="content">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'card',
props: {
title: {
type: String,
default: '',
},
padding: {
type: Boolean,
default: true,
},
hasClose: {
type: Boolean,
default: false,
},
},
}
</script>

View file

@ -2,18 +2,10 @@
<div class="modal-mask keyboard-shortcuts-modal"> <div class="modal-mask keyboard-shortcuts-modal">
<div @click.self="close()" class="modal-container"> <div @click.self="close()" class="modal-container">
<div class="modal-content"> <div class="modal-content">
<div class="card has-background-white has-no-shadow"> <card class="has-background-white has-no-shadow" title="Available Keyboard Shortcuts">
<header class="card-header">
<p class="card-header-title">Available Keyboard Shortcuts</p>
</header>
<div class="card-content content">
<p> <p>
<strong>Toggle The Menu</strong> <strong>Toggle The Menu</strong>
<span class="shortcuts"> <shortcut :keys="['ctrl', 'e']"/>
<span>ctrl</span>
<i>+</i>
<span>e</span>
</span>
</p> </p>
<h3>Kanban</h3> <h3>Kanban</h3>
<div class="message is-primary" v-if="$route.name === 'list.kanban'"> <div class="message is-primary" v-if="$route.name === 'list.kanban'">
@ -23,50 +15,37 @@
</div> </div>
<p> <p>
<strong>Mark a task as done</strong> <strong>Mark a task as done</strong>
<span class="shortcuts"> <shortcut :keys="['ctrl', 'click']"/>
<span>ctrl</span>
<i>+</i>
<span>click</span>
</span>
</p> </p>
<h3>Task Page</h3> <h3>Task Page</h3>
<div class="message is-primary" v-if="$route.name === 'task.detail' || $route.name === 'task.list.detail' || $route.name === 'task.gantt.detail' || $route.name === 'task.kanban.detail' || $route.name === 'task.detail'"> <div
class="message is-primary"
v-if="$route.name === 'task.detail' || $route.name === 'task.list.detail' || $route.name === 'task.gantt.detail' || $route.name === 'task.kanban.detail' || $route.name === 'task.detail'">
<div class="message-body"> <div class="message-body">
These shortcuts work on the current page. These shortcuts work on the current page.
</div> </div>
</div> </div>
<p> <p>
<strong>Assign this task to a user</strong> <strong>Assign this task to a user</strong>
<span class="shortcuts"> <shortcut :keys="['a']"/>
<span>a</span>
</span>
</p> </p>
<p> <p>
<strong>Add labels to this task</strong> <strong>Add labels to this task</strong>
<span class="shortcuts"> <shortcut :keys="['l']"/>
<span>l</span>
</span>
</p> </p>
<p> <p>
<strong>Change the due date of this task</strong> <strong>Change the due date of this task</strong>
<span class="shortcuts"> <shortcut :keys="['d']"/>
<span>d</span>
</span>
</p> </p>
<p> <p>
<strong>Add an attachment to this task</strong> <strong>Add an attachment to this task</strong>
<span class="shortcuts"> <shortcut :keys="['f']"/>
<span>f</span>
</span>
</p> </p>
<p> <p>
<strong>Modify related tasks of this task</strong> <strong>Modify related tasks of this task</strong>
<span class="shortcuts"> <shortcut :keys="['r']"/>
<span>r</span>
</span>
</p> </p>
</div> </card>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -74,9 +53,11 @@
<script> <script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types' import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import Shortcut from '@/components/misc/shortcut'
export default { export default {
name: 'keyboard-shortcuts', name: 'keyboard-shortcuts',
components: {Shortcut},
methods: { methods: {
close() { close() {
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false) this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)

View file

@ -1,27 +1,40 @@
<template> <template>
<notifications position="bottom left" :max="2" class="global-notification"> <notifications position="bottom left" :max="2" class="global-notification">
<template slot="body" slot-scope="props"> <template slot="body" slot-scope="props">
<div :class="['vue-notification-template', 'vue-notification', props.item.type]" @click="close(props)"> <div
:class="[
'vue-notification-template',
'vue-notification',
props.item.type,
]"
@click="close(props)"
>
<div <div
class="notification-title" class="notification-title"
v-html="props.item.title" v-html="props.item.title"
v-if="props.item.title" v-if="props.item.title"
> ></div>
</div>
<div <div
class="notification-content" class="notification-content"
v-html="props.item.text" v-html="props.item.text"
> ></div>
</div>
<div <div
class="buttons is-right" class="buttons is-right"
v-if="props.item.data && props.item.data.actions && props.item.data.actions.length > 0"> v-if="
<button props.item.data &&
:key="'action_'+i" props.item.data.actions &&
props.item.data.actions.length > 0
"
>
<x-button
:key="'action_' + i"
@click="action.callback" @click="action.callback"
class="button has-no-shadow is-small" v-for="(action, i) in props.item.data.actions"> :shadow="false"
class="is-small"
v-for="(action, i) in props.item.data.actions"
>
{{ action.title }} {{ action.title }}
</button> </x-button>
</div> </div>
</div> </div>
</template> </template>
@ -40,12 +53,11 @@ export default {
</script> </script>
<style scoped> <style scoped>
.vue-notification { .vue-notification {
z-index: 9999; z-index: 9999;
} }
.buttons { .buttons {
margin-top: .5em; margin-top: 0.5em;
} }
</style> </style>

View file

@ -0,0 +1,20 @@
<template>
<span class="shortcuts">
<template v-for="(k, i) in keys">
<span :key="i">{{ k }}</span>
<i v-if="i < keys.length - 1" :key="`plus${i}`">+</i>
</template>
</span>
</template>
<script>
export default {
name: 'shortcut',
props: {
keys: {
type: Array,
required: true,
}
},
}
</script>

View file

@ -11,8 +11,20 @@
<slot name="text"></slot> <slot name="text"></slot>
</div> </div>
<div class="actions"> <div class="actions">
<button @click="$emit('close')" class="button is-text has-text-danger is-inverted has-no-shadow underline-none">Cancel</button> <x-button
<button @click="$emit('submit')" class="button is-primary has-no-shadow">Do it!</button> @click="$emit('close')"
type="tertary"
class="has-text-danger"
>
Cancel
</x-button>
<x-button
@click="$emit('submit')"
type="primary"
:shadow="false"
>
Do it!
</x-button>
</div> </div>
</slot> </slot>
</div> </div>

View file

@ -1,33 +1,29 @@
<template> <template>
<div class="card is-fullwidth"> <card title="Share links" class="is-fullwidth" :padding="false">
<header class="card-header"> <div class="sharables-list">
<p class="card-header-title"> <div class="p-4">
Share links <p>Share with a link:</p>
</p>
</header>
<div class="card-content content sharables-list">
<form @submit.prevent="add()" class="add-form">
<p>
Share with a link:
</p>
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="selectedRight"> <select v-model="selectedRight">
<option :value="rights.READ">Read only</option> <option :value="rights.READ">Read only</option>
<option :value="rights.READ_WRITE">Read & write</option> <option :value="rights.READ_WRITE">
Read & write
</option>
<option :value="rights.ADMIN">Admin</option> <option :value="rights.ADMIN">Admin</option>
</select> </select>
</div> </div>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-primary" type="submit"> <x-button @click="add"> Share</x-button>
Share
</button>
</div> </div>
</div> </div>
</form> </div>
<table class="table is-striped is-hoverable is-fullwidth link-share-list" v-if="linkShares.length > 0"> <table
class="table is-striped is-hoverable is-fullwidth link-share-list"
v-if="linkShares.length > 0"
>
<thead> <thead>
<tr> <tr>
<th>Link</th> <th>Link</th>
@ -41,14 +37,23 @@
<td> <td>
<div class="field has-addons no-input-mobile"> <div class="field has-addons no-input-mobile">
<div class="control"> <div class="control">
<input :value="getShareLink(s.hash)" class="input" readonly type="text"/> <input
:value="getShareLink(s.hash)"
class="input"
readonly
type="text"
/>
</div> </div>
<div class="control"> <div class="control">
<a @click="copy(getShareLink(s.hash))" class="button is-primary has-no-shadow" v-tooltip="'Copy to clipboard'"> <x-button
@click="copy(getShareLink(s.hash))"
:shadow="false"
v-tooltip="'Copy to clipboard'"
>
<span class="icon"> <span class="icon">
<icon icon="paste"/> <icon icon="paste"/>
</span> </span>
</a> </x-button>
</div> </div>
</div> </div>
</td> </td>
@ -76,11 +81,16 @@
</template> </template>
</td> </td>
<td class="actions"> <td class="actions">
<button @click="() => {linkIdToDelete = s.id; showDeleteModal = true}" class="button is-danger icon-only"> <x-button
<span class="icon"> @click="
<icon icon="trash-alt"/> () => {
</span> linkIdToDelete = s.id
</button> showDeleteModal = true
}
"
class="is-danger"
icon="trash-alt"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -90,13 +100,17 @@
<modal <modal
@close="showDeleteModal = false" @close="showDeleteModal = false"
@submit="remove()" @submit="remove()"
v-if="showDeleteModal"> v-if="showDeleteModal"
>
<span slot="header">Remove a link share</span> <span slot="header">Remove a link share</span>
<p slot="text">Are you sure you want to remove this link share?<br/> <p slot="text">
It will no longer be possible to access this list with this link share.<br/> Are you sure you want to remove this link share?<br/>
<b>This CANNOT BE UNDONE!</b></p> It will no longer be possible to access this list with this link
share.<br/>
<b>This CANNOT BE UNDONE!</b>
</p>
</modal> </modal>
</div> </card>
</template> </template>
<script> <script>
@ -135,12 +149,13 @@ export default {
this.load() this.load()
}, },
watch: { watch: {
listId() { // watch it listId() {
// watch it
this.load() this.load()
}, },
}, },
computed: mapState({ computed: mapState({
frontendUrl: state => state.config.frontendUrl, frontendUrl: (state) => state.config.frontendUrl,
}), }),
methods: { methods: {
load() { load() {
@ -149,34 +164,49 @@ export default {
return return
} }
this.linkShareService.getAll({listId: this.listId}) this.linkShareService
.then(r => { .getAll({listId: this.listId})
.then((r) => {
this.linkShares = r this.linkShares = r
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
add() { add() {
let newLinkShare = new LinkShareModel({right: this.selectedRight, listId: this.listId}) let newLinkShare = new LinkShareModel({
this.linkShareService.create(newLinkShare) right: this.selectedRight,
listId: this.listId,
})
this.linkShareService
.create(newLinkShare)
.then(() => { .then(() => {
this.selectedRight = rights.READ this.selectedRight = rights.READ
this.success({message: 'The link share was successfully created'}, this) this.success(
{message: 'The link share was successfully created'},
this
)
this.load() this.load()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
remove() { remove() {
let linkshare = new LinkShareModel({id: this.linkIdToDelete, listId: this.listId}) let linkshare = new LinkShareModel({
this.linkShareService.delete(linkshare) id: this.linkIdToDelete,
listId: this.listId,
})
this.linkShareService
.delete(linkshare)
.then(() => { .then(() => {
this.success({message: 'The link share was successfully deleted'}, this) this.success(
{message: 'The link share was successfully deleted'},
this
)
this.load() this.load()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
.finally(() => { .finally(() => {

View file

@ -1,13 +1,11 @@
<template> <template>
<div class="card is-fullwidth has-overflow"> <card class="is-fullwidth has-overflow" :title="`Shared with these ${shareType}s`" :padding="false">
<header class="card-header"> <div class="p-4" v-if="userIsAdmin">
<p class="card-header-title">
Shared with these {{ shareType }}s
</p>
</header>
<div class="card-content" v-if="userIsAdmin">
<div class="field has-addons"> <div class="field has-addons">
<p class="control is-expanded" v-bind:class="{ 'is-loading': searchService.loading}"> <p
class="control is-expanded"
v-bind:class="{ 'is-loading': searchService.loading }"
>
<multiselect <multiselect
:loading="searchService.loading" :loading="searchService.loading"
placeholder="Type to search..." placeholder="Type to search..."
@ -18,9 +16,7 @@
/> />
</p> </p>
<p class="control"> <p class="control">
<button class="button is-primary" @click="add()"> <x-button @click="add()"> Share </x-button>
Share
</button>
</p> </p>
</div> </div>
</div> </div>
@ -37,7 +33,12 @@
</template> </template>
<template v-if="shareType === 'team'"> <template v-if="shareType === 'team'">
<td> <td>
<router-link :to="{name: 'teams.edit', params: {id: s.id}}"> <router-link
:to="{
name: 'teams.edit',
params: { id: s.id },
}"
>
{{ s.name }} {{ s.name }}
</router-link> </router-link>
</td> </td>
@ -45,39 +46,60 @@
<td class="type"> <td class="type">
<template v-if="s.right === rights.ADMIN"> <template v-if="s.right === rights.ADMIN">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="lock"/> <icon icon="lock" />
</span> </span>
Admin Admin
</template> </template>
<template v-else-if="s.right === rights.READ_WRITE"> <template v-else-if="s.right === rights.READ_WRITE">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="pen"/> <icon icon="pen" />
</span> </span>
Write Write
</template> </template>
<template v-else> <template v-else>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="users"/> <icon icon="users" />
</span> </span>
Read-only Read-only
</template> </template>
</td> </td>
<td class="actions" v-if="userIsAdmin"> <td class="actions" v-if="userIsAdmin">
<div class="select"> <div class="select">
<select @change="toggleType(s)" class="button buttonright" v-model="selectedRight[s.id]"> <select
<option :selected="s.right === rights.READ" :value="rights.READ">Read only</option> @change="toggleType(s)"
<option :selected="s.right === rights.READ_WRITE" :value="rights.READ_WRITE">Read & class="button mr-2"
write v-model="selectedRight[s.id]"
>
<option
:selected="s.right === rights.READ"
:value="rights.READ"
>
Read only
</option>
<option
:selected="s.right === rights.READ_WRITE"
:value="rights.READ_WRITE"
>
Read & write
</option>
<option
:selected="s.right === rights.ADMIN"
:value="rights.ADMIN"
>
Admin
</option> </option>
<option :selected="s.right === rights.ADMIN" :value="rights.ADMIN">Admin</option>
</select> </select>
</div> </div>
<button @click="() => {sharable = s; showDeleteModal = true}" <x-button
class="button is-danger icon-only"> @click="
<span class="icon"> () => {
<icon icon="trash-alt"/> sharable = s
</span> showDeleteModal = true
</button> }
"
class="is-danger"
icon="trash-alt"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -86,16 +108,22 @@
<modal <modal
@close="showDeleteModal = false" @close="showDeleteModal = false"
@submit="deleteSharable()" @submit="deleteSharable()"
v-if="showDeleteModal"> v-if="showDeleteModal"
<span slot="header">Remove a {{ shareType }} from the {{ typeString }}</span> >
<p slot="text">Are you sure you want to remove this {{ shareType }} from the {{ typeString }}?<br/> <span slot="header"
<b>This CANNOT BE UNDONE!</b></p> >Remove a {{ shareType }} from the {{ typeString }}</span
>
<p slot="text">
Are you sure you want to remove this {{ shareType }} from the
{{ typeString }}?<br />
<b>This CANNOT BE UNDONE!</b>
</p>
</modal> </modal>
</div> </card>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import { mapState } from 'vuex'
import UserNamespaceService from '../../services/userNamespace' import UserNamespaceService from '../../services/userNamespace'
import UserNamespaceModel from '../../models/userNamespace' import UserNamespaceModel from '../../models/userNamespace'
@ -155,10 +183,9 @@ export default {
Multiselect, Multiselect,
}, },
computed: mapState({ computed: mapState({
userInfo: state => state.auth.info, userInfo: (state) => state.auth.info,
}), }),
created() { created() {
if (this.shareType === 'user') { if (this.shareType === 'user') {
this.searchService = new UserService() this.searchService = new UserService()
this.sharable = new UserModel() this.sharable = new UserModel()
@ -167,11 +194,13 @@ export default {
if (this.type === 'list') { if (this.type === 'list') {
this.typeString = `list` this.typeString = `list`
this.stuffService = new UserListService() this.stuffService = new UserListService()
this.stuffModel = new UserListModel({listId: this.id}) this.stuffModel = new UserListModel({ listId: this.id })
} else if (this.type === 'namespace') { } else if (this.type === 'namespace') {
this.typeString = `namespace` this.typeString = `namespace`
this.stuffService = new UserNamespaceService() this.stuffService = new UserNamespaceService()
this.stuffModel = new UserNamespaceModel({namespaceId: this.id}) this.stuffModel = new UserNamespaceModel({
namespaceId: this.id,
})
} else { } else {
throw new Error('Unknown type: ' + this.type) throw new Error('Unknown type: ' + this.type)
} }
@ -183,11 +212,13 @@ export default {
if (this.type === 'list') { if (this.type === 'list') {
this.typeString = `list` this.typeString = `list`
this.stuffService = new TeamListService() this.stuffService = new TeamListService()
this.stuffModel = new TeamListModel({listId: this.id}) this.stuffModel = new TeamListModel({ listId: this.id })
} else if (this.type === 'namespace') { } else if (this.type === 'namespace') {
this.typeString = `namespace` this.typeString = `namespace`
this.stuffService = new TeamNamespaceService() this.stuffService = new TeamNamespaceService()
this.stuffModel = new TeamNamespaceModel({namespaceId: this.id}) this.stuffModel = new TeamNamespaceModel({
namespaceId: this.id,
})
} else { } else {
throw new Error('Unknown type: ' + this.type) throw new Error('Unknown type: ' + this.type)
} }
@ -199,36 +230,51 @@ export default {
}, },
methods: { methods: {
load() { load() {
this.stuffService.getAll(this.stuffModel) this.stuffService
.then(r => { .getAll(this.stuffModel)
.then((r) => {
this.$set(this, 'sharables', r) this.$set(this, 'sharables', r)
r.forEach(s => this.$set(this.selectedRight, s.id, s.right)) r.forEach((s) =>
this.$set(this.selectedRight, s.id, s.right)
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
deleteSharable() { deleteSharable() {
if (this.shareType === 'user') { if (this.shareType === 'user') {
this.stuffModel.userId = this.sharable.username this.stuffModel.userId = this.sharable.username
} else if (this.shareType === 'team') { } else if (this.shareType === 'team') {
this.stuffModel.teamId = this.sharable.id this.stuffModel.teamId = this.sharable.id
} }
this.stuffService.delete(this.stuffModel) this.stuffService
.delete(this.stuffModel)
.then(() => { .then(() => {
this.showDeleteModal = false this.showDeleteModal = false
for (const i in this.sharables) { for (const i in this.sharables) {
if ( if (
(this.sharables[i].id === this.stuffModel.userId && this.shareType === 'user') || (this.sharables[i].id === this.stuffModel.userId &&
(this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team') this.shareType === 'user') ||
(this.sharables[i].id === this.stuffModel.teamId &&
this.shareType === 'team')
) { ) {
this.sharables.splice(i, 1) this.sharables.splice(i, 1)
} }
} }
this.success({message: 'The ' + this.shareType + ' was successfully deleted from the ' + this.typeString + '.'}, this) this.success(
{
message:
'The ' +
this.shareType +
' was successfully deleted from the ' +
this.typeString +
'.',
},
this
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
@ -247,17 +293,27 @@ export default {
this.stuffModel.teamId = this.sharable.id this.stuffModel.teamId = this.sharable.id
} }
this.stuffService.create(this.stuffModel) this.stuffService
.create(this.stuffModel)
.then(() => { .then(() => {
this.success({message: 'The ' + this.shareType + ' was successfully added.'}, this) this.success(
{
message:
'The ' +
this.shareType +
' was successfully added.',
},
this
)
this.load() this.load()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
toggleType(sharable) { toggleType(sharable) {
if (this.selectedRight[sharable.id] !== rights.ADMIN && if (
this.selectedRight[sharable.id] !== rights.ADMIN &&
this.selectedRight[sharable.id] !== rights.READ && this.selectedRight[sharable.id] !== rights.READ &&
this.selectedRight[sharable.id] !== rights.READ_WRITE this.selectedRight[sharable.id] !== rights.READ_WRITE
) { ) {
@ -265,26 +321,37 @@ export default {
} }
this.stuffModel.right = this.selectedRight[sharable.id] this.stuffModel.right = this.selectedRight[sharable.id]
if (this.shareType === 'user') { if (this.shareType === 'user') {
this.stuffModel.userId = sharable.username this.stuffModel.userId = sharable.username
} else if (this.shareType === 'team') { } else if (this.shareType === 'team') {
this.stuffModel.teamId = sharable.id this.stuffModel.teamId = sharable.id
} }
this.stuffService.update(this.stuffModel) this.stuffService
.then(r => { .update(this.stuffModel)
.then((r) => {
for (const i in this.sharables) { for (const i in this.sharables) {
if ( if (
(this.sharables[i].username === this.stuffModel.userId && this.shareType === 'user') || (this.sharables[i].username ===
(this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team') this.stuffModel.userId &&
this.shareType === 'user') ||
(this.sharables[i].id === this.stuffModel.teamId &&
this.shareType === 'team')
) { ) {
this.$set(this.sharables[i], 'right', r.right) this.$set(this.sharables[i], 'right', r.right)
} }
} }
this.success({message: 'The ' + this.shareType + ' right was successfully updated.'}, this) this.success(
{
message:
'The ' +
this.shareType +
' right was successfully updated.',
},
this
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
@ -294,11 +361,12 @@ export default {
return return
} }
this.searchService.getAll({}, {s: query}) this.searchService
.then(response => { .getAll({}, { s: query })
.then((response) => {
this.$set(this, 'found', response) this.$set(this, 'found', response)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },

View file

@ -4,7 +4,7 @@
<label class="label" for="tasktext">Task Text</label> <label class="label" for="tasktext">Task Text</label>
<div class="control"> <div class="control">
<input <input
:class="{ 'disabled': taskService.loading}" :class="{ disabled: taskService.loading }"
:disabled="taskService.loading" :disabled="taskService.loading"
@change="editTaskSubmit()" @change="editTaskSubmit()"
class="input" class="input"
@ -12,7 +12,8 @@
placeholder="The task text is here..." placeholder="The task text is here..."
type="text" type="text"
v-focus v-focus
v-model="taskEditTask.title"/> v-model="taskEditTask.title"
/>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
@ -29,20 +30,24 @@
</div> </div>
<b>Reminder Dates</b> <b>Reminder Dates</b>
<reminders @change="editTaskSubmit()" v-model="taskEditTask.reminderDates"/> <reminders
@change="editTaskSubmit()"
v-model="taskEditTask.reminderDates"
/>
<div class="field"> <div class="field">
<label class="label" for="taskduedate">Due Date</label> <label class="label" for="taskduedate">Due Date</label>
<div class="control"> <div class="control">
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ disabled: taskService.loading }"
:config="flatPickerConfig" :config="flatPickerConfig"
:disabled="taskService.loading" :disabled="taskService.loading"
@on-close="editTaskSubmit()" @on-close="editTaskSubmit()"
class="input" class="input"
id="taskduedate" id="taskduedate"
placeholder="The tasks due date is here..." placeholder="The tasks due date is here..."
v-model="taskEditTask.dueDate"> v-model="taskEditTask.dueDate"
>
</flat-pickr> </flat-pickr>
</div> </div>
</div> </div>
@ -52,26 +57,28 @@
<div class="control columns"> <div class="control columns">
<div class="column"> <div class="column">
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ disabled: taskService.loading }"
:config="flatPickerConfig" :config="flatPickerConfig"
:disabled="taskService.loading" :disabled="taskService.loading"
@on-close="editTaskSubmit()" @on-close="editTaskSubmit()"
class="input" class="input"
id="taskduedate" id="taskduedate"
placeholder="Start date" placeholder="Start date"
v-model="taskEditTask.startDate"> v-model="taskEditTask.startDate"
>
</flat-pickr> </flat-pickr>
</div> </div>
<div class="column"> <div class="column">
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ disabled: taskService.loading }"
:config="flatPickerConfig" :config="flatPickerConfig"
:disabled="taskService.loading" :disabled="taskService.loading"
@on-close="editTaskSubmit()" @on-close="editTaskSubmit()"
class="input" class="input"
id="taskduedate" id="taskduedate"
placeholder="End date" placeholder="End date"
v-model="taskEditTask.endDate"> v-model="taskEditTask.endDate"
>
</flat-pickr> </flat-pickr>
</div> </div>
</div> </div>
@ -79,27 +86,36 @@
<div class="field"> <div class="field">
<label class="label" for="">Repeat after</label> <label class="label" for="">Repeat after</label>
<repeat-after @change="editTaskSubmit()" v-model="taskEditTask.repeatAfter"/> <repeat-after
@change="editTaskSubmit()"
v-model="taskEditTask.repeatAfter"
/>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="">Priority</label> <label class="label" for="">Priority</label>
<div class="control priority-select"> <div class="control priority-select">
<priority-select @change="editTaskSubmit()" v-model="taskEditTask.priority"/> <priority-select
@change="editTaskSubmit()"
v-model="taskEditTask.priority"
/>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Percent Done</label> <label class="label">Percent Done</label>
<div class="control"> <div class="control">
<percent-done-select @change="editTaskSubmit()" v-model="taskEditTask.percentDone"/> <percent-done-select
@change="editTaskSubmit()"
v-model="taskEditTask.percentDone"
/>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Color</label> <label class="label">Color</label>
<div class="control"> <div class="control">
<color-picker v-model="taskEditTask.hexColor"/> <color-picker v-model="taskEditTask.hexColor" />
</div> </div>
</div> </div>
@ -109,7 +125,7 @@
<li :key="a.id" v-for="(a, index) in taskEditTask.assignees"> <li :key="a.id" v-for="(a, index) in taskEditTask.assignees">
{{ a.getDisplayName() }} {{ a.getDisplayName() }}
<a @click="deleteAssigneeByIndex(index)"> <a @click="deleteAssigneeByIndex(index)">
<icon icon="times"/> <icon icon="times" />
</a> </a>
</li> </li>
</ul> </ul>
@ -120,14 +136,18 @@
<edit-assignees <edit-assignees
:initial-assignees="taskEditTask.assignees" :initial-assignees="taskEditTask.assignees"
:list-id="taskEditTask.listId" :list-id="taskEditTask.listId"
:task-id="taskEditTask.id"/> :task-id="taskEditTask.id"
/>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Labels</label> <label class="label">Labels</label>
<div class="control"> <div class="control">
<edit-labels :task-id="taskEditTask.id" v-model="taskEditTask.labels"/> <edit-labels
:task-id="taskEditTask.id"
v-model="taskEditTask.labels"
/>
</div> </div>
</div> </div>
@ -138,10 +158,13 @@
class="is-narrow" class="is-narrow"
/> />
<button :class="{ 'is-loading': taskService.loading}" class="button is-primary is-fullwidth" type="submit"> <x-button
:loading="taskService.loading"
class="is-fullwidth"
@click="editTaskSubmit()"
>
Save Save
</button> </x-button>
</form> </form>
</template> </template>
@ -199,7 +222,9 @@ export default {
PrioritySelect, PrioritySelect,
flatPickr, flatPickr,
editor: () => ({ editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'), component: import(
/* webpackChunkName: "editor" */ '../../components/input/editor'
),
loading: LoadingComponent, loading: LoadingComponent,
error: ErrorComponent, error: ErrorComponent,
timeout: 60000, timeout: 60000,
@ -226,24 +251,30 @@ export default {
}, },
methods: { methods: {
initTaskFields() { initTaskFields() {
this.taskEditTask.dueDate = +new Date(this.task.dueDate) === 0 ? null : this.task.dueDate this.taskEditTask.dueDate =
this.taskEditTask.startDate = +new Date(this.task.startDate) === 0 ? null : this.task.startDate +new Date(this.task.dueDate) === 0 ? null : this.task.dueDate
this.taskEditTask.endDate = +new Date(this.task.endDate) === 0 ? null : this.task.endDate this.taskEditTask.startDate =
+new Date(this.task.startDate) === 0
? null
: this.task.startDate
this.taskEditTask.endDate =
+new Date(this.task.endDate) === 0 ? null : this.task.endDate
// This makes the editor trigger its mounted function again which makes it forget every input // 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 // 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 // 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. // not update if new content from the outside was made available.
// See https://github.com/NikulinIlya/vue-easymde/issues/3 // See https://github.com/NikulinIlya/vue-easymde/issues/3
this.editorActive = false this.editorActive = false
this.$nextTick(() => this.editorActive = true) this.$nextTick(() => (this.editorActive = true))
}, },
editTaskSubmit() { editTaskSubmit() {
this.taskService.update(this.taskEditTask) this.taskService
.then(r => { .update(this.taskEditTask)
.then((r) => {
this.$set(this, 'taskEditTask', r) this.$set(this, 'taskEditTask', r)
this.initTaskFields() this.initTaskFields()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },

View file

@ -2,12 +2,13 @@
<div class="gantt-chart box"> <div class="gantt-chart box">
<div class="filter-container"> <div class="filter-container">
<div class="items"> <div class="items">
<button @click.prevent.stop="showTaskFilter = !showTaskFilter" class="button"> <x-button
<span class="icon is-small"> @click.prevent.stop="showTaskFilter = !showTaskFilter"
<icon icon="filter"/> type="secondary"
</span> icon="filter"
>
Filters Filters
</button> </x-button>
</div> </div>
<filter-popup <filter-popup
@change="loadTasks" @change="loadTasks"
@ -18,21 +19,37 @@
<div class="dates"> <div class="dates">
<template v-for="(y, yk) in days"> <template v-for="(y, yk) in days">
<div :key="yk + 'year'" class="months"> <div :key="yk + 'year'" class="months">
<div :key="mk + 'month'" class="month" v-for="(m, mk) in days[yk]"> <div
{{ new Date((new Date(yk)).setMonth(mk)).toLocaleString('en-us', {month: 'long'}) }}, :key="mk + 'month'"
{{ (new Date(yk)).getFullYear() }} class="month"
v-for="(m, mk) in days[yk]"
>
{{
new Date(
new Date(yk).setMonth(mk)
).toLocaleString('en-us', {month: 'long'})
}},
{{ new Date(yk).getFullYear() }}
<div class="days"> <div class="days">
<div <div
:class="{'today': d.toDateString() === now.toDateString()}" :class="{
today:
d.toDateString() === now.toDateString(),
}"
:key="dk + 'day'" :key="dk + 'day'"
:style="{'width': dayWidth + 'px'}" :style="{ width: dayWidth + 'px' }"
class="day" class="day"
v-for="(d, dk) in days[yk][mk]"> v-for="(d, dk) in days[yk][mk]"
>
<span class="theday" v-if="dayWidth > 25"> <span class="theday" v-if="dayWidth > 25">
{{ d.getDate() }} {{ d.getDate() }}
</span> </span>
<span class="weekday" v-if="dayWidth > 25"> <span class="weekday" v-if="dayWidth > 25">
{{ d.toLocaleString('en-us', {weekday: 'short'}) }} {{
d.toLocaleString('en-us', {
weekday: 'short',
})
}}
</span> </span>
</div> </div>
</div> </div>
@ -40,18 +57,28 @@
</div> </div>
</template> </template>
</div> </div>
<div :style="{'width': fullWidth + 'px'}" class="tasks"> <div :style="{ width: fullWidth + 'px' }" class="tasks">
<div <div
:key="t.id" :key="t.id"
:style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}" :style="{
background:
'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' +
(k % 2 === 0
? '#fafafa 1px, #fafafa '
: '#fff 1px, #fff ') +
dayWidth +
'px)',
}"
class="row" class="row"
v-for="(t, k) in theTasks"> v-for="(t, k) in theTasks"
>
<VueDragResize <VueDragResize
:class="{ :class="{
'done': t.done, done: t.done,
'is-current-edit': taskToEdit !== null && taskToEdit.id === t.id, 'is-current-edit':
taskToEdit !== null && taskToEdit.id === t.id,
'has-light-text': !colorIsDark(t.hexColor), 'has-light-text': !colorIsDark(t.hexColor),
'has-dark-text': colorIsDark(t.hexColor) 'has-dark-text': colorIsDark(t.hexColor),
}" }"
:gridX="dayWidth" :gridX="dayWidth"
:h="31" :h="31"
@ -61,7 +88,10 @@
:parentW="fullWidth" :parentW="fullWidth"
:snapToGrid="true" :snapToGrid="true"
:sticks="['mr', 'ml']" :sticks="['mr', 'ml']"
:style="{'border-color': t.hexColor, 'background-color': t.hexColor}" :style="{
'border-color': t.hexColor,
'background-color': t.hexColor,
}"
:w="t.durationDays * dayWidth" :w="t.durationDays * dayWidth"
:x="t.offsetDays * dayWidth - 6" :x="t.offsetDays * dayWidth - 6"
:y="0" :y="0"
@ -71,11 +101,16 @@
axis="x" axis="x"
class="task" class="task"
> >
<span :class="{ <span
:class="{
'has-high-priority': t.priority >= priorities.HIGH, 'has-high-priority': t.priority >= priorities.HIGH,
'has-not-so-high-priority': t.priority === priorities.HIGH, 'has-not-so-high-priority':
'has-super-high-priority': t.priority === priorities.DO_NOW t.priority === priorities.HIGH,
}">{{ t.title }}</span> 'has-super-high-priority':
t.priority === priorities.DO_NOW,
}"
>{{ t.title }}</span
>
<priority-label :priority="t.priority"/> <priority-label :priority="t.priority"/>
<!-- using the key here forces vue to use the updated version model and not the response returned by the api --> <!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
<a @click="editTask(theTasks[k])" class="edit-toggle"> <a @click="editTask(theTasks[k])" class="edit-toggle">
@ -86,9 +121,18 @@
<template v-if="showTaskswithoutDates"> <template v-if="showTaskswithoutDates">
<div <div
:key="t.id" :key="t.id"
:style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}" :style="{
background:
'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' +
(k % 2 === 0
? '#fafafa 1px, #fafafa '
: '#fff 1px, #fff ') +
dayWidth +
'px)',
}"
class="row" class="row"
v-for="(t, k) in tasksWithoutDates"> v-for="(t, k) in tasksWithoutDates"
>
<VueDragResize <VueDragResize
:gridX="dayWidth" :gridX="dayWidth"
:h="31" :h="31"
@ -112,7 +156,11 @@
</div> </div>
</template> </template>
</div> </div>
<form @submit.prevent="addNewTask()" class="add-new-task" v-if="canWrite"> <form
@submit.prevent="addNewTask()"
class="add-new-task"
v-if="canWrite"
>
<transition name="width"> <transition name="width">
<input <input
@blur="hideCrateNewTask" @blur="hideCrateNewTask"
@ -124,31 +172,20 @@
v-model="newTaskTitle" v-model="newTaskTitle"
/> />
</transition> </transition>
<button @click="showCreateNewTask" class="button is-primary has-no-shadow"> <x-button @click="showCreateNewTask" :shadow="false" icon="plus">
<span class="icon is-small">
<icon icon="plus"/>
</span>
Add a new task Add a new task
</button> </x-button>
</form> </form>
<transition name="fade"> <transition name="fade">
<div class="card taskedit" v-if="isTaskEdit"> <card
<header class="card-header"> v-if="isTaskEdit"
<p class="card-header-title"> class="taskedit"
Edit Task title="Edit Task"
</p> @close="() => {isTaskEdit = false;taskToEdit = null}"
<a @click="() => {isTaskEdit = false; taskToEdit = null}" class="card-header-icon"> :has-close="true"
<span class="icon"> >
<icon icon="times"/>
</span>
</a>
</header>
<div class="card-content">
<div class="content">
<edit-task :task="taskToEdit"/> <edit-task :task="taskToEdit"/>
</div> </card>
</div>
</div>
</transition> </transition>
</div> </div>
</template> </template>
@ -184,10 +221,10 @@ export default {
default: false, default: false,
}, },
dateFrom: { dateFrom: {
default: new Date((new Date()).setDate((new Date()).getDate() - 15)), default: new Date(new Date().setDate(new Date().getDate() - 15)),
}, },
dateTo: { dateTo: {
default: new Date((new Date()).setDate((new Date()).getDate() + 30)), default: new Date(new Date().setDate(new Date().getDate() + 30)),
}, },
// The width of a day in pixels, used to calculate all sorts of things. // The width of a day in pixels, used to calculate all sorts of things.
dayWidth: { dayWidth: {
@ -226,9 +263,9 @@ export default {
} }
}, },
watch: { watch: {
'dateFrom': 'buildTheGanttChart', dateFrom: 'buildTheGanttChart',
'dateTo': 'buildTheGanttChart', dateTo: 'buildTheGanttChart',
'listId': 'parseTasks', listId: 'parseTasks',
}, },
created() { created() {
this.now = new Date() this.now = new Date()
@ -240,7 +277,7 @@ export default {
this.buildTheGanttChart() this.buildTheGanttChart()
}, },
computed: mapState({ computed: mapState({
canWrite: state => state.currentList.maxRight > Rights.READ, canWrite: (state) => state.currentList.maxRight > Rights.READ,
}), }),
methods: { methods: {
buildTheGanttChart() { buildTheGanttChart() {
@ -252,17 +289,26 @@ export default {
this.startDate = new Date(this.dateFrom) this.startDate = new Date(this.dateFrom)
this.endDate = new Date(this.dateTo) this.endDate = new Date(this.dateTo)
this.dayOffsetUntilToday = Math.floor((this.now - this.startDate) / 1000 / 60 / 60 / 24) + 1 this.dayOffsetUntilToday =
Math.floor((this.now - this.startDate) / 1000 / 60 / 60 / 24) +
1
}, },
prepareGanttDays() { prepareGanttDays() {
// Layout: years => [months => [days]] // Layout: years => [months => [days]]
let years = {} let years = {}
for (let d = this.startDate; d <= this.endDate; d.setDate(d.getDate() + 1)) { for (
let d = this.startDate;
d <= this.endDate;
d.setDate(d.getDate() + 1)
) {
let date = new Date(d) let date = new Date(d)
if (years[date.getFullYear() + ''] === undefined) { if (years[date.getFullYear() + ''] === undefined) {
years[date.getFullYear() + ''] = {} years[date.getFullYear() + ''] = {}
} }
if (years[date.getFullYear() + ''][date.getMonth() + ''] === undefined) { if (
years[date.getFullYear() + ''][date.getMonth() + ''] ===
undefined
) {
years[date.getFullYear() + ''][date.getMonth() + ''] = [] years[date.getFullYear() + ''][date.getMonth() + ''] = []
} }
years[date.getFullYear() + ''][date.getMonth() + ''].push(date) years[date.getFullYear() + ''][date.getMonth() + ''].push(date)
@ -279,79 +325,86 @@ export default {
this.$set(this, 'tasksWithoutDates', []) this.$set(this, 'tasksWithoutDates', [])
const getAllTasks = (page = 1) => { const getAllTasks = (page = 1) => {
return this.taskCollectionService.getAll({listId: this.listId}, this.params, page) return this.taskCollectionService
.then(tasks => { .getAll({listId: this.listId}, this.params, page)
.then((tasks) => {
if (page < this.taskCollectionService.totalPages) { if (page < this.taskCollectionService.totalPages) {
return getAllTasks(page + 1) return getAllTasks(page + 1).then((nextTasks) => {
.then(nextTasks => {
return tasks.concat(nextTasks) return tasks.concat(nextTasks)
}) })
} else { } else {
return tasks return tasks
} }
}) })
.catch(e => { .catch((e) => {
return Promise.reject(e) return Promise.reject(e)
}) })
} }
getAllTasks() getAllTasks()
.then(tasks => { .then((tasks) => {
this.theTasks = tasks this.theTasks = tasks
.filter(t => { .filter((t) => {
if (t.startDate === null && !t.done) { if (t.startDate === null && !t.done) {
this.tasksWithoutDates.push(t) this.tasksWithoutDates.push(t)
} }
return t.startDate >= this.startDate && t.endDate <= this.endDate return (
t.startDate >= this.startDate &&
t.endDate <= this.endDate
)
}) })
.map(t => { .map((t) => {
return this.addGantAttributes(t) return this.addGantAttributes(t)
}) })
.sort(function (a, b) { .sort(function (a, b) {
if (a.startDate < b.startDate) if (a.startDate < b.startDate) return -1
return -1 if (a.startDate > b.startDate) return 1
if (a.startDate > b.startDate)
return 1
return 0 return 0
}) })
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
addGantAttributes(t) { addGantAttributes(t) {
t.endDate === null ? this.endDate : t.endDate t.endDate === null ? this.endDate : t.endDate
t.durationDays = Math.floor((t.endDate - t.startDate) / 1000 / 60 / 60 / 24) + 1 t.durationDays =
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24) + 1 Math.floor((t.endDate - t.startDate) / 1000 / 60 / 60 / 24) + 1
t.offsetDays =
Math.floor(
(t.startDate - this.startDate) / 1000 / 60 / 60 / 24
) + 1
return t return t
}, },
setTaskDragged(t) { setTaskDragged(t) {
this.taskDragged = t this.taskDragged = t
}, },
resizeTask(newRect) { resizeTask(newRect) {
// Timeout to definitly catch if the user clicked on taskedit // Timeout to definitly catch if the user clicked on taskedit
setTimeout(() => { setTimeout(() => {
if (this.isTaskEdit) { if (this.isTaskEdit) {
return return
} }
let didntHaveDates = this.taskDragged.startDate === null ? true : false let didntHaveDates =
this.taskDragged.startDate === null ? true : false
let startDate = new Date(this.startDate) let startDate = new Date(this.startDate)
startDate.setDate(startDate.getDate() + newRect.left / this.dayWidth) startDate.setDate(
startDate.getDate() + newRect.left / this.dayWidth
)
startDate.setUTCHours(0) startDate.setUTCHours(0)
startDate.setUTCMinutes(0) startDate.setUTCMinutes(0)
startDate.setUTCSeconds(0) startDate.setUTCSeconds(0)
startDate.setUTCMilliseconds(0) startDate.setUTCMilliseconds(0)
this.taskDragged.startDate = startDate this.taskDragged.startDate = startDate
let endDate = new Date(startDate) let endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + newRect.width / this.dayWidth) endDate.setDate(
startDate.getDate() + newRect.width / this.dayWidth
)
this.taskDragged.startDate = startDate this.taskDragged.startDate = startDate
this.taskDragged.endDate = endDate this.taskDragged.endDate = endDate
// We take the task from the overall tasks array because the one in it has bad data after it was updated once. // We take the task from the overall tasks array because the one in it has bad data after it was updated once.
// FIXME: This is a workaround. We should use a better mechanism to get the task or, even better, // FIXME: This is a workaround. We should use a better mechanism to get the task or, even better,
// prevent it from containing outdated Data in the first place. // prevent it from containing outdated Data in the first place.
@ -362,8 +415,9 @@ export default {
} }
} }
this.taskService.update(this.taskDragged) this.taskService
.then(r => { .update(this.taskDragged)
.then((r) => {
// If the task didn't have dates before, we'll update the list // If the task didn't have dates before, we'll update the list
if (didntHaveDates) { if (didntHaveDates) {
for (const t in this.tasksWithoutDates) { for (const t in this.tasksWithoutDates) {
@ -376,13 +430,17 @@ export default {
} else { } else {
for (const tt in this.theTasks) { for (const tt in this.theTasks) {
if (this.theTasks[tt].id === r.id) { if (this.theTasks[tt].id === r.id) {
this.$set(this.theTasks, tt, this.addGantAttributes(r)) this.$set(
this.theTasks,
tt,
this.addGantAttributes(r)
)
break break
} }
} }
} }
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, 100) }, 100)
@ -402,21 +460,25 @@ export default {
}, },
hideCrateNewTask() { hideCrateNewTask() {
if (this.newTaskTitle === '') { if (this.newTaskTitle === '') {
this.$nextTick(() => this.newTaskFieldActive = false) this.$nextTick(() => (this.newTaskFieldActive = false))
} }
}, },
addNewTask() { addNewTask() {
if (!this.newTaskFieldActive) { if (!this.newTaskFieldActive) {
return return
} }
let task = new TaskModel({title: this.newTaskTitle, listId: this.listId}) let task = new TaskModel({
this.taskService.create(task) title: this.newTaskTitle,
.then(r => { listId: this.listId,
})
this.taskService
.create(task)
.then((r) => {
this.tasksWithoutDates.push(this.addGantAttributes(r)) this.tasksWithoutDates.push(this.addGantAttributes(r))
this.newTaskTitle = '' this.newTaskTitle = ''
this.hideCrateNewTask() this.hideCrateNewTask()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },

View file

@ -2,7 +2,7 @@
<div class="attachments"> <div class="attachments">
<h3> <h3>
<span class="icon is-grey"> <span class="icon is-grey">
<icon icon="paperclip"/> <icon icon="paperclip" />
</span> </span>
Attachments Attachments
</h3> </h3>
@ -14,12 +14,14 @@
multiple multiple
ref="files" ref="files"
type="file" type="file"
v-if="editEnabled"/> v-if="editEnabled"
/>
<progress <progress
:value="attachmentService.uploadProgress" :value="attachmentService.uploadProgress"
class="progress is-primary" class="progress is-primary"
max="100" max="100"
v-if="attachmentService.uploadProgress > 0"> v-if="attachmentService.uploadProgress > 0"
>
{{ attachmentService.uploadProgress }}% {{ attachmentService.uploadProgress }}%
</progress> </progress>
@ -28,13 +30,22 @@
class="attachment" class="attachment"
v-for="a in attachments" v-for="a in attachments"
:key="a.id" :key="a.id"
@click="viewOrDownload(a)"> @click="viewOrDownload(a)"
>
<div class="filename">{{ a.file.name }}</div> <div class="filename">{{ a.file.name }}</div>
<div class="info"> <div class="info">
<p class="collapses"> <p class="collapses">
<span> <span>
created <span v-tooltip="formatDate(a.created)">{{ formatDateSince(a.created) }}</span> by created
<user :avatar-size="24" :user="a.createdBy" :is-inline="true"/> <span v-tooltip="formatDate(a.created)">{{
formatDateSince(a.created)
}}</span>
by
<user
:avatar-size="24"
:user="a.createdBy"
:is-inline="true"
/>
</span> </span>
<span> <span>
{{ a.file.getHumanSize() }} {{ a.file.getHumanSize() }}
@ -46,13 +57,20 @@
<p> <p>
<a <a
@click="downloadAttachment(a)" @click="downloadAttachment(a)"
v-tooltip="'Download this attachment'"> v-tooltip="'Download this attachment'"
>
Download Download
</a> </a>
<a <a
@click="() => {attachmentToDelete = a; showDeleteModal = true}" @click="
() => {
attachmentToDelete = a
showDeleteModal = true
}
"
v-if="editEnabled" v-if="editEnabled"
v-tooltip="'Delete this attachment'"> v-tooltip="'Delete this attachment'"
>
Delete Delete
</a> </a>
</p> </p>
@ -60,24 +78,29 @@
</a> </a>
</div> </div>
<a <x-button
v-if="editEnabled"
:disabled="attachmentService.loading" :disabled="attachmentService.loading"
@click="$refs.files.click()" @click="$refs.files.click()"
class="button mb-4 has-no-shadow" class="mb-4"
v-if="editEnabled"> icon="cloud-upload-alt"
<span class="icon is-small"><icon icon="cloud-upload-alt"/></span> type="secondary"
:shadow="false"
>
Upload attachment Upload attachment
</a> </x-button>
<!-- Dropzone --> <!-- Dropzone -->
<div :class="{ 'hidden': !showDropzone }" class="dropzone" v-if="editEnabled"> <div
:class="{ hidden: !showDropzone }"
class="dropzone"
v-if="editEnabled"
>
<div class="drop-hint"> <div class="drop-hint">
<div class="icon"> <div class="icon">
<icon icon="cloud-upload-alt"/> <icon icon="cloud-upload-alt" />
</div>
<div class="hint">
Drop files here to upload
</div> </div>
<div class="hint">Drop files here to upload</div>
</div> </div>
</div> </div>
@ -85,18 +108,27 @@
<modal <modal
@close="showDeleteModal = false" @close="showDeleteModal = false"
v-if="showDeleteModal" v-if="showDeleteModal"
@submit="deleteAttachment()"> @submit="deleteAttachment()"
>
<span slot="header">Delete attachment</span> <span slot="header">Delete attachment</span>
<p slot="text">Are you sure you want to delete the attachment {{ attachmentToDelete.file.name }}?<br/> <p slot="text">
<b>This CANNOT BE UNDONE!</b></p> Are you sure you want to delete the attachment
{{ attachmentToDelete.file.name }}?<br />
<b>This CANNOT BE UNDONE!</b>
</p>
</modal> </modal>
<transition name="modal"> <transition name="modal">
<modal <modal
@close="() => {showImageModal = false; attachmentImageBlobUrl = null}" @close="
() => {
showImageModal = false
attachmentImageBlobUrl = null
}
"
v-if="showImageModal" v-if="showImageModal"
> >
<img :src="attachmentImageBlobUrl" alt=""/> <img :src="attachmentImageBlobUrl" alt="" />
</modal> </modal>
</transition> </transition>
</div> </div>
@ -106,7 +138,7 @@
import AttachmentService from '../../../services/attachment' import AttachmentService from '../../../services/attachment'
import AttachmentModel from '../../../models/attachment' import AttachmentModel from '../../../models/attachment'
import User from '../../misc/user' import User from '../../misc/user'
import {mapState} from 'vuex' import { mapState } from 'vuex'
export default { export default {
name: 'attachments', name: 'attachments',
@ -141,28 +173,28 @@ export default {
this.attachmentService = new AttachmentService() this.attachmentService = new AttachmentService()
}, },
computed: mapState({ computed: mapState({
attachments: state => state.attachments.attachments, attachments: (state) => state.attachments.attachments,
}), }),
mounted() { mounted() {
document.addEventListener('dragenter', e => { document.addEventListener('dragenter', (e) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
this.showDropzone = true this.showDropzone = true
}) })
window.addEventListener('dragleave', e => { window.addEventListener('dragleave', (e) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
this.showDropzone = false this.showDropzone = false
}) })
document.addEventListener('dragover', e => { document.addEventListener('dragover', (e) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
this.showDropzone = true this.showDropzone = true
}) })
document.addEventListener('drop', e => { document.addEventListener('drop', (e) => {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
@ -183,32 +215,40 @@ export default {
this.uploadFiles(this.$refs.files.files) this.uploadFiles(this.$refs.files.files)
}, },
uploadFiles(files) { uploadFiles(files) {
const attachmentModel = new AttachmentModel({taskId: this.taskId}) const attachmentModel = new AttachmentModel({ taskId: this.taskId })
this.attachmentService.create(attachmentModel, files) this.attachmentService
.then(r => { .create(attachmentModel, files)
.then((r) => {
if (r.success !== null) { if (r.success !== null) {
r.success.forEach(a => { r.success.forEach((a) => {
this.$store.commit('attachments/add', a) this.$store.commit('attachments/add', a)
this.$store.dispatch('tasks/addTaskAttachment', {taskId: this.taskId, attachment: a}) this.$store.dispatch('tasks/addTaskAttachment', {
taskId: this.taskId,
attachment: a,
})
}) })
} }
if (r.errors !== null) { if (r.errors !== null) {
r.errors.forEach(m => { r.errors.forEach((m) => {
this.error(m) this.error(m)
}) })
} }
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
deleteAttachment() { deleteAttachment() {
this.attachmentService.delete(this.attachmentToDelete) this.attachmentService
.then(r => { .delete(this.attachmentToDelete)
this.$store.commit('attachments/removeById', this.attachmentToDelete.id) .then((r) => {
this.$store.commit(
'attachments/removeById',
this.attachmentToDelete.id
)
this.success(r, this) this.success(r, this)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
.finally(() => { .finally(() => {
@ -216,13 +256,14 @@ export default {
}) })
}, },
viewOrDownload(attachment) { viewOrDownload(attachment) {
if (attachment.file.name.endsWith('.jpg') || if (
attachment.file.name.endsWith('.jpg') ||
attachment.file.name.endsWith('.png') || attachment.file.name.endsWith('.png') ||
attachment.file.name.endsWith('.bmp') || attachment.file.name.endsWith('.bmp') ||
attachment.file.name.endsWith('.gif')) { attachment.file.name.endsWith('.gif')
) {
this.showImageModal = true this.showImageModal = true
this.attachmentService.getBlobUrl(attachment) this.attachmentService.getBlobUrl(attachment).then((url) => {
.then(url => {
this.attachmentImageBlobUrl = url this.attachmentImageBlobUrl = url
}) })
} else { } else {

View file

@ -2,33 +2,70 @@
<div class="content details"> <div class="content details">
<h3 v-if="canWrite || comments.length > 0"> <h3 v-if="canWrite || comments.length > 0">
<span class="icon is-grey"> <span class="icon is-grey">
<icon :icon="['far', 'comments']"/> <icon :icon="['far', 'comments']" />
</span> </span>
Comments Comments
</h3> </h3>
<div class="comments"> <div class="comments">
<span class="is-inline-flex is-align-items-center" v-if="taskCommentService.loading && saving === null && !creating"> <span
class="is-inline-flex is-align-items-center"
v-if="
taskCommentService.loading && saving === null && !creating
"
>
<span class="loader is-inline-block mr-2"></span> <span class="loader is-inline-block mr-2"></span>
Loading comments... Loading comments...
</span> </span>
<div :key="c.id" class="media comment" v-for="c in comments"> <div :key="c.id" class="media comment" v-for="c in comments">
<figure class="media-left is-hidden-mobile"> <figure class="media-left is-hidden-mobile">
<img :src="c.author.getAvatarUrl(48)" alt="" class="image is-avatar" height="48" width="48"/> <img
:src="c.author.getAvatarUrl(48)"
alt=""
class="image is-avatar"
height="48"
width="48"
/>
</figure> </figure>
<div class="media-content"> <div class="media-content">
<div class="comment-info"> <div class="comment-info">
<img :src="c.author.getAvatarUrl(20)" alt="" class="image is-avatar" height="20" width="20"/> <img
<strong>{{ c.author.getDisplayName() }}</strong>&nbsp; :src="c.author.getAvatarUrl(20)"
<span v-tooltip="formatDate(c.created)">{{ formatDateSince(c.created) }}</span> alt=""
<span v-if="+new Date(c.created) !== +new Date(c.updated)" v-tooltip="formatDate(c.updated)"> class="image is-avatar"
height="20"
width="20"
/>
<strong>{{ c.author.getDisplayName() }}</strong
>&nbsp;
<span v-tooltip="formatDate(c.created)">{{
formatDateSince(c.created)
}}</span>
<span
v-if="+new Date(c.created) !== +new Date(c.updated)"
v-tooltip="formatDate(c.updated)"
>
· edited {{ formatDateSince(c.updated) }} · edited {{ formatDateSince(c.updated) }}
</span> </span>
<transition name="fade"> <transition name="fade">
<span class="is-inline-flex" v-if="taskCommentService.loading && saving === c.id"> <span
<span class="loader is-inline-block mr-2"></span> class="is-inline-flex"
v-if="
taskCommentService.loading &&
saving === c.id
"
>
<span
class="loader is-inline-block mr-2"
></span>
Saving... Saving...
</span> </span>
<span class="has-text-success" v-if="!taskCommentService.loading && saved === c.id"> <span
class="has-text-success"
v-if="
!taskCommentService.loading &&
saved === c.id
"
>
Saved! Saved!
</span> </span>
</transition> </transition>
@ -38,7 +75,12 @@
:is-edit-enabled="canWrite" :is-edit-enabled="canWrite"
:upload-callback="attachmentUpload" :upload-callback="attachmentUpload"
:upload-enabled="true" :upload-enabled="true"
@change="() => {toggleEdit(c);editComment()}" @change="
() => {
toggleEdit(c)
editComment()
}
"
v-model="c.comment" v-model="c.comment"
:has-edit-bottom="true" :has-edit-bottom="true"
:bottom-actions="actions[c.id]" :bottom-actions="actions[c.id]"
@ -47,19 +89,34 @@
</div> </div>
<div class="media comment" v-if="canWrite"> <div class="media comment" v-if="canWrite">
<figure class="media-left is-hidden-mobile"> <figure class="media-left is-hidden-mobile">
<img :src="userAvatar" alt="" class="image is-avatar" height="48" width="48"/> <img
:src="userAvatar"
alt=""
class="image is-avatar"
height="48"
width="48"
/>
</figure> </figure>
<div class="media-content"> <div class="media-content">
<div class="form"> <div class="form">
<transition name="fade"> <transition name="fade">
<span class="is-inline-flex" v-if="taskCommentService.loading && creating"> <span
<span class="loader is-inline-block mr-2"></span> class="is-inline-flex"
v-if="taskCommentService.loading && creating"
>
<span
class="loader is-inline-block mr-2"
></span>
Creating comment... Creating comment...
</span> </span>
</transition> </transition>
<div class="field"> <div class="field">
<editor <editor
:class="{'is-loading': taskCommentService.loading && !isCommentEdit}" :class="{
'is-loading':
taskCommentService.loading &&
!isCommentEdit,
}"
:has-preview="false" :has-preview="false"
:upload-callback="attachmentUpload" :upload-callback="attachmentUpload"
:upload-enabled="true" :upload-enabled="true"
@ -69,10 +126,15 @@
/> />
</div> </div>
<div class="field"> <div class="field">
<button :class="{'is-loading': taskCommentService.loading && !isCommentEdit}" <x-button
:loading="
taskCommentService.loading && !isCommentEdit
"
:disabled="newComment.comment === ''" :disabled="newComment.comment === ''"
@click="addComment()" class="button is-primary">Comment @click="addComment()"
</button> >
Comment
</x-button>
</div> </div>
</div> </div>
</div> </div>
@ -81,10 +143,13 @@
<modal <modal
@close="showDeleteModal = false" @close="showDeleteModal = false"
@submit="deleteComment()" @submit="deleteComment()"
v-if="showDeleteModal"> v-if="showDeleteModal"
>
<span slot="header">Delete this comment</span> <span slot="header">Delete this comment</span>
<p slot="text">Are you sure you want to delete this comment? <p slot="text">
<br/>This <b>CANNOT BE UNDONE!</b></p> Are you sure you want to delete this comment? <br />This
<b>CANNOT BE UNDONE!</b>
</p>
</modal> </modal>
</div> </div>
</template> </template>
@ -100,15 +165,15 @@ export default {
name: 'comments', name: 'comments',
components: { components: {
editor: () => ({ editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../input/editor'), component: import(
/* webpackChunkName: "editor" */ '../../input/editor'
),
loading: LoadingComponent, loading: LoadingComponent,
error: ErrorComponent, error: ErrorComponent,
timeout: 60000, timeout: 60000,
}), }),
}, },
mixins: [ mixins: [attachmentUpload],
attachmentUpload,
],
props: { props: {
taskId: { taskId: {
type: Number, type: Number,
@ -140,9 +205,9 @@ export default {
}, },
created() { created() {
this.taskCommentService = new TaskCommentService() this.taskCommentService = new TaskCommentService()
this.newComment = new TaskCommentModel({taskId: this.taskId}) this.newComment = new TaskCommentModel({ taskId: this.taskId })
this.commentEdit = new TaskCommentModel({taskId: this.taskId}) this.commentEdit = new TaskCommentModel({ taskId: this.taskId })
this.commentToDelete = new TaskCommentModel({taskId: this.taskId}) this.commentToDelete = new TaskCommentModel({ taskId: this.taskId })
this.comments = [] this.comments = []
}, },
mounted() { mounted() {
@ -163,12 +228,13 @@ export default {
}, },
methods: { methods: {
loadComments() { loadComments() {
this.taskCommentService.getAll({taskId: this.taskId}) this.taskCommentService
.then(r => { .getAll({ taskId: this.taskId })
.then((r) => {
this.$set(this, 'comments', r) this.$set(this, 'comments', r)
this.makeActions() this.makeActions()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
@ -183,16 +249,20 @@ export default {
// not update if new content from the outside was made available. // not update if new content from the outside was made available.
// See https://github.com/NikulinIlya/vue-easymde/issues/3 // See https://github.com/NikulinIlya/vue-easymde/issues/3
this.editorActive = false this.editorActive = false
this.$nextTick(() => this.editorActive = true) this.$nextTick(() => (this.editorActive = true))
this.creating = true this.creating = true
this.taskCommentService.create(this.newComment) this.taskCommentService
.then(r => { .create(this.newComment)
.then((r) => {
this.comments.push(r) this.comments.push(r)
this.newComment.comment = '' this.newComment.comment = ''
this.success({message: 'The comment was added successfully.'}, this) this.success(
{ message: 'The comment was added successfully.' },
this
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
.finally(() => { .finally(() => {
@ -215,8 +285,9 @@ export default {
this.saving = this.commentEdit.id this.saving = this.commentEdit.id
this.commentEdit.taskId = this.taskId this.commentEdit.taskId = this.taskId
this.taskCommentService.update(this.commentEdit) this.taskCommentService
.then(r => { .update(this.commentEdit)
.then((r) => {
for (const c in this.comments) { for (const c in this.comments) {
if (this.comments[c].id === this.commentEdit.id) { if (this.comments[c].id === this.commentEdit.id) {
this.$set(this.comments, c, r) this.$set(this.comments, c, r)
@ -227,7 +298,7 @@ export default {
this.saved = null this.saved = null
}, 2000) }, 2000)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
.finally(() => { .finally(() => {
@ -236,7 +307,8 @@ export default {
}) })
}, },
deleteComment() { deleteComment() {
this.taskCommentService.delete(this.commentToDelete) this.taskCommentService
.delete(this.commentToDelete)
.then(() => { .then(() => {
for (const a in this.comments) { for (const a in this.comments) {
if (this.comments[a].id === this.commentToDelete.id) { if (this.comments[a].id === this.commentToDelete.id) {
@ -244,7 +316,7 @@ export default {
} }
} }
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
.finally(() => { .finally(() => {
@ -253,14 +325,16 @@ export default {
}, },
makeActions() { makeActions() {
if (this.canWrite) { if (this.canWrite) {
this.comments.forEach(c => { this.comments.forEach((c) => {
this.$set(this.actions, c.id, [{ this.$set(this.actions, c.id, [
{
action: () => this.toggleDelete(c.id), action: () => this.toggleDelete(c.id),
title: 'Remove', title: 'Remove',
}]) },
])
}) })
} }
} },
}, },
} }
</script> </script>

View file

@ -1,13 +1,34 @@
<template> <template>
<div :class="{'is-loading': taskService.loading}" class="defer-task loading-container"> <div
:class="{ 'is-loading': taskService.loading }"
class="defer-task loading-container"
>
<label class="label">Defer due date</label> <label class="label">Defer due date</label>
<div class="defer-days"> <div class="defer-days">
<button @click="() => deferDays(1)" class="button has-no-shadow">1 day</button> <x-button
<button @click="() => deferDays(3)" class="button has-no-shadow">3 days</button> @click.prevent.stop="() => deferDays(1)"
<button @click="() => deferDays(7)" class="button has-no-shadow">1 week</button> :shadow="false"
type="secondary"
>
1 day
</x-button>
<x-button
@click.prevent.stop="() => deferDays(3)"
:shadow="false"
type="secondary"
>
3 days
</x-button>
<x-button
@click.prevent.stop="() => deferDays(7)"
:shadow="false"
type="secondary"
>
1 week
</x-button>
</div> </div>
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ disabled: taskService.loading }"
:config="flatPickerConfig" :config="flatPickerConfig"
:disabled="taskService.loading" :disabled="taskService.loading"
class="input" class="input"
@ -97,13 +118,14 @@ export default {
} }
this.task.dueDate = new Date(this.dueDate) this.task.dueDate = new Date(this.dueDate)
this.taskService.update(this.task) this.taskService
.then(r => { .update(this.task)
.then((r) => {
this.lastValue = r.dueDate this.lastValue = r.dueDate
this.task = r this.task = r
this.$emit('input', r) this.$emit('input', r)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },

View file

@ -1,14 +1,14 @@
<template> <template>
<div class="task-relations"> <div class="task-relations">
<button <x-button
class="button is-pulled-right add-task-relation-button"
:class="{'is-active': showNewRelationForm}"
@click="showNewRelationForm = !showNewRelationForm"
v-tooltip="'Add a New Task Relation'"
v-if="Object.keys(relatedTasks).length > 0" v-if="Object.keys(relatedTasks).length > 0"
> @click="showNewRelationForm = !showNewRelationForm"
<icon icon="plus"/> class="is-pulled-right add-task-relation-button"
</button> :class="{'is-active': showNewRelationForm}"
v-tooltip="'Add a New Task Relation'"
type="secondary"
icon="plus"
/>
<transition-group name="fade"> <transition-group name="fade">
<template v-if="editEnabled && showCreate"> <template v-if="editEnabled && showCreate">
<label class="label" key="label"> <label class="label" key="label">
@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="control"> <div class="control">
<a @click="addTaskRelation()" class="button is-primary">Add Task Relation</a> <x-button @click="addTaskRelation()">Add Task Relation</x-button>
</div> </div>
</div> </div>
</template> </template>
@ -248,7 +248,7 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
@import '@/styles/theme/variables'; @import '@/styles/theme/variables';
.add-task-relation-button { .add-task-relation-button {

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="control repeat-after-input"> <div class="control repeat-after-input">
<div class="buttons has-addons is-centered mt-2"> <div class="buttons has-addons is-centered mt-2">
<button class="button is-small" @click="() => setRepeatAfter(1, 'days')">Every Day</button> <x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">Every Day</x-button>
<button class="button is-small" @click="() => setRepeatAfter(1, 'weeks')">Every Week</button> <x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">Every Week</x-button>
<button class="button is-small" @click="() => setRepeatAfter(1, 'months')">Every Month</button> <x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">Every Month</x-button>
</div> </div>
<div class="columns is-align-items-center"> <div class="columns is-align-items-center">
<div class="is-flex column"> <div class="is-flex column">

View file

@ -1,11 +1,5 @@
<template> <template>
<div class="card"> <card title="Avatar">
<header class="card-header">
<p class="card-header-title">
Avatar
</p>
</header>
<div class="card-content">
<div class="control mb-4"> <div class="control mb-4">
<label class="radio"> <label class="radio">
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="default"/> <input name="avatarProvider" type="radio" v-model="avatarProvider" value="default"/>
@ -33,13 +27,12 @@
ref="avatarUploadInput" ref="avatarUploadInput"
type="file" type="file"
/> />
<a <x-button
:class="{ 'is-loading': avatarService.loading || loading}" :loading="avatarService.loading || loading"
@click="$refs.avatarUploadInput.click()" @click="$refs.avatarUploadInput.click()"
class="button is-primary"
v-if="!isCropAvatar"> v-if="!isCropAvatar">
Upload Avatar Upload Avatar
</a> </x-button>
<template v-else> <template v-else>
<cropper <cropper
:src="avatarToCrop" :src="avatarToCrop"
@ -47,23 +40,25 @@
@ready="() => loading = false" @ready="() => loading = false"
class="mb-4" class="mb-4"
ref="cropper"/> ref="cropper"/>
<a <x-button
:class="{ 'is-loading': avatarService.loading || loading}" :loading="avatarService.loading || loading"
@click="uploadAvatar" @click="uploadAvatar"
class="button is-primary"> >
Upload Avatar Upload Avatar
</a> </x-button>
</template> </template>
</template> </template>
<div class="bigbuttons" v-if="avatarProvider !== 'upload'"> <div class="mt-2" v-if="avatarProvider !== 'upload'">
<button :class="{ 'is-loading': avatarService.loading || loading}" @click="updateAvatarStatus()" <x-button
class="button is-primary is-fullwidth"> :loading="avatarService.loading || loading"
@click="updateAvatarStatus()"
class="is-fullwidth"
>
Save Save
</button> </x-button>
</div>
</div>
</div> </div>
</card>
</template> </template>
<script> <script>

View file

@ -162,6 +162,12 @@ const formatDate = (date, f) => {
return date ? format(date, f) : '' return date ? format(date, f) : ''
} }
import Button from '@/components/input/button'
Vue.component('x-button', Button)
import Card from '@/components/misc/card'
Vue.component('card', Card)
Vue.mixin({ Vue.mixin({
methods: { methods: {
formatDateSince: date => { formatDateSince: date => {

View file

@ -81,7 +81,9 @@
text-transform: none; text-transform: none;
font-family: $family-sans-serif; font-family: $family-sans-serif;
font-weight: normal; font-weight: normal;
padding: 0; padding: .5rem 0;
border: none;
cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -125,7 +125,7 @@
&.description .editor { &.description .editor {
&.is-pulled-up { &.is-pulled-up {
margin-top: -4rem; margin-top: -3rem;
} }
.tabs { .tabs {

View file

@ -119,14 +119,6 @@
height: 100%; height: 100%;
} }
.bigbuttons {
margin-top: 0.5rem;
}
.buttonright {
margin-right: 0.5rem;
}
.control.has-icons-left .icon, .control.has-icons-right .icon { .control.has-icons-left .icon, .control.has-icons-right .icon {
z-index: 0; z-index: 0;
} }

View file

@ -3,13 +3,13 @@
<h2>Hi {{ userInfo.name !== '' ? userInfo.name : userInfo.username }}!</h2> <h2>Hi {{ userInfo.name !== '' ? userInfo.name : userInfo.username }}!</h2>
<template v-if="!hasTasks"> <template v-if="!hasTasks">
<p>Click on a list or namespace on the left to get started.</p> <p>Click on a list or namespace on the left to get started.</p>
<router-link <x-button
:to="{name: 'migrate.start'}" :to="{name: 'migrate.start'}"
class="button is-primary has-no-shadow" :shadow="false"
v-if="migratorsEnabled" v-if="migratorsEnabled"
> >
Import your data into Vikunja Import your data into Vikunja
</router-link> </x-button>
</template> </template>
<ShowTasks :show-all="true"/> <ShowTasks :show-all="true"/>
</div> </div>

View file

@ -2,11 +2,7 @@
<div class="modal-mask keyboard-shortcuts-modal"> <div class="modal-mask keyboard-shortcuts-modal">
<div @click.self="$router.back()" class="modal-container"> <div @click.self="$router.back()" class="modal-container">
<div class="modal-content"> <div class="modal-content">
<div class="card has-background-white has-no-shadow"> <card class="has-background-white has-no-shadow" title="Create A Saved Filter">
<header class="card-header">
<p class="card-header-title">Create A Saved Filter</p>
</header>
<div class="card-content content">
<p> <p>
A saved filter is a virtual list which is computed from a set of filters each time it is A saved filter is a virtual list which is computed from a set of filters each time it is
accessed. Once created, it will appear in a special namespace. accessed. Once created, it will appear in a special namespace.
@ -51,15 +47,15 @@
/> />
</div> </div>
</div> </div>
<button <x-button
:class="{ 'disabled': savedFilterService.loading}" :loading="savedFilterService.loading"
:disabled="savedFilterService.loading" :disabled="savedFilterService.loading"
@click="create()" @click="create()"
class="button is-primary is-fullwidth"> class="is-fullwidth"
>
Create new saved filter Create new saved filter
</button> </x-button>
</div> </card>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,13 +1,6 @@
<template> <template>
<div :class="{ 'is-loading': filterService.loading}" class="loader-container edit-list is-max-width-desktop"> <div :class="{ 'is-loading': filterService.loading}" class="loader-container edit-list is-max-width-desktop">
<div class="card"> <card title="Edit Saved Filter">
<header class="card-header">
<p class="card-header-title">
Edit Saved Filter
</p>
</header>
<div class="card-content">
<div class="content">
<form @submit.prevent="save()"> <form @submit.prevent="save()">
<div class="field"> <div class="field">
<label class="label" for="listtext">Filter Name</label> <label class="label" for="listtext">Filter Name</label>
@ -52,27 +45,23 @@
<div class="field has-addons mt-4"> <div class="field has-addons mt-4">
<div class="control is-fullwidth"> <div class="control is-fullwidth">
<button <x-button
@click="save()" @click="save()"
:class="{ 'is-loading': filterService.loading}" :loading="filterService.loading"
class="button is-primary is-fullwidth"> class="is-fullwidth">
Save Save
</button> </x-button>
</div> </div>
<div class="control"> <div class="control">
<button <x-button
@click="showDeleteModal = true" @click="showDeleteModal = true"
:class="{ 'is-loading': filterService.loading}" :loading="filterService.loading"
class="button is-danger"> class="is-danger"
<span class="icon"> icon="trash-alt"
<icon icon="trash-alt"/> />
</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</card>
<modal <modal
@close="showDeleteModal = false" @close="showDeleteModal = false"

View file

@ -1,17 +1,23 @@
<template> <template>
<div :class="{ 'is-loading': labelService.loading}" class="loader-container content"> <div :class="{ 'is-loading': labelService.loading}" class="loader-container">
<router-link :to="{name:'labels.create'}" class="button is-primary button-right"> <x-button
<span class="icon is-small"> :to="{name:'labels.create'}"
<icon icon="plus"/> class="is-pulled-right"
</span> icon="plus"
>
New label New label
</router-link> </x-button>
<div class="content">
<h1>Manage labels</h1> <h1>Manage labels</h1>
<p> <p>
Click on a label to edit it. Click on a label to edit it.
You can edit all labels you created, you can use all labels which are associated with a task to whose list You can edit all labels you created, you can use all labels which are associated with a task to whose
list
you have access. you have access.
</p> </p>
</div>
<div class="columns"> <div class="columns">
<div class="labels-list column"> <div class="labels-list column">
<span <span
@ -35,18 +41,7 @@
</span> </span>
</div> </div>
<div class="column is-4" v-if="isLabelEdit"> <div class="column is-4" v-if="isLabelEdit">
<div class="card"> <card title="Edit Label" :has-close="true" @close="() => isLabelEdit = false">
<header class="card-header">
<span class="card-header-title">
Edit Label
</span>
<a @click="isLabelEdit = false" class="card-header-icon">
<span class="icon">
<icon icon="times"/>
</span>
</a>
</header>
<div class="card-content">
<form @submit.prevent="editLabelSubmit()"> <form @submit.prevent="editLabelSubmit()">
<div class="field"> <div class="field">
<label class="label">Title</label> <label class="label">Title</label>
@ -77,24 +72,24 @@
</div> </div>
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<button :class="{ 'is-loading': labelService.loading}" class="button is-fullwidth is-primary" <x-button
type="submit"> :loading="labelService.loading"
class="is-fullwidth"
@click="editLabelSubmit()"
>
Save Save
</button> </x-button>
</div> </div>
<div class="control"> <div class="control">
<a <x-button
@click="() => {deleteLabel(labelEditLabel);isLabelEdit = false}" @click="() => {deleteLabel(labelEditLabel);isLabelEdit = false}"
class="button has-icon is-danger"> icon="trash-alt"
<span class="icon"> class="is-danger"
<icon icon="trash-alt"/> />
</span>
</a>
</div> </div>
</div> </div>
</form> </form>
</div> </card>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,12 +15,13 @@
v-model="label.title"/> v-model="label.title"/>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-primary has-no-shadow" type="submit"> <x-button
<span class="icon is-small"> :shadow="false"
<icon icon="plus"/> icon="plus"
</span> @click="newlabel"
>
Add Add
</button> </x-button>
</p> </p>
</div> </div>
<p class="help is-danger" v-if="showError && label.title === ''"> <p class="help is-danger" v-if="showError && label.title === ''">

View file

@ -4,14 +4,7 @@
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>
<div class="card"> <card title="Edit List">
<header class="card-header">
<p class="card-header-title">
Edit List
</p>
</header>
<div class="card-content">
<div class="content">
<form @submit.prevent="submit()"> <form @submit.prevent="submit()">
<div class="field"> <div class="field">
<label class="label" for="listtext">List Name</label> <label class="label" for="listtext">List Name</label>
@ -81,36 +74,26 @@
<div class="field has-addons mt-4"> <div class="field has-addons mt-4">
<div class="control is-fullwidth"> <div class="control is-fullwidth">
<button <x-button
@click="submit()" @click="submit()"
:class="{ 'is-loading': listService.loading}" :loading="listService.loading"
class="button is-primary is-fullwidth"> class="is-fullwidth">
Save Save
</button> </x-button>
</div> </div>
<div class="control"> <div class="control">
<button <x-button
@click="showDeleteModal = true" @click="showDeleteModal = true"
:class="{ 'is-loading': listService.loading}" :locading="listService.loading"
class="button is-danger"> icon="trash-alt"
<span class="icon"> class="is-danger"
<icon icon="trash-alt"/> />
</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</card>
<div class="card has-overflow"> <!-- Duplicate list -->
<header class="card-header"> <card class="has-overflow" title="Duplicate this list">
<p class="card-header-title">
Duplicate this list
</p>
</header>
<div class="card-content">
<div class="content">
<p>Select a namespace which should hold the duplicated list:</p> <p>Select a namespace which should hold the duplicated list:</p>
<div class="field has-addons"> <div class="field has-addons">
@ -118,18 +101,15 @@
<namespace-search @selected="selectNamespace"/> <namespace-search @selected="selectNamespace"/>
</div> </div>
<div class="control"> <div class="control">
<button <x-button
:class="{'is-loading': listDuplicateService.loading}" :loading="listDuplicateService.loading"
@click="duplicateList" @click="duplicateList"
class="button is-primary" >
type="submit">
Duplicate Duplicate
</button> </x-button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</card>
<background :list-id="$route.params.id"/> <background :list-id="$route.params.id"/>

View file

@ -18,12 +18,14 @@
v-model="list.title"/> v-model="list.title"/>
</p> </p>
<p class="control"> <p class="control">
<button :disabled="list.title === ''" @click="newList()" class="button is-primary has-no-shadow"> <x-button
<span class="icon is-small"> :disabled="list.title === ''"
<icon icon="plus"/> @click="newList()"
</span> icon="plus"
:shadow="false"
>
Add Add
</button> </x-button>
</p> </p>
</div> </div>
<p class="help is-danger" v-if="showError && list.title === ''"> <p class="help is-danger" v-if="showError && list.title === ''">

View file

@ -2,12 +2,13 @@
<div class="kanban-view"> <div class="kanban-view">
<div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()"> <div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()">
<div class="items"> <div class="items">
<button @click.prevent.stop="showFilters = !showFilters" class="button"> <x-button
<span class="icon is-small"> @click.prevent.stop="showFilters = !showFilters"
<icon icon="filter"/> icon="filter"
</span> type="secondary"
>
Filters Filters
</button> </x-button>
</div> </div>
<filter-popup <filter-popup
@change="() => {filtersChanged = true; loadBuckets()}" @change="() => {filtersChanged = true; loadBuckets()}"
@ -59,11 +60,10 @@
/> />
</div> </div>
<div class="control"> <div class="control">
<a class="button is-primary has-no-shadow"> <x-button
<span class="icon"> :icon="['far', 'save']"
<icon :icon="['far', 'save']"/> :shadow="false"
</span> />
</a>
</div> </div>
</div> </div>
<template v-else> <template v-else>
@ -191,20 +191,21 @@
Please specify a title. Please specify a title.
</p> </p>
</div> </div>
<a <x-button
@click="toggleShowNewTaskInput(bucket.id)" @click="toggleShowNewTaskInput(bucket.id)"
class="button has-no-shadow is-transparent is-fullwidth has-text-centered" class="is-transparent is-fullwidth has-text-centered"
v-if="!showNewTaskInput[bucket.id]"> :shadow="false"
<span class="icon is-small"> v-if="!showNewTaskInput[bucket.id]"
<icon icon="plus"/> icon="plus"
</span> type="secondary"
<span v-if="bucket.tasks.length === 0"> >
<template v-if="bucket.tasks.length === 0">
Add a task Add a task
</span> </template>
<span v-else> <template v-else>
Add another task Add another task
</span> </template>
</a> </x-button>
</div> </div>
</div> </div>
@ -222,16 +223,16 @@
v-if="showNewBucketInput" v-if="showNewBucketInput"
v-model="newBucketTitle" v-model="newBucketTitle"
/> />
<a <x-button
@click="() => showNewBucketInput = true" @click="() => showNewBucketInput = true"
class="button has-no-shadow is-transparent is-fullwidth has-text-centered" v-if="!showNewBucketInput"> :shadow="false"
<span class="icon is-small"> class="is-transparent is-fullwidth has-text-centered"
<icon icon="plus"/> v-if="!showNewBucketInput"
</span> type="secondary"
<span> icon="plus"
>
Create a new bucket Create a new bucket
</span> </x-button>
</a>
</div> </div>
</div> </div>
@ -269,7 +270,7 @@ 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'
export default { export default {

View file

@ -1,5 +1,7 @@
<template> <template>
<div :class="{ 'is-loading': taskCollectionService.loading}" class="loader-container is-max-width-desktop list-view"> <div
:class="{ 'is-loading': taskCollectionService.loading}"
class="loader-container is-max-width-desktop list-view">
<div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()"> <div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()">
<div class="items"> <div class="items">
<div class="search"> <div class="search">
@ -18,26 +20,29 @@
</span> </span>
</div> </div>
<div class="control"> <div class="control">
<button <x-button
:class="{'is-loading': taskCollectionService.loading}" :loading="taskCollectionService.loading"
@click="searchTasks" @click="searchTasks"
class="button has-no-shadow is-primary"> :shadow="false"
>
Search Search
</button> </x-button>
</div> </div>
</div> </div>
<button @click="showTaskSearch = !showTaskSearch" class="button" v-if="!showTaskSearch"> <x-button
<span class="icon"> @click="showTaskSearch = !showTaskSearch"
<icon icon="search"/> icon="search"
</span> type="secondary"
</button> v-if="!showTaskSearch"
/>
</div> </div>
<button @click.prevent.stop="showTaskFilter = !showTaskFilter" class="button"> <x-button
<span class="icon is-small"> @click.prevent.stop="showTaskFilter = !showTaskFilter"
<icon icon="filter"/> type="secondary"
</span> icon="filter"
>
Filters Filters
</button> </x-button>
</div> </div>
<filter-popup <filter-popup
@change="loadTasks(1)" @change="loadTasks(1)"
@ -62,12 +67,13 @@
</span> </span>
</p> </p>
<p class="control"> <p class="control">
<button :disabled="newTaskText.length === 0" @click="addTask()" class="button is-primary"> <x-button
<span class="icon is-small"> :disabled="newTaskText.length === 0"
<icon icon="plus"/> @click="addTask()"
</span> icon="plus"
>
Add Add
</button> </x-button>
</p> </p>
</div> </div>
<p class="help is-danger" v-if="showError && newTaskText === ''"> <p class="help is-danger" v-if="showError && newTaskText === ''">
@ -97,23 +103,9 @@
</div> </div>
</div> </div>
<div class="column is-4" v-if="isTaskEdit"> <div class="column is-4" v-if="isTaskEdit">
<div class="card taskedit"> <card class="taskedit" title="Edit Task" :has-close="true" @close="() => isTaskEdit = false">
<header class="card-header">
<p class="card-header-title">
Edit Task
</p>
<a @click="isTaskEdit = false" class="card-header-icon">
<span class="icon">
<icon icon="angle-right"/>
</span>
</a>
</header>
<div class="card-content">
<div class="content">
<edit-task :task="taskEditTask"/> <edit-task :task="taskEditTask"/>
</div> </card>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -2,24 +2,23 @@
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container"> <div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
<div class="filter-container"> <div class="filter-container">
<div class="items"> <div class="items">
<button @click.prevent.stop="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}" <x-button
class="button"> @click.prevent.stop="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}"
<span class="icon is-small"> icon="th"
<icon icon="th"/> type="secondary"
</span> >
Columns Columns
</button> </x-button>
<button @click.prevent.stop="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}" <x-button
class="button"> @click.prevent.stop="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}"
<span class="icon is-small"> icon="filter"
<icon icon="filter"/> type="secondary"
</span> >
Filters Filters
</button> </x-button>
</div> </div>
<transition name="fade"> <transition name="fade">
<div class="card" v-if="showActiveColumnsFilter"> <card v-if="showActiveColumnsFilter">
<div class="card-content">
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">Done</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">Done</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">Title</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">Title</fancycheckbox>
@ -33,8 +32,7 @@
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">Created</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">Created</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">Updated</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">Updated</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">Created By</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">Created By</fancycheckbox>
</div> </card>
</div>
</transition> </transition>
<filter-popup <filter-popup
@change="loadTasks(1)" @change="loadTasks(1)"
@ -174,7 +172,8 @@
:aria-label="'Goto page ' + p.number" :aria-label="'Goto page ' + p.number"
:class="{'is-current': p.number === currentPage}" :class="{'is-current': p.number === currentPage}"
:to="getRouteForPagination(p.number, 'table')" :to="getRouteForPagination(p.number, 'table')"
class="pagination-link">{{ p.number }} class="pagination-link">
{{ p.number }}
</router-link> </router-link>
</li> </li>
</template> </template>

View file

@ -4,14 +4,7 @@
This namespace is archived. This namespace is archived.
It is not possible to create new lists or edit it. It is not possible to create new lists or edit it.
</div> </div>
<div class="card"> <card title="Edit Namespace">
<header class="card-header">
<p class="card-header-title">
Edit Namespace
</p>
</header>
<div class="card-content">
<div class="content">
<form @submit.prevent="submit()"> <form @submit.prevent="submit()">
<div class="field"> <div class="field">
<label class="label" for="namespacetext">Namespace Name</label> <label class="label" for="namespacetext">Namespace Name</label>
@ -61,27 +54,24 @@
<div class="field has-addons mt-4"> <div class="field has-addons mt-4">
<div class="control is-fullwidth"> <div class="control is-fullwidth">
<button <x-button
@click="submit()" @click="submit()"
:class="{ 'is-loading': namespaceService.loading}" :loading="namespaceService.loading"
class="button is-primary is-fullwidth"> class="is-fullwidth"
>
Save Save
</button> </x-button>
</div> </div>
<div class="control"> <div class="control">
<button <x-button
@click="showDeleteModal = true" @click="showDeleteModal = true"
:class="{ 'is-loading': namespaceService.loading}" :loading="namespaceService.loading"
class="button is-danger"> class="is-danger"
<span class="icon"> icon="trash-alt"
<icon icon="trash-alt"/> />
</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</card>
<component <component
:id="namespace.id" :id="namespace.id"

View file

@ -1,32 +1,26 @@
<template> <template>
<div class="content namespaces-list loader-container" :class="{'is-loading': loading}"> <div class="content namespaces-list loader-container" :class="{'is-loading': loading}">
<router-link :to="{name: 'namespace.create'}" class="button is-primary new-namespace"> <x-button :to="{name: 'namespace.create'}" class="new-namespace" icon="plus">
<span class="icon is-small">
<icon icon="plus"/>
</span>
Create namespace Create namespace
</router-link> </x-button>
<router-link :to="{name: 'filters.create'}" class="button is-primary new-namespace"> <x-button :to="{name: 'filters.create'}" class="new-namespace" icon="filter">
<span class="icon is-small">
<icon icon="filter"/>
</span>
Create saved filter Create saved filter
</router-link> </x-button>
<fancycheckbox class="show-archived-check" v-model="showArchived"> <fancycheckbox class="show-archived-check" v-model="showArchived">
Show Archived Show Archived
</fancycheckbox> </fancycheckbox>
<div :key="`n${n.id}`" class="namespace" v-for="n in namespaces"> <div :key="`n${n.id}`" class="namespace" v-for="n in namespaces">
<router-link <x-button
:to="{name: 'list.create', params: {id: n.id}}" :to="{name: 'list.create', params: {id: n.id}}"
class="button is-pulled-right" class="is-pulled-right"
v-if="n.id > 0"> type="secondary"
<span class="icon is-small"> v-if="n.id > 0"
<icon icon="plus"/> icon="plus"
</span> >
Create list Create list
</router-link> </x-button>
<h1> <h1>
<span>{{ n.title }}</span> <span>{{ n.title }}</span>

View file

@ -18,12 +18,14 @@
v-model="namespace.title"/> v-model="namespace.title"/>
</p> </p>
<p class="control"> <p class="control">
<button :disabled="namespace.title === ''" @click="newNamespace()" class="button is-primary has-no-shadow"> <x-button
<span class="icon is-small"> :disabled="namespace.title === ''"
<icon icon="plus"/> @click="newNamespace()"
</span> :shadow="false"
icon="plus"
>
Add Add
</button> </x-button>
</p> </p>
</div> </div>
<p class="help is-danger" v-if="showError && namespace.title === ''"> <p class="help is-danger" v-if="showError && namespace.title === ''">

View file

@ -30,9 +30,9 @@
/> />
</h3> </h3>
<div v-if="!showAll"> <div v-if="!showAll">
<a @click="showTodaysTasks()" class="button has-no-shadow mr-2">Today</a> <x-button type="secondary" @click="showTodaysTasks()" :shadow="false" class="mr-2">Today</x-button>
<a @click="setDatesToNextWeek()" class="button has-no-shadow mr-2">Next Week</a> <x-button type="secondary" @click="setDatesToNextWeek()" :shadow="false" class="mr-2">Next Week</x-button>
<a @click="setDatesToNextMonth()" class="button has-no-shadow">Next Month</a> <x-button type="secondary" @click="setDatesToNextMonth()" :shadow="false">Next Month</x-button>
</div> </div>
<template v-if="!taskService.loading && (!hasUndoneTasks || !tasks || tasks.length === 0)"> <template v-if="!taskService.loading && (!hasUndoneTasks || !tasks || tasks.length === 0)">
<h3 class="nothing">Nothing to do - Have a nice day!</h3> <h3 class="nothing">Nothing to do - Have a nice day!</h3>

View file

@ -57,7 +57,10 @@
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="dueDate" ref="dueDate"
/> />
<a @click="() => {task.dueDate = null;saveTask()}" v-if="task.dueDate && canWrite" class="remove"> <a
@click="() => {task.dueDate = null;saveTask()}"
v-if="task.dueDate && canWrite"
class="remove">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -94,7 +97,11 @@
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="startDate" ref="startDate"
/> />
<a @click="() => {task.startDate = null;saveTask()}" v-if="task.startDate && canWrite" class="remove"> <a
@click="() => {task.startDate = null;saveTask()}"
v-if="task.startDate && canWrite"
class="remove"
>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -117,7 +124,10 @@
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="endDate" ref="endDate"
/> />
<a @click="() => {task.endDate = null;saveTask()}" v-if="task.endDate && canWrite" class="remove"> <a
@click="() => {task.endDate = null;saveTask()}"
v-if="task.endDate && canWrite"
class="remove">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -235,94 +245,124 @@
<comments :can-write="canWrite" :task-id="taskId"/> <comments :can-write="canWrite" :task-id="taskId"/>
</div> </div>
<div class="column is-one-third action-buttons" v-if="canWrite"> <div class="column is-one-third action-buttons" v-if="canWrite">
<a <x-button
:class="{'is-success': !task.done, 'has-no-shadow': !task.done}" :class="{'is-success': !task.done}"
:shadow="task.done"
@click="toggleTaskDone()" @click="toggleTaskDone()"
class="button is-outlined has-no-border"> class="is-outlined has-no-border"
<span class="icon is-small"><icon icon="check-double"/></span> icon="check-double"
<template v-if="task.done"> type="secondary"
Mark as undone >
</template> {{ task.done ? 'Mark as undone' : 'Done!' }}
<template v-else> </x-button>
Done! <x-button
</template>
</a>
<a
@click="setFieldActive('assignees')" @click="setFieldActive('assignees')"
@shortkey="setFieldActive('assignees')" @shortkey="setFieldActive('assignees')"
class="button" type="secondary"
v-shortkey="['a']"> v-shortkey="['a']">
<span class="icon is-small"><icon icon="users"/></span> <span class="icon is-small"><icon icon="users"/></span>
Assign this task to a user Assign this task to a user
</a> </x-button>
<a <x-button
@click="setFieldActive('labels')" @click="setFieldActive('labels')"
@shortkey="setFieldActive('labels')" @shortkey="setFieldActive('labels')"
class="button" type="secondary"
v-shortkey="['l']"> v-shortkey="['l']"
<span class="icon is-small"><icon icon="tags"/></span> icon="tags"
>
Add labels Add labels
</a> </x-button>
<a @click="setFieldActive('priority')" class="button"> <x-button
<span class="icon is-small"><icon :icon="['far', 'star']"/></span> @click="setFieldActive('priority')"
type="secondary"
:icon="['far', 'star']"
>
Set Priority Set Priority
</a> </x-button>
<a <x-button
@click="setFieldActive('dueDate')" @click="setFieldActive('dueDate')"
@shortkey="setFieldActive('dueDate')" @shortkey="setFieldActive('dueDate')"
class="button" type="secondary"
v-shortkey="['d']"> v-shortkey="['d']"
<span class="icon is-small"><icon icon="calendar"/></span> icon="calendar"
>
Set Due Date Set Due Date
</a> </x-button>
<a @click="setFieldActive('startDate')" class="button"> <x-button
<span class="icon is-small"><icon icon="calendar-week"/></span> @click="setFieldActive('startDate')"
type="secondary"
icon="calendar-week"
>
Set a Start Date Set a Start Date
</a> </x-button>
<a @click="setFieldActive('endDate')" class="button"> <x-button
<span class="icon is-small"><icon icon="calendar-week"/></span> @click="setFieldActive('endDate')"
type="secondary"
icon="calendar-week"
>
Set an End Date Set an End Date
</a> </x-button>
<a @click="setFieldActive('reminders')" class="button"> <x-button
<span class="icon is-small"><icon icon="history"/></span> @click="setFieldActive('reminders')"
type="secondary"
icon="history"
>
Set Reminders Set Reminders
</a> </x-button>
<a @click="setFieldActive('repeatAfter')" class="button"> <x-button
<span class="icon is-small"><icon :icon="['far', 'clock']"/></span> @click="setFieldActive('repeatAfter')"
type="secondary"
:icon="['far', 'clock']"
>
Set a repeating interval Set a repeating interval
</a> </x-button>
<a @click="setFieldActive('percentDone')" class="button"> <x-button
<span class="icon is-small"><icon icon="percent"/></span> @click="setFieldActive('percentDone')"
type="secondary"
icon="percent"
>
Set Percent Done Set Percent Done
</a> </x-button>
<a <x-button
@click="setFieldActive('attachments')" @click="setFieldActive('attachments')"
@shortkey="setFieldActive('attachments')" @shortkey="setFieldActive('attachments')"
class="button" type="secondary"
v-shortkey="['f']"> v-shortkey="['f']"
<span class="icon is-small"><icon icon="paperclip"/></span> icon="paperclip"
>
Add attachments Add attachments
</a> </x-button>
<a <x-button
@click="setFieldActive('relatedTasks')" @click="setFieldActive('relatedTasks')"
@shortkey="setFieldActive('relatedTasks')" @shortkey="setFieldActive('relatedTasks')"
class="button" type="secondary"
v-shortkey="['r']"> v-shortkey="['r']"
<span class="icon is-small"><icon icon="tasks"/></span> icon="tasks"
>
Add task relations Add task relations
</a> </x-button>
<a @click="setFieldActive('moveList')" class="button"> <x-button
<span class="icon is-small"><icon icon="list"/></span> @click="setFieldActive('moveList')"
type="secondary"
icon="list"
>
Move task Move task
</a> </x-button>
<a @click="setFieldActive('color')" class="button"> <x-button
<span class="icon is-small"><icon icon="fill-drip"/></span> @click="setFieldActive('color')"
type="secondary"
icon="fill-drip"
>
Set task color Set task color
</a> </x-button>
<a @click="showDeleteModal = true" class="button is-danger is-outlined has-no-shadow has-no-border"> <x-button
<span class="icon is-small"><icon icon="trash-alt"/></span> @click="showDeleteModal = true"
icon="trash-alt"
:shadow="false"
class="is-danger is-outlined has-no-border"
>
Delete task Delete task
</a> </x-button>
<!-- Created / Updated [by] --> <!-- Created / Updated [by] -->
<p class="created"> <p class="created">
@ -568,13 +608,17 @@ export default {
this.activeFields[fieldName] = true this.activeFields[fieldName] = true
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs[fieldName]) { if (this.$refs[fieldName]) {
this.$refs[fieldName].$el.focus(); this.$refs[fieldName].$el.focus()
// scroll the field to the center of the screen if not in viewport already // scroll the field to the center of the screen if not in viewport already
const boundingRect = this.$refs[fieldName].$el.getBoundingClientRect(); const boundingRect = this.$refs[fieldName].$el.getBoundingClientRect()
if (boundingRect.top > (window.scrollY + window.innerHeight) || boundingRect.top < window.scrollY) if (boundingRect.top > (window.scrollY + window.innerHeight) || boundingRect.top < window.scrollY)
this.$refs[fieldName].$el.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"}); this.$refs[fieldName].$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
})
} }
}) })
}, },

View file

@ -1,36 +1,42 @@
<template> <template>
<div class="loader-container is-max-width-desktop" :class="{ 'is-loading': teamService.loading}"> <div
<div class="card is-fullwidth" v-if="userIsAdmin"> class="loader-container is-max-width-desktop"
<header class="card-header"> :class="{ 'is-loading': teamService.loading }"
<p class="card-header-title"> >
Edit Team <card class="is-fullwidth" v-if="userIsAdmin" title="Edit Team">
</p>
</header>
<div class="card-content">
<div class="content">
<form @submit.prevent="save()"> <form @submit.prevent="save()">
<div class="field"> <div class="field">
<label class="label" for="teamtext">Team Name</label> <label class="label" for="teamtext"
>Team Name</label
>
<div class="control"> <div class="control">
<input <input
:class="{ 'disabled': teamMemberService.loading}" :class="{
disabled: teamMemberService.loading,
}"
:disabled="teamMemberService.loading" :disabled="teamMemberService.loading"
class="input" class="input"
id="teamtext" id="teamtext"
placeholder="The team text is here..." placeholder="The team text is here..."
type="text" type="text"
v-focus v-focus
v-model="team.name"/> v-model="team.name"
/>
</div> </div>
</div> </div>
<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>
<div class="field"> <div class="field">
<label class="label" for="teamdescription">Description</label> <label class="label" for="teamdescription"
>Description</label
>
<div class="control"> <div class="control">
<editor <editor
:class="{ 'disabled': teamService.loading}" :class="{ disabled: teamService.loading }"
:disabled="teamService.loading" :disabled="teamService.loading"
:preview-is-default="false" :preview-is-default="false"
id="teamdescription" id="teamdescription"
@ -43,35 +49,27 @@
<div class="field has-addons mt-4"> <div class="field has-addons mt-4">
<div class="control is-fullwidth"> <div class="control is-fullwidth">
<button <x-button
@click="save()" @click="save()"
:class="{ 'is-loading': teamService.loading}" :loading="teamService.loading"
class="button is-primary is-fullwidth"> class="is-fullwidth"
>
Save Save
</button> </x-button>
</div> </div>
<div class="control"> <div class="control">
<button <x-button
@click="showDeleteModal = true" @click="showDeleteModal = true"
:class="{ 'is-loading': teamService.loading}" :loading="teamService.loading"
class="button is-danger"> class="is-danger"
<span class="icon"> icon="trash-alt"
<icon icon="trash-alt"/> />
</span>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</card>
<div class="card is-fullwidth has-overflow"> <card class="is-fullwidth has-overflow" title="Team Members">
<header class="card-header"> <div class="p-4" v-if="userIsAdmin">
<p class="card-header-title">
Team Members
</p>
</header>
<div class="card-content" v-if="userIsAdmin">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<multiselect <multiselect
@ -84,12 +82,9 @@
/> />
</div> </div>
<div class="control"> <div class="control">
<button class="button is-primary" @click="addUser"> <x-button @click="addUser" icon="plus">
<span class="icon is-small">
<icon icon="plus"/>
</span>
Add To Team Add To Team
</button> </x-button>
</div> </div>
</div> </div>
</div> </div>
@ -117,44 +112,55 @@
</template> </template>
</td> </td>
<td class="actions" v-if="userIsAdmin"> <td class="actions" v-if="userIsAdmin">
<button :class="{'is-loading': teamMemberService.loading}" @click="() => toggleUserType(m)" <x-button
class="button buttonright is-primary" :loading="teamMemberService.loading"
v-if="m.id !== userInfo.id"> @click="() => toggleUserType(m)"
class="mr-2"
v-if="m.id !== userInfo.id"
>
Make {{ m.admin ? 'Member' : 'Admin' }} Make {{ m.admin ? 'Member' : 'Admin' }}
</button> </x-button>
<button :class="{'is-loading': teamMemberService.loading}" <x-button
:loading="teamMemberService.loading"
@click="() => {member = m; showUserDeleteModal = true}" @click="() => {member = m; showUserDeleteModal = true}"
class="button is-danger" class="is-danger"
v-if="m.id !== userInfo.id"> v-if="m.id !== userInfo.id"
<span class="icon"> icon="trash-alt"
<icon icon="trash-alt"/> />
</span>
</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </card>
<!-- Team delete modal --> <!-- Team delete modal -->
<modal <modal
@close="showDeleteModal = false" @close="showDeleteModal = false"
@submit="deleteTeam()" @submit="deleteTeam()"
v-if="showDeleteModal"> v-if="showDeleteModal"
>
<span slot="header">Delete the team</span> <span slot="header">Delete the team</span>
<p slot="text">Are you sure you want to delete this team and all of its members?<br/> <p slot="text">
All team members will loose access to lists and namespaces shared with this team.<br/> Are you sure you want to delete this team and all of its
<b>This CANNOT BE UNDONE!</b></p> members?<br/>
All team members will loose access to lists and namespaces
shared with this team.<br/>
<b>This CANNOT BE UNDONE!</b>
</p>
</modal> </modal>
<!-- User delete modal --> <!-- User delete modal -->
<modal <modal
@close="showUserDeleteModal = false" @close="showUserDeleteModal = false"
@submit="deleteUser()" @submit="deleteUser()"
v-if="showUserDeleteModal"> v-if="showUserDeleteModal"
>
<span slot="header">Remove a user from the team</span> <span slot="header">Remove a user from the team</span>
<p slot="text">Are you sure you want to remove this user from the team?<br/> <p slot="text">
They will loose access to all lists and namespaces this team has access to.<br/> Are you sure you want to remove this user from the team?<br/>
<b>This CANNOT BE UNDONE!</b></p> They will loose access to all lists and namespaces this team has
access to.<br/>
<b>This CANNOT BE UNDONE!</b>
</p>
</modal> </modal>
</div> </div>
</template> </template>
@ -199,7 +205,9 @@ export default {
components: { components: {
Multiselect, Multiselect,
editor: () => ({ editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'), component: import(
/* webpackChunkName: "editor" */ '../../components/input/editor'
),
loading: LoadingComponent, loading: LoadingComponent,
error: ErrorComponent, error: ErrorComponent,
timeout: 60000, timeout: 60000,
@ -213,25 +221,30 @@ export default {
}, },
watch: { watch: {
// call again the method if the route changes // call again the method if the route changes
'$route': 'loadTeam', $route: 'loadTeam',
}, },
computed: { computed: {
userIsAdmin() { userIsAdmin() {
return this.team && this.team.maxRight && this.team.maxRight > Rights.READ return (
this.team &&
this.team.maxRight &&
this.team.maxRight > Rights.READ
)
}, },
...mapState({ ...mapState({
userInfo: state => state.auth.info, userInfo: (state) => state.auth.info,
}), }),
}, },
methods: { methods: {
loadTeam() { loadTeam() {
this.team = new TeamModel({id: this.teamId}) this.team = new TeamModel({id: this.teamId})
this.teamService.get(this.team) this.teamService
.then(response => { .get(this.team)
.then((response) => {
this.$set(this, 'team', response) this.$set(this, 'team', response)
this.setTitle(`Edit Team ${this.team.name}`) this.setTitle(`Edit Team ${this.team.name}`)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
@ -242,32 +255,47 @@ export default {
} }
this.showError = false this.showError = false
this.teamService.update(this.team) this.teamService
.then(response => { .update(this.team)
.then((response) => {
this.team = response this.team = response
this.success({message: 'The team was successfully updated.'}, this) this.success(
{message: 'The team was successfully updated.'},
this
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
deleteTeam() { deleteTeam() {
this.teamService.delete(this.team) this.teamService
.delete(this.team)
.then(() => { .then(() => {
this.success({message: 'The team was successfully deleted.'}, this) this.success(
{message: 'The team was successfully deleted.'},
this
)
router.push({name: 'teams.index'}) router.push({name: 'teams.index'})
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
deleteUser() { deleteUser() {
this.teamMemberService.delete(this.member) this.teamMemberService
.delete(this.member)
.then(() => { .then(() => {
this.success({message: 'The user was successfully deleted from the team.'}, this) this.success(
{
message:
'The user was successfully deleted from the team.',
},
this
)
this.loadTeam() this.loadTeam()
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
.finally(() => { .finally(() => {
@ -279,29 +307,42 @@ export default {
teamId: this.teamId, teamId: this.teamId,
username: this.newMember.username, username: this.newMember.username,
}) })
this.teamMemberService.create(newMember) this.teamMemberService
.create(newMember)
.then(() => { .then(() => {
this.loadTeam() this.loadTeam()
this.success({message: 'The team member was successfully added.'}, this) this.success(
{message: 'The team member was successfully added.'},
this
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
toggleUserType(member) { toggleUserType(member) {
member.admin = !member.admin member.admin = !member.admin
member.teamId = this.teamId member.teamId = this.teamId
this.teamMemberService.update(member) this.teamMemberService
.then(r => { .update(member)
.then((r) => {
for (const tm in this.team.members) { for (const tm in this.team.members) {
if (this.team.members[tm].id === member.id) { if (this.team.members[tm].id === member.id) {
this.$set(this.team.members[tm], 'admin', r.admin) this.$set(this.team.members[tm], 'admin', r.admin)
break break
} }
} }
this.success({message: 'The team member was successfully made ' + (member.admin ? 'admin' : 'member') + '.'}, this) this.success(
{
message:
'The team member was successfully made ' +
(member.admin ? 'admin' : 'member') +
'.',
},
this
)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },
@ -311,11 +352,12 @@ export default {
return return
} }
this.userService.getAll({}, {s: query}) this.userService
.then(response => { .getAll({}, {s: query})
.then((response) => {
this.$set(this, 'foundUsers', response) this.$set(this, 'foundUsers', response)
}) })
.catch(e => { .catch((e) => {
this.error(e, this) this.error(e, this)
}) })
}, },

View file

@ -1,11 +1,12 @@
<template> <template>
<div class="content loader-container is-max-width-desktop" v-bind:class="{ 'is-loading': teamService.loading}"> <div class="content loader-container is-max-width-desktop" v-bind:class="{ 'is-loading': teamService.loading}">
<router-link :to="{name:'teams.create'}" class="button is-primary button-right"> <x-button
<span class="icon is-small"> :to="{name:'teams.create'}"
<icon icon="plus"/> class="is-pulled-right"
</span> icon="plus"
>
New Team New Team
</router-link> </x-button>
<h1>Teams</h1> <h1>Teams</h1>
<ul class="teams box" v-if="teams.length > 0"> <ul class="teams box" v-if="teams.length > 0">

View file

@ -16,12 +16,9 @@
v-model="team.name"/> v-model="team.name"/>
</p> </p>
<p class="control"> <p class="control">
<button class="button is-primary has-no-shadow" type="submit"> <x-button :shadow="false" @click="newTeam" icon="plus">
<span class="icon is-small">
<icon icon="plus"/>
</span>
Add Add
</button> </x-button>
</p> </p>
</div> </div>
<p class="help is-danger" v-if="showError && team.name === ''"> <p class="help is-danger" v-if="showError && team.name === ''">

View file

@ -54,12 +54,19 @@
<div class="field is-grouped login-buttons"> <div class="field is-grouped login-buttons">
<div class="control is-expanded"> <div class="control is-expanded">
<button class="button is-primary" type="submit" v-bind:class="{ 'is-loading': loading}"> <x-button
@click="submit"
:loading="loading"
>
Login Login
</button> </x-button>
<router-link :to="{ name: 'user.register' }" class="button" v-if="registrationEnabled"> <x-button
:to="{ name: 'user.register' }"
v-if="registrationEnabled"
type="secondary"
>
Register Register
</router-link> </x-button>
</div> </div>
<div class="control"> <div class="control">
<router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link"> <router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link">
@ -73,9 +80,15 @@
</form> </form>
<div v-if="openidConnect.enabled && openidConnect.providers && openidConnect.providers.length > 0" class="mt-4"> <div v-if="openidConnect.enabled && openidConnect.providers && openidConnect.providers.length > 0" class="mt-4">
<a @click="redirectToProvider(p)" v-for="(p, k) in openidConnect.providers" :key="k" class="button is-fullwidth"> <x-button
@click="redirectToProvider(p)"
v-for="(p, k) in openidConnect.providers"
:key="k"
type="secondary"
class="is-fullwidth"
>
Log in with {{ p.name }} Log in with {{ p.name }}
</a> </x-button>
</div> </div>
<legal/> <legal/>

View file

@ -35,9 +35,12 @@
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button :class="{ 'is-loading': this.passwordResetService.loading}" class="button is-primary" <x-button
type="submit">Reset your password :loading="this.passwordResetService.loading"
</button> @click="submit"
>
Reset your password
</x-button>
</div> </div>
</div> </div>
<div class="notification is-info" v-if="this.passwordResetService.loading"> <div class="notification is-info" v-if="this.passwordResetService.loading">
@ -51,7 +54,9 @@
<div class="notification is-success"> <div class="notification is-success">
{{ successMessage }} {{ successMessage }}
</div> </div>
<router-link :to="{ name: 'user.login' }" class="button is-primary">Login</router-link> <x-button :to="{ name: 'user.login' }">
Login
</x-button>
</div> </div>
<legal/> <legal/>
</div> </div>

View file

@ -62,10 +62,15 @@
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-primary" type="submit" :class="{ 'is-loading': loading}" id="register-submit"> <x-button
:loading="loading"
id="register-submit"
@click="submit"
class="mr-2"
>
Register Register
</button> </x-button>
<router-link :to="{ name: 'user.login' }" class="button">Login</router-link> <x-button :to="{ name: 'user.login' }" type="secondary">Login</x-button>
</div> </div>
</div> </div>
<div class="notification is-info" v-if="loading"> <div class="notification is-info" v-if="loading">
@ -136,9 +141,3 @@ export default {
}, },
} }
</script> </script>
<style scoped>
.button {
margin: 0 0.4em 0 0;
}
</style>

View file

@ -20,11 +20,13 @@
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<button class="button is-primary" type="submit" <x-button
v-bind:class="{ 'is-loading': passwordResetService.loading}">Send me a password reset @click="submit"
link :loading="passwordResetService.loading"
</button> >
<router-link :to="{ name: 'user.login' }" class="button">Login</router-link> Send me a password reset link
</x-button>
<x-button :to="{ name: 'user.login' }" type="secondary">Login</x-button>
</div> </div>
</div> </div>
<div class="notification is-danger" v-if="errorMsg"> <div class="notification is-danger" v-if="errorMsg">
@ -35,7 +37,7 @@
<div class="notification is-success"> <div class="notification is-success">
Check your inbox! You should have a mail with instructions on how to reset your password. Check your inbox! You should have a mail with instructions on how to reset your password.
</div> </div>
<router-link :to="{ name: 'user.login' }" class="button is-primary">Login</router-link> <x-button :to="{ name: 'user.login' }">Login</x-button>
</div> </div>
<legal/> <legal/>
</div> </div>

View file

@ -3,14 +3,7 @@
:class="{ 'is-loading': passwordUpdateService.loading || emailUpdateService.loading || totpService.loading }" :class="{ 'is-loading': passwordUpdateService.loading || emailUpdateService.loading || totpService.loading }"
class="loader-container is-max-width-desktop"> class="loader-container is-max-width-desktop">
<!-- Password update --> <!-- Password update -->
<div class="card"> <card title="Update Your Password">
<header class="card-header">
<p class="card-header-title">
Update Your Password
</p>
</header>
<div class="card-content">
<div class="content">
<form @submit.prevent="updatePassword()"> <form @submit.prevent="updatePassword()">
<div class="field"> <div class="field">
<label class="label" for="newPassword">New Password</label> <label class="label" for="newPassword">New Password</label>
@ -50,25 +43,16 @@
</div> </div>
</form> </form>
<div class="bigbuttons"> <x-button
<button :class="{ 'is-loading': passwordUpdateService.loading}" @click="updatePassword()" :loading="passwordUpdateService.loading"
class="button is-primary is-fullwidth"> @click="updatePassword()"
class="is-fullwidth mt-4">
Save Save
</button> </x-button>
</div> </card>
</div>
</div>
</div>
<!-- Update E-Mail --> <!-- Update E-Mail -->
<div class="card"> <card title="Update Your E-Mail Address">
<header class="card-header">
<p class="card-header-title">
Update Your E-Mail Address
</p>
</header>
<div class="card-content">
<div class="content">
<form @submit.prevent="updateEmail()"> <form @submit.prevent="updateEmail()">
<div class="field"> <div class="field">
<label class="label" for="newEmail">New Email Address</label> <label class="label" for="newEmail">New Email Address</label>
@ -96,25 +80,16 @@
</div> </div>
</form> </form>
<div class="bigbuttons"> <x-button
<button :class="{ 'is-loading': emailUpdateService.loading}" @click="updateEmail()" :loading="emailUpdateService.loading"
class="button is-primary is-fullwidth"> @click="updateEmail()"
class="is-fullwidth mt-4">
Save Save
</button> </x-button>
</div> </card>
</div>
</div>
</div>
<!-- General --> <!-- General -->
<div class="card update-name"> <card title="General Settings" class="general-settings">
<header class="card-header">
<p class="card-header-title">
General Settings
</p>
</header>
<div class="card-content">
<div class="content">
<div class="field"> <div class="field">
<label class="label" for="newName">Name</label> <label class="label" for="newName">Name</label>
<div class="control"> <div class="control">
@ -134,35 +109,27 @@
</label> </label>
</div> </div>
<div class="bigbuttons"> <x-button
<button :class="{ 'is-loading': userSettingsService.loading}" @click="updateSettings()" :loading="userSettingsService.loading"
class="button is-primary is-fullwidth"> @click="updateSettings()"
class="is-fullwidth mt-4"
>
Save Save
</button> </x-button>
</div> </card>
</div>
</div>
</div>
<!-- Avatar --> <!-- Avatar -->
<avatar-settings/> <avatar-settings/>
<!-- TOTP --> <!-- TOTP -->
<div class="card" v-if="totpEnabled"> <card title="Two Factor Authentication" v-if="totpEnabled">
<header class="card-header"> <x-button
<p class="card-header-title"> :loading="totpService.loading"
Two Factor Authentication
</p>
</header>
<div class="card-content">
<a
:class="{ 'is-loading': totpService.loading }"
@click="totpEnroll()" @click="totpEnroll()"
class="button is-primary"
v-if="!totpEnrolled && totp.secret === ''"> v-if="!totpEnrolled && totp.secret === ''">
Enroll Enroll
</a> </x-button>
<div class="content" v-else-if="totp.secret !== '' && !totp.enabled"> <template v-else-if="totp.secret !== '' && !totp.enabled">
<p> <p>
To finish your setup, use this secret in your totp app (Google Authenticator or similar): To finish your setup, use this secret in your totp app (Google Authenticator or similar):
<strong>{{ totp.secret }}</strong><br/> <strong>{{ totp.secret }}</strong><br/>
@ -184,14 +151,14 @@
v-model="totpConfirmPasscode"/> v-model="totpConfirmPasscode"/>
</div> </div>
</div> </div>
<a @click="totpConfirm()" class="button is-primary">Confirm</a> <x-button @click="totpConfirm()">Confirm</x-button>
</div> </template>
<div class="content" v-else-if="totp.secret !== '' && totp.enabled"> <template v-else-if="totp.secret !== '' && totp.enabled">
<p> <p>
You've sucessfully set up two factor authentication! You've sucessfully set up two factor authentication!
</p> </p>
<p v-if="!totpDisableForm"> <p v-if="!totpDisableForm">
<a @click="totpDisableForm = true" class="button is-danger">Disable</a> <x-button @click="totpDisableForm = true" class="is-danger">Disable</x-button>
</p> </p>
<div v-if="totpDisableForm"> <div v-if="totpDisableForm">
<div class="field"> <div class="field">
@ -207,38 +174,22 @@
v-model="totpDisablePassword"/> v-model="totpDisablePassword"/>
</div> </div>
</div> </div>
<a @click="totpDisable()" class="button is-danger">Disable two factor authentication</a> <x-button @click="totpDisable()" class="is-danger">Disable two factor authentication</x-button>
</div>
</div>
</div>
</div> </div>
</template>
</card>
<!-- Migration --> <!-- Migration -->
<div class="card" v-if="migratorsEnabled"> <card title="Migrate from other services to Vikunja" v-if="migratorsEnabled">
<header class="card-header"> <x-button
<p class="card-header-title">
Migrate from other services to Vikunja
</p>
</header>
<div class="card-content">
<router-link
:to="{name: 'migrate.start'}" :to="{name: 'migrate.start'}"
class="button is-primary"
v-if="migratorsEnabled"
> >
Import your data into Vikunja Import your data into Vikunja
</router-link> </x-button>
</div> </card>
</div>
<!-- Caldav --> <!-- Caldav -->
<div class="card" v-if="caldavEnabled"> <card v-if="caldavEnabled" title="Caldav">
<header class="card-header">
<p class="card-header-title">
Caldav
</p>
</header>
<div class="card-content content">
<p> <p>
You can connect Vikunja to caldav clients to view and manage all tasks from different clients. You can connect Vikunja to caldav clients to view and manage all tasks from different clients.
Enter this url into your client: Enter this url into your client:
@ -248,11 +199,12 @@
<input type="text" v-model="caldavUrl" class="input" readonly/> <input type="text" v-model="caldavUrl" class="input" readonly/>
</div> </div>
<div class="control"> <div class="control">
<a @click="copy(caldavUrl)" class="button is-primary has-no-shadow" v-tooltip="'Copy to clipboard'"> <x-button
<span class="icon"> @click="copy(caldavUrl)"
<icon icon="paste"/> :shadow="false"
</span> v-tooltip="'Copy to clipboard'"
</a> icon="paste"
/>
</div> </div>
</div> </div>
<p> <p>
@ -260,8 +212,7 @@
More information about caldav in Vikunja More information about caldav in Vikunja
</a> </a>
</p> </p>
</div> </card>
</div>
</div> </div>
</template> </template>