Make add task a component #520

Merged
konrad merged 39 commits from sytone/frontend:main into main 2021-07-17 21:21:47 +00:00
37 changed files with 342 additions and 163 deletions

View File

@ -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 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
sytone marked this conversation as resolved Outdated

That should be lf

That should be `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.
## 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
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

@ -0,0 +1,102 @@
<template>
<div class="task-add">
sytone marked this conversation as resolved Outdated
The validation error should be in this component as well: https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/list/views/List.vue#L82-L84

Added and will verify.

Added and will verify.
<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 !== ''">
sytone marked this conversation as resolved Outdated

This will never be shown because the whole component only appears if validListIdAvailable is true. And if that's the case, this v-if can never evaluate to true.

This will never be shown because the whole component only appears if `validListIdAvailable` is true. And if that's the case, this `v-if` can never evaluate to true.
{{ errorMessage }}
</p>
sytone marked this conversation as resolved Outdated
Validation error for an empty title still missing: https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/list/views/List.vue#L82-L84

Not sure what you mean by title. I have updated the component to have a div control the input vs warning message. Look in current commit.

Not sure what you mean by title. I have updated the component to have a div control the input vs warning message. Look in current commit.

The user has to enter a title for a new task. If none is provided they should see an error message as they currently do. Please correct me if I'm wrong, but I did not find that here in this new component.

The user has to enter a title for a new task. If none is provided they should see an error message as they currently do. Please correct me if I'm wrong, but I did not find that here in this new component.
<quick-add-magic v-if="errorMessage === ''"/>
</div>
</template>
<script>
import ListService from '../../services/list'
sytone marked this conversation as resolved Outdated

If the component is not used, please remove the import.

If the component is not used, please remove the import.
import TaskService from '../../services/task'
import LabelService from '../../services/label'
import LabelTaskService from '../../services/labelTask'
sytone marked this conversation as resolved Outdated

Please remove unused imports.

Please remove unused imports.

Done

Done
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,
},
sytone marked this conversation as resolved Outdated

Please remove the empty block.

Please remove the empty block.
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 = ''
sytone marked this conversation as resolved Outdated

Could you move these variables declared inside this block into computed properties?

Could you move these variables declared inside this block into computed properties?

I was following the convention where the services were set in the created function. Can you point me to an example of how to do it in another component so I Can understand more.

I was following the convention where the services were set in the created function. Can you point me to an example of how to do it in another component so I Can understand more.

Oh I didn't mean the services, I mean the listIdForNewTask and validListIdAvailable variables declared in beforeMount().

Check out the Vue docs about computed properties and an example of using a computed property.

You'd define one computed property for each variable and then place the code inside them, something like this:

listIdForNewTask() {
	if (this.listId !== undefined) {
    	return this.listId
    }
    
    if(this.$store.state.auth.settings.defaultListId !== undefined) {
		return this.$store.state.auth.settings.defaultListId
    }
    
    return null // or undefined	
},
validListIdAvailable() {
	return this.listIdForNewTask !== undefined
}
Oh I didn't mean the services, I mean the `listIdForNewTask` and `validListIdAvailable` variables declared in `beforeMount()`. Check out the [Vue docs about computed properties](https://vuejs.org/v2/guide/computed.html#Computed-Properties) and [an example of using a computed property](https://kolaente.dev/vikunja/frontend/src/branch/main/src/components/quick-actions/quick-actions.vue#L100-L106). You'd define one computed property for each variable and then place the code inside them, something like this: ```js listIdForNewTask() { if (this.listId !== undefined) { return this.listId } if(this.$store.state.auth.settings.defaultListId !== undefined) { return this.$store.state.auth.settings.defaultListId } return null // or undefined }, validListIdAvailable() { return this.listIdForNewTask !== undefined } ```
this.createNewTask(this.newTaskTitle, 0, this.$store.state.auth.settings.defaultListId)
.then(task => {
sytone marked this conversation as resolved Outdated

This should be const instead of let.

This should be `const` instead of `let`.
this.newTaskTitle = ''
this.$emit('taskAdded', task)
sytone marked this conversation as resolved Outdated

Needs a , at the end.

Needs a `,` at the end.
})
.catch(e => {
if (e === 'NO_LIST') {
this.errorMessage = this.$t('list.create.addListRequired')
sytone marked this conversation as resolved Outdated

That's something that would be required to get this merged. I'd like to make that a setting at the api level though.

Another approach would be to only merge the refactoring of the component and add the rest (=adding a task from the home page) later, once the default list is implemented.

That's something that would be required to get this merged. I'd like to make that a setting at the api level though. Another approach would be to only merge the refactoring of the component and add the rest (=adding a task from the home page) later, once the default list is implemented.

The API is completed now and I update updated the code to use the settings.

The API is completed now and I update updated the code to use the settings.
return
}
this.error(e)
})
},
},
}
sytone marked this conversation as resolved Outdated

