Move everything to models and services (#17)
This commit is contained in:
parent
8559d8bb97
commit
9b0c842ae1
50 changed files with 2165 additions and 1206 deletions
184
docs/models-services.md
Normal file
184
docs/models-services.md
Normal 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.
|
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"bulma": "^0.7.1",
|
||||
"lodash": "^4.17.11",
|
||||
"v-tooltip": "^2.0.0-rc.33",
|
||||
"vue": "^2.5.17"
|
||||
},
|
||||
|
|
20
src/App.vue
20
src/App.vue
|
@ -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">
|
||||
|
@ -117,9 +117,9 @@
|
|||
|
||||
<script>
|
||||
import auth from './auth'
|
||||
import {HTTP} from './http-common'
|
||||
import message from './message'
|
||||
import router from './router'
|
||||
import NamespaceService from './services/namespace'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
|
@ -127,7 +127,6 @@
|
|||
data() {
|
||||
return {
|
||||
user: auth.user,
|
||||
loading: false,
|
||||
namespaces: [],
|
||||
mobileMenuActive: false,
|
||||
fullpage: false,
|
||||
|
@ -165,15 +164,13 @@
|
|||
return 'https://www.gravatar.com/avatar/' + this.user.infos.avatar + '?s=50'
|
||||
},
|
||||
loadNamespaces() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.get(`namespaces`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(response => {
|
||||
this.$set(this, 'namespaces', response.data)
|
||||
cancel()
|
||||
let namespaceService = new NamespaceService()
|
||||
namespaceService.getAll()
|
||||
.then(r => {
|
||||
this.$set(this, 'namespaces', r)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
loadNamespacesIfNeeded(e){
|
||||
|
@ -189,9 +186,6 @@
|
|||
setFullPage() {
|
||||
this.fullpage = true;
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -11,13 +11,14 @@ export default {
|
|||
},
|
||||
|
||||
login(context, creds, redirect) {
|
||||
localStorage.removeItem('token') // Delete an eventually preexisting old token
|
||||
|
||||
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)
|
||||
|
||||
// Tell others the user is autheticated
|
||||
|
|
|
@ -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"
|
||||
|
@ -59,21 +58,25 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
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,
|
||||
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: {
|
||||
|
@ -85,10 +88,9 @@
|
|||
if (!auth.user.authenticated) {
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
|
||||
this.list.id = this.$route.params.id
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
|
@ -97,61 +99,49 @@
|
|||
},
|
||||
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) {
|
||||
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()
|
||||
// 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 => {
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
submit() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.post(`lists/` + this.$route.params.id, this.list, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(response => {
|
||||
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 === response.data.id) {
|
||||
this.$set(this.$parent.namespaces[n].lists, l, response.data)
|
||||
if (lists[l].id === r.id) {
|
||||
this.$set(this.$parent.namespaces[n].lists, l, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.handleSuccess({message: 'The list was successfully updated.'})
|
||||
cancel()
|
||||
message.success({message: 'The list was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteList() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.delete(`lists/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.listService.delete(this.list)
|
||||
.then(() => {
|
||||
this.handleSuccess({message: 'The list was successfully deleted.'})
|
||||
cancel()
|
||||
message.success({message: 'The list was successfully deleted.'}, this)
|
||||
router.push({name: 'home'})
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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">
|
||||
|
@ -26,16 +26,16 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
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
|
||||
list: ListModel,
|
||||
listService: ListService,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -45,33 +45,26 @@
|
|||
}
|
||||
},
|
||||
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')}})
|
||||
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}})
|
||||
message.success({message: 'The list was successfully created.'}, this)
|
||||
router.push({name: 'showList', params: {id: response.id}})
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
back() {
|
||||
router.go(-1)
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -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>
|
||||
|
||||
|
@ -202,26 +202,30 @@
|
|||
<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 flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
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: {text: ''},
|
||||
error: '',
|
||||
loading: false,
|
||||
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,
|
||||
|
@ -242,6 +246,9 @@
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.taskService = new TaskService()
|
||||
this.newTask = new TaskModel()
|
||||
this.loadList()
|
||||
},
|
||||
watch: {
|
||||
|
@ -251,243 +258,89 @@
|
|||
methods: {
|
||||
loadList() {
|
||||
this.isTaskEdit = false
|
||||
const cancel = message.setLoading(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
|
||||
// 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 => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
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
|
||||
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 => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
|
||||
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)
|
||||
},
|
||||
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 = [];
|
||||
}
|
||||
|
||||
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
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.taskEditTask.repeatAfter = repeatAfterSeconds
|
||||
|
||||
HTTP.post(`tasks/` + this.taskEditTask.id, this.taskEditTask, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(response => {
|
||||
// Update the task in the list
|
||||
this.updateTaskByID(this.taskEditTask.id, response.data)
|
||||
|
||||
// Also update the current taskedit object so the ui changes
|
||||
response.data.reminderDates.push(null) // To be able to add a new one
|
||||
this.$set(this, 'taskEditTask', response.data)
|
||||
this.handleSuccess({message: 'The task was successfully updated.'})
|
||||
cancel() // cancel the timers
|
||||
this.taskService.update(this.taskEditTask)
|
||||
.then(r => {
|
||||
this.updateTaskInList(r)
|
||||
this.$set(this, 'taskEditTask', r)
|
||||
message.success({message: 'The task was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
addSubtask() {
|
||||
this.newTask.parentTaskID = this.taskEditTask.id
|
||||
this.addTask()
|
||||
},
|
||||
updateTaskByID(id, updatedTask) {
|
||||
updateTaskInList(task) {
|
||||
// We need to do the update here in the component, because there is no way of notifiying vue of the change otherwise.
|
||||
for (const t in this.list.tasks) {
|
||||
if (this.list.tasks[t].id === id) {
|
||||
this.$set(this.list.tasks, t, this.fixStuffComingFromAPI(updatedTask))
|
||||
if (this.list.tasks[t].id === task.id) {
|
||||
this.$set(this.list.tasks, t, task)
|
||||
break
|
||||
}
|
||||
|
||||
if (this.list.tasks[t].id === updatedTask.parentTaskID) {
|
||||
if (this.list.tasks[t].id === task.parentTaskID) {
|
||||
for (const s in this.list.tasks[t].subtasks) {
|
||||
if (this.list.tasks[t].subtasks[s].id === updatedTask.id) {
|
||||
this.$set(this.list.tasks[t].subtasks, s, updatedTask)
|
||||
if (this.list.tasks[t].subtasks[s].id === task.id) {
|
||||
this.$set(this.list.tasks[t].subtasks, s, task)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.list.tasks = this.sortTasks(this.list.tasks)
|
||||
},
|
||||
fixStuffComingFromAPI(task) {
|
||||
// Make date objects from timestamps
|
||||
task.dueDate = this.parseDateIfNessecary(task.dueDate)
|
||||
task.startDate = this.parseDateIfNessecary(task.startDate)
|
||||
task.endDate = this.parseDateIfNessecary(task.endDate)
|
||||
|
||||
for (const rd in task.reminderDates) {
|
||||
task.reminderDates[rd] = this.parseDateIfNessecary(task.reminderDates[rd])
|
||||
}
|
||||
|
||||
// Make subtasks into empty array if null
|
||||
if (task.subtasks === null) {
|
||||
task.subtasks = []
|
||||
}
|
||||
|
||||
return task
|
||||
},
|
||||
sortTasks(tasks) {
|
||||
if (tasks === null) {
|
||||
return tasks
|
||||
}
|
||||
return tasks.sort(function(a,b) {
|
||||
if (a.done < b.done)
|
||||
return -1
|
||||
if (a.done > b.done)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
parseDateIfNessecary(dateUnix) {
|
||||
let dateobj = (+new Date(dateUnix * 1000))
|
||||
if (dateobj === 0 || dateUnix === 0) {
|
||||
dateUnix = null
|
||||
} else {
|
||||
dateUnix = dateobj
|
||||
}
|
||||
return dateUnix
|
||||
this.list.sortTasks()
|
||||
},
|
||||
updateLastReminderDate(selectedDates) {
|
||||
this.lastReminder = +new Date(selectedDates[0])
|
||||
|
@ -513,23 +366,6 @@
|
|||
this.taskEditTask.reminderDates.splice(index, 1)
|
||||
// Reset the last to 0 to have the "add reminder" button
|
||||
this.taskEditTask.reminderDates[this.taskEditTask.reminderDates.length - 1] = null
|
||||
},
|
||||
removeNullsFromArray(array) {
|
||||
for (const index in array) {
|
||||
if (array[index] === null) {
|
||||
array.splice(index, 1)
|
||||
}
|
||||
}
|
||||
return array
|
||||
},
|
||||
formatUnixDate(dateUnix) {
|
||||
return (new Date(dateUnix)).toLocaleString()
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import TaskService from '../../services/task'
|
||||
|
||||
export default {
|
||||
name: "ShowTasks",
|
||||
|
@ -39,7 +40,8 @@
|
|||
return {
|
||||
loading: true,
|
||||
tasks: [],
|
||||
hasUndoneTasks: false
|
||||
hasUndoneTasks: false,
|
||||
taskService: TaskService,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -48,10 +50,22 @@
|
|||
showAll: Boolean,
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
this.loadPendingTasks()
|
||||
},
|
||||
methods: {
|
||||
loadPendingTasks() {
|
||||
// We can't really make this code work until 0.6 is released which will make this exact thing a lot easier.
|
||||
// Because the feature we need here (specifying sort order and start/end date via query parameters) is already in master, we'll just wait and use the legacy method until then.
|
||||
/*
|
||||
let taskDummy = new TaskModel() // Used to specify options for the request
|
||||
this.taskService.getAll(taskDummy)
|
||||
.then(r => {
|
||||
this.tasks = r
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})*/
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
let url = `tasks/all/duedate`
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="loader-container" v-bind:class="{ 'is-loading': loading}">
|
||||
<div class="loader-container" v-bind:class="{ 'is-loading': namespaceService.loading}">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
|
@ -12,25 +12,25 @@
|
|||
<div class="field">
|
||||
<label class="label" for="namespacetext">Namespace Name</label>
|
||||
<div class="control">
|
||||
<input v-focus :class="{ 'disabled': loading}" :disabled="loading" class="input" type="text" id="namespacetext" placeholder="The namespace text is here..." v-model="namespace.name">
|
||||
<input v-focus :class="{ 'disabled': namespaceService.loading}" :disabled="namespaceService.loading" class="input" type="text" id="namespacetext" placeholder="The namespace text is here..." v-model="namespace.name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="namespacedescription">Description</label>
|
||||
<div class="control">
|
||||
<textarea :class="{ 'disabled': loading}" :disabled="loading" class="textarea" placeholder="The namespaces description goes here..." id="namespacedescription" v-model="namespace.description"></textarea>
|
||||
<textarea :class="{ 'disabled': namespaceService.loading}" :disabled="namespaceService.loading" class="textarea" placeholder="The namespaces description goes here..." id="namespacedescription" v-model="namespace.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': namespaceService.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': namespaceService.loading}">
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -41,9 +41,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<manageusers :id="namespace.id" type="namespace" :userIsAdmin="userIsAdmin" />
|
||||
|
||||
<manageteams :id="namespace.id" type="namespace" :userIsAdmin="userIsAdmin" />
|
||||
<component :is="manageUsersComponent" :id="namespace.id" type="namespace" :userIsAdmin="userIsAdmin"></component>
|
||||
<component :is="manageTeamsComponent" :id="namespace.id" type="namespace" :userIsAdmin="userIsAdmin"></component>
|
||||
|
||||
<modal
|
||||
v-if="showDeleteModal"
|
||||
|
@ -59,21 +58,24 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import manageusers from '../sharing/user'
|
||||
import manageteams from '../sharing/team'
|
||||
import NamespaceService from '../../services/namespace'
|
||||
import NamespaceModel from '../../models/namespace'
|
||||
|
||||
export default {
|
||||
name: "EditNamespace",
|
||||
data() {
|
||||
return {
|
||||
namespace: {title: '', description:''},
|
||||
error: '',
|
||||
loading: false,
|
||||
namespaceService: NamespaceService,
|
||||
userIsAdmin: false,
|
||||
manageUsersComponent: '',
|
||||
manageTeamsComponent: '',
|
||||
|
||||
namespace: NamespaceModel,
|
||||
showDeleteModal: false,
|
||||
user: auth.user,
|
||||
userIsAdmin: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -89,6 +91,7 @@
|
|||
this.namespace.id = this.$route.params.id
|
||||
},
|
||||
created() {
|
||||
this.namespaceService = new NamespaceService()
|
||||
this.loadNamespace()
|
||||
},
|
||||
watch: {
|
||||
|
@ -97,66 +100,47 @@
|
|||
},
|
||||
methods: {
|
||||
loadNamespace() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.get(`namespaces/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(response => {
|
||||
this.$set(this, 'namespace', response.data)
|
||||
if (response.data.owner.id === this.user.infos.id) {
|
||||
let namespace = new NamespaceModel({id: this.$route.params.id})
|
||||
this.namespaceService.get(namespace)
|
||||
.then(r => {
|
||||
this.$set(this, 'namespace', r)
|
||||
if (r.owner.id === this.user.infos.id) {
|
||||
this.userIsAdmin = true
|
||||
}
|
||||
cancel()
|
||||
// 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 => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
submit() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.post(`namespaces/` + this.$route.params.id, this.namespace, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(response => {
|
||||
this.namespaceService.update(this.namespace)
|
||||
.then(r => {
|
||||
// Update the namespace in the parent
|
||||
for (const n in this.$parent.namespaces) {
|
||||
if (this.$parent.namespaces[n].id === response.data.id) {
|
||||
response.data.lists = this.$parent.namespaces[n].lists
|
||||
this.$set(this.$parent.namespaces, n, response.data)
|
||||
if (this.$parent.namespaces[n].id === r.id) {
|
||||
r.lists = this.$parent.namespaces[n].lists
|
||||
this.$set(this.$parent.namespaces, n, r)
|
||||
}
|
||||
}
|
||||
this.handleSuccess({message: 'The namespace was successfully updated.'})
|
||||
cancel()
|
||||
message.success({message: 'The namespace was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteNamespace() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.delete(`namespaces/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.namespaceService.delete(this.namespace)
|
||||
.then(() => {
|
||||
this.handleSuccess({message: 'The namespace was successfully deleted.'})
|
||||
cancel()
|
||||
message.success({message: 'The namespace was successfully deleted.'}, this)
|
||||
router.push({name: 'home'})
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
})
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bigbuttons{
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
|
@ -7,8 +7,8 @@
|
|||
<h3>Create a new namespace</h3>
|
||||
<form @submit.prevent="newNamespace" @keyup.esc="back()">
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': loading}">
|
||||
<input v-focus class="input" v-bind:class="{ 'disabled': loading}" v-model="namespace.name" type="text" placeholder="The namespace's name goes here...">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': namespaceService.loading}">
|
||||
<input v-focus class="input" v-bind:class="{ 'disabled': namespaceService.loading}" v-model="namespace.name" type="text" placeholder="The namespace's name goes here...">
|
||||
</p>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-success noshadow">
|
||||
|
@ -27,16 +27,16 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import NamespaceModel from "../../models/namespace";
|
||||
import NamespaceService from "../../services/namespace";
|
||||
|
||||
export default {
|
||||
name: "NewNamespace",
|
||||
data() {
|
||||
return {
|
||||
namespace: {title: ''},
|
||||
error: '',
|
||||
loading: false
|
||||
namespace: NamespaceModel,
|
||||
namespaceService: NamespaceService,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -46,32 +46,24 @@
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.namespace = new NamespaceModel()
|
||||
this.namespaceService = new NamespaceService()
|
||||
this.$parent.setFullPage();
|
||||
},
|
||||
methods: {
|
||||
newNamespace() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.put(`namespaces`, this.namespace, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.namespaceService.create(this.namespace)
|
||||
.then(() => {
|
||||
this.$parent.loadNamespaces()
|
||||
this.handleSuccess({message: 'The namespace was successfully created.'})
|
||||
cancel()
|
||||
message.success({message: 'The namespace was successfully created.'}, this)
|
||||
router.push({name: 'home'})
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
back() {
|
||||
router.go(-1)
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
<div class="card-content content teams-list">
|
||||
<form @submit.prevent="addTeam()" class="add-team-form" v-if="userIsAdmin">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': loading}">
|
||||
<input class="input" v-bind:class="{ 'disabled': loading}" v-model.number="newTeam.team_id" type="text" placeholder="Add a new team...">
|
||||
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': this.teamService.loading}">
|
||||
<input class="input" v-bind:class="{ 'disabled': this.teamService.loading}" v-model.number="teamStuffModel.teamID" type="text" placeholder="Add a new team...">
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
|
@ -86,9 +86,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {HTTP} from '../../http-common'
|
||||
import auth from '../../auth'
|
||||
import message from '../../message'
|
||||
import TeamNamespaceService from '../../services/teamNamespace'
|
||||
import TeamNamespaceModel from '../../models/teamNamespace'
|
||||
import TeamListModel from '../../models/teamList'
|
||||
import TeamListService from '../../services/teamList'
|
||||
|
||||
export default {
|
||||
name: 'team',
|
||||
|
@ -99,11 +102,13 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
teamService: Object, // This team service is either a teamNamespaceService or a teamListService, depending on the type we are using
|
||||
teamStuffModel: Object,
|
||||
|
||||
currentUser: auth.user.infos,
|
||||
typeString: '',
|
||||
listTeams: [],
|
||||
newTeam: {team_id: 0},
|
||||
newTeam: {teamID: 0},
|
||||
showTeamDeleteModal: false,
|
||||
teamToDelete: 0,
|
||||
}
|
||||
|
@ -111,8 +116,12 @@
|
|||
created() {
|
||||
if (this.type === 'list') {
|
||||
this.typeString = `list`
|
||||
this.teamService = new TeamListService()
|
||||
this.teamStuffModel = new TeamListModel({listID: this.id})
|
||||
} else if (this.type === 'namespace') {
|
||||
this.typeString = `namespace`
|
||||
this.teamService = new TeamNamespaceService()
|
||||
this.teamStuffModel = new TeamNamespaceModel({namespaceID: this.id})
|
||||
} else {
|
||||
throw new Error('Unknown type: ' + this.type)
|
||||
}
|
||||
|
@ -121,75 +130,61 @@
|
|||
},
|
||||
methods: {
|
||||
loadTeams() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.get(this.typeString + `s/` + this.id + `/teams`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(response => {
|
||||
this.$set(this, 'listTeams', response.data)
|
||||
cancel()
|
||||
this.teamService.getAll(this.teamStuffModel)
|
||||
.then(r => {
|
||||
this.$set(this, 'listTeams', r)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteTeam() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.delete(this.typeString + `s/` + this.id + `/teams/` + this.teamToDelete, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.delete(this.teamStuffModel)
|
||||
.then(() => {
|
||||
this.showTeamDeleteModal = false;
|
||||
this.handleSuccess({message: 'The team was successfully deleted from the ' + this.typeString + '.'})
|
||||
message.success({message: 'The team was successfully deleted from the ' + this.typeString + '.'}, this)
|
||||
// FIXME: this should remove the team from the list instead of loading it again
|
||||
this.loadTeams()
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
addTeam(admin) {
|
||||
const cancel = message.setLoading(this)
|
||||
if(admin === null) {
|
||||
admin = false
|
||||
}
|
||||
this.newTeam.right = 0
|
||||
this.teamStuffModel.right = 0
|
||||
if (admin) {
|
||||
this.newTeam.right = 2
|
||||
this.teamStuffModel.right = 2
|
||||
}
|
||||
|
||||
HTTP.put(this.typeString + `s/` + this.id + `/teams`, this.newTeam, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.create(this.teamStuffModel)
|
||||
.then(() => {
|
||||
// FIXME: this should add the team to the list instead of loading it again
|
||||
this.loadTeams()
|
||||
this.handleSuccess({message: 'The team was successfully added.'})
|
||||
cancel()
|
||||
message.success({message: 'The team was successfully added.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
toggleTeamType(teamid, current) {
|
||||
const cancel = message.setLoading(this)
|
||||
let right = 0
|
||||
this.teamStuffModel.teamID = teamid
|
||||
this.teamStuffModel.right = 0
|
||||
if (!current) {
|
||||
right = 2
|
||||
this.teamStuffModel.right = 2
|
||||
}
|
||||
|
||||
HTTP.post(this.typeString + `s/` + this.id + `/teams/` + teamid, {right: right}, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.update(this.teamStuffModel)
|
||||
.then(() => {
|
||||
// FIXME: this should update the team in the list instead of loading it again
|
||||
this.loadTeams()
|
||||
this.handleSuccess({message: 'The team right was successfully updated.'})
|
||||
cancel()
|
||||
message.success({message: 'The team right was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
})
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -9,21 +9,20 @@
|
|||
<div class="card-content content users-list">
|
||||
<form @submit.prevent="addUser()" class="add-user-form" v-if="userIsAdmin">
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': loading}">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': userStuffService.loading}">
|
||||
<multiselect
|
||||
v-model="newUser"
|
||||
v-model="user"
|
||||
:options="foundUsers"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:loading="loading"
|
||||
:loading="userService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findUsers"
|
||||
placeholder="Type to search a user"
|
||||
label="username"
|
||||
track-by="user_id">
|
||||
|
||||
track-by="id">
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div class="multiselect__clear" v-if="newUser.id !== 0" @mousedown.prevent.stop="clearAll(props.search)"></div>
|
||||
<div class="multiselect__clear" v-if="user.id !== 0" @mousedown.prevent.stop="clearAll(props.search)"></div>
|
||||
</template>
|
||||
<span slot="noResult">Oops! No users found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
|
@ -77,7 +76,7 @@
|
|||
Admin
|
||||
</template>
|
||||
</button>
|
||||
<button @click="userToDelete = u.id; showUserDeleteModal = true" class="button is-danger" v-if="u.id !== currentUser.id">
|
||||
<button @click="user = u; showUserDeleteModal = true" class="button is-danger" v-if="u.id !== currentUser.id">
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -100,12 +99,18 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {HTTP} from '../../http-common'
|
||||
import auth from '../../auth'
|
||||
import message from '../../message'
|
||||
import multiselect from 'vue-multiselect'
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css'
|
||||
|
||||
import UserService from '../../services/user'
|
||||
import UserNamespaceModel from '../../models/userNamespace'
|
||||
import UserListModel from '../../models/userList'
|
||||
import UserListService from '../../services/userList'
|
||||
import UserNamespaceService from '../../services/userNamespace'
|
||||
import UserModel from '../../models/user'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
props: {
|
||||
|
@ -115,14 +120,15 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
userService: UserService, // To search for users
|
||||
user: UserModel,
|
||||
userStuff: Object, // This will be either UserNamespaceModel or UserListModel
|
||||
userStuffService: Object, // This will be either UserListService or UserNamespaceService
|
||||
|
||||
currentUser: auth.user.infos,
|
||||
typeString: '',
|
||||
showUserDeleteModal: false,
|
||||
users: [],
|
||||
newUser: {username: '', user_id: 0},
|
||||
userToDelete: 0,
|
||||
newUserid: 0,
|
||||
foundUsers: [],
|
||||
}
|
||||
},
|
||||
|
@ -130,10 +136,17 @@
|
|||
multiselect
|
||||
},
|
||||
created() {
|
||||
this.userService = new UserService()
|
||||
this.user = new UserModel()
|
||||
|
||||
if (this.type === 'list') {
|
||||
this.typeString = `list`
|
||||
this.userStuffService = new UserListService()
|
||||
this.userStuff = new UserListModel({listID: this.id})
|
||||
} else if (this.type === 'namespace') {
|
||||
this.typeString = `namespace`
|
||||
this.userStuffService = new UserNamespaceService()
|
||||
this.userStuff = new UserNamespaceModel({namespaceID: this.id})
|
||||
} else {
|
||||
throw new Error('Unknown type: ' + this.type)
|
||||
}
|
||||
|
@ -142,100 +155,76 @@
|
|||
},
|
||||
methods: {
|
||||
loadUsers() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.get(this.typeString + `s/` + this.id + `/users`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.userStuffService.getAll(this.userStuff)
|
||||
.then(response => {
|
||||
//response.data.push(this.list.owner)
|
||||
this.$set(this, 'users', response.data)
|
||||
cancel()
|
||||
this.$set(this, 'users', response)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteUser() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.delete(this.typeString + `s/` + this.id + `/users/` + this.userToDelete, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
// The api wants the user id as userID
|
||||
let usr = this.user
|
||||
this.userStuff.userID = usr.id
|
||||
|
||||
this.userStuffService.delete(this.userStuff)
|
||||
.then(() => {
|
||||
this.showUserDeleteModal = false;
|
||||
this.handleSuccess({message: 'The user was successfully deleted from the ' + this.typeString + '.'})
|
||||
message.success({message: 'The user was successfully deleted from the ' + this.typeString + '.'}, this)
|
||||
this.loadUsers()
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
addUser(admin) {
|
||||
const cancel = message.setLoading(this)
|
||||
if(admin === null) {
|
||||
admin = false
|
||||
}
|
||||
this.newUser.right = 0
|
||||
addUser(admin = false) {
|
||||
this.userStuff.right = 0
|
||||
if (admin) {
|
||||
this.newUser.right = 2
|
||||
this.userStuff.right = 2
|
||||
}
|
||||
|
||||
// The api wants the user id as userID
|
||||
this.userStuff.userID = this.user.id
|
||||
this.$set(this, 'foundUsers', [])
|
||||
|
||||
HTTP.put(this.typeString + `s/` + this.id + `/users`, this.newUser, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.userStuffService.create(this.userStuff)
|
||||
.then(() => {
|
||||
this.loadUsers()
|
||||
this.newUser = {}
|
||||
this.handleSuccess({message: 'The user was successfully added.'})
|
||||
cancel()
|
||||
message.success({message: 'The user was successfully added.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
toggleUserType(userid, current) {
|
||||
const cancel = message.setLoading(this)
|
||||
let right = 0
|
||||
this.userStuff.userID = userid
|
||||
this.userStuff.right = 0
|
||||
if (!current) {
|
||||
right = 2
|
||||
this.userStuff.right = 2
|
||||
}
|
||||
|
||||
HTTP.post(this.typeString + `s/` + this.id + `/users/` + userid, {right: right}, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.userStuffService.update(this.userStuff)
|
||||
.then(() => {
|
||||
this.loadUsers()
|
||||
this.handleSuccess({message: 'The user right was successfully updated.'})
|
||||
cancel()
|
||||
message.success({message: 'The user right was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
findUsers(query) {
|
||||
const cancel = message.setLoading(this)
|
||||
if(query === '') {
|
||||
this.$set(this, 'foundUsers', [])
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
this.$set(this, 'newUser', {username: '', user_id: 0})
|
||||
|
||||
HTTP.get(`users?s=` + query, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.userService.getAll({}, {s: query})
|
||||
.then(response => {
|
||||
this.$set(this, 'foundUsers', [])
|
||||
|
||||
for (const u in response.data) {
|
||||
this.foundUsers.push({
|
||||
username: response.data[u].username,
|
||||
user_id: response.data[u].id,
|
||||
})
|
||||
}
|
||||
|
||||
cancel()
|
||||
this.$set(this, 'foundUsers', response)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
clearAll () {
|
||||
|
@ -244,12 +233,6 @@
|
|||
limitText (count) {
|
||||
return `and ${count} others`
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="loader-container" v-bind:class="{ 'is-loading': loading}">
|
||||
<div class="loader-container" v-bind:class="{ 'is-loading': teamService.loading}">
|
||||
<div class="card" v-if="userIsAdmin">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
|
@ -12,25 +12,25 @@
|
|||
<div class="field">
|
||||
<label class="label" for="teamtext">Team Name</label>
|
||||
<div class="control">
|
||||
<input v-focus :class="{ 'disabled': loading}" :disabled="loading" class="input" type="text" id="teamtext" placeholder="The team text is here..." v-model="team.name">
|
||||
<input v-focus :class="{ 'disabled': teamMemberService.loading}" :disabled="teamMemberService.loading" class="input" type="text" id="teamtext" placeholder="The team text is here..." v-model="team.name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="teamdescription">Description</label>
|
||||
<div class="control">
|
||||
<textarea :class="{ 'disabled': loading}" :disabled="loading" class="textarea" placeholder="The teams description goes here..." id="teamdescription" v-model="team.description"></textarea>
|
||||
<textarea :class="{ 'disabled': teamService.loading}" :disabled="teamService.loading" class="textarea" placeholder="The teams description goes here..." id="teamdescription" v-model="team.description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="columns bigbuttons">
|
||||
<div class="column">
|
||||
<button @click="submit()" class="button is-success is-fullwidth" :class="{ 'is-loading': loading}">
|
||||
<button @click="submit()" class="button is-success is-fullwidth" :class="{ 'is-loading': teamService.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': teamService.loading}">
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -50,8 +50,8 @@
|
|||
<div class="card-content content team-members">
|
||||
<form @submit.prevent="addUser()" class="add-member-form" v-if="userIsAdmin">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': loading}">
|
||||
<input class="input" v-bind:class="{ 'disabled': loading}" v-model.number="newUser.id" type="text" placeholder="Add a new user...">
|
||||
<p class="control has-icons-left is-expanded" v-bind:class="{ 'is-loading': teamMemberService.loading}">
|
||||
<input class="input" v-bind:class="{ 'disabled': teamMemberService.loading}" v-model.number="member.id" type="text" placeholder="Add a new user...">
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="user"/>
|
||||
</span>
|
||||
|
@ -90,7 +90,7 @@
|
|||
</template>
|
||||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
<button @click="toggleUserType(m.id, m.admin)" class="button buttonright is-primary" v-if="m.id !== user.infos.id">
|
||||
<button @click="toggleUserType(m)" class="button buttonright is-primary" v-if="m.id !== user.infos.id">
|
||||
Make
|
||||
<template v-if="!m.admin">
|
||||
Admin
|
||||
|
@ -99,7 +99,7 @@
|
|||
Member
|
||||
</template>
|
||||
</button>
|
||||
<button @click="userToDelete = m.id; showUserDeleteModal = true" class="button is-danger" v-if="m.id !== user.infos.id">
|
||||
<button @click="member = m; showUserDeleteModal = true" class="button is-danger" v-if="m.id !== user.infos.id">
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -137,22 +137,26 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
|
||||
import TeamService from '../../services/team'
|
||||
import TeamModel from '../../models/team'
|
||||
import TeamMemberService from '../../services/teamMember'
|
||||
import TeamMemberModel from '../../models/teamMember'
|
||||
|
||||
export default {
|
||||
name: "EditTeam",
|
||||
data() {
|
||||
return {
|
||||
team: {title: '', description:''},
|
||||
error: '',
|
||||
loading: false,
|
||||
teamService: TeamService,
|
||||
teamMemberService: TeamMemberService,
|
||||
team: TeamModel,
|
||||
member: TeamMemberModel,
|
||||
|
||||
showDeleteModal: false,
|
||||
showUserDeleteModal: false,
|
||||
user: auth.user,
|
||||
userIsAdmin: false,
|
||||
userToDelete: 0,
|
||||
newUser: {id:0},
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -162,6 +166,8 @@
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.teamService = new TeamService()
|
||||
this.teamMemberService = new TeamMemberService()
|
||||
this.loadTeam()
|
||||
},
|
||||
watch: {
|
||||
|
@ -170,97 +176,71 @@
|
|||
},
|
||||
methods: {
|
||||
loadTeam() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.get(`teams/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.member = new TeamMemberModel({teamID: this.$route.params.id})
|
||||
this.team = new TeamModel({id: this.$route.params.id})
|
||||
this.teamService.get(this.team)
|
||||
.then(response => {
|
||||
this.$set(this, 'team', response.data)
|
||||
let members = response.data.members
|
||||
this.$set(this, 'team', response)
|
||||
let members = response.members
|
||||
for (const m in members) {
|
||||
members[m].teamID = this.$route.params.id
|
||||
if (members[m].id === this.user.infos.id && members[m].admin) {
|
||||
this.userIsAdmin = true
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
submit() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.post(`teams/` + this.$route.params.id, this.team, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.update(this.team)
|
||||
.then(response => {
|
||||
// Update the team in the parent
|
||||
for (const n in this.$parent.teams) {
|
||||
if (this.$parent.teams[n].id === response.data.id) {
|
||||
response.data.lists = this.$parent.teams[n].lists
|
||||
this.$set(this.$parent.teams, n, response.data)
|
||||
}
|
||||
}
|
||||
this.handleSuccess({message: 'The team was successfully updated.'})
|
||||
cancel()
|
||||
this.team = response
|
||||
message.success({message: 'The team was successfully updated.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteTeam() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.delete(`teams/` + this.$route.params.id, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.delete(this.team)
|
||||
.then(() => {
|
||||
this.handleSuccess({message: 'The team was successfully deleted.'})
|
||||
cancel()
|
||||
router.push({name: 'home'})
|
||||
message.success({message: 'The team was successfully deleted.'}, this)
|
||||
router.push({name: 'listTeams'})
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
deleteUser() {
|
||||
const cancel = message.setLoading(this)
|
||||
HTTP.delete(`teams/` + this.$route.params.id + `/members/` + this.userToDelete, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamMemberService.delete(this.member)
|
||||
.then(() => {
|
||||
this.showUserDeleteModal = false;
|
||||
this.handleSuccess({message: 'The user was successfully deleted from the team.'})
|
||||
message.success({message: 'The user was successfully deleted from the team.'}, this)
|
||||
this.loadTeam()
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
})
|
||||
},
|
||||
addUser(admin) {
|
||||
const cancel = message.setLoading(this)
|
||||
if(admin === null) {
|
||||
admin = false
|
||||
}
|
||||
HTTP.put(`teams/` + this.$route.params.id + `/members`, {admin: admin, user_id: this.newUser.id}, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
.then(() => {
|
||||
this.loadTeam()
|
||||
this.handleSuccess({message: 'The team member was successfully added.'})
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
})
|
||||
},
|
||||
toggleUserType(userid, current) {
|
||||
this.userToDelete = userid
|
||||
this.newUser.id = userid
|
||||
this.deleteUser()
|
||||
this.addUser(!current)
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
})
|
||||
.finally(() => {
|
||||
this.showUserDeleteModal = false
|
||||
})
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
addUser() {
|
||||
this.teamMemberService.create(this.member)
|
||||
.then(() => {
|
||||
this.loadTeam()
|
||||
message.success({message: 'The team member was successfully added.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
toggleUserType(member) {
|
||||
this.member = member
|
||||
this.member.admin = !member.admin
|
||||
this.deleteUser()
|
||||
this.addUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="content loader-container" v-bind:class="{ 'is-loading': loading}">
|
||||
<div class="content loader-container" v-bind:class="{ 'is-loading': teamService.loading}">
|
||||
<router-link :to="{name:'newTeam'}" class="button is-success button-right" >
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
|
@ -20,16 +20,15 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import TeamService from '../../services/team'
|
||||
|
||||
export default {
|
||||
name: "ListTeams",
|
||||
data() {
|
||||
return {
|
||||
teamService: TeamService,
|
||||
teams: [],
|
||||
error: '',
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -39,23 +38,18 @@
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.teamService = new TeamService()
|
||||
this.loadTeams()
|
||||
},
|
||||
methods: {
|
||||
loadTeams() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.get(`teams`, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.getAll()
|
||||
.then(response => {
|
||||
this.$set(this, 'teams', response.data)
|
||||
cancel()
|
||||
this.$set(this, 'teams', response)
|
||||
})
|
||||
.catch(e => {
|
||||
this.handleError(e)
|
||||
})
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
<h3>Create a new team</h3>
|
||||
<form @submit.prevent="newTeam" @keyup.esc="back()">
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': loading}">
|
||||
<input v-focus class="input" v-bind:class="{ 'disabled': loading}" v-model="team.name" type="text" placeholder="The team's name goes here...">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': teamService.loading}">
|
||||
<input v-focus class="input" v-bind:class="{ 'disabled': teamService.loading}" v-model="team.name" type="text" placeholder="The team's name goes here...">
|
||||
</p>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-success noshadow">
|
||||
|
@ -26,16 +26,16 @@
|
|||
<script>
|
||||
import auth from '../../auth'
|
||||
import router from '../../router'
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import TeamModel from '../../models/team'
|
||||
import TeamService from '../../services/team'
|
||||
|
||||
export default {
|
||||
name: "NewTeam",
|
||||
data() {
|
||||
return {
|
||||
team: {title: ''},
|
||||
error: '',
|
||||
loading: false
|
||||
teamService: TeamService,
|
||||
team: TeamModel,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
|
@ -45,32 +45,23 @@
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.teamService = new TeamService()
|
||||
this.$parent.setFullPage();
|
||||
},
|
||||
methods: {
|
||||
newTeam() {
|
||||
const cancel = message.setLoading(this)
|
||||
|
||||
HTTP.put(`teams`, this.team, {headers: {'Authorization': 'Bearer ' + localStorage.getItem('token')}})
|
||||
this.teamService.create(this.team)
|
||||
.then(response => {
|
||||
router.push({name:'editTeam', params:{id: response.data.id}})
|
||||
this.handleSuccess({message: 'The team was successfully created.'})
|
||||
cancel()
|
||||
router.push({name:'editTeam', params:{id: response.id}})
|
||||
message.success({message: 'The team was successfully created.'}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
message.error(e, this)
|
||||
})
|
||||
},
|
||||
back() {
|
||||
router.go(-1)
|
||||
},
|
||||
handleError(e) {
|
||||
message.error(e, this)
|
||||
},
|
||||
handleSuccess(e) {
|
||||
message.success(e, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
},
|
||||
beforeMount() {
|
||||
// Try to verify the email
|
||||
// FIXME: Why is this here? Can we find a better place for this?
|
||||
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||
if (emailVerifyToken) {
|
||||
const cancel = message.setLoading(this)
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary" v-bind:class="{ 'is-loading': loading}">Reset your password</button>
|
||||
<button type="submit" class="button is-primary" :class="{ 'is-loading': this.passwordResetService.loading}">Reset your password</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification is-info" v-if="loading">
|
||||
<div class="notification is-info" v-if="this.passwordResetService.loading">
|
||||
Loading...
|
||||
</div>
|
||||
<div class="notification is-danger" v-if="error">
|
||||
|
@ -37,53 +37,42 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import PasswordResetModel from '../../models/passwordReset'
|
||||
import PasswordResetService from '../../services/passwordReset'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
passwordResetService: PasswordResetService,
|
||||
credentials: {
|
||||
password: '',
|
||||
password2: '',
|
||||
},
|
||||
error: '',
|
||||
successMessage: '',
|
||||
loading: false
|
||||
successMessage: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.passwordResetService = new PasswordResetService()
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const cancel = message.setLoading(this)
|
||||
this.error = ''
|
||||
|
||||
if (this.credentials.password2 !== this.credentials.password) {
|
||||
cancel()
|
||||
this.error = 'Passwords don\'t match'
|
||||
return
|
||||
}
|
||||
|
||||
let resetPasswordPayload = {
|
||||
token: localStorage.getItem('passwordResetToken'),
|
||||
new_password: this.credentials.password
|
||||
}
|
||||
|
||||
HTTP.post(`user/password/reset`, resetPasswordPayload)
|
||||
let passwordReset = new PasswordResetModel({new_password: this.credentials.password})
|
||||
this.passwordResetService.resetPassword(passwordReset)
|
||||
.then(response => {
|
||||
this.handleSuccess(response)
|
||||
this.successMessage = response.data.message
|
||||
localStorage.removeItem('passwordResetToken')
|
||||
cancel()
|
||||
})
|
||||
.catch(e => {
|
||||
this.error = e.response.data.message
|
||||
cancel()
|
||||
})
|
||||
},
|
||||
handleError(e) {
|
||||
this.error = e.response.data.message
|
||||
},
|
||||
handleSuccess(e) {
|
||||
this.successMessage = e.data.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<form id="loginform" @submit.prevent="submit" v-if="!isSuccess">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input v-focus type="text" class="input" name="email" placeholder="Email-Adress" v-model="email" required>
|
||||
<input v-focus type="text" class="input" name="email" placeholder="Email-Adress" v-model="passwordReset.email" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary" v-bind:class="{ 'is-loading': loading}">Send me a password reset link</button>
|
||||
<button type="submit" class="button is-primary" v-bind:class="{ 'is-loading': passwordResetService.loading}">Send me a password reset link</button>
|
||||
<router-link :to="{ name: 'login' }" class="button">Login</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,38 +30,32 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {HTTP} from '../../http-common'
|
||||
import message from '../../message'
|
||||
import PasswordResetModel from '../../models/passwordReset'
|
||||
import PasswordResetService from '../../services/passwordReset'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
passwordResetService: PasswordResetService,
|
||||
passwordReset: PasswordResetModel,
|
||||
error: '',
|
||||
isSuccess: false,
|
||||
loading: false
|
||||
isSuccess: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.passwordResetService = new PasswordResetService()
|
||||
this.passwordReset = new PasswordResetModel()
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const cancel = message.setLoading(this)
|
||||
this.error = ''
|
||||
let credentials = {
|
||||
email: this.email,
|
||||
}
|
||||
|
||||
HTTP.post(`user/password/token`, credentials)
|
||||
this.passwordResetService.requestResetPassword(this.passwordReset)
|
||||
.then(() => {
|
||||
cancel()
|
||||
this.isSuccess = true
|
||||
})
|
||||
.catch(e => {
|
||||
cancel()
|
||||
this.handleError(e)
|
||||
})
|
||||
},
|
||||
handleError(e) {
|
||||
this.error = e.response.data.message
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
21
src/models/abstractModel.js
Normal file
21
src/models/abstractModel.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {defaults, omitBy, isNil} from 'lodash'
|
||||
|
||||
export default class AbstractModel {
|
||||
|
||||
/**
|
||||
* The abstract constructor takes an object and merges its data with the default data of this model.
|
||||
* @param data
|
||||
*/
|
||||
constructor(data) {
|
||||
// Put all data in our model while overriding those with a value of null or undefined with their defaults
|
||||
defaults(this, omitBy(data, isNil), this.defaults())
|
||||
}
|
||||
|
||||
/**
|
||||
* Default attributes that define the "empty" state.
|
||||
* @return {{}}
|
||||
*/
|
||||
defaults() {
|
||||
return {}
|
||||
}
|
||||
}
|
1
src/models/config.js
Normal file
1
src/models/config.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const URL_PREFIX = '/api/v1' // _without_ slash at the end
|
111
src/models/list.js
Normal file
111
src/models/list.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import TaskModel from './task'
|
||||
import UserModel from './user'
|
||||
|
||||
export default class ListModel extends AbstractModel {
|
||||
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
// Make all tasks to task models
|
||||
this.tasks = this.tasks.map(t => {
|
||||
return new TaskModel(t)
|
||||
})
|
||||
|
||||
this.owner = new UserModel(this.owner)
|
||||
this.sortTasks()
|
||||
}
|
||||
|
||||
// Default attributes that define the "empty" state.
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
title: '',
|
||||
description: '',
|
||||
owner: UserModel,
|
||||
tasks: [],
|
||||
namespaceID: 0,
|
||||
|
||||
created: 0,
|
||||
updated: 0,
|
||||
}
|
||||
}
|
||||
|
||||
////////
|
||||
// Helpers
|
||||
//////
|
||||
|
||||
/**
|
||||
* Sorts all tasks according to their due date
|
||||
* @returns {this}
|
||||
*/
|
||||
sortTasks() {
|
||||
if (this.tasks === null || this.tasks === []) {
|
||||
return
|
||||
}
|
||||
return this.tasks.sort(function(a,b) {
|
||||
if (a.done < b.done)
|
||||
return -1
|
||||
if (a.done > b.done)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a task to the task array of this list. Usually only used when creating a new task
|
||||
* @param task
|
||||
*/
|
||||
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.tasks.push(task)
|
||||
} else {
|
||||
for (const t in this.tasks) {
|
||||
if (this.tasks[t].id === task.parentTaskID) {
|
||||
this.tasks[t].subtasks.push(task)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sortTasks()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a task by its ID by looping through all tasks.
|
||||
* @param id
|
||||
* @returns {TaskModel}
|
||||
*/
|
||||
getTaskByID(id) {
|
||||
// TODO: Binary search?
|
||||
for (const t in this.tasks) {
|
||||
if (this.tasks[t].id === parseInt(id)) {
|
||||
return this.tasks[t]
|
||||
}
|
||||
}
|
||||
return {} // FIXME: This should probably throw something to make it clear to the user noting was found
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through all tasks and updates the one with the id it has
|
||||
* @param task
|
||||
*/
|
||||
updateTaskByID(task) {
|
||||
for (const t in this.tasks) {
|
||||
if (this.tasks[t].id === task.id) {
|
||||
this.tasks[t] = task
|
||||
break
|
||||
}
|
||||
|
||||
if (this.tasks[t].id === task.parentTaskID) {
|
||||
for (const s in this.tasks[t].subtasks) {
|
||||
if (this.tasks[t].subtasks[s].id === task.id) {
|
||||
this.tasks[t].subtasks[s] = task
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sortTasks()
|
||||
}
|
||||
}
|
28
src/models/namespace.js
Normal file
28
src/models/namespace.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import ListModel from './list'
|
||||
import UserModel from './user'
|
||||
|
||||
export default class NamespaceModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
this.lists = this.lists.map(l => {
|
||||
return new ListModel(l)
|
||||
})
|
||||
this.owner = new UserModel(this.owner)
|
||||
}
|
||||
|
||||
// Default attributes that define the 'empty' state.
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
description: '',
|
||||
owner: UserModel,
|
||||
lists: [],
|
||||
|
||||
created: 0,
|
||||
updated: 0,
|
||||
}
|
||||
}
|
||||
}
|
17
src/models/passwordReset.js
Normal file
17
src/models/passwordReset.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import AbstractModel from "./abstractModel";
|
||||
|
||||
export default class PasswordResetModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
this.token = localStorage.getItem('passwordResetToken')
|
||||
}
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
token: '',
|
||||
new_password: '',
|
||||
email: '',
|
||||
}
|
||||
}
|
||||
}
|
99
src/models/task.js
Normal file
99
src/models/task.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import AbstractModel from './abstractModel';
|
||||
import UserModel from './user'
|
||||
|
||||
export default class TaskModel extends AbstractModel {
|
||||
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
// Make date objects from timestamps
|
||||
this.dueDate = this.parseDateIfNessecary(this.dueDate)
|
||||
this.startDate = this.parseDateIfNessecary(this.startDate)
|
||||
this.endDate = this.parseDateIfNessecary(this.endDate)
|
||||
|
||||
this.reminderDates = this.reminderDates.map(d => {
|
||||
return this.parseDateIfNessecary(d)
|
||||
})
|
||||
this.reminderDates.push(null) // To trigger the datepicker
|
||||
|
||||
// Parse the repeat after into something usable
|
||||
this.parseRepeatAfter()
|
||||
|
||||
// Parse the assignees into user models
|
||||
this.assignees = this.assignees.map(a => {
|
||||
return new UserModel(a)
|
||||
})
|
||||
this.createdBy = new UserModel(this.createdBy)
|
||||
}
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
text: '',
|
||||
description: '',
|
||||
done: false,
|
||||
priority: 0,
|
||||
labels: [],
|
||||
assignees: [],
|
||||
|
||||
dueDate: 0,
|
||||
startDate: 0,
|
||||
endDate: 0,
|
||||
repeatAfter: 0,
|
||||
reminderDates: [],
|
||||
subtasks: [],
|
||||
parentTaskID: 0,
|
||||
|
||||
createdBy: UserModel,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
|
||||
listID: 0, // Meta, only used when creating a new task
|
||||
sortBy: 'duedate', // Meta, only used when listing all tasks
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
///////////////
|
||||
|
||||
/**
|
||||
* Makes a js date object from a unix timestamp (in seconds).
|
||||
* @param unixTimestamp
|
||||
* @returns {*}
|
||||
*/
|
||||
parseDateIfNessecary(unixTimestamp) {
|
||||
let dateobj = new Date(unixTimestamp * 1000)
|
||||
if (unixTimestamp === 0) {
|
||||
return null
|
||||
}
|
||||
return dateobj
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the "repeat after x seconds" from the task into a usable js object inside the task.
|
||||
* This function should only be called from the constructor.
|
||||
*/
|
||||
parseRepeatAfter() {
|
||||
let repeatAfterHours = (this.repeatAfter / 60) / 60
|
||||
this.repeatAfter = {type: 'hours', amount: repeatAfterHours}
|
||||
|
||||
// if its dividable by 24, its something with days, otherwise hours
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
src/models/team.js
Normal file
29
src/models/team.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import UserModel from './user'
|
||||
import TeamMemberModel from './teamMember'
|
||||
|
||||
export default class TeamModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
// Make the members to usermodels
|
||||
this.members = this.members.map(m => {
|
||||
return new TeamMemberModel(m)
|
||||
})
|
||||
this.createdBy = new UserModel(this.createdBy)
|
||||
}
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
description: '',
|
||||
members: [],
|
||||
right: 0,
|
||||
|
||||
createdBy: {},
|
||||
created: 0,
|
||||
updated: 0
|
||||
}
|
||||
}
|
||||
}
|
13
src/models/teamList.js
Normal file
13
src/models/teamList.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import TeamShareBaseModel from './teamShareBase'
|
||||
import {merge} from "lodash";
|
||||
|
||||
export default class TeamListModel extends TeamShareBaseModel {
|
||||
defaults() {
|
||||
return merge(
|
||||
super.defaults(),
|
||||
{
|
||||
listID: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
14
src/models/teamMember.js
Normal file
14
src/models/teamMember.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import UserModel from './user'
|
||||
import {merge} from 'lodash'
|
||||
|
||||
export default class TeamMemberModel extends UserModel {
|
||||
defaults() {
|
||||
return merge(
|
||||
super.defaults(),
|
||||
{
|
||||
admin: false,
|
||||
teamID: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
13
src/models/teamNamespace.js
Normal file
13
src/models/teamNamespace.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import TeamShareBaseModel from './teamShareBase'
|
||||
import {merge} from 'lodash'
|
||||
|
||||
export default class TeamNamespaceModel extends TeamShareBaseModel {
|
||||
defaults() {
|
||||
return merge(
|
||||
super.defaults(),
|
||||
{
|
||||
namespaceID: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
17
src/models/teamShareBase.js
Normal file
17
src/models/teamShareBase.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
|
||||
/**
|
||||
* This class is a base class for common team sharing model.
|
||||
* It is extended in a way so it can be used for namespaces as well for lists.
|
||||
*/
|
||||
export default class TeamShareBaseModel extends AbstractModel {
|
||||
defaults() {
|
||||
return {
|
||||
teamID: 0,
|
||||
right: 0,
|
||||
|
||||
created: 0,
|
||||
updated: 0
|
||||
}
|
||||
}
|
||||
}
|
13
src/models/user.js
Normal file
13
src/models/user.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
|
||||
export default class UserModel extends AbstractModel {
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
email: '',
|
||||
username: '',
|
||||
created: 0,
|
||||
updated: 0
|
||||
}
|
||||
}
|
||||
}
|
14
src/models/userList.js
Normal file
14
src/models/userList.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import UserShareBaseModel from './userShareBase'
|
||||
import {merge} from 'lodash'
|
||||
|
||||
// This class extends the user share model with a 'rights' parameter which is used in sharing
|
||||
export default class UserListModel extends UserShareBaseModel {
|
||||
defaults() {
|
||||
return merge(
|
||||
super.defaults(),
|
||||
{
|
||||
listID: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
14
src/models/userNamespace.js
Normal file
14
src/models/userNamespace.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import UserShareBaseModel from "./userShareBase";
|
||||
import {merge} from 'lodash'
|
||||
|
||||
// This class extends the user share model with a 'rights' parameter which is used in sharing
|
||||
export default class UserNamespaceModel extends UserShareBaseModel {
|
||||
defaults() {
|
||||
return merge(
|
||||
super.defaults(),
|
||||
{
|
||||
namespaceID: 0,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
13
src/models/userShareBase.js
Normal file
13
src/models/userShareBase.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
|
||||
export default class UserShareBaseModel extends AbstractModel {
|
||||
defaults() {
|
||||
return {
|
||||
userID: 0,
|
||||
right: 0,
|
||||
|
||||
created: 0,
|
||||
updated: 0,
|
||||
}
|
||||
}
|
||||
}
|
354
src/services/abstractService.js
Normal file
354
src/services/abstractService.js
Normal file
|
@ -0,0 +1,354 @@
|
|||
import axios from 'axios'
|
||||
import {reduce, replace} from 'lodash'
|
||||
|
||||
let config = require('../../public/config.json')
|
||||
|
||||
export default class AbstractService {
|
||||
|
||||
/////////////////////////////
|
||||
// Initial variable definitions
|
||||
///////////////////////////
|
||||
|
||||
http = null
|
||||
loading = false
|
||||
paths = {
|
||||
create: '',
|
||||
get: '',
|
||||
getAll: '',
|
||||
update: '',
|
||||
delete: '',
|
||||
}
|
||||
|
||||
/////////////
|
||||
// Service init
|
||||
///////////
|
||||
|
||||
/**
|
||||
* The abstract constructor.
|
||||
* @param paths An object with all paths. Default values are specified above.
|
||||
*/
|
||||
constructor(paths) {
|
||||
this.http = axios.create({
|
||||
baseURL: config.VIKUNJA_API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Set the default auth header if we have a token
|
||||
if (
|
||||
localStorage.getItem('token') !== '' &&
|
||||
localStorage.getItem('token') !== null &&
|
||||
localStorage.getItem('token') !== undefined
|
||||
) {
|
||||
this.http.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
|
||||
this.paths = {
|
||||
create: paths.create !== undefined ? paths.create : '',
|
||||
get: paths.get !== undefined ? paths.get : '',
|
||||
getAll: paths.getAll !== undefined ? paths.getAll : '',
|
||||
update: paths.update !== undefined ? paths.update : '',
|
||||
delete: paths.delete !== undefined ? paths.delete : '',
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// Global error handler
|
||||
///////////////////
|
||||
|
||||
/**
|
||||
* Handles the error and rejects the promise.
|
||||
* @param error
|
||||
* @returns {Promise<never>}
|
||||
*/
|
||||
errorHandler(error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
///////////////
|
||||
|
||||
/**
|
||||
* Returns an object with all route parameters and their values.
|
||||
* @param route
|
||||
* @returns object
|
||||
*/
|
||||
getRouteReplacements(route) {
|
||||
let parameters = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}
|
||||
let replace$$1 = {}
|
||||
let pattern = this.getRouteParameterPattern()
|
||||
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
|
||||
|
||||
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
||||
replace$$1[parameter[0]] = parameters[parameter[1]];
|
||||
}
|
||||
|
||||
return replace$$1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the replacement pattern for url paths, can be overwritten by implementations.
|
||||
* @return {RegExp}
|
||||
*/
|
||||
getRouteParameterPattern() {
|
||||
return /{([^}]+)}/
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
|
||||
* @param path
|
||||
* @param pathparams
|
||||
* @return string
|
||||
*/
|
||||
getReplacedRoute(path, pathparams) {
|
||||
let replacements = this.getRouteReplacements(path, pathparams)
|
||||
return reduce(replacements, function (result, value, parameter) {
|
||||
return replace(result, parameter, value)
|
||||
}, path)
|
||||
}
|
||||
|
||||
/**
|
||||
* setLoading is a method which sets the loading variable to true, after a timeout of 100ms.
|
||||
* It has the timeout to prevent the loading indicator from showing for only a blink of an eye in the
|
||||
* case the api returns a response in < 100ms.
|
||||
* But because the timeout is created using setTimeout, it will still trigger even if the request is
|
||||
* already finished, so we return a method to call in that case.
|
||||
* @returns {Function}
|
||||
*/
|
||||
setLoading() {
|
||||
const timeout = setTimeout(() => {
|
||||
this.loading = true
|
||||
}, 100)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////
|
||||
// Default factories
|
||||
// It is possible to specify a factory for each type of request.
|
||||
// This makes it possible to have different models returned from different routes.
|
||||
// Specific factories for each request are completly optional, if these are not specified, the defautl factory is used.
|
||||
////////////////
|
||||
|
||||
/**
|
||||
* The modelFactory returns an model from an object.
|
||||
* This one here is the default one, usually the service definitions for a model will override this.
|
||||
* @param data
|
||||
* @returns {*}
|
||||
*/
|
||||
modelFactory(data) {
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get requests.
|
||||
* @param data
|
||||
* @return {*}
|
||||
*/
|
||||
modelGetFactory(data) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get all requests.
|
||||
* @param data
|
||||
* @return {*}
|
||||
*/
|
||||
modelGetAllFactory(data) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for create requests.
|
||||
* @param data
|
||||
* @return {*}
|
||||
*/
|
||||
modelCreateFactory(data) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for update requests.
|
||||
* @param data
|
||||
* @return {*}
|
||||
*/
|
||||
modelUpdateFactory(data) {
|
||||
return this.modelFactory(data)
|
||||
}
|
||||
|
||||
//////////////
|
||||
// Preprocessors
|
||||
////////////
|
||||
|
||||
/**
|
||||
* Default preprocessor for get requests
|
||||
* @param model
|
||||
* @return {*}
|
||||
*/
|
||||
beforeGet(model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for create requests
|
||||
* @param model
|
||||
* @return {*}
|
||||
*/
|
||||
beforeCreate(model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for update requests
|
||||
* @param model
|
||||
* @return {*}
|
||||
*/
|
||||
beforeUpdate(model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for delete requests
|
||||
* @param model
|
||||
* @return {*}
|
||||
*/
|
||||
beforeDelete(model) {
|
||||
return model
|
||||
}
|
||||
|
||||
///////////////
|
||||
// Global actions
|
||||
/////////////
|
||||
|
||||
/**
|
||||
* Performs a get request to the url specified before.
|
||||
* @param model The model to use. The request path is built using the values from the model.
|
||||
* @param params Optional query parameters
|
||||
* @returns {Q.Promise<any>}
|
||||
*/
|
||||
get(model, params = {}) {
|
||||
if (this.paths.get === '') {
|
||||
return Promise.reject({message: 'This model is not able to get data.'})
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
model = this.beforeGet(model)
|
||||
return this.http.get(this.getReplacedRoute(this.paths.get, model), {params: params})
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
return Promise.resolve(this.modelGetFactory(response.data))
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a get request to the url specified before.
|
||||
* The difference between this and get() is this one is used to get a bunch of data (an array), not just a single object.
|
||||
* @param model The model to use. The request path is built using the values from the model.
|
||||
* @param params Optional query parameters
|
||||
* @returns {Q.Promise<any>}
|
||||
*/
|
||||
getAll(model = {}, params = {}) {
|
||||
if (this.paths.getAll === '') {
|
||||
return Promise.reject({message: 'This model is not able to get data.'})
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
model = this.beforeGet(model)
|
||||
return this.http.get(this.getReplacedRoute(this.paths.getAll, model), {params: params})
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
if (Array.isArray(response.data)) {
|
||||
return Promise.resolve(response.data.map(entry => {
|
||||
return this.modelGetAllFactory(entry)
|
||||
}))
|
||||
}
|
||||
return Promise.resolve(this.modelGetAllFactory(response.data))
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a put request to the url specified before
|
||||
* @param model
|
||||
* @returns {Promise<any | never>}
|
||||
*/
|
||||
create(model) {
|
||||
if (this.paths.create === '') {
|
||||
return Promise.reject({message: 'This model is not able to create data.'})
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
model = this.beforeCreate(model)
|
||||
return this.http.put(this.getReplacedRoute(this.paths.create, model), model)
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
return Promise.resolve(this.modelCreateFactory(response.data))
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a post request to the update url
|
||||
* @param model
|
||||
* @returns {Q.Promise<any>}
|
||||
*/
|
||||
update(model) {
|
||||
if (this.paths.update === '') {
|
||||
return Promise.reject({message: 'This model is not able to update data.'})
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
model = this.beforeUpdate(model)
|
||||
return this.http.post(this.getReplacedRoute(this.paths.update, model), model)
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
return Promise.resolve(this.modelUpdateFactory(response.data))
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a delete request to the update url
|
||||
* @param model
|
||||
* @returns {Q.Promise<any>}
|
||||
*/
|
||||
delete(model) {
|
||||
if (this.paths.delete === '') {
|
||||
return Promise.reject({message: 'This model is not able to delete data.'})
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
model = this.beforeUpdate(model)
|
||||
return this.http.delete(this.getReplacedRoute(this.paths.delete, model), model)
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
return Promise.resolve(response.data)
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
26
src/services/list.js
Normal file
26
src/services/list.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import AbstractService from './abstractService'
|
||||
import ListModel from '../models/list'
|
||||
import TaskService from './task'
|
||||
|
||||
export default class ListService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceID}/lists',
|
||||
get: '/lists/{id}',
|
||||
update: '/lists/{id}',
|
||||
delete: '/lists/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new ListModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
let taskService = new TaskService()
|
||||
model.tasks = model.tasks.map(task => {
|
||||
return taskService.beforeUpdate(task)
|
||||
})
|
||||
return model
|
||||
}
|
||||
}
|
18
src/services/namespace.js
Normal file
18
src/services/namespace.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import AbstractService from './abstractService'
|
||||
import NamespaceModel from '../models/namespace'
|
||||
|
||||
export default class NamespaceService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces',
|
||||
get: '/namespaces/{id}',
|
||||
getAll: '/namespaces',
|
||||
update: '/namespaces/{id}',
|
||||
delete: '/namespaces/{id}',
|
||||
});
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new NamespaceModel(data)
|
||||
}
|
||||
}
|
45
src/services/passwordReset.js
Normal file
45
src/services/passwordReset.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import AbstractService from './abstractService'
|
||||
import PasswordResetModel from '../models/passwordReset'
|
||||
|
||||
export default class PasswordResetService extends AbstractService {
|
||||
|
||||
constructor() {
|
||||
super({})
|
||||
this.paths = {
|
||||
reset: '/user/password/reset',
|
||||
requestReset: '/user/password/token',
|
||||
}
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new PasswordResetModel(data)
|
||||
}
|
||||
|
||||
resetPassword(model) {
|
||||
const cancel = this.setLoading()
|
||||
return this.http.post(this.paths.reset, model)
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
return Promise.resolve(this.modelFactory(response.data))
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
requestResetPassword(model) {
|
||||
const cancel = this.setLoading()
|
||||
return this.http.post(this.paths.requestReset, model)
|
||||
.catch(error => {
|
||||
return this.errorHandler(error)
|
||||
})
|
||||
.then(response => {
|
||||
return Promise.resolve(this.modelFactory(response.data))
|
||||
})
|
||||
.finally(() => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
61
src/services/task.js
Normal file
61
src/services/task.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import AbstractService from './abstractService'
|
||||
import TaskModel from '../models/task'
|
||||
|
||||
export default class TaskService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listID}',
|
||||
getAll: '/tasks/all/{sortBy}/{startDate}/{endDate}',
|
||||
update: '/tasks/{id}',
|
||||
delete: '/tasks/{id}',
|
||||
});
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TaskModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
// Convert the date in a unix timestamp
|
||||
model.dueDate = (+ new Date(model.dueDate)) / 1000
|
||||
model.startDate = (+ new Date(model.startDate)) / 1000
|
||||
model.endDate = (+ new Date(model.endDate)) / 1000
|
||||
|
||||
// remove all nulls, these would create empty reminders
|
||||
for (const index in model.reminderDates) {
|
||||
if (model.reminderDates[index] === null) {
|
||||
model.reminderDates.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Make normal timestamps from js dates
|
||||
model.reminderDates = model.reminderDates.map(r => {
|
||||
return Math.round(r / 1000)
|
||||
})
|
||||
|
||||
// Make the repeating amount to seconds
|
||||
let repeatAfterSeconds = 0
|
||||
if (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0) {
|
||||
switch (model.repeatAfter.type) {
|
||||
case 'hours':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60
|
||||
break
|
||||
case 'days':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24
|
||||
break
|
||||
case 'weeks':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24 * 7
|
||||
break
|
||||
case 'months':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24 * 30
|
||||
break
|
||||
case 'years':
|
||||
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60 * 24 * 365
|
||||
break
|
||||
}
|
||||
}
|
||||
model.repeatAfter = repeatAfterSeconds
|
||||
|
||||
return model
|
||||
}
|
||||
}
|
18
src/services/team.js
Normal file
18
src/services/team.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import AbstractService from './abstractService'
|
||||
import TeamModel from '../models/team'
|
||||
|
||||
export default class TeamService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/teams',
|
||||
get: '/teams/{id}',
|
||||
getAll: '/teams',
|
||||
update: '/teams/{id}',
|
||||
delete: '/teams/{id}',
|
||||
});
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamModel(data)
|
||||
}
|
||||
}
|
22
src/services/teamList.js
Normal file
22
src/services/teamList.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import AbstractService from './abstractService'
|
||||
import TeamListModel from '../models/teamList'
|
||||
import TeamModel from '../models/team'
|
||||
|
||||
export default class TeamListService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listID}/teams',
|
||||
getAll: '/lists/{listID}/teams',
|
||||
update: '/lists/{listID}/teams/{teamID}',
|
||||
delete: '/lists/{listID}/teams/{teamID}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamListModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new TeamModel(data)
|
||||
}
|
||||
}
|
21
src/services/teamMember.js
Normal file
21
src/services/teamMember.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import AbstractService from './abstractService'
|
||||
import TeamMemberModel from '../models/teamMember'
|
||||
|
||||
export default class TeamMemberService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/teams/{teamID}/members',
|
||||
delete: '/teams/{teamID}/members/{id}', // "id" is the user id because we're intheriting from a normal user
|
||||
});
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamMemberModel(data)
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
model.userID = model.id // The api wants to get the user id as userID
|
||||
model.admin = model.admin === null ? false : model.admin
|
||||
return model
|
||||
}
|
||||
}
|
22
src/services/teamNamespace.js
Normal file
22
src/services/teamNamespace.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import AbstractService from './abstractService'
|
||||
import TeamNamespaceModel from '../models/teamNamespace'
|
||||
import TeamModel from '../models/team'
|
||||
|
||||
export default class TeamNamespaceService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceID}/teams',
|
||||
getAll: '/namespaces/{namespaceID}/teams',
|
||||
update: '/namespaces/{namespaceID}/teams/{teamID}',
|
||||
delete: '/namespaces/{namespaceID}/teams/{teamID}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamNamespaceModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new TeamModel(data)
|
||||
}
|
||||
}
|
14
src/services/user.js
Normal file
14
src/services/user.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import AbstractService from './abstractService'
|
||||
import UserModel from '../models/user'
|
||||
|
||||
export default class UserService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/users'
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
22
src/services/userList.js
Normal file
22
src/services/userList.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import AbstractService from './abstractService'
|
||||
import UserListModel from '../models/userList'
|
||||
import UserModel from '../models/user'
|
||||
|
||||
export default class UserListService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listID}/users',
|
||||
getAll: '/lists/{listID}/users',
|
||||
update: '/lists/{listID}/users/{userID}',
|
||||
delete: '/lists/{listID}/users/{userID}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserListModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
22
src/services/userNamespace.js
Normal file
22
src/services/userNamespace.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import AbstractService from './abstractService'
|
||||
import UserNamespaceModel from '../models/userNamespace'
|
||||
import UserModel from '../models/user'
|
||||
|
||||
export default class UserNamespaceService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceID}/users',
|
||||
getAll: '/namespaces/{namespaceID}/users',
|
||||
update: '/namespaces/{namespaceID}/users/{userID}',
|
||||
delete: '/namespaces/{namespaceID}/users/{userID}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserNamespaceModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
return new UserModel(data)
|
||||
}
|
||||
}
|
27
todo.md
27
todo.md
|
@ -77,6 +77,9 @@
|
|||
* [x] Einzelne Teams ansehbar
|
||||
* [x] In den Teams, in denen der Nutzer admin ist, Bearbeitung ermöglichen
|
||||
* [x] Löschen ermöglichen
|
||||
* [x] Subtasks
|
||||
* [x] Start/Enddatum für Tasks
|
||||
* [x] Tasks in time range
|
||||
* [ ] Search everything
|
||||
* [ ] Lists
|
||||
* [ ] Tasks
|
||||
|
@ -86,21 +89,31 @@
|
|||
* [ ] Users with access to a namespace
|
||||
* [ ] Teams with access to a list
|
||||
* [ ] Teams with access to a namespace
|
||||
* [x] Subtasks
|
||||
* [x] Start/Enddatum für Tasks
|
||||
* [x] Tasks in time range
|
||||
* [ ] Priorities
|
||||
* [ ] Sachen mit hoher Prio irgendwie hervorheben (rotes Dreieck zb)
|
||||
* [ ] Listen Kopieren
|
||||
* [ ] "Move to Vikunja" -> Migrator von Wunderlist/todoist/etc
|
||||
* [ ] Assignees
|
||||
* [ ] Labels
|
||||
* [ ] Timeline/Calendar view -> Dazu tasks die in einem Bestimmten Bereich due sind, macht dann das Frontend
|
||||
|
||||
## Other features
|
||||
|
||||
* [ ] Copy lists
|
||||
* [ ] "Move to Vikunja" -> Migrator von Wunderlist/todoist/etc
|
||||
|
||||
## Refactor
|
||||
|
||||
* [x] Move everything to models
|
||||
* [x] Make sure all loading properties are depending on its service
|
||||
* [x] Fix the first request afer login being made with an old token
|
||||
* [ ] Team sharing
|
||||
* [ ] Refactor team sharing to not make a new request every time something was changed
|
||||
* [ ] Team sharing should be able to search for a team instead of its ID, like it's the case with users
|
||||
* [ ] Dropdown for rights
|
||||
|
||||
## Waiting for backend
|
||||
|
||||
* [ ] Assignees
|
||||
* [ ] In und Out webhooks, mit Templates vom Payload
|
||||
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> nur UI?
|
||||
* [ ] "Performance-Statistik" -> Wie viele Tasks man in bestimmten Zeiträumen so geschafft hat etc
|
||||
* [ ] Activity Feed, so à la "der und der hat das und das gemacht etc"
|
||||
* [ ] Attachments for tasks
|
||||
* [ ] Labels
|
38
yarn.lock
38
yarn.lock
|
@ -2710,7 +2710,7 @@ debug@^4.1.0:
|
|||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debuglog@*, debuglog@^1.0.1:
|
||||
debuglog@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
|
||||
|
@ -4365,7 +4365,7 @@ import-local@^2.0.0:
|
|||
pkg-dir "^3.0.0"
|
||||
resolve-cwd "^2.0.0"
|
||||
|
||||
imurmurhash@*, imurmurhash@^0.1.4:
|
||||
imurmurhash@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
|
||||
|
@ -5165,11 +5165,6 @@ lockfile@^1.0.4:
|
|||
dependencies:
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
lodash._baseindexof@*:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
|
||||
integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
|
||||
|
||||
lodash._baseuniq@~4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
||||
|
@ -5178,33 +5173,11 @@ lodash._baseuniq@~4.6.0:
|
|||
lodash._createset "~4.0.0"
|
||||
lodash._root "~3.0.0"
|
||||
|
||||
lodash._bindcallback@*:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
|
||||
integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
|
||||
|
||||
lodash._cacheindexof@*:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
|
||||
integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
|
||||
|
||||
lodash._createcache@*:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
|
||||
integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
|
||||
dependencies:
|
||||
lodash._getnative "^3.0.0"
|
||||
|
||||
lodash._createset@~4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
|
||||
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
|
||||
|
||||
lodash._getnative@*, lodash._getnative@^3.0.0:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
|
||||
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
|
||||
|
||||
lodash._root@~3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
|
||||
|
@ -5250,11 +5223,6 @@ lodash.mergewith@^4.6.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
|
||||
|
||||
lodash.restparam@*:
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
|
||||
integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
|
||||
|
||||
lodash.tail@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
|
||||
|
@ -7384,7 +7352,7 @@ readable-stream@~1.1.10:
|
|||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
|
||||
readdir-scoped-modules@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
|
||||
integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c=
|
||||
|
|
Loading…
Reference in a new issue