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,68 +1,65 @@
<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> <input
</header> @change="uploadBackground"
<div class="card-content"> accept="image/*"
<div class="content" v-if="uploadBackgroundEnabled"> class="is-hidden"
<input ref="backgroundUploadInput"
@change="uploadBackground" type="file"
accept="image/*" />
class="is-hidden" <x-button
ref="backgroundUploadInput" :loading="backgroundUploadService.loading"
type="file" @click="$refs.backgroundUploadInput.click()"
/> type="primary"
<a >
:class="{'is-loading': backgroundUploadService.loading}" Choose a background from your pc
@click="$refs.backgroundUploadInput.click()" </x-button>
class="button is-primary"
>
Choose a background from your pc
</a>
</div>
<div class="content" v-if="unsplashBackgroundEnabled">
<input
:class="{'is-loading': backgroundService.loading}"
@keyup="() => newBackgroundSearch()"
class="input is-expanded"
placeholder="Search for a background..."
type="text"
v-model="backgroundSearchTerm"
/>
<p class="unsplash-link"><a href="https://unsplash.com" target="_blank">Powered by Unsplash</a></p>
<div class="image-search-result">
<a
:key="im.id"
:style="{'background-image': `url(${backgroundThumbs[im.id]})`}"
@click="() => setBackground(im.id)"
class="image"
v-for="im in backgroundSearchResult">
<a :href="`https://unsplash.com/@${im.info.author}`" target="_blank" class="info">
{{ im.info.authorName }}
</a>
</a>
</div>
<a
:disabled="backgroundService.loading"
@click="() => searchBackgrounds(currentPage + 1)"
class="button is-centered is-load-more-button has-no-shadow mt-4"
v-if="backgroundSearchResult.length > 0"
>
<template v-if="backgroundService.loading">
Loading...
</template>
<template v-else>
Load more photos
</template>
</a>
</div>
</div> </div>
</div> <template v-if="unsplashBackgroundEnabled">
<input
:class="{'is-loading': backgroundService.loading}"
@keyup="() => newBackgroundSearch()"
class="input is-expanded"
placeholder="Search for a background..."
type="text"
v-model="backgroundSearchTerm"
/>
<p class="unsplash-link"><a href="https://unsplash.com" target="_blank">Powered by Unsplash</a></p>
<div class="image-search-result">
<a
:key="im.id"
:style="{'background-image': `url(${backgroundThumbs[im.id]})`}"
@click="() => setBackground(im.id)"
class="image"
v-for="im in backgroundSearchResult">
<a :href="`https://unsplash.com/@${im.info.author}`" target="_blank" class="info">
{{ im.info.authorName }}
</a>
</a>
</div>
<x-button
:disabled="backgroundService.loading"
@click="() => searchBackgrounds(currentPage + 1)"
class="is-load-more-button mt-4"
:shadow="false"
type="secondary"
v-if="backgroundSearchResult.length > 0"
>
<template v-if="backgroundService.loading">
Loading...
</template>
<template v-else>
Load more photos
</template>
</x-button>
</template>
</card>
</template> </template>
<script> <script>

View file