If it's not used, please remove it.

If it's not used, please remove it.
</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)
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
}

View File

@ -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)
})
}),
)
})

View File

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

View File

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

View File

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

View File

@ -32,6 +32,11 @@ export default {
foundLists: [],
}
},
props: {
value: {
required: false,
},
},
konrad marked this conversation as resolved Outdated

I think this should either expose the internal list or (better) use the v-model prop to populate the selected list. That one is bidirectional.

I think this should either expose the internal list or (better) [use the v-model](https://www.digitalocean.com/community/tutorials/how-to-add-v-model-support-to-custom-vue-js-components) prop to populate the selected list. That one is bidirectional.
components: {
Multiselect,
},
@ -39,6 +44,14 @@ export default {
this.listSerivce = new ListService()
this.list = new ListModel()
sytone marked this conversation as resolved Outdated

Good catch!

Good catch!
},
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)

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ export default class UserSettingsModel extends AbstractModel {
discoverableByName: false,
discoverableByEmail: false,
overdueTasksRemindersEnabled: true,
defaultListId: undefined,
sytone marked this conversation as resolved Outdated

That should be 0 - you don't know if the current user has access to that list.

That should be `0` - you don't know if the current user has access to that list.
weekStart: 0,
}
}

View File

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

View File

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

View File

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

View File

@ -3,10 +3,15 @@
<h2>
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
sytone marked this conversation as resolved Outdated

Would you mind moving this to a separate PR?

Would you mind moving this to a separate PR?

np, reverted that change for this PR.

np, reverted that change for this PR.
</h2>
<add-task
konrad marked this conversation as resolved Outdated

I suppose that's just left from debugging?

Maybe check out the vue devtools extension, it is great to look into the values of props and data in a component.

I suppose that's just left from debugging? Maybe check out the vue devtools extension, it is great to look into the values of props and data in a component.

yea, was from merge and should not be there. I am using the tools now.

yea, was from merge and should not be there. I am using the tools now.
: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'
sytone marked this conversation as resolved Outdated

That could be a computed property, mapped from state (see below in this component)

That could be a computed property, mapped from state (see below in this component)
export default {
name: 'Home',
components: {
ListCard,
ShowTasks,
AddTask,
},
data() {
return {
loading: false,
currentDate: new Date(),
tasks: [],
showTasksKey: 0,
}
},
sytone marked this conversation as resolved Outdated

I'd set that statically and to a much higher value - or maybe even remove the timeout alltogether. I don't think people will let the start page of Vikunja sit still all day without using it and then complain about the welcome text not updating. Should be enough to only render it when the component is rendered. And then this whole thing could be refactored into a computed property.

I'd set that statically and to a much higher value - or maybe even remove the timeout alltogether. I don't think people will let the start page of Vikunja sit still all day without using it and then complain about the welcome text not updating. Should be enough to only render it when the component is rendered. And then this whole thing could be refactored into a computed property.

NA, will move to seperate PR.

NA, will move to seperate PR.
computed: {
@ -86,10 +94,13 @@ export default {
})
},
sytone marked this conversation as resolved Outdated

What is that good for? AFAIU the only place this is used is the key on the task list component? Why does that component need a key?

What is that good for? AFAIU the only place this is used is the `key` on the task list component? Why does that component need a key?

I looked up the best way to refresh a vue component when there was a change, this was the recommended way outside of manually invalidating and causing a render which was a sledgehammer.

I looked up the best way to refresh a vue component when there was a change, this was the recommended way outside of manually invalidating and causing a render which was a sledgehammer.
...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
sytone marked this conversation as resolved Outdated

Maybe an interval would be better suited here?

Maybe an interval would be better suited here?

NA moving to seperate PR.

NA moving to seperate PR.
@ -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>

View File

@ -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
sytone marked this conversation as resolved Outdated

Does that still work when the add task input is in a new component?

Does that still work when the add task input is in a new component?

yes it does, I validated in a few different combinations and looked this up on how to do it with Vue

yes it does, I validated in a few different combinations and looked this up on how to do it with Vue
: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() {
sytone marked this conversation as resolved Outdated

Please add a semicolon at the end.

Please add a semicolon at the end.
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>

View File

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

View File

@ -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) => {

View File

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

View File

@ -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(() => {

View File

@ -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',
}
]
},
],
},
}
},
}