Browse Source

Add default list setting & creating tasks from home (#520)

Co-authored-by: sytone <github@sytone.com>
Co-authored-by: Sytone <github@sytone.com>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#520
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: sytone <kolaente@sytone.com>
Co-committed-by: sytone <kolaente@sytone.com>
feature/crowdin-cli
sytone 5 months ago
committed by konrad
parent
commit
306a926c66
  1. 2
      .drone.yml
  2. 22
      .editorconfig
  3. 12
      README.md
  4. 4
      babel.config.js
  5. 1
      cypress/integration/user/settings.spec.js
  6. 21
      package.json
  7. 2
      src/components/input/button.vue
  8. 6
      src/components/input/datepicker.vue
  9. 2
      src/components/input/editor.vue
  10. 14
      src/components/input/multiselect.vue
  11. 2
      src/components/misc/dropdown-item.vue
  12. 2
      src/components/misc/nothing.vue
  13. 2
      src/components/misc/shortcut.vue
  14. 4
      src/components/misc/subscription.vue
  15. 2
      src/components/quick-actions/quick-actions.vue
  16. 10
      src/components/sharing/userTeam.vue
  17. 102
      src/components/tasks/add-task.vue
  18. 6
      src/components/tasks/gantt-component.vue
  19. 15
      src/components/tasks/mixins/createTask.js
  20. 2
      src/components/tasks/partials/attachments.vue
  21. 2
      src/components/tasks/partials/description.vue
  22. 2
      src/components/tasks/partials/heading.vue
  23. 15
      src/components/tasks/partials/listSearch.vue
  24. 6
      src/components/tasks/partials/singleTaskInList.vue
  25. 2
      src/helpers/time/createDateFromString.js
  26. 7
      src/i18n/lang/en.json
  27. 1
      src/models/userSettings.js
  28. 2
      src/router/index.js
  29. 20
      src/store/modules/auth.js
  30. 9
      src/styles/components/list.scss
  31. 26
      src/views/Home.vue
  32. 131
      src/views/list/views/List.vue
  33. 2
      src/views/tasks/TaskDetailView.vue
  34. 2
      src/views/teams/EditTeam.vue
  35. 2
      src/views/user/Login.vue
  36. 15
      src/views/user/Settings.vue
  37. 28
      vue.config.js

2
.drone.yml

@ -95,7 +95,7 @@ steps:
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 20000
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
commands:
- sed -i 's/localhost/api/g' public/index.html
- yarn serve & npx wait-on http://localhost:8080

22
.editorconfig

@ -0,0 +1,22 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.vue]
indent_style = tab
[*.{yaml,yml}]
indent_style = space
indent_size = 2
[*.json]
indent_style = space
indent_size = 2

12
README.md

@ -20,21 +20,25 @@ If you find any security-related issues you don't want to disclose publicly, ple
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
## Project setup
```
```shell
yarn install
```
### Compiles and hot-reloads for development
```
```shell
yarn run serve
```
### Compiles and minifies for production
```
```shell
yarn run build
```
### Lints and fixes files
```
```shell
yarn run lint
```

4
babel.config.js

@ -1,5 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
'@vue/app',
],
}

1
cypress/integration/user/settings.spec.js

@ -36,7 +36,6 @@ describe('User Settings', () => {
.contains('Save')
.click()
cy.wait(3000) // Wait for the request to finish
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.navbar .user .username')

21
package.json

@ -69,7 +69,24 @@
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"rules": {
"vue/html-quotes": [
"error",
"double"
],
"quotes": [
"error",
"single"
],
"comma-dangle": [
"error",
"always-multiline"
],
"semi": [
"error",
"never"
]
},
"parserOptions": {
"parser": "babel-eslint"
},
@ -95,4 +112,4 @@
],
"testEnvironment": "jsdom"
}
}
}

2
src/components/input/button.vue

