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: https://kolaente.dev/vikunja/frontend/pulls/520
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: sytone <kolaente@sytone.com>
Co-committed-by: sytone <kolaente@sytone.com>
This commit is contained in:
sytone 2021-07-17 21:21:46 +00:00 committed by konrad
parent bad5e3d0ec
commit 306a926c66
37 changed files with 342 additions and 163 deletions

View file

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

22
.editorconfig Normal file
View file

@ -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

View file

@ -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. There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
## Project setup ## Project setup
```
```shell
yarn install yarn install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
```
```shell
yarn run serve yarn run serve
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
```
```shell
yarn run build yarn run build
``` ```
### Lints and fixes files ### Lints and fixes files
```
```shell
yarn run lint yarn run lint
``` ```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -366,7 +366,7 @@ export default {
link: (href, title, text) => { link: (href, title, text) => {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`) const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text) 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) { highlight: function (code, language) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -235,11 +235,11 @@ export default {
this.searchLabel = 'username' this.searchLabel = 'username'
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({ this.stuffModel = new UserNamespaceModel({
namespaceId: this.id, namespaceId: this.id,
@ -253,11 +253,11 @@ export default {
this.searchLabel = 'name' this.searchLabel = 'name'
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({ this.stuffModel = new TeamNamespaceModel({
namespaceId: this.id, namespaceId: this.id,
@ -278,7 +278,7 @@ export default {
.then((r) => { .then((r) => {
this.$set(this, 'sharables', r) this.$set(this, 'sharables', r)
r.forEach((s) => r.forEach((s) =>
this.$set(this.selectedRight, s.id, s.right) this.$set(this.selectedRight, s.id, s.right),
) )
}) })
.catch((e) => { .catch((e) => {

View file

@ -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>

View file

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

View file

@ -26,6 +26,11 @@ export default {
const parsedTask = parseTaskText(newTaskTitle) const parsedTask = parseTaskText(newTaskTitle)
const assignees = [] 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 let listId = null
if (parsedTask.list !== null) { if (parsedTask.list !== null) {
const list = this.$store.getters['lists/findListByExactname'](parsedTask.list) const list = this.$store.getters['lists/findListByExactname'](parsedTask.list)
@ -35,6 +40,10 @@ export default {
listId = lId !== 0 ? lId : this.$route.params.listId 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 // 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. // task create request. Because _that_ happens in a promise, we'll need something to call when it resolves.
const createTask = () => { const createTask = () => {
@ -83,7 +92,7 @@ export default {
.then(res => { .then(res => {
return addLabelToTask(res) return addLabelToTask(res)
}) })
.catch(e => Promise.reject(e)) .catch(e => Promise.reject(e)),
) )
} }
}) })
@ -110,7 +119,7 @@ export default {
assignees.push(user) assignees.push(user)
} }
return Promise.resolve(users) return Promise.resolve(users)
}) }),
) )
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -65,7 +65,8 @@
"weekStart": "Week starts on", "weekStart": "Week starts on",
"weekStartSunday": "Sunday", "weekStartSunday": "Sunday",
"weekStartMonday": "Monday", "weekStartMonday": "Monday",
"language": "Language" "language": "Language",
"defaultList": "Default List"
}, },
"totp": { "totp": {
"title": "Two Factor Authentication", "title": "Two Factor Authentication",
@ -109,7 +110,8 @@
"header": "Create a new list", "header": "Create a new list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "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": { "archive": {
"title": "Archive \"{list}\"", "title": "Archive \"{list}\"",
@ -204,7 +206,6 @@
"title": "List", "title": "List",
"add": "Add", "add": "Add",
"addPlaceholder": "Add a new task…", "addPlaceholder": "Add a new task…",
"addTitleRequired": "Please specify a title.",
"empty": "This list is currently empty.", "empty": "This list is currently empty.",
"newTaskCta": "Create a new task.", "newTaskCta": "Create a new task.",
"editTask": "Edit Task" "editTask": "Edit Task"

View file

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

View file

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

View file

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

View file

@ -3,6 +3,11 @@
<h2> <h2>
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}! {{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2> </h2>
<add-task
:listId="defaultListId"
@taskAdded="updateTaskList"
class="is-max-width-desktop"
/>
<template v-if="!hasTasks"> <template v-if="!hasTasks">
<p>{{ $t('home.list.newText') }}</p> <p>{{ $t('home.list.newText') }}</p>
<x-button <x-button
@ -34,7 +39,7 @@
/> />
</div> </div>
</div> </div>
<ShowTasks :show-all="true" v-if="hasLists"/> <ShowTasks :show-all="true" v-if="hasLists" :key="showTasksKey"/>
</div> </div>
</template> </template>
@ -43,18 +48,21 @@ import {mapState} from 'vuex'
import ShowTasks from './tasks/ShowTasks' import ShowTasks from './tasks/ShowTasks'
import {getHistory} from '@/modules/listHistory' import {getHistory} from '@/modules/listHistory'
import ListCard from '@/components/list/partials/list-card' import ListCard from '@/components/list/partials/list-card'
import AddTask from '../components/tasks/add-task'
export default { export default {
name: 'Home', name: 'Home',
components: { components: {
ListCard, ListCard,
ShowTasks, ShowTasks,
AddTask,
}, },
data() { data() {
return { return {
loading: false, loading: false,
currentDate: new Date(), currentDate: new Date(),
tasks: [], tasks: [],
showTasksKey: 0,
} }
}, },
computed: { computed: {
@ -86,10 +94,13 @@ export default {
}) })
}, },
...mapState({ ...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, authenticated: state => state.auth.authenticated,
userInfo: state => state.auth.info, userInfo: state => state.auth.info,
hasTasks: state => state.hasTasks, hasTasks: state => state.hasTasks,
defaultListId: state => state.auth.defaultListId,
defaultNamespaceId: state => { defaultNamespaceId: state => {
if (state.namespaces.namespaces.length === 0) { if (state.namespaces.namespaces.length === 0) {
return 0 return 0
@ -105,6 +116,13 @@ export default {
return state.namespaces.namespaces[0].lists.length > 0 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> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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