Move everything to models and services (#17)

This commit is contained in:
konrad 2019-03-02 10:25:10 +00:00 committed by Gitea
parent 8559d8bb97
commit 9b0c842ae1
50 changed files with 2165 additions and 1206 deletions

184
docs/models-services.md Normal file
View File

@ -0,0 +1,184 @@
# Models and services
The architecture of this web app is in general divided in two parts:
Models and services.
Services handle all "raw" requests, models contain data and methods to work with it.
A service takes (in most cases) a model and returns one.
## Table of Contents
* [Services](#services)
* [Requests](#requests)
* [Loading](#loading)
* [Factories](#factories)
* [Before Request](#before-request)
* [After Request?](#after-request-)
* [Models](#models)
* [Default Values](#default-values)
* [Constructor](#constructor)
* [Access Model Data](#access-to-the-data)
## Services
Services are located in `src/services`.
All services must inherit `AbstractService` which holds most of the methods.
A basic service can look like this:
```javascript
import AbstractService from './abstractService'
import ListModel from '../models/list'
export default class ListService extends AbstractService {
constructor() {
super({
getAll: '/lists',
get: '/lists/{id}',
create: '/namespaces/{namespaceID}/lists',
update: '/lists/{id}',
delete: '/lists/{id}',
})
}
modelFactory(data) {
return new ListModel(data)
}
}
```
The `constructor` calls its parent constructor and provides the paths to make the requests.
The parent constructor will take these and save them in the service.
All paths are optional. Calling a method which doesn't have a path defined will fail.
The placeholder values in the urls are replaced with the contens of variables with the same name in the
corresponding model (the one you pass to the functions).
#### Requests
Several request types are possible:
| Name | HTTP Method |
|------|-------------|
| `get` | `GET` |
| `getAll` | `GET` |
| `create` | `PUT` |
| `update` | `POST` |
| `delete` | `DELETE` |
Each method can take a model and optional url parameters as function parameters.
With the exception of `getAll()`, a model is always mandatory while parameters are not.
Each method returns a promise, so you can access a request result like so:
```javascript
service.getAll().then(result => {
// Do something with result
})
```
The result is a ready-to-use model returned by the model factory.
##### Loading
Each service has a `loading` property, provided by `AbstractModel`.
This property is a `boolean`, it is automatically set to `true` (with a 100ms delay to avoid flickering)
once the request is started and set to `false` once the request is finished.
You can use this to show and hide a loading animation in the frontend.
#### Factories
The `modelFactory` takes data, and returns a model. The result of all requests (with the exception
of the `delete` method) is run through this factory. The factory should return the appropriate model, see
[models](#models) down below on how to handle data in models.
`getAll()` checks if the response is an array, if that's the case, it will run each entry in it through
the `modelFactory`.
It is possible to define a different factory for each request. This is done by implementing a method called
`model{TYPE}Factory(data)` in your service. As a fallback if the specific factory is not defined,
`modelFactory` will be used.
#### Before Request
For each request exists a `before{TYPE}(model)` method. It recieves the model, can alter it and should return
the modified version.
This is useful to make unix timestamps from javascript dates, for example.
#### After Request ?
There is no `after{TYPE}` method which would be called after a request is done.
Processing raw api data should be done in the constructor of the model, see more on that below.
## Models
Models are a bit simpler than services.
They usually consist of a declaration of defaults and an optional constructor.
Models are located in `src/models`.
Each model should extend the `AbstractModel`.
This handles the default value parsing.
A model _does not_ handle any http requests, that's what services are for.
A simple model can look like this:
```javascript
import AbstractModel from './abstractModel'
import TaskModel from './task'
import UserModel from './user'
export default class ListModel extends AbstractModel {
constructor(data) {
// The constructor of AbstractModel handles all the default parsing.
super(data)
// Make all tasks to task models
this.tasks = this.tasks.map(t => {
return new TaskModel(t)
})
this.owner = new UserModel(this.owner)
}
// Default attributes that define the "empty" state.
defaults() {
return {
id: 0,
title: '',
description: '',
owner: UserModel,
tasks: [],
namespaceID: 0,
created: 0,
updated: 0,
}
}
}
```
#### Default values
The `defaults()` functions provides all default values.
The `AbstractModel` constructor will take all the data provided to it, and fill any non-existent,
`undefined` or `null` value with the default provided by the function.
#### Constructor
The `AbstractModel` constructor handles all the default value parsing.
In your model, the constructor can do additional parsing, like making js date object from unix timestamps
or parsing the contents of a child-array into a model.
If the model does nothing like this, you don't need to define a constructor at all.
The parent will handle it all.
#### Access to the data
After initializing a model, it is possible to access all properties via `model.property`.
To make sure the property actually exists, provide it as a default.

View File

@ -9,6 +9,7 @@
},
"dependencies": {
"bulma": "^0.7.1",
"lodash": "^4.17.11",
"v-tooltip": "^2.0.0-rc.33",
"vue": "^2.5.17"
},

View File

@ -70,7 +70,7 @@
</ul>
</div>
<aside class="menu namespaces-lists">
<div class="spinner" :class="{ 'is-loading': loading}"></div>
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
<template v-for="n in namespaces">
<div :key="n.id">
<router-link v-tooltip.right="'Settings'" :to="{name: 'editNamespace', params: {id: n.id} }" class="nsettings" v-if="n.id > 0">
@ -116,70 +116,67 @@
</template>
<script>
import auth from './auth'
import {HTTP} from './http-common'
import auth from './auth'
import message from './message'
import router from './router'
import router from './router'
import NamespaceService from './services/namespace'
export default {
name: 'app',
export default {
name: 'app',
data() {
return {
user: auth.user,
loading: false,
namespaces: [],
data() {
return {
user: auth.user,
namespaces: [],
mobileMenuActive: false,
fullpage: false,
currentDate: new Date(),
}
},
beforeMount() {
// Password reset
if(this.$route.query.userPasswordReset !== undefined) {
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
router.push({name: 'passwordReset'})
}
// Email verification
if(this.$route.query.userEmailConfirm !== undefined) {
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
router.push({name: 'login'})
}
},
created() {
if (this.user.authenticated) {
this.loadNamespaces()
beforeMount() {
// Password reset
if(this.$route.query.userPasswordReset !== undefined) {
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
router.push({name: 'passwordReset'})
}
},
watch: {
// call the method again if the route changes
'$route': 'doStuffAfterRoute'
},
methods: {
logout() {
auth.logout()
},
gravatar() {
return 'https://www.gravatar.com/avatar/' + this.user.infos.avatar + '?s=50'
// Email verification
if(this.$route.query.userEmailConfirm !== undefined) {
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
router.push({name: 'login'})
}
},
created() {
if (this.user.authenticated) {
this.loadNamespaces()
}
},
watch: {
// call the method again if the route changes
'$route': 'doStuffAfterRoute'
},
methods: {
logout() {
auth.logout()
},
gravatar() {
return 'https://www.gravatar.com/avatar/' + this.user.infos.avatar + '?s=50'
},
loadNamespaces() {
let namespaceService = new NamespaceService()
namespaceService.getAll()
.then(r => {
this.$set(this, 'namespaces', r)
})
.catch(e => {
message.error(e, this)
})
},
loadNamespaces() {
const cancel = message.setLoading(this)
HTTP.get(`namespaces`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
this.$set(this, 'namespaces', response.data)
cancel()
})
.catch(e => {
cancel()
this.handleError(e)
})
},
loadNamespacesIfNeeded(e){
if (this.user.authenticated && e.name === 'home') {
this.loadNamespaces()
}
if (this.user.authenticated && e.name === 'home') {
this.loadNamespaces()
}
},
doStuffAfterRoute(e) {
this.fullpage = false;
@ -189,9 +186,6 @@
setFullPage() {
this.fullpage = true;
},
handleError(e) {
message.error(e, this)
}
},
}
},
}
</script>

View File

@ -5,110 +5,111 @@ import router from '../router'
export default {
user: {
authenticated: false,
infos: {}
},
user: {
authenticated: false,
infos: {}
},
login (context, creds, redirect) {
HTTP.post('login', {
username: creds.username,
password: creds.password
})
.then(response => {
// Save the token to local storage for later use
localStorage.removeItem('token') // Delete an eventually preexisting old token
localStorage.setItem('token', response.data.token)
login(context, creds, redirect) {
localStorage.removeItem('token') // Delete an eventually preexisting old token
// Tell others the user is autheticated
this.user.authenticated = true
this.getUserInfos()
HTTP.post('login', {
username: creds.username,
password: creds.password
})
.then(response => {
// Save the token to local storage for later use
localStorage.setItem('token', response.data.token)
// Hide the loader
context.loading = false
// Tell others the user is autheticated
this.user.authenticated = true
this.getUserInfos()
// Redirect if nessecary
if (redirect) {
router.push({ name: redirect })
}
})
.catch(e => {
// Hide the loader
context.loading = false
if (e.response) {
context.error = e.response.data.message
if (e.response.status === 401) {
context.error = 'Wrong username or password.'
}
}
})
},
// Hide the loader
context.loading = false
register (context, creds, redirect) {
HTTP.post('register', {
username: creds.username,
email: creds.email,
password: creds.password
})
.then(response => {
// eslint-disable-next-line
console.log(response)
this.login(context, creds, redirect)
})
.catch(e => {
// Hide the loader
context.loading = false
if (e.response) {
context.error = e.response.data.message
if (e.response.status === 401) {
context.error = 'Wrong username or password.'
}
}
})
},
// Redirect if nessecary
if (redirect) {
router.push({name: redirect})
}
})
.catch(e => {
// Hide the loader
context.loading = false
if (e.response) {
context.error = e.response.data.message
if (e.response.status === 401) {
context.error = 'Wrong username or password.'
}
}
})
},
logout () {
localStorage.removeItem('token')
router.push({ name: 'login' })
this.user.authenticated = false
},
register(context, creds, redirect) {
HTTP.post('register', {
username: creds.username,
email: creds.email,
password: creds.password
})
.then(response => {
// eslint-disable-next-line
console.log(response)
this.login(context, creds, redirect)
})
.catch(e => {
// Hide the loader
context.loading = false
if (e.response) {
context.error = e.response.data.message
if (e.response.status === 401) {
context.error = 'Wrong username or password.'
}
}
})
},
checkAuth () {
let jwt = localStorage.getItem('token')
this.getUserInfos()
this.user.authenticated = false
if (jwt) {
let infos = this.user.infos
let ts = Math.round((new Date()).getTime() / 1000)
if (infos.exp >= ts) {
this.user.authenticated = true
}
}
},
logout() {
localStorage.removeItem('token')
router.push({name: 'login'})
this.user.authenticated = false
},
getUserInfos () {
let jwt = localStorage.getItem('token')
if (jwt) {
this.user.infos = this.parseJwt(localStorage.getItem('token'))
return this.parseJwt(localStorage.getItem('token'))
} else {
return {}
}
},
checkAuth() {
let jwt = localStorage.getItem('token')
this.getUserInfos()
this.user.authenticated = false
if (jwt) {
let infos = this.user.infos
let ts = Math.round((new Date()).getTime() / 1000)
if (infos.exp >= ts) {
this.user.authenticated = true
}
}
},
parseJwt (token) {
let base64Url = token.split('.')[1]
let base64 = base64Url.replace('-', '+').replace('_', '/')
return JSON.parse(window.atob(base64))
},
getUserInfos() {
let jwt = localStorage.getItem('token')
if (jwt) {
this.user.infos = this.parseJwt(localStorage.getItem('token'))
return this.parseJwt(localStorage.getItem('token'))
} else {
return {}
}
},
getAuthHeader () {
return {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
},
parseJwt(token) {
let base64Url = token.split('.')[1]
let base64 = base64Url.replace('-', '+').replace('_', '/')
return JSON.parse(window.atob(base64))
},
getToken () {
return localStorage.getItem('token')
}
getAuthHeader() {
return {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
},
getToken() {
return localStorage.getItem('token')
}
}

View File

@ -7,29 +7,29 @@
</template>
<script>
import auth from '../auth'
import router from '../router'
import auth from '../auth'
import router from '../router'
export default {
name: "Home",
data() {
return {
user: auth.user,
export default {
name: "Home",
data() {
return {
user: auth.user,
loading: false,
currentDate: new Date(),
tasks: []
}
},
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
}
},
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'login'})
}
},
methods: {
logout() {
auth.logout()
},
},
}
router.push({name: 'login'})
}
},
methods: {
logout() {
auth.logout()
},
},
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="loader-container" :class="{ 'is-loading': loading}">
<div class="loader-container" :class="{ 'is-loading': listService.loading}">
<div class="card">
<header class="card-header">
<p class="card-header-title">
@ -12,25 +12,25 @@
<div class="field">
<label class="label" for="listtext">List Name</label>
<div class="control">
<input v-focus :class="{ 'disabled': loading}" :disabled="loading" class="input" type="text" id="listtext" placeholder="The list title goes here..." v-model="list.title">
<input v-focus :class="{ 'disabled': listService.loading}" :disabled="listService.loading" class="input" type="text" id="listtext" placeholder="The list title goes here..." v-model="list.title">
</div>
</div>
<div class="field">
<label class="label" for="listdescription">Description</label>
<div class="control">
<textarea :class="{ 'disabled': loading}" :disabled="loading" class="textarea" placeholder="The lists description goes here..." id="listdescription" v-model="list.description"></textarea>
<textarea :class="{ 'disabled': listService.loading}" :disabled="listService.loading" class="textarea" placeholder="The lists description goes here..." id="listdescription" v-model="list.description"></textarea>
</div>
</div>
</form>
<div class="columns bigbuttons">
<div class="column">
<button @click="submit()" class="button is-primary is-fullwidth" :class="{ 'is-loading': loading}">
<button @click="submit()" class="button is-primary is-fullwidth" :class="{ 'is-loading': listService.loading}">
Save
</button>
</div>
<div class="column is-1">
<button @click="showDeleteModal = true" class="button is-danger is-fullwidth" :class="{ 'is-loading': loading}">
<button @click="showDeleteModal = true" class="button is-danger is-fullwidth" :class="{ 'is-loading': listService.loading}">
<span class="icon is-small">
<icon icon="trash-alt"/>
</span>
@ -41,9 +41,8 @@
</div>
</div>
<manageusers :id="list.id" type="list" :userIsAdmin="userIsAdmin" />
<manageteams :id="list.id" type="list" :userIsAdmin="userIsAdmin" />
<component :is="manageUsersComponent" :id="list.id" type="list" :userIsAdmin="userIsAdmin"></component>
<component :is="manageTeamsComponent" :id="list.id" type="list" :userIsAdmin="userIsAdmin"></component>
<modal
v-if="showDeleteModal"
@ -57,101 +56,92 @@
</template>
<script>
import auth from '../../auth'
import router from '../../router'
import {HTTP} from '../../http-common'
import auth from '../../auth'
import router from '../../router'
import message from '../../message'
import manageusers from '../sharing/user'
import manageteams from '../sharing/team'
import ListModel from '../../models/list'
import ListService from '../../services/list'
export default {
name: "EditList",
data() {
return {
list: {title: '', description:''},
error: '',
loading: false,
showDeleteModal: false,
export default {
name: "EditList",
data() {
return {
list: ListModel,
listService: ListService,
showDeleteModal: false,
user: auth.user,
userIsAdmin: false,
}
},
userIsAdmin: false, // FIXME: we should be able to know somehow if the user is admin, not only based on if he's the owner
manageUsersComponent: '',
manageTeamsComponent: '',
}
},
components: {
manageusers,
manageteams,
},
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'home'})
}
this.list.id = this.$route.params.id
},
created() {
this.loadList()
},
watch: {
// call again the method if the route changes
'$route': 'loadList'
},
methods: {
loadList() {
const cancel = message.setLoading(this)
HTTP.get(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
this.$set(this, 'list', response.data)
if (response.data.owner.id === this.user.infos.id) {
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'home'})
}
},
created() {
this.listService = new ListService()
this.loadList()
},
watch: {
// call again the method if the route changes
'$route': 'loadList'
},
methods: {
loadList() {
let list = new ListModel({id: this.$route.params.id})
this.listService.get(list)
.then(r => {
this.$set(this, 'list', r)
if (r.owner.id === this.user.infos.id) {
this.userIsAdmin = true
}
cancel()
})
.catch(e => {
this.handleError(e)
})
},
submit() {
const cancel = message.setLoading(this)
HTTP.post(`lists/` + this.$route.params.id, this.list, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
// Update the list in the parent
for (const n in this.$parent.namespaces) {
let lists = this.$parent.namespaces[n].lists
for (const l in lists) {
if (lists[l].id === response.data.id) {
this.$set(this.$parent.namespaces[n].lists, l, response.data)
}
}
}
this.handleSuccess({message: 'The list was successfully updated.'})
cancel()
})
.catch(e => {
cancel()
this.handleError(e)
})
},
deleteList() {
const cancel = message.setLoading(this)
HTTP.delete(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(() => {
this.handleSuccess({message: 'The list was successfully deleted.'})
cancel()
router.push({name: 'home'})
})
.catch(e => {
cancel()
this.handleError(e)
})
},
handleError(e) {
message.error(e, this)
},
handleSuccess(e) {
message.success(e, this)
}
}
}
// This will trigger the dynamic loading of components once we actually have all the data to pass to them
this.manageTeamsComponent = 'manageteams'
this.manageUsersComponent = 'manageusers'
})
.catch(e => {
message.error(e, this)
})
},
submit() {
this.listService.update(this.list)
.then(r => {
// Update the list in the parent
for (const n in this.$parent.namespaces) {
let lists = this.$parent.namespaces[n].lists
for (const l in lists) {
if (lists[l].id === r.id) {
this.$set(this.$parent.namespaces[n].lists, l, r)
}
}
}
message.success({message: 'The list was successfully updated.'}, this)
})
.catch(e => {
message.error(e, this)
})
},
deleteList() {
this.listService.delete(this.list)
.then(() => {
message.success({message: 'The list was successfully deleted.'}, this)
router.push({name: 'home'})
})
.catch(e => {
message.error(e, this)
})
},
}
}
</script>

View File

@ -7,8 +7,8 @@
<h3>Create a new list</h3>
<form @submit.prevent="newList" @keyup.esc="back()">
<div class="field is-grouped">
<p class="control is-expanded" :class="{ 'is-loading': loading}">
<input v-focus class="input" :class="{ 'disabled': loading}" v-model="list.title" type="text" placeholder="The list's name goes here...">
<p class="control is-expanded" :class="{ 'is-loading': listService.loading}">
<input v-focus class="input" :class="{ 'disabled': listService.loading}" v-model="list.title" type="text" placeholder="The list's name goes here...">
</p>
<p class="control">
<button type="submit" class="button is-success noshadow">
@ -24,54 +24,47 @@
</template>
<script>
import auth from '../../auth'
import router from '../../router'
import {HTTP} from '../../http-common'
import message from '../../message'
import auth from '../../auth'
import router from '../../router'
import message from '../../message'
import ListService from '../../services/list'
import ListModel from '../../models/list'
export default {
name: "NewList",
data() {
return {
list: {title: ''},
error: '',
loading: false
}
},
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'home'})
}
},
export default {
name: "NewList",
data() {
return {
list: ListModel,
listService: ListService,
}
},
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'home'})
}
},
created() {
this.list = new ListModel()
this.listService = new ListService()
this.$parent.setFullPage();
},
methods: {
newList() {
const cancel = message.setLoading(this)
HTTP.put(`namespaces/` + this.$route.params.id + `/lists`, this.list, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
methods: {
newList() {
this.list.namespaceID = this.$route.params.id
this.listService.create(this.list)
.then(response => {
this.$parent.loadNamespaces()
this.handleSuccess({message: 'The list was successfully created.'})
cancel()
router.push({name: 'showList', params: {id: response.data.id}})
})
.catch(e => {
cancel()
this.handleError(e)
})
},
message.success({message: 'The list was successfully created.'}, this)
router.push({name: 'showList', params: {id: response.id}})
})
.catch(e => {
message.error(e, this)
})
},
back() {
router.go(-1)
},
handleError(e) {
message.error(e, this)
},
handleSuccess(e) {
message.success(e, this)
}
}
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="loader-container" :class="{ 'is-loading': loading}">
<div class="loader-container" :class="{ 'is-loading': listService.loading}">
<div class="content">
<router-link :to="{ name: 'editList', params: { id: list.id } }" class="icon settings is-medium">
<icon icon="cog" size="2x"/>
@ -8,8 +8,8 @@
</div>
<form @submit.prevent="addTask()">
<div class="field is-grouped">
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': loading}">
<input v-focus class="input" :class="{ 'disabled': loading}" v-model="newTask.text" type="text" placeholder="Add a new task...">
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
<input v-focus class="input" :class="{ 'disabled': taskService.loading}" v-model="newTask.text" type="text" placeholder="Add a new task...">
<span class="icon is-small is-left">
<icon icon="tasks"/>
</span>
@ -68,21 +68,21 @@
<div class="field">
<label class="label" for="tasktext">Task Text</label>
<div class="control">
<input v-focus :class="{ 'disabled': loading}" :disabled="loading" class="input" type="text" id="tasktext" placeholder="The task text is here..." v-model="taskEditTask.text">
<input v-focus :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="input" type="text" id="tasktext" placeholder="The task text is here..." v-model="taskEditTask.text">
</div>
</div>
<div class="field">
<label class="label" for="taskdescription">Description</label>
<div class="control">
<textarea :class="{ 'disabled': loading}" :disabled="loading" class="textarea" placeholder="The tasks description goes here..." id="taskdescription" v-model="taskEditTask.description"></textarea>
<textarea :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="textarea" placeholder="The tasks description goes here..." id="taskdescription" v-model="taskEditTask.description"></textarea>
</div>
</div>
<b>Reminder Dates</b>
<div class="reminder-input" :class="{ 'overdue': (r < nowUnix && index !== (taskEditTask.reminderDates.length - 1))}" v-for="(r, index) in taskEditTask.reminderDates" :key="index">
<flat-pickr
:class="{ 'disabled': loading}"
:disabled="loading"
:class="{ 'disabled': taskService.loading}"
:disabled="taskService.loading"
:v-model="taskEditTask.reminderDates"
:config="flatPickerConfig"
:id="'taskreminderdate' + index"
@ -97,9 +97,9 @@
<label class="label" for="taskduedate">Due Date</label>
<div class="control">
<flat-pickr
:class="{ 'disabled': loading}"
:class="{ 'disabled': taskService.loading}"
class="input"
:disabled="loading"
:disabled="taskService.loading"
v-model="taskEditTask.dueDate"
:config="flatPickerConfig"
id="taskduedate"
@ -113,9 +113,9 @@
<div class="control columns">
<div class="column">
<flat-pickr
:class="{ 'disabled': loading}"
:class="{ 'disabled': taskService.loading}"
class="input"
:disabled="loading"
:disabled="taskService.loading"
v-model="taskEditTask.startDate"
:config="flatPickerConfig"
id="taskduedate"
@ -124,9 +124,9 @@
</div>
<div class="column">
<flat-pickr
:class="{ 'disabled': loading}"
:class="{ 'disabled': taskService.loading}"
class="input"
:disabled="loading"
:disabled="taskService.loading"
v-model="taskEditTask.endDate"
:config="flatPickerConfig"
id="taskduedate"
@ -140,11 +140,11 @@
<label class="label" for="">Repeat after</label>
<div class="control repeat-after-input columns">
<div class="column">
<input class="input" placeholder="Specify an amount..." v-model="repeatAfter.amount"/>
<input class="input" placeholder="Specify an amount..." v-model="taskEditTask.repeatAfter.amount"/>
</div>
<div class="column is-3">
<div class="select">
<select v-model="repeatAfter.type">
<select v-model="taskEditTask.repeatAfter.type">
<option value="hours">Hours</option>
<option value="days">Days</option>
<option value="weeks">Weeks</option>
@ -179,14 +179,14 @@
</div>
<div class="field has-addons">
<div class="control is-expanded">
<input @keyup.enter="addSubtask()" :class="{ 'disabled': loading}" :disabled="loading" class="input" type="text" id="tasktext" placeholder="New subtask" v-model="newTask.text"/>
<input @keyup.enter="addSubtask()" :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="input" type="text" id="tasktext" placeholder="New subtask" v-model="newTask.text"/>
</div>
<div class="control">
<a class="button" @click="addSubtask()"><icon icon="plus"></icon></a>
</div>
</div>
<button type="submit" class="button is-success is-fullwidth" :class="{ 'is-loading': loading}">
<button type="submit" class="button is-success is-fullwidth" :class="{ 'is-loading': taskService.loading}">
Save
</button>
@ -200,294 +200,147 @@
</template>
<script>
import auth from '../../auth'
import router from '../../router'
import {HTTP} from '../../http-common'
import message from '../../message'
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
import auth from '../../auth'
import router from '../../router'
import message from '../../message'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
export default {
data() {
return {
listID: this.$route.params.id,
list: {},
newTask: {text: ''},
error: '',
loading: false,
import ListService from '../../services/list'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import ListModel from '../../models/list'
export default {
data() {
return {
listID: this.$route.params.id,
listService: ListService,
taskService: TaskService,
list: {},
newTask: TaskModel,
isTaskEdit: false,
taskEditTask: {
subtasks: [],
},
lastReminder: 0,
nowUnix: new Date(),
repeatAfter: {type: 'days', amount: null},
flatPickerConfig:{
altFormat: 'j M Y H:i',
altInput: true,
dateFormat: 'Y-m-d H:i',
flatPickerConfig:{
altFormat: 'j M Y H:i',
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
onOpen: this.updateLastReminderDate,
onClose: this.addReminderDate,
},
}
},
}
},
components: {
flatPickr
},
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'home'})
}
},
created() {
this.loadList()
},
watch: {
// call again the method if the route changes
'$route': 'loadList'
},
methods: {
loadList() {
this.isTaskEdit = false
const cancel = message.setLoading(this)
beforeMount() {
// Check if the user is already logged in, if so, redirect him to the homepage
if (!auth.user.authenticated) {
router.push({name: 'home'})
}
},
created() {
this.listService = new ListService()
this.taskService = new TaskService()
this.newTask = new TaskModel()
this.loadList()
},
watch: {
// call again the method if the route changes
'$route': 'loadList'
},
methods: {
loadList() {
this.isTaskEdit = false
// We create an extra list object instead of creating it in this.list because that would trigger a ui update which would result in bad ux.
let list = new ListModel({id: this.$route.params.id})
this.listService.get(list)
.then(r => {
this.$set(this, 'list', r)
})
.catch(e => {
message.error(e, this)
})
},
addTask() {
this.newTask.listID = this.$route.params.id
this.taskService.create(this.newTask)
.then(r => {
this.list.addTaskToList(r)
message.success({message: 'The task was successfully created.'}, this)
})
.catch(e => {
message.error(e, this)
})
HTTP.get(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
for (const t in response.data.tasks) {
response.data.tasks[t] = this.fixStuffComingFromAPI(response.data.tasks[t])
}
// This adds a new elemednt "list" to our object which contains all lists
response.data.tasks = this.sortTasks(response.data.tasks)
this.$set(this, 'list', response.data)
if (this.list.tasks === null) {
this.list.tasks = []
}
cancel() // cancel the timer
})
.catch(e => {
cancel()
this.handleError(e)
})
},
addTask() {
const cancel = message.setLoading(this)
HTTP.put(`lists/` + this.$route.params.id, this.newTask, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
this.addTaskToList(response.data)
this.handleSuccess({message: 'The task was successfully created.'})
cancel() // cancel the timer
})
.catch(e => {
cancel()
this.handleError(e)
})
this.newTask = {}
},
addTaskToList(task) {
// If it's a subtask, add it to its parent, otherwise append it to the list of tasks
if (task.parentTaskID === 0) {
this.list.tasks.push(task)
} else {
for (const t in this.list.tasks) {
if (this.list.tasks[t].id === task.parentTaskID) {
this.list.tasks[t].subtasks.push(task)
break
}
}
}
// Update the current edit task if needed
if (task.ParentTask === this.taskEditTask.id) {
this.taskEditTask.subtasks.push(task)
}
this.list.tasks = this.sortTasks(this.list.tasks)
this.newTask = {}
},
markAsDone(e) {
let context = this
if (e.target.checked) {
setTimeout(doTheDone, 300); // Delay it to show the animation when marking a task as done
} else {
doTheDone() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
function doTheDone() {
const cancel = message.setLoading(context)
HTTP.post(`tasks/` + e.target.id, {done: e.target.checked}, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
.then(response => {
context.updateTaskByID(parseInt(e.target.id), response.data)
context.handleSuccess({message: 'The task was successfully ' + (e.target.checked ? '' : 'un-') + 'marked as done.'})
cancel() // To not set the spinner to loading when the request is made in less than 100ms, would lead to loading infinitly.
let updateFunc = () => {
// We get the task, update the 'done' property and then push it to the api.
let task = this.list.getTaskByID(e.target.id)
task.done = e.target.checked
this.taskService.update(task)
.then(r => {
this.updateTaskInList(r)
message.success({message: 'The task was successfully ' + (task.done ? '' : 'un-') + 'marked as done.'}, this)
})
.catch(e => {
cancel()
context.handleError(e)
message.error(e, this)
})
}
if (e.target.checked) {
setTimeout(updateFunc(), 300); // Delay it to show the animation when marking a task as done
} else {
updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
},
editTask(id) {
// Find the selected task and set it to the current object
for (const t in this.list.tasks) {
if (this.list.tasks[t].id === id) {
this.taskEditTask = this.list.tasks[t]
break
}
}
if (this.taskEditTask.reminderDates === null) {
this.taskEditTask.reminderDates = []
}
this.taskEditTask.reminderDates = this.removeNullsFromArray(this.taskEditTask.reminderDates)
this.taskEditTask.reminderDates.push(null)
// Re-convert the the amount from seconds to be used with our form
let repeatAfterHours = (this.taskEditTask.repeatAfter / 60) / 60
// if its dividable by 24, its something with days
if (repeatAfterHours % 24 === 0) {
let repeatAfterDays = repeatAfterHours / 24
if (repeatAfterDays % 7 === 0) {
this.repeatAfter.type = 'weeks'
this.repeatAfter.amount = repeatAfterDays / 7
} else if (repeatAfterDays % 30 === 0) {
this.repeatAfter.type = 'months'
this.repeatAfter.amount = repeatAfterDays / 30
} else if (repeatAfterDays % 365 === 0) {
this.repeatAfter.type = 'years'
this.repeatAfter.amount = repeatAfterDays / 365
} else {
this.repeatAfter.type = 'days'
this.repeatAfter.amount = repeatAfterDays
}
} else {
// otherwise hours
this.repeatAfter.type = 'hours'
this.repeatAfter.amount = repeatAfterHours
}
if(this.taskEditTask.subtasks === null) {
this.taskEditTask.subtasks = [];
}
// Find the selected task and set it to the current object
let theTask = this.list.getTaskByID(id) // Somehow this does not work if we directly assign this to this.taskEditTask
this.taskEditTask = theTask
this.isTaskEdit = true
},
editTaskSubmit() {
const cancel = message.setLoading(this)
// Convert the date in a unix timestamp
this.taskEditTask.dueDate = (+ new Date(this.taskEditTask.dueDate)) / 1000
this.taskEditTask.startDate = (+ new Date(this.taskEditTask.startDate)) / 1000
this.taskEditTask.endDate = (+ new Date(this.taskEditTask.endDate)) / 1000
// remove all nulls
this.taskEditTask.reminderDates = this.removeNullsFromArray(this.taskEditTask.reminderDates)
// Make normal timestamps from js timestamps
for (const t in this.taskEditTask.reminderDates) {
this.taskEditTask.reminderDates[t] = Math.round(this.taskEditTask.reminderDates[t] / 1000)
}
// Make the repeating amount to seconds
let repeatAfterSeconds = 0
if (this.repeatAfter.amount !== null || this.repeatAfter.amount !== 0) {
switch (this.repeatAfter.type) {
case 'hours':
repeatAfterSeconds = this.repeatAfter.amount * 60 * 60
break;
case 'days':
repeatAfterSeconds = this.repeatAfter.amount * 60 * 60 * 24
break;
case 'weeks':
repeatAfterSeconds = this.repeatAfter.amount * 60 * 60 * 24 * 7
break;
case 'months':
repeatAfterSeconds = this.repeatAfter.amount * 60 * 60 * 24 * 30
break;
case 'years':
repeatAfterSeconds = this.repeatAfter.amount * 60 * 60 * 24 * 365