@ -1,182 +1,180 @@
<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> <fancycheckbox
<fancycheckbox v-model="filters.requireAllFilters"
v-model="filters.requireAllFilters" @change="setFilterConcat()"
@change="setFilterConcat()" >
> Require all filters to be true for a task to show up
Require all filters to be true for a task to show up </fancycheckbox>
</fancycheckbox> <div class="field">
<div class="field"> <label class="label">Show Done Tasks</label>
<label class="label">Show Done Tasks</label> <div class="control">
<div class="control"> <fancycheckbox @change="setDoneFilter" v-model="filters.done">
<fancycheckbox @change="setDoneFilter" v-model="filters.done"> Show Done Tasks
Show Done Tasks </fancycheckbox>
</fancycheckbox>
</div>
</div> </div>
<div class="field"> </div>
<label class="label">Priority</label> <div class="field">
<div class="control single-value-control"> <label class="label">Priority</label>
<priority-select <div class="control single-value-control">
:disabled="!filters.usePriority" <priority-select
v-model.number="filters.priority" :disabled="!filters.usePriority"
@change="setPriority" v-model.number="filters.priority"
/> @change="setPriority"
<fancycheckbox />
v-model="filters.usePriority" <fancycheckbox
@change="setPriority" v-model="filters.usePriority"
> @change="setPriority"
Enable Filter By Priority >
</fancycheckbox> Enable Filter By Priority
</div> </fancycheckbox>
</div> </div>
<div class="field"> </div>
<label class="label">Percent Done</label> <div class="field">
<div class="control single-value-control"> <label class="label">Percent Done</label>
<percent-done-select <div class="control single-value-control">
v-model.number="filters.percentDone" <percent-done-select
@change="setPercentDoneFilter" v-model.number="filters.percentDone"
:disabled="!filters.usePercentDone" @change="setPercentDoneFilter"
/> :disabled="!filters.usePercentDone"
<fancycheckbox />
v-model="filters.usePercentDone" <fancycheckbox
@change="setPercentDoneFilter" v-model="filters.usePercentDone"
> @change="setPercentDoneFilter"
Enable Filter By Percent Done >
</fancycheckbox> Enable Filter By Percent Done
</div> </fancycheckbox>
</div> </div>
<div class="field"> </div>
<label class="label">Due Date</label> <div class="field">
<div class="control"> <label class="label">Due Date</label>
<flat-pickr <div class="control">
:config="flatPickerConfig" <flat-pickr
@on-close="setDueDateFilter" :config="flatPickerConfig"
class="input" @on-close="setDueDateFilter"
placeholder="Due Date Range" class="input"
v-model="filters.dueDate" placeholder="Due Date Range"
/> v-model="filters.dueDate"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label">Start Date</label> <div class="field">
<div class="control"> <label class="label">Start Date</label>
<flat-pickr <div class="control">
:config="flatPickerConfig" <flat-pickr
@on-close="setStartDateFilter" :config="flatPickerConfig"
class="input" @on-close="setStartDateFilter"
placeholder="Start Date Range" class="input"
v-model="filters.startDate" placeholder="Start Date Range"
/> v-model="filters.startDate"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label">End Date</label> <div class="field">
<div class="control"> <label class="label">End Date</label>
<flat-pickr <div class="control">
:config="flatPickerConfig" <flat-pickr
@on-close="setEndDateFilter" :config="flatPickerConfig"
class="input" @on-close="setEndDateFilter"
placeholder="End Date Range" class="input"
v-model="filters.endDate" placeholder="End Date Range"
/> v-model="filters.endDate"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label">Reminders</label> <div class="field">
<div class="control"> <label class="label">Reminders</label>
<flat-pickr <div class="control">
:config="flatPickerConfig" <flat-pickr
@on-close="setReminderFilter" :config="flatPickerConfig"
class="input" @on-close="setReminderFilter"
placeholder="Reminder Date Range" class="input"
v-model="filters.reminders" placeholder="Reminder Date Range"
/> v-model="filters.reminders"
</div> />
</div> </div>
</div>
<div class="field"> <div class="field">
<label class="label">Assignees</label> <label class="label">Assignees</label>
<div class="control"> <div class="control">
<multiselect <multiselect
:loading="usersService.loading" :loading="usersService.loading"
placeholder="Type to search for a user..." placeholder="Type to search for a user..."
@search="query => find('users', query)" @search="query => find('users', query)"
:search-results="foundusers" :search-results="foundusers"
@select="() => add('users', 'assignees')" @select="() => add('users', 'assignees')"
label="username" label="username"
:multiple="true" :multiple="true"
@remove="() => remove('users', 'assignees')" @remove="() => remove('users', 'assignees')"
v-model="users" v-model="users"
/> />
</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">
<multiselect <multiselect
:loading="labelService.loading" :loading="labelService.loading"
placeholder="Type to search for a label..." placeholder="Type to search for a label..."
@search="findLabels" @search="findLabels"
:search-results="foundLabels" :search-results="foundLabels"
@select="label => addLabel(label)" @select="label => addLabel(label)"
label="title" label="title"
:multiple="true" :multiple="true"
v-model="labels" v-model="labels"
> >
<template v-slot:tag="props"> <template v-slot:tag="props">
<span <span
:style="{'background': props.item.hexColor, 'color': props.item.textColor}" :style="{'background': props.item.hexColor, 'color': props.item.textColor}"
class="tag ml-2 mt-2"> class="tag ml-2 mt-2">
<span>{{ props.item.title }}</span> <span>{{ props.item.title }}</span>
<a @click="removeLabel(props.item)" class="delete is-small"></a> <a @click="removeLabel(props.item)" class="delete is-small"></a>
</span> </span>
</template> </template>
</multiselect> </multiselect>
</div>
</div>
<template v-if="$route.name === 'filters.create' || $route.name === 'list.edit'">
<div class="field">
<label class="label">Lists</label>
<div class="control">
<multiselect
:loading="listsService.loading"
placeholder="Type to search for a list..."
@search="query => find('lists', query)"
:search-results="foundlists"
@select="() => add('lists', 'list_id')"
label="title"
@remove="() => remove('lists', 'list_id')"
:multiple="true"
v-model="lists"
/>
</div> </div>
</div> </div>
<div class="field">
<template v-if="$route.name === 'filters.create' || $route.name === 'list.edit'"> <label class="label">Namespaces</label>
<div class="field"> <div class="control">
<label class="label">Lists</label> <multiselect
<div class="control"> :loading="namespaceService.loading"
<multiselect placeholder="Type to search for a namespace..."
:loading="listsService.loading" @search="query => find('namespace', query)"
placeholder="Type to search for a list..." :search-results="foundnamespace"
@search="query => find('lists', query)" @select="() => add('namespace', 'namespace')"
:search-results="foundlists" label="title"
@select="() => add('lists', 'list_id')" @remove="() => remove('namespace', 'namespace')"
label="title" :multiple="true"
@remove="() => remove('lists', 'list_id')" v-model="namespace"
:multiple="true" />
v-model="lists"
/>
</div>
</div> </div>
<div class="field"> </div>
<label class="label">Namespaces</label> </template>
<div class="control"> </card>
<multiselect
:loading="namespaceService.loading"
placeholder="Type to search for a namespace..."
@search="query => find('namespace', query)"
:search-results="foundnamespace"
@select="() => add('namespace', 'namespace')"
label="title"
@remove="() => remove('namespace', 'namespace')"
:multiple="true"
v-model="namespace"
/>
</div>
</div>
</template>
</div>
</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,71 +2,50 @@
<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>
<p class="card-header-title">Available Keyboard Shortcuts</p> <strong>Toggle The Menu</strong>
</header> <shortcut :keys="['ctrl', 'e']"/>
<div class="card-content content"> </p>
<p> <h3>Kanban</h3>
<strong>Toggle The Menu</strong> <div class="message is-primary" v-if="$route.name === 'list.kanban'">
<span class="shortcuts"> <div class="message-body">
<span>ctrl</span> These shortcuts work on the current page.
<i>+</i>
<span>e</span>
</span>
</p>
<h3>Kanban</h3>
<div class="message is-primary" v-if="$route.name === 'list.kanban'">
<div class="message-body">
These shortcuts work on the current page.
</div>
</div> </div>
<p>
<strong>Mark a task as done</strong>
<span class="shortcuts">
<span>ctrl</span>
<i>+</i>
<span>click</span>
</span>
</p>
<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-body">
These shortcuts work on the current page.
</div>
</div>
<p>
<strong>Assign this task to a user</strong>
<span class="shortcuts">
<span>a</span>
</span>
</p>
<p>
<strong>Add labels to this task</strong>
<span class="shortcuts">
<span>l</span>
</span>
</p>
<p>
<strong>Change the due date of this task</strong>
<span class="shortcuts">
<span>d</span>
</span>
</p>
<p>
<strong>Add an attachment to this task</strong>
<span class="shortcuts">
<span>f</span>
</span>
</p>
<p>
<strong>Modify related tasks of this task</strong>
<span class="shortcuts">
<span>r</span>
</span>
</p>
</div> </div>
</div> <p>
<strong>Mark a task as done</strong>
<shortcut :keys="['ctrl', 'click']"/>
</p>
<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-body">
These shortcuts work on the current page.
</div>
</div>
<p>
<strong>Assign this task to a user</strong>
<shortcut :keys="['a']"/>
</p>
<p>
<strong>Add labels to this task</strong>
<shortcut :keys="['l']"/>
</p>
<p>
<strong>Change the due date of this task</strong>
<shortcut :keys="['d']"/>
</p>
<p>
<strong>Add an attachment to this task</strong>
<shortcut :keys="['f']"/>
</p>
<p>
<strong>Modify related tasks of this task</strong>
<shortcut :keys="['r']"/>
</p>
</card>
</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