@ -55,7 +55,7 @@ export default {
computed: {
showIconOnly() {
return this.icon !== '' && typeof this.$slots.default === 'undefined'
}
},
},
methods: {
click(e) {

6
src/components/input/datepicker.vue

@ -137,18 +137,18 @@ export default {
},
props: {
value: {
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string'
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
},
chooseDateLabel: {
type: String,
default() {
return this.$t('input.datepicker.chooseDate')
}
},
},
disabled: {
type: Boolean,
default: false,
}
},
},
mounted() {
this.setDateValue(this.value)

2
src/components/input/editor.vue

@ -366,7 +366,7 @@ export default {
link: (href, title, text) => {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text)
return isLocal ? html : html.replace(/^<a /, `<a target="_blank" rel="noreferrer noopener nofollow" `)
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
},
},
highlight: function (code, language) {

14
src/components/input/multiselect.vue

@ -108,21 +108,21 @@ export default {
type: Boolean,
default() {
return false
}
},
},
// The placeholder of the search input
placeholder: {
type: String,
default() {
return ''
}
},
},
// The search results where the @search listener needs to put the results into
searchResults: {
type: Array,
default() {
return []
}
},
},
// The name of the property of the searched object to show the user.
// If empty the component will show all raw data of an entry.
@ -130,13 +130,13 @@ export default {
type: String,
default() {
return ''
}
},
},
// The object with the value, updated every time an entry is selected.
value: {
default() {
return null
}
},
},
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
creatable: {
@ -150,14 +150,14 @@ export default {
type: String,
default() {
return this.$t('input.multiselect.createPlaceholder')
}
},
},
// The text shown next to an option.
selectPlaceholder: {
type: String,
default() {
return this.$t('input.multiselect.selectPlaceholder')
}
},
},
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
multiple: {

2
src/components/misc/dropdown-item.vue

@ -22,7 +22,7 @@ export default {
type: String,
required: false,
default: '',
}
},
},
}
</script>

2
src/components/misc/nothing.vue

@ -6,6 +6,6 @@
<script>
export default {
name: 'nothing'
name: 'nothing',
}
</script>

2
src/components/misc/shortcut.vue

@ -14,7 +14,7 @@ export default {
keys: {
type: Array,
required: true,
}
},
},
}
</script>

4
src/components/misc/subscription.vue

@ -57,7 +57,7 @@ export default {
if (this.disabled) {
return this.$t('task.subscription.subscribedThroughParent', {
entity: this.entity,
parent: this.subscription.entity
parent: this.subscription.entity,
})
}
@ -118,7 +118,7 @@ export default {
.catch(e => {
this.error(e)
})
}
},
},
}
</script>

2
src/components/quick-actions/quick-actions.vue

@ -481,7 +481,7 @@ export default {
reset() {
this.query = ''
this.selectedCmd = null
}
},
},
}
</script>

10
src/components/sharing/userTeam.vue