@ -4,16 +4,28 @@
<div class="modal-container" @click.prevent.stop="$emit('close')"> <div class="modal-container" @click.prevent.stop="$emit('close')">
<div class="modal-content"> <div class="modal-content">
<slot> <slot>
<div class="header"> <div class="header">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<div class="content"> <div class="content">
<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')"
</div> type="tertary"
class="has-text-danger"
>
Cancel
</x-button>
<x-button
@click="$emit('submit')"
type="primary"
:shadow="false"
>
Do it!
</x-button>
</div>
</slot> </slot>
</div> </div>
</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
<span class="icon"> @click="copy(getShareLink(s.hash))"
<icon icon="paste"/> :shadow="false"
</span> v-tooltip="'Copy to clipboard'"
</a> >
<span class="icon">
<icon icon="paste"/>
</span>
</x-button>
</div> </div>
</div> </div>
</td> </td>
@ -57,30 +62,35 @@
</td> </td>
<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"> <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,84 +16,114 @@
/> />
</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>
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table is-striped is-hoverable is-fullwidth">
<tbody> <tbody>
<tr :key="s.id" v-for="s in sharables"> <tr :key="s.id" v-for="s in sharables">
<template v-if="shareType === 'user'"> <template v-if="shareType === 'user'">
<td>{{ s.getDisplayName() }}</td> <td>{{ s.getDisplayName() }}</td>
<td> <td>
<template v-if="s.id === userInfo.id"> <template v-if="s.id === userInfo.id">
<b class="is-success">You</b> <b class="is-success">You</b>
</template>
</td>
</template>
<template v-if="shareType === 'team'">
<td>
<router-link
:to="{
name: 'teams.edit',
params: { id: s.id },
}"
>
{{ s.name }}
</router-link>
</td>
</template>
<td class="type">
<template v-if="s.right === rights.ADMIN">
<span class="icon is-small">
<icon icon="lock" />
</span>
Admin
</template>
<template v-else-if="s.right === rights.READ_WRITE">
<span class="icon is-small">
<icon icon="pen" />
</span>
Write
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users" />
</span>
Read-only
</template> </template>
</td> </td>
</template> <td class="actions" v-if="userIsAdmin">
<template v-if="shareType === 'team'"> <div class="select">
<td> <select
<router-link :to="{name: 'teams.edit', params: {id: s.id}}"> @change="toggleType(s)"
{{ s.name }} class="button mr-2"
</router-link> 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>
</select>
</div>
<x-button
@click="
() => {
sharable = s
showDeleteModal = true
}
"
class="is-danger"
icon="trash-alt"
/>
</td> </td>
</template> </tr>
<td class="type">
<template v-if="s.right === rights.ADMIN">
<span class="icon is-small">
<icon icon="lock"/>
</span>
Admin
</template>
<template v-else-if="s.right === rights.READ_WRITE">
<span class="icon is-small">
<icon icon="pen"/>
</span>
Write
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users"/>
</span>
Read-only
</template>
</td>
<td class="actions" v-if="userIsAdmin">
<div class="select">
<select @change="toggleType(s)" class="button buttonright" 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>
</select>
</div>
<button @click="() => {sharable = s; showDeleteModal = true}"
class="button is-danger icon-only">
<span class="icon">
<icon icon="trash-alt"/>
</span>
</button>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<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,19 +57,29 @@
</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':
'has-light-text': !colorIsDark(t.hexColor), taskToEdit !== null && taskToEdit.id === t.id,
'has-dark-text': colorIsDark(t.hexColor) 'has-light-text': !colorIsDark(t.hexColor),
}" 'has-dark-text': colorIsDark(t.hexColor),
}"
:gridX="dayWidth" :gridX="dayWidth"
:h="31" :h="31"
:isActive="canWrite" :isActive="canWrite"
@ -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
'has-high-priority': t.priority >= priorities.HIGH, :class="{
'has-not-so-high-priority': t.priority === priorities.HIGH, 'has-high-priority': t.priority >= priorities.HIGH,
'has-super-high-priority': t.priority === priorities.DO_NOW 'has-not-so-high-priority':
}">{{ t.title }}</span> t.priority === priorities.HIGH,
'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"/> <edit-task :task="taskToEdit"/>
</span> </card>
</a>
</header>
<div class="card-content">
<div class="content">
<edit-task :task="taskToEdit"/>
</div>
</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,15 +256,16 @@ 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 {
this.downloadAttachment(attachment) this.downloadAttachment(attachment)
} }

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
:disabled="newComment.comment === ''" :loading="
@click="addComment()" class="button is-primary">Comment taskCommentService.loading && !isCommentEdit
</button> "
:disabled="newComment.comment === ''"
@click="addComment()"
>
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), {
title: 'Remove', action: () => this.toggleDelete(c.id),
}]) 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,69 +1,64 @@
<template> <template>
<div class="card"> <card title="Avatar">
<header class="card-header"> <div class="control mb-4">
<p class="card-header-title"> <label class="radio">
Avatar <input name="avatarProvider" type="radio" v-model="avatarProvider" value="default"/>
</p> Default
</header> </label>
<div class="card-content"> <label class="radio">
<div class="control mb-4"> <input name="avatarProvider" type="radio" v-model="avatarProvider" value="initials"/>
<label class="radio"> Initials
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="default"/> </label>
Default <label class="radio">
</label> <input name="avatarProvider" type="radio" v-model="avatarProvider" value="gravatar"/>
<label class="radio"> Gravatar
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="initials"/> </label>
Initials <label class="radio">
</label> <input name="avatarProvider" type="radio" v-model="avatarProvider" value="upload"/>
<label class="radio"> Upload
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="gravatar"/> </label>
Gravatar
</label>
<label class="radio">
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="upload"/>
Upload
</label>
</div>
<template v-if="avatarProvider === 'upload'">
<input
@change="cropAvatar"
accept="image/*"
class="is-hidden"
ref="avatarUploadInput"
type="file"
/>
<a
:class="{ 'is-loading': avatarService.loading || loading}"
@click="$refs.avatarUploadInput.click()"
class="button is-primary"
v-if="!isCropAvatar">
Upload Avatar
</a>
<template v-else>
<cropper
:src="avatarToCrop"
:stencil-props="{aspectRatio: 1}"
@ready="() => loading = false"
class="mb-4"
ref="cropper"/>
<a
:class="{ 'is-loading': avatarService.loading || loading}"
@click="uploadAvatar"
class="button is-primary">
Upload Avatar
</a>
</template>
</template>
<div class="bigbuttons" v-if="avatarProvider !== 'upload'">
<button :class="{ 'is-loading': avatarService.loading || loading}" @click="updateAvatarStatus()"
class="button is-primary is-fullwidth">
Save
</button>
</div>
</div> </div>
</div>
<template v-if="avatarProvider === 'upload'">
<input
@change="cropAvatar"
accept="image/*"
class="is-hidden"
ref="avatarUploadInput"
type="file"
/>
<x-button
:loading="avatarService.loading || loading"
@click="$refs.avatarUploadInput.click()"
v-if="!isCropAvatar">
Upload Avatar
</x-button>
<template v-else>
<cropper
:src="avatarToCrop"
:stencil-props="{aspectRatio: 1}"
@ready="() => loading = false"
class="mb-4"
ref="cropper"/>
<x-button
:loading="avatarService.loading || loading"
@click="uploadAvatar"
>
Upload Avatar
</x-button>
</template>
</template>
<div class="mt-2" v-if="avatarProvider !== 'upload'">
<x-button
:loading="avatarService.loading || loading"
@click="updateAvatarStatus()"
class="is-fullwidth"
>
Save
</x-button>
</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,64 +2,60 @@
<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>
<p class="card-header-title">Create A Saved Filter</p> A saved filter is a virtual list which is computed from a set of filters each time it is
</header> accessed. Once created, it will appear in a special namespace.
<div class="card-content content"> </p>
<p> <div class="field">
A saved filter is a virtual list which is computed from a set of filters each time it is <label class="label" for="title">Title</label>
accessed. Once created, it will appear in a special namespace. <div class="control">
</p> <input
<div class="field"> v-model="savedFilter.title"
<label class="label" for="title">Title</label> :class="{ 'disabled': savedFilterService.loading}"
<div class="control"> :disabled="savedFilterService.loading"
<input class="input"
v-model="savedFilter.title" id="Title"
:class="{ 'disabled': savedFilterService.loading}" placeholder="The saved filter title goes here..."
:disabled="savedFilterService.loading" type="text"
class="input" v-focus
id="Title" />
placeholder="The saved filter title goes here..."
type="text"
v-focus
/>
</div>
</div> </div>
<div class="field">
<label class="label" for="description">Description</label>
<div class="control">
<editor
v-model="savedFilter.description"
:class="{ 'disabled': savedFilterService.loading}"
:disabled="savedFilterService.loading"
:preview-is-default="false"
id="description"
placeholder="The description goes here..."
v-if="editorActive"
/>
</div>
</div>
<div class="field">
<label class="label" for="filters">Filters</label>
<div class="control">
<filters
:class="{ 'disabled': savedFilterService.loading}"
:disabled="savedFilterService.loading"
class="has-no-shadow has-no-border"
v-model="filters"
/>
</div>
</div>
<button
:class="{ 'disabled': savedFilterService.loading}"
:disabled="savedFilterService.loading"
@click="create()"
class="button is-primary is-fullwidth">
Create new saved filter
</button>
</div> </div>
</div> <div class="field">
<label class="label" for="description">Description</label>
<div class="control">
<editor
v-model="savedFilter.description"
:class="{ 'disabled': savedFilterService.loading}"
:disabled="savedFilterService.loading"
:preview-is-default="false"
id="description"
placeholder="The description goes here..."
v-if="editorActive"
/>
</div>
</div>
<div class="field">
<label class="label" for="filters">Filters</label>
<div class="control">
<filters
:class="{ 'disabled': savedFilterService.loading}"
:disabled="savedFilterService.loading"
class="has-no-shadow has-no-border"
v-model="filters"
/>
</div>
</div>
<x-button
:loading="savedFilterService.loading"
:disabled="savedFilterService.loading"
@click="create()"
class="is-fullwidth"
>
Create new saved filter
</x-button>
</card>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,78 +1,67 @@
<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"> <form @submit.prevent="save()">
<p class="card-header-title"> <div class="field">
Edit Saved Filter <label class="label" for="listtext">Filter Name</label>
</p> <div class="control">
</header> <input
<div class="card-content"> :class="{ 'disabled': filterService.loading}"
<div class="content"> :disabled="filterService.loading"
<form @submit.prevent="save()"> @keyup.enter="save"
<div class="field"> class="input"
<label class="label" for="listtext">Filter Name</label> id="listtext"
<div class="control"> placeholder="The list title goes here..."
<input type="text"
:class="{ 'disabled': filterService.loading}" v-focus
:disabled="filterService.loading" v-model="filter.title"/>
@keyup.enter="save"
class="input"
id="listtext"
placeholder="The list title goes here..."
type="text"
v-focus
v-model="filter.title"/>
</div>
</div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="filter.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="filters">Filters</label>
<div class="control">
<filters
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
class="has-no-shadow has-no-border"
v-model="filters"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<button
@click="save()"
:class="{ 'is-loading': filterService.loading}"
class="button is-primary is-fullwidth">
Save
</button>
</div>
<div class="control">
<button
@click="showDeleteModal = true"
:class="{ 'is-loading': filterService.loading}"
class="button is-danger">
<span class="icon">
<icon icon="trash-alt"/>
</span>
</button>
</div>
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="filter.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="filters">Filters</label>
<div class="control">
<filters
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading"
class="has-no-shadow has-no-border"
v-model="filters"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="save()"
:loading="filterService.loading"
class="is-fullwidth">
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:loading="filterService.loading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div> </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>
<h1>Manage labels</h1>
<p> <div class="content">
Click on a label to edit it. <h1>Manage labels</h1>
You can edit all labels you created, you can use all labels which are associated with a task to whose list <p>
you have access. Click on a label to edit it.
</p> You can edit all labels you created, you can use all labels which are associated with a task to whose
list
you have access.
</p>
</div>
<div class="columns"> <div class="columns">
<div class="labels-list column"> <div class="labels-list column">
<span <span
@ -35,66 +41,55 @@
</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"> <form @submit.prevent="editLabelSubmit()">
<span class="card-header-title"> <div class="field">
Edit Label <label class="label">Title</label>
</span> <div class="control">
<a @click="isLabelEdit = false" class="card-header-icon"> <input
<span class="icon"> class="input"
<icon icon="times"/> placeholder="Label title"
</span> type="text"
</a> v-model="labelEditLabel.title"/>
</header>
<div class="card-content">
<form @submit.prevent="editLabelSubmit()">
<div class="field">
<label class="label">Title</label>
<div class="control">
<input
class="input"
placeholder="Label title"
type="text"
v-model="labelEditLabel.title"/>
</div>
</div> </div>
<div class="field"> </div>
<label class="label">Description</label> <div class="field">
<div class="control"> <label class="label">Description</label>
<editor <div class="control">
:preview-is-default="false" <editor
placeholder="Label description" :preview-is-default="false"
v-if="editorActive" placeholder="Label description"
v-model="labelEditLabel.description" v-if="editorActive"
/> v-model="labelEditLabel.description"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label">Color</label> <div class="field">
<div class="control"> <label class="label">Color</label>
<color-picker v-model="labelEditLabel.hexColor"/> <div class="control">
</div> <color-picker v-model="labelEditLabel.hexColor"/>
</div> </div>
<div class="field has-addons"> </div>
<div class="control is-expanded"> <div class="field has-addons">
<button :class="{ 'is-loading': labelService.loading}" class="button is-fullwidth is-primary" <div class="control is-expanded">
type="submit"> <x-button
Save :loading="labelService.loading"
</button> class="is-fullwidth"
</div> @click="editLabelSubmit()"
<div class="control"> >
<a Save
@click="() => {deleteLabel(labelEditLabel);isLabelEdit = false}" </x-button>
class="button has-icon is-danger">
<span class="icon">
<icon icon="trash-alt"/>
</span>
</a>
</div>
</div> </div>
</form> <div class="control">
</div> <x-button
</div> @click="() => {deleteLabel(labelEditLabel);isLabelEdit = false}"
icon="trash-alt"
class="is-danger"
/>
</div>
</div>
</form>
</card>
</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,132 +4,112 @@
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"> <form @submit.prevent="submit()">
<p class="card-header-title"> <div class="field">
Edit List <label class="label" for="listtext">List Name</label>
</p> <div class="control">
</header> <input
<div class="card-content"> :class="{ 'disabled': listService.loading}"
<div class="content"> :disabled="listService.loading"
<form @submit.prevent="submit()"> @keyup.enter="submit"
<div class="field"> class="input"
<label class="label" for="listtext">List Name</label> id="listtext"
<div class="control"> placeholder="The list title goes here..."
<input type="text"
:class="{ 'disabled': listService.loading}" v-focus
:disabled="listService.loading" v-model="list.title"/>
@keyup.enter="submit"
class="input"
id="listtext"
placeholder="The list title goes here..."
type="text"
v-focus
v-model="list.title"/>
</div>
</div>
<div class="field">
<label
class="label"
for="listtext"
v-tooltip="'The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.'">
List Identifier
</label>
<div class="control">
<input
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
@keyup.enter="submit"
class="input"
id="listtext"
placeholder="The list identifier goes here..."
type="text"
v-focus
v-model="list.identifier"/>
</div>
</div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="list.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="list.isArchived"
v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
This list is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="list.hexColor"/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<button
@click="submit()"
:class="{ 'is-loading': listService.loading}"
class="button is-primary is-fullwidth">
Save
</button>
</div>
<div class="control">
<button
@click="showDeleteModal = true"
:class="{ 'is-loading': listService.loading}"
class="button is-danger">
<span class="icon">
<icon icon="trash-alt"/>
</span>
</button>
</div>
</div> </div>
</div> </div>
</div> <div class="field">
</div> <label
class="label"
<div class="card has-overflow"> for="listtext"
<header class="card-header"> v-tooltip="'The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.'">
<p class="card-header-title"> List Identifier
Duplicate this list </label>
</p> <div class="control">
</header> <input
<div class="card-content"> :class="{ 'disabled': listService.loading}"
<div class="content"> :disabled="listService.loading"
<p>Select a namespace which should hold the duplicated list:</p> @keyup.enter="submit"
class="input"
<div class="field has-addons"> id="listtext"
<div class="control is-expanded"> placeholder="The list identifier goes here..."
<namespace-search @selected="selectNamespace"/> type="text"
</div> v-focus
<div class="control"> v-model="list.identifier"/>
<button
:class="{'is-loading': listDuplicateService.loading}"
@click="duplicateList"
class="button is-primary"
type="submit">
Duplicate
</button>
</div>
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': listService.loading}"
:disabled="listService.loading"
:preview-is-default="false"
id="listdescription"
placeholder="The lists description goes here..."
v-model="list.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="list.isArchived"
v-tooltip="'If a list is archived, you cannot create new tasks or edit the list or existing tasks.'">
This list is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="list.hexColor"/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="submit()"
:loading="listService.loading"
class="is-fullwidth">
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:locading="listService.loading"
icon="trash-alt"
class="is-danger"
/>
</div>
</div> </div>
</div> </card>
<!-- Duplicate list -->
<card class="has-overflow" title="Duplicate this list">
<p>Select a namespace which should hold the duplicated list:</p>
<div class="field has-addons">
<div class="control is-expanded">
<namespace-search @selected="selectNamespace"/>
</div>
<div class="control">
<x-button
:loading="listDuplicateService.loading"
@click="duplicateList"
>
Duplicate
</x-button>
</div>
</div>
</card>
<background :list-id="$route.params.id"/> <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"> >
Add a task <template v-if="bucket.tasks.length === 0">
</span> Add a task
<span v-else> </template>
Add another task <template v-else>
</span> Add another task
</a> </template>
</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 >
</span> Create a new bucket
</a> </x-button>
</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"> <edit-task :task="taskEditTask"/>
<p class="card-header-title"> </card>
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"/>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -2,39 +2,37 @@
<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> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">Priority</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">Priority</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">Labels</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">Labels</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">Assignees</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">Assignees</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">Due Date</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">Due Date</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">Start Date</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">Start Date</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">End Date</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">End Date</fancycheckbox> <fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">% Done</fancycheckbox>
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">% Done</fancycheckbox> <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> </card>
</div>
</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,84 +4,74 @@
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"> <form @submit.prevent="submit()">
<p class="card-header-title"> <div class="field">
Edit Namespace <label class="label" for="namespacetext">Namespace Name</label>
</p> <div class="control">
</header> <input
<div class="card-content"> :class="{ 'disabled': namespaceService.loading}"
<div class="content"> :disabled="namespaceService.loading"
<form @submit.prevent="submit()"> class="input"
<div class="field"> id="namespacetext"
<label class="label" for="namespacetext">Namespace Name</label> placeholder="The namespace text is here..."
<div class="control"> type="text"
<input v-focus
:class="{ 'disabled': namespaceService.loading}" v-model="namespace.title"/>
:disabled="namespaceService.loading"
class="input"
id="namespacetext"
placeholder="The namespace text is here..."
type="text"
v-focus
v-model="namespace.title"/>
</div>
</div>
<div class="field">
<label class="label" for="namespacedescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
:preview-is-default="false"
id="namespacedescription"
placeholder="The namespaces description goes here..."
v-if="editorActive"
v-model="namespace.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="namespace.isArchived"
v-tooltip="'If a namespace is archived, you cannot create new lists or edit it.'">
This namespace is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="namespace.hexColor"/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<button
@click="submit()"
:class="{ 'is-loading': namespaceService.loading}"
class="button is-primary is-fullwidth">
Save
</button>
</div>
<div class="control">
<button
@click="showDeleteModal = true"
:class="{ 'is-loading': namespaceService.loading}"
class="button is-danger">
<span class="icon">
<icon icon="trash-alt"/>
</span>
</button>
</div>
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="namespacedescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': namespaceService.loading}"
:disabled="namespaceService.loading"
:preview-is-default="false"
id="namespacedescription"
placeholder="The namespaces description goes here..."
v-if="editorActive"
v-model="namespace.description"
/>
</div>
</div>
<div class="field">
<label class="label" for="isArchivedCheck">Is Archived</label>
<div class="control">
<fancycheckbox
v-model="namespace.isArchived"
v-tooltip="'If a namespace is archived, you cannot create new lists or edit it.'">
This namespace is archived
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Color</label>
<div class="control">
<color-picker v-model="namespace.hexColor"/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="submit()"
:loading="namespaceService.loading"
class="is-fullwidth"
>
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:loading="namespaceService.loading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div> </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

@ -56,8 +56,11 @@
choose-date-label="Click here to set a due date" choose-date-label="Click here to set a due date"
: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">
@ -335,7 +375,7 @@
</template> </template>
<template v-if="task.done"> <template v-if="task.done">
<br/> <br/>
Done <span v-tooltip="doneFormatted">{{ doneSince }}</span> Done <span v-tooltip="doneFormatted">{{ doneSince }}</span>
</template> </template>
</p> </p>
</div> </div>
@ -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,77 +1,75 @@
<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> <form @submit.prevent="save()">
</header> <div class="field">
<div class="card-content"> <label class="label" for="teamtext"
<div class="content"> >Team Name</label
<form @submit.prevent="save()"> >
<div class="field"> <div class="control">
<label class="label" for="teamtext">Team Name</label> <input
<div class="control"> :class="{
<input 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>
<p class="help is-danger" v-if="showError && team.name === ''">
Please specify a name.
</p>
<div class="field">
<label class="label" for="teamdescription">Description</label>
<div class="control">
<editor
:class="{ 'disabled': teamService.loading}"
:disabled="teamService.loading"
:preview-is-default="false"
id="teamdescription"
placeholder="The teams description goes here..."
v-model="team.description"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<button
@click="save()"
:class="{ 'is-loading': teamService.loading}"
class="button is-primary is-fullwidth">
Save
</button>
</div>
<div class="control">
<button
@click="showDeleteModal = true"
:class="{ 'is-loading': teamService.loading}"
class="button is-danger">
<span class="icon">
<icon icon="trash-alt"/>
</span>
</button>
</div>
</div> </div>
</div> </div>
</div> <p
</div> class="help is-danger"
v-if="showError && team.name === ''"
<div class="card is-fullwidth has-overflow"> >
<header class="card-header"> Please specify a name.
<p class="card-header-title">
Team Members
</p> </p>
</header> <div class="field">
<div class="card-content" v-if="userIsAdmin"> <label class="label" for="teamdescription"
>Description</label
>
<div class="control">
<editor
:class="{ disabled: teamService.loading }"
:disabled="teamService.loading"
:preview-is-default="false"
id="teamdescription"
placeholder="The teams description goes here..."
v-model="team.description"
/>
</div>
</div>
</form>
<div class="field has-addons mt-4">
<div class="control is-fullwidth">
<x-button
@click="save()"
:loading="teamService.loading"
class="is-fullwidth"
>
Save
</x-button>
</div>
<div class="control">
<x-button
@click="showDeleteModal = true"
:loading="teamService.loading"
class="is-danger"
icon="trash-alt"
/>
</div>
</div>
</card>
<card class="is-fullwidth has-overflow" title="Team Members">
<div class="p-4" 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>
@ -104,57 +99,68 @@
</td> </td>
<td class="type"> <td class="type">
<template v-if="m.admin"> <template v-if="m.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> <template v-else>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="user"/> <icon icon="user"/>
</span> </span>
Member Member
</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,265 +3,216 @@
: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"> <form @submit.prevent="updatePassword()">
<p class="card-header-title"> <div class="field">
Update Your Password <label class="label" for="newPassword">New Password</label>
</p> <div class="control">
</header> <input
<div class="card-content"> @keyup.enter="updatePassword"
<div class="content"> class="input"
<form @submit.prevent="updatePassword()"> id="newPassword"
<div class="field"> placeholder="The new password..."
<label class="label" for="newPassword">New Password</label> type="password"
<div class="control"> v-model="passwordUpdate.newPassword"/>
<input
@keyup.enter="updatePassword"
class="input"
id="newPassword"
placeholder="The new password..."
type="password"
v-model="passwordUpdate.newPassword"/>
</div>
</div>
<div class="field">
<label class="label" for="newPasswordConfirm">New Password Confirmation</label>
<div class="control">
<input
@keyup.enter="updatePassword"
class="input"
id="newPasswordConfirm"
placeholder="Confirm your new password..."
type="password"
v-model="passwordConfirm"/>
</div>
</div>
<div class="field">
<label class="label" for="currentPassword">Current Password</label>
<div class="control">
<input
@keyup.enter="updatePassword"
class="input"
id="currentPassword"
placeholder="Your current password"
type="password"
v-model="passwordUpdate.oldPassword"/>
</div>
</div>
</form>
<div class="bigbuttons">
<button :class="{ 'is-loading': passwordUpdateService.loading}" @click="updatePassword()"
class="button is-primary is-fullwidth">
Save
</button>
</div> </div>
</div> </div>
</div> <div class="field">
</div> <label class="label" for="newPasswordConfirm">New Password Confirmation</label>
<div class="control">
<input
@keyup.enter="updatePassword"
class="input"
id="newPasswordConfirm"
placeholder="Confirm your new password..."
type="password"
v-model="passwordConfirm"/>
</div>
</div>
<div class="field">
<label class="label" for="currentPassword">Current Password</label>
<div class="control">
<input
@keyup.enter="updatePassword"
class="input"
id="currentPassword"
placeholder="Your current password"
type="password"
v-model="passwordUpdate.oldPassword"/>
</div>
</div>
</form>
<x-button
:loading="passwordUpdateService.loading"
@click="updatePassword()"
class="is-fullwidth mt-4">
Save
</x-button>
</card>
<!-- Update E-Mail --> <!-- Update E-Mail -->
<div class="card"> <card title="Update Your E-Mail Address">
<header class="card-header"> <form @submit.prevent="updateEmail()">
<p class="card-header-title"> <div class="field">
Update Your E-Mail Address <label class="label" for="newEmail">New Email Address</label>
</p> <div class="control">
</header> <input
<div class="card-content"> @keyup.enter="updateEmail"
<div class="content"> class="input"
<form @submit.prevent="updateEmail()"> id="newEmail"
<div class="field"> placeholder="The new email address..."
<label class="label" for="newEmail">New Email Address</label> type="email"
<div class="control"> v-model="emailUpdate.newEmail"/>
<input
@keyup.enter="updateEmail"
class="input"
id="newEmail"
placeholder="The new email address..."
type="email"
v-model="emailUpdate.newEmail"/>
</div>
</div>
<div class="field">
<label class="label" for="currentPassword">Current Password</label>
<div class="control">
<input
@keyup.enter="updateEmail"
class="input"
id="currentPassword"
placeholder="Your current password"
type="password"
v-model="emailUpdate.password"/>
</div>
</div>
</form>
<div class="bigbuttons">
<button :class="{ 'is-loading': emailUpdateService.loading}" @click="updateEmail()"
class="button is-primary is-fullwidth">
Save
</button>
</div> </div>
</div> </div>
</div> <div class="field">
</div> <label class="label" for="currentPassword">Current Password</label>
<div class="control">
<input
@keyup.enter="updateEmail"
class="input"
id="currentPassword"
placeholder="Your current password"
type="password"
v-model="emailUpdate.password"/>
</div>
</div>
</form>
<x-button
:loading="emailUpdateService.loading"
@click="updateEmail()"
class="is-fullwidth mt-4">
Save
</x-button>
</card>
<!-- General --> <!-- General -->
<div class="card update-name"> <card title="General Settings" class="general-settings">
<header class="card-header"> <div class="field">
<p class="card-header-title"> <label class="label" for="newName">Name</label>
General Settings <div class="control">
</p> <input
</header> @keyup.enter="updateSettings"
<div class="card-content"> class="input"
<div class="content"> id="newName"
<div class="field"> placeholder="The new name"
<label class="label" for="newName">Name</label> type="text"
<div class="control"> v-model="settings.name"/>
<input
@keyup.enter="updateSettings"
class="input"
id="newName"
placeholder="The new name"
type="text"
v-model="settings.name"/>
</div>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
Send me Reminders for tasks via Email
</label>
</div>
<div class="bigbuttons">
<button :class="{ 'is-loading': userSettingsService.loading}" @click="updateSettings()"
class="button is-primary is-fullwidth">
Save
</button>
</div>
</div> </div>
</div> </div>
</div> <div class="field">
<label class="checkbox">
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
Send me Reminders for tasks via Email
</label>
</div>
<x-button
:loading="userSettingsService.loading"
@click="updateSettings()"
class="is-fullwidth mt-4"
>
Save
</x-button>
</card>
<!-- 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 @click="totpEnroll()"
v-if="!totpEnrolled && totp.secret === ''">
Enroll
</x-button>
<template v-else-if="totp.secret !== '' && !totp.enabled">
<p>
To finish your setup, use this secret in your totp app (Google Authenticator or similar):
<strong>{{ totp.secret }}</strong><br/>
After that, enter a code from your app below.
</p> </p>
</header> <p>
<div class="card-content"> Alternatively you can scan this QR code:<br/>
<a <img :src="totpQR" alt=""/>
:class="{ 'is-loading': totpService.loading }" </p>
@click="totpEnroll()" <div class="field">
class="button is-primary" <label class="label" for="totpConfirmPasscode">Passcode</label>
v-if="!totpEnrolled && totp.secret === ''"> <div class="control">
Enroll <input
</a> @keyup.enter="totpConfirm()"
<div class="content" v-else-if="totp.secret !== '' && !totp.enabled"> class="input"
<p> id="totpConfirmPasscode"
To finish your setup, use this secret in your totp app (Google Authenticator or similar): placeholder="A code generated by your totp application"
<strong>{{ totp.secret }}</strong><br/> type="text"
After that, enter a code from your app below. v-model="totpConfirmPasscode"/>
</p> </div>
<p> </div>
Alternatively you can scan this QR code:<br/> <x-button @click="totpConfirm()">Confirm</x-button>
<img :src="totpQR" alt=""/> </template>
</p> <template v-else-if="totp.secret !== '' && totp.enabled">
<p>
You've sucessfully set up two factor authentication!
</p>
<p v-if="!totpDisableForm">
<x-button @click="totpDisableForm = true" class="is-danger">Disable</x-button>
</p>
<div v-if="totpDisableForm">
<div class="field"> <div class="field">
<label class="label" for="totpConfirmPasscode">Passcode</label> <label class="label" for="currentPassword">Please Enter Your Password</label>
<div class="control"> <div class="control">
<input <input
@keyup.enter="totpConfirm()" @keyup.enter="totpDisable"
class="input" class="input"
id="totpConfirmPasscode" id="currentPassword"
placeholder="A code generated by your totp application" placeholder="Your current password"
type="text" type="password"
v-model="totpConfirmPasscode"/> v-focus
v-model="totpDisablePassword"/>
</div> </div>
</div> </div>
<a @click="totpConfirm()" class="button is-primary">Confirm</a> <x-button @click="totpDisable()" class="is-danger">Disable two factor authentication</x-button>
</div> </div>
<div class="content" v-else-if="totp.secret !== '' && totp.enabled"> </template>
<p> </card>
You've sucessfully set up two factor authentication!
</p>
<p v-if="!totpDisableForm">
<a @click="totpDisableForm = true" class="button is-danger">Disable</a>
</p>
<div v-if="totpDisableForm">
<div class="field">
<label class="label" for="currentPassword">Please Enter Your Password</label>
<div class="control">
<input
@keyup.enter="totpDisable"
class="input"
id="currentPassword"
placeholder="Your current password"
type="password"
v-focus
v-model="totpDisablePassword"/>
</div>
</div>
<a @click="totpDisable()" class="button is-danger">Disable two factor authentication</a>
</div>
</div>
</div>
</div>
<!-- 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"> :to="{name: 'migrate.start'}"
Migrate from other services to Vikunja >
</p> Import your data into Vikunja
</header> </x-button>
<div class="card-content"> </card>
<router-link
:to="{name: 'migrate.start'}"
class="button is-primary"
v-if="migratorsEnabled"
>
Import your data into Vikunja
</router-link>
</div>
</div>
<!-- Caldav --> <!-- Caldav -->
<div class="card" v-if="caldavEnabled"> <card v-if="caldavEnabled" title="Caldav">
<header class="card-header"> <p>
<p class="card-header-title"> You can connect Vikunja to caldav clients to view and manage all tasks from different clients.
Caldav Enter this url into your client:
</p> </p>
</header> <div class="field has-addons no-input-mobile">
<div class="card-content content"> <div class="control is-expanded">
<p> <input type="text" v-model="caldavUrl" class="input" readonly/>
You can connect Vikunja to caldav clients to view and manage all tasks from different clients. </div>
Enter this url into your client: <div class="control">
</p> <x-button
<div class="field has-addons no-input-mobile"> @click="copy(caldavUrl)"
<div class="control is-expanded"> :shadow="false"
<input type="text" v-model="caldavUrl" class="input" readonly/> v-tooltip="'Copy to clipboard'"
</div> icon="paste"
<div class="control"> />
<a @click="copy(caldavUrl)" class="button is-primary has-no-shadow" v-tooltip="'Copy to clipboard'">
<span class="icon">
<icon icon="paste"/>
</span>
</a>
</div>
</div> </div>
<p>
<a href="https://vikunja.io/docs/caldav/" target="_blank">
More information about caldav in Vikunja
</a>
</p>
</div> </div>
</div> <p>
<a href="https://vikunja.io/docs/caldav/" target="_blank">
More information about caldav in Vikunja
</a>
</p>
</card>
</div> </div>
</template> </template>