@ -235,11 +235,11 @@ export default {
this.searchLabel = 'username'
if (this.type === 'list') {
this.typeString = `list`
this.typeString = 'list'
this.stuffService = new UserListService()
this.stuffModel = new UserListModel({listId: this.id})
} else if (this.type === 'namespace') {
this.typeString = `namespace`
this.typeString = 'namespace'
this.stuffService = new UserNamespaceService()
this.stuffModel = new UserNamespaceModel({
namespaceId: this.id,
@ -253,11 +253,11 @@ export default {
this.searchLabel = 'name'
if (this.type === 'list') {
this.typeString = `list`
this.typeString = 'list'
this.stuffService = new TeamListService()
this.stuffModel = new TeamListModel({listId: this.id})
} else if (this.type === 'namespace') {
this.typeString = `namespace`
this.typeString = 'namespace'
this.stuffService = new TeamNamespaceService()
this.stuffModel = new TeamNamespaceModel({
namespaceId: this.id,
@ -278,7 +278,7 @@ export default {
.then((r) => {
this.$set(this, 'sharables', r)
r.forEach((s) =>
this.$set(this.selectedRight, s.id, s.right)
this.$set(this.selectedRight, s.id, s.right),
)
})
.catch((e) => {

102
src/components/tasks/add-task.vue

@ -0,0 +1,102 @@
<template>
<div class="task-add">
<div class="field is-grouped">
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
<input
:class="{ 'disabled': taskService.loading}"
@keyup.enter="addTask()"
class="input"
:placeholder="$t('list.list.addPlaceholder')"
type="text"
v-focus
v-model="newTaskTitle"
ref="newTaskInput"
@keyup="errorMessage = ''"
/>
<span class="icon is-small is-left">
<icon icon="tasks"/>
</span>
</p>
<p class="control">
<x-button
:disabled="newTaskTitle.length === 0"
@click="addTask()"
icon="plus"
>
{{ $t('list.list.add') }}
</x-button>
</p>
</div>
<p class="help is-danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</p>
<quick-add-magic v-if="errorMessage === ''"/>
</div>
</template>
<script>
import ListService from '../../services/list'
import TaskService from '../../services/task'
import LabelService from '../../services/label'
import LabelTaskService from '../../services/labelTask'
import createTask from '@/components/tasks/mixins/createTask'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
export default {
name: 'add-task',
data() {
return {
newTaskTitle: '',
listService: ListService,
taskService: TaskService,
labelService: LabelService,
labelTaskService: LabelTaskService,
errorMessage: '',
}
},
mixins: [
createTask,
],
components: {
QuickAddMagic,
},
created() {
this.listService = new ListService()
this.taskService = new TaskService()
this.labelService = new LabelService()
this.labelTaskService = new LabelTaskService()
},
methods: {
addTask() {
if (this.newTaskTitle === '') {
this.errorMessage = this.$t('list.create.addTitleRequired')
return
}
this.errorMessage = ''
this.createNewTask(this.newTaskTitle, 0, this.$store.state.auth.settings.defaultListId)
.then(task => {
this.newTaskTitle = ''
this.$emit('taskAdded', task)
})
.catch(e => {
if (e === 'NO_LIST') {
this.errorMessage = this.$t('list.create.addListRequired')
return
}
this.error(e)
})
},
},
}
</script>
<style lang="scss" scoped>
.task-add {
margin-bottom: 0;
.button {
height: 2.5rem;
}
}
</style>

6
src/components/tasks/gantt-component.vue

@ -388,7 +388,7 @@ export default {
let startDate = new Date(this.startDate)
startDate.setDate(
startDate.getDate() + newRect.left / this.dayWidth
startDate.getDate() + newRect.left / this.dayWidth,
)
startDate.setUTCHours(0)
startDate.setUTCMinutes(0)
@ -397,7 +397,7 @@ export default {
this.taskDragged.startDate = startDate
let endDate = new Date(startDate)
endDate.setDate(
startDate.getDate() + newRect.width / this.dayWidth
startDate.getDate() + newRect.width / this.dayWidth,
)
this.taskDragged.startDate = startDate
this.taskDragged.endDate = endDate
@ -440,7 +440,7 @@ export default {
this.$set(
this.theTasks,
tt,
this.addGantAttributes(r)
this.addGantAttributes(r),
)
break
}

15
src/components/tasks/mixins/createTask.js

@ -26,6 +26,11 @@ export default {
const parsedTask = parseTaskText(newTaskTitle)
const assignees = []
// Uses the following ways to get the list id of the new task:
// 1. If specified in quick add magic, look in store if it exists and use it if it does
// 2. Else check if a list was passed as parameter
// 3. Otherwise use the id from the route parameter
// 4. If none of the above worked, reject the promise with an error.
let listId = null
if (parsedTask.list !== null) {
const list = this.$store.getters['lists/findListByExactname'](parsedTask.list)
@ -34,7 +39,11 @@ export default {
if (listId === null) {
listId = lId !== 0 ? lId : this.$route.params.listId
}
if (typeof listId === 'undefined' || listId === 0) {
return Promise.reject('NO_LIST')
}
// Separate closure because we need to wait for the results of the user search if users were entered in the
// task create request. Because _that_ happens in a promise, we'll need something to call when it resolves.
const createTask = () => {
@ -83,7 +92,7 @@ export default {
.then(res => {
return addLabelToTask(res)
})
.catch(e => Promise.reject(e))
.catch(e => Promise.reject(e)),
)
}
})
@ -110,7 +119,7 @@ export default {
assignees.push(user)
}
return Promise.resolve(users)
})
}),
)
})

2
src/components/tasks/partials/attachments.vue

@ -229,7 +229,7 @@ export default {
.then((r) => {
this.$store.commit(
'attachments/removeById',
this.attachmentToDelete.id
this.attachmentToDelete.id,
)
this.success(r)
})

2
src/components/tasks/partials/description.vue

@ -91,7 +91,7 @@ export default {
.finally(() => {
this.saving = false
})
}
},
},
}
</script>

2
src/components/tasks/partials/heading.vue

@ -95,7 +95,7 @@ export default {
.finally(() => {
this.saving = false
})
}
},
},
}
</script>

15
src/components/tasks/partials/listSearch.vue

@ -32,6 +32,11 @@ export default {
foundLists: [],
}
},
props: {
value: {
required: false,
},
},
components: {
Multiselect,
},
@ -39,6 +44,14 @@ export default {
this.listSerivce = new ListService()
this.list = new ListModel()
},
watch: {
value(newVal) {
this.list = newVal
},
},
mounted() {
this.list = this.value
},
methods: {
findLists(query) {
if (query === '') {
@ -58,7 +71,9 @@ export default {
this.$set(this, 'foundLists', [])
},
select(list) {
this.list = list
this.$emit('selected', list)
this.$emit('input', list)
},
namespace(namespaceId) {
const namespace = this.$store.getters['namespaces/getNamespaceById'](namespaceId)

6
src/components/tasks/partials/singleTaskInList.vue

@ -135,7 +135,7 @@ export default {
showListColor: {
type: Boolean,
default: true,
}
},
},
watch: {
theTask(newVal) {
@ -178,13 +178,13 @@ export default {
this.success({
message: this.task.done ?
this.$t('task.doneSuccess') :
this.$t('task.undoneSuccess')
this.$t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback: () => {
this.task.done = !this.task.done
this.markAsDone(!checked)
}
},
}])
})
.catch(e => {

2
src/helpers/time/createDateFromString.js

@ -12,7 +12,7 @@ export const createDateFromString = dateString => {
}
if (dateString.includes('-')) {
dateString = dateString.replace(/-/g, "/")
dateString = dateString.replace(/-/g, '/')
}
return new Date(dateString)

7
src/i18n/lang/en.json

@ -65,7 +65,8 @@
"weekStart": "Week starts on",
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language"
"language": "Language",
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -109,7 +110,8 @@
"header": "Create a new list",
"titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created."
"createdSuccess": "The list was successfully created.",
"addListRequired": "Please specify a list or set a default list in the settings."
},
"archive": {
"title": "Archive \"{list}\"",
@ -204,7 +206,6 @@
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"addTitleRequired": "Please specify a title.",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"

1
src/models/userSettings.js

@ -9,6 +9,7 @@ export default class UserSettingsModel extends AbstractModel {
discoverableByName: false,
discoverableByEmail: false,
overdueTasksRemindersEnabled: true,
defaultListId: undefined,
weekStart: 0,
}
}

2
src/router/index.js

@ -171,7 +171,7 @@ export default new Router({
name: 'list.create',
components: {
popup: NewListComponent,
}
},
},
{
path: '/namespaces/:id/settings/edit',

20
src/store/modules/auth.js

@ -1,5 +1,5 @@
import {HTTPFactory} from '@/http-common'
import {ERROR_MESSAGE, LOADING} from '../mutation-types'
import { HTTPFactory } from '@/http-common'
import { ERROR_MESSAGE, LOADING } from '../mutation-types'
import UserModel from '../../models/user'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
@ -58,7 +58,7 @@ export default {
// Logs a user in with a set of credentials.
login(ctx, credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
ctx.commit(LOADING, true, { root: true })
// Delete an eventually preexisting old token
removeToken()
@ -93,7 +93,7 @@ export default {
return Promise.reject(e)
})
.finally(() => {
ctx.commit(LOADING, false, {root: true})
ctx.commit(LOADING, false, { root: true })
})
},
// Registers a new user and logs them in.
@ -110,18 +110,18 @@ export default {
})
.catch(e => {
if (e.response && e.response.data && e.response.data.message) {
ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true})
ctx.commit(ERROR_MESSAGE, e.response.data.message, { root: true })
}
return Promise.reject(e)
})
.finally(() => {
ctx.commit(LOADING, false, {root: true})
ctx.commit(LOADING, false, { root: true })
})
},
openIdAuth(ctx, {provider, code}) {
openIdAuth(ctx, { provider, code }) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
ctx.commit(LOADING, true, { root: true })
const data = {
code: code,
@ -143,10 +143,10 @@ export default {
return Promise.reject(e)
})
.finally(() => {
ctx.commit(LOADING, false, {root: true})
ctx.commit(LOADING, false, { root: true })
})
},
linkShareAuth(ctx, {hash, password}) {
linkShareAuth(ctx, { hash, password }) {
const HTTP = HTTPFactory()
return HTTP.post('/shares/' + hash + '/auth', {
password: password,

9
src/styles/components/list.scss

@ -10,13 +10,8 @@
}
}
.task-add {
padding: 1rem 1rem 0;
margin-bottom: 0;
.button {
height: 40px;
}
.list-view .task-add {
padding: 1rem 1rem 0;
}
.list-title {

26
src/views/Home.vue

@ -3,10 +3,15 @@
<h2>
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2>
<add-task
:listId="defaultListId"
@taskAdded="updateTaskList"
class="is-max-width-desktop"
/>
<template v-if="!hasTasks">
<p>{{ $t('home.list.newText') }}</p>
<x-button
:to="{name: 'list.create', params: { id: defaultNamespaceId }}"
:to="{ name: 'list.create', params: { id: defaultNamespaceId } }"
:shadow="false"
class="ml-2"
v-if="defaultNamespaceId > 0"
@ -34,7 +39,7 @@
/>
</div>
</div>
<ShowTasks :show-all="true" v-if="hasLists"/>
<ShowTasks :show-all="true" v-if="hasLists" :key="showTasksKey"/>
</div>
</template>
@ -43,18 +48,21 @@ import {mapState} from 'vuex'
import ShowTasks from './tasks/ShowTasks'
import {getHistory} from '@/modules/listHistory'
import ListCard from '@/components/list/partials/list-card'
import AddTask from '../components/tasks/add-task'
export default {
name: 'Home',
components: {
ListCard,
ShowTasks,
AddTask,
},
data() {
return {
loading: false,
currentDate: new Date(),
tasks: [],
showTasksKey: 0,
}
},
computed: {
@ -86,10 +94,13 @@ export default {
})
},
...mapState({
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
migratorsEnabled: state =>
state.config.availableMigrators !== null &&
state.config.availableMigrators.length > 0,
authenticated: state => state.auth.authenticated,
userInfo: state => state.auth.info,
hasTasks: state => state.hasTasks,
defaultListId: state => state.auth.defaultListId,
defaultNamespaceId: state => {
if (state.namespaces.namespaces.length === 0) {
return 0
@ -105,6 +116,13 @@ export default {
return state.namespaces.namespaces[0].lists.length > 0
},
}),
}
},
methods: {
// This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?)
updateTaskList() {
this.showTasksKey++
},
},
}
</script>

131
src/views/list/views/List.vue

@ -1,11 +1,15 @@
<template>
<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()">
: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="items">
<div class="search">
<div :class="{ 'hidden': !showTaskSearch }" class="field has-addons">
<div :class="{ hidden: !showTaskSearch }" class="field has-addons">
<div class="control has-icons-left has-icons-right">
<input
@blur="hideSearchBar()"
@ -14,9 +18,10 @@
:placeholder="$t('misc.search')"
type="text"
v-focus
v-model="searchTerm"/>
v-model="searchTerm"
/>
<span class="icon is-left">
<icon icon="search"/>
<icon icon="search" />
</span>
</div>
<div class="control">
@ -52,48 +57,30 @@
</div>
<card :padding="false" :has-content="false" class="has-overflow">
<div class="field task-add" v-if="!list.isArchived && canWrite && list.id > 0">
<div class="field is-grouped">
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
<input
:class="{ 'disabled': taskService.loading}"
@keyup.enter="addTask()"
class="input"
:placeholder="$t('list.list.addPlaceholder')"
type="text"
v-focus
v-model="newTaskText"
ref="newTaskInput"
/>
<span class="icon is-small is-left">
<icon icon="tasks"/>
</span>
</p>
<p class="control">
<x-button
:disabled="newTaskText.length === 0"
@click="addTask()"
icon="plus"
>
{{ $t('list.list.add') }}
</x-button>
</p>
</div>
<p class="help is-danger" v-if="showError && newTaskText === ''">
{{ $t('list.list.addTitleRequired') }}
</p>
<quick-add-magic v-if="!showError"/>
</div>
<template
v-if="!list.isArchived && canWrite && list.id > 0"
>
<add-task
@taskAdded="updateTaskList"
ref="newTaskInput"
/>
</template>
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
{{ $t('list.list.empty') }}
<a @click="$refs.newTaskInput.focus()">
<a @click="focusNewTaskInput()">
{{ $t('list.list.newTaskCta') }}
</a>
</nothing>
<div class="tasks-container">
<div :class="{'short': isTaskEdit}" class="tasks mt-0" v-if="tasks && tasks.length > 0">
<div
:class="{ short: isTaskEdit }"
class="tasks mt-0"
v-if="tasks && tasks.length > 0"
>
<single-task-in-list
:show-list-color="false"
:disabled="!canWrite"
@ -103,8 +90,12 @@
task-detail-route="task.detail"
v-for="t in tasks"
>
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
<icon icon="pencil-alt"/>
<div
@click="editTask(t.id)"
class="icon settings"
v-if="!list.isArchived && canWrite"
>
<icon icon="pencil-alt" />
</div>
</single-task-in-list>
</div>
@ -121,7 +112,8 @@
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
v-if="taskCollectionService.totalPages > 1">
v-if="taskCollectionService.totalPages > 1"
>
<router-link
:disabled="currentPage === 1"
:to="getRouteForPagination(currentPage - 1)"
@ -138,13 +130,16 @@
</router-link>
<ul class="pagination-list">
<template v-for="(p, i) in pages">
<li :key="'page'+i" v-if="p.isEllipsis"><span class="pagination-ellipsis">&hellip;</span></li>
<li :key="'page'+i" v-else>
<li :key="'page' + i" v-if="p.isEllipsis">
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li :key="'page' + i" v-else>
<router-link
:aria-label="'Goto page ' + p.number"
:class="{'is-current': p.number === currentPage}"
:class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)"
class="pagination-link">
class="pagination-link"
>
{{ p.number }}
</router-link>
</li>
@ -155,10 +150,8 @@
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
<transition name="modal">
<router-view/>
<router-view />
</transition>
</div>
</template>
@ -167,15 +160,16 @@ import TaskService from '../../../services/task'
import TaskModel from '../../../models/task'
import EditTask from '../../../components/tasks/edit-task'
import AddTask from '../../../components/tasks/add-task'
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
import taskList from '../../../components/tasks/mixins/taskList'
import {saveListView} from '@/helpers/saveListView'
import { saveListView } from '@/helpers/saveListView'
import Rights from '../../../models/rights.json'
import {mapState} from 'vuex'
import { mapState } from 'vuex'
import FilterPopup from '@/components/list/partials/filter-popup'
import { HAS_TASKS } from '@/store/mutation-types'
import Nothing from '@/components/misc/nothing'
import createTask from '@/components/tasks/mixins/createTask'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
export default {
name: 'List',
@ -184,8 +178,6 @@ export default {
taskService: TaskService,
isTaskEdit: false,
taskEditTask: TaskModel,
newTaskText: '',
showError: false,
ctaVisible: false,
}
},
@ -194,11 +186,11 @@ export default {
createTask,
],
components: {
QuickAddMagic,
Nothing,
FilterPopup,
SingleTaskInList,
EditTask,
AddTask,
},
created() {
this.taskService = new TaskService()
@ -212,7 +204,7 @@ export default {
list: state => state.currentList,
}),
mounted() {
this.$nextTick(() => this.ctaVisible = true)
this.$nextTick(() => (this.ctaVisible = true))
},
methods: {
// This function initializes the tasks page and loads the first page of tasks
@ -221,22 +213,13 @@ export default {
this.isTaskEdit = false
this.loadTasks(page, search)
},
addTask() {
if (this.newTaskText === '') {
this.showError = true
return
}
this.showError = false
this.createNewTask(this.newTaskText)
.then(task => {
this.tasks.push(task)
this.sortTasks()
this.newTaskText = ''
})
.catch(e => {
this.error(e)
})
focusNewTaskInput() {
this.$refs.newTaskInput.$refs.newTaskInput.focus()
},
updateTaskList(task) {
this.tasks.push(task)
this.sortTasks()
this.$store.commit(HAS_TASKS, true)
},
editTask(id) {
// Find the selected task and set it to the current object
@ -263,4 +246,4 @@ export default {
},
},
}
</script>
</script>

2
src/views/tasks/TaskDetailView.vue

@ -645,7 +645,7 @@ export default {
this.$refs[fieldName].$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
inline: 'nearest',
})
}
})

2
src/views/teams/EditTeam.vue

@ -312,7 +312,7 @@ export default {
this.success({
message: member.admin ?
this.$t('team.edit.madeAdmin') :
this.$t('team.edit.madeMember')
this.$t('team.edit.madeMember'),
})
})
.catch((e) => {

2
src/views/user/Login.vue

@ -129,7 +129,7 @@ export default {
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
if (emailVerifyToken) {
const cancel = this.setLoading()
HTTP.post(`user/confirm`, {token: emailVerifyToken})
HTTP.post('user/confirm', {token: emailVerifyToken})
.then(() => {
localStorage.removeItem('emailConfirmToken')
this.confirmedEmailSuccess = true

15
src/views/user/Settings.vue

@ -16,6 +16,12 @@
v-model="settings.name"/>
</div>
</div>
<div class="field">
<label class="label">
{{ $t('user.settings.general.defaultList') }}
</label>
<list-search v-model="defaultList"/>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
@ -282,6 +288,7 @@ import {mapState} from 'vuex'
import AvatarSettings from '../../components/user/avatar-settings'
import copy from 'copy-to-clipboard'
import ListSearch from '@/components/tasks/partials/listSearch'
export default {
name: 'Settings',
@ -306,9 +313,12 @@ export default {
settings: UserSettingsModel,
userSettingsService: UserSettingsService,
defaultList: null,
}
},
components: {
ListSearch,
AvatarSettings,
},
created() {
@ -326,6 +336,8 @@ export default {
this.playSoundWhenDone = localStorage.getItem(playSoundWhenDoneKey) === 'true' || localStorage.getItem(playSoundWhenDoneKey) === null
this.defaultList = this.$store.getters['lists/getListById'](this.settings.defaultListId)
this.totpStatus()
},
mounted() {
@ -351,7 +363,7 @@ export default {
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
caldavEnabled: state => state.config.caldavEnabled,
userInfo: state => state.auth.info,
})
}),
},
methods: {
updatePassword() {
@ -428,6 +440,7 @@ export default {
updateSettings() {
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
saveLanguage(this.language)
this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0
this.userSettingsService.update(this.settings)
.then(() => {

28
vue.config.js

@ -20,22 +20,22 @@ module.exports = {
msTileImage: 'images/icons/msapplication-icon-144x144.png',
},
manifestOptions: {
"icons": [
'icons': [
{
"src": "./images/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
'src': './images/icons/android-chrome-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
{
"src": "./images/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
'src': './images/icons/android-chrome-512x512.png',
'sizes': '512x512',
'type': 'image/png',
},
{
"src": "./images/icons/icon-maskable.png",
"sizes": "1024x1024",
"type": "image/png",
"purpose": "maskable"
'src': './images/icons/icon-maskable.png',
'sizes': '1024x1024',
'type': 'image/png',
'purpose': 'maskable',
},
],
shortcuts: [
@ -62,8 +62,8 @@ module.exports = {
name: 'Teams Overview',
short_name: 'Teams',
url: '/teams',
}
]
},
],
},
}
},
}
Loading…
Cancel
